mirror of
https://github.com/morluto/rea.git
synced 2026-11-07 05:56:19 -10:00
fix(process): report retained capture root
This commit is contained in:
1 parent
768b58c6db
commit
17da88f117
3 files changed
+91
-15
No files matched your search
@@ -1184,7 +1184,9 @@ class ProcessCaptureResourceOwner {
|
||||
}
|
||||
|
||||
get cleanupResources(): readonly string[] {
|
||||
return unresolvedCleanupResources(this.report);
|
||||
return unresolvedCleanupResources(this.report).map((resource) =>
|
||||
resource === "temporary_root" ? this.options.temporaryRoot : resource,
|
||||
);
|
||||
}
|
||||
|
||||
get report(): ProcessCaptureCleanupReport {
|
||||
|
||||
@@ -5,7 +5,16 @@ import {
|
||||
captureProcessScenario,
|
||||
retainRootSignaller,
|
||||
} from "./ProcessHarness.js";
|
||||
import { settleProcessCaptureJournal } from "./ProcessCaptureLifecycle.js";
|
||||
import {
|
||||
settleProcessCaptureJournal,
|
||||
releaseProcessResources,
|
||||
ProcessCaptureResourceScope,
|
||||
createProcessCaptureObservationBuffer,
|
||||
prepareProcessCapture,
|
||||
resolveProcessResult,
|
||||
type ProcessCaptureCleanupOutcome,
|
||||
type ProcessCaptureCleanupHost,
|
||||
} from "./ProcessCaptureLifecycle.js";
|
||||
import { parseProcessCapture } from "../../domain/process/processCaptureParsing.js";
|
||||
import { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "../../domain/process/processCaptureExample.js";
|
||||
import { projectAnalysisError } from "../../domain/analysisErrorProjection.js";
|
||||
@@ -19,15 +28,6 @@ import {
|
||||
normalizeCaptureFailure,
|
||||
processCaptureCancelled,
|
||||
} from "./ProcessCaptureError.js";
|
||||
import {
|
||||
releaseProcessResources,
|
||||
ProcessCaptureResourceScope,
|
||||
createProcessCaptureObservationBuffer,
|
||||
prepareProcessCapture,
|
||||
resolveProcessResult,
|
||||
type ProcessCaptureCleanupOutcome,
|
||||
type ProcessCaptureCleanupHost,
|
||||
} from "./ProcessCaptureLifecycle.js";
|
||||
import { DarwinProcessOwnershipInspectionError } from "../DarwinProcessRunTokenReader.js";
|
||||
|
||||
import {
|
||||
@@ -353,7 +353,10 @@ it("keeps empty cleanup exception messages actionable in the report", async () =
|
||||
|
||||
it("retains observations and retries the temporary root when process cleanup throws", async () => {
|
||||
const cleanupFailure = new Error("termination inspection failed");
|
||||
const removeTemporaryRoot = vi.fn(async () => undefined);
|
||||
let rootRemovalFails = true;
|
||||
const removeTemporaryRoot = vi.fn(async () => {
|
||||
if (rootRemovalFails) throw new Error("root removal denied");
|
||||
});
|
||||
let failing = true;
|
||||
const scope = new ProcessCaptureResourceScope();
|
||||
const host: ProcessCaptureCleanupHost = {
|
||||
@@ -382,6 +385,10 @@ it("retains observations and retries the temporary root when process cleanup thr
|
||||
state: "failed",
|
||||
reason: cleanupFailure.message,
|
||||
});
|
||||
expect(cleanup.cleanupLease?.cleanupResources).toEqual([
|
||||
"owned_process_group",
|
||||
"/fixture/root",
|
||||
]);
|
||||
const executionFailure = new Error("capture snapshot failed");
|
||||
const capture = parseProcessCapture({
|
||||
...accountFullyObservedProcessCapture({
|
||||
@@ -401,6 +408,7 @@ it("retains observations and retries the temporary root when process cleanup thr
|
||||
).toMatchObject({
|
||||
code: "cleanup_incomplete",
|
||||
details: {
|
||||
resources: ["owned_process_group", "/fixture/root"],
|
||||
cleanup_report: cleanup.report,
|
||||
execution_failure: "capture snapshot failed",
|
||||
partial_observation: {
|
||||
@@ -410,10 +418,15 @@ it("retains observations and retries the temporary root when process cleanup thr
|
||||
});
|
||||
}
|
||||
failing = false;
|
||||
await expect(scope.retryCleanup()).rejects.toMatchObject({
|
||||
cleanupResources: ["/fixture/root"],
|
||||
});
|
||||
expect(cleanup.cleanupLease?.cleanupResources).toEqual(["/fixture/root"]);
|
||||
rootRemovalFails = false;
|
||||
await scope.retryCleanup();
|
||||
expect(removeTemporaryRoot).toHaveBeenCalledExactlyOnceWith("/fixture/root");
|
||||
expect(removeTemporaryRoot).toHaveBeenCalledWith("/fixture/root");
|
||||
await scope.close();
|
||||
expect(removeTemporaryRoot).toHaveBeenCalledTimes(1);
|
||||
expect(removeTemporaryRoot).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("retains unrelated withheld processes through cleanup, capture, and JSON", async () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import { readFile, readdir, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
import { createEvidence } from "../../../src/domain/evidence.js";
|
||||
import { PROCESS_PROVIDER } from "../../../src/domain/process/processEvidenceProvider.js";
|
||||
import { INVESTIGATION_EXAMPLES } from "../../../src/contracts/investigationExamples.js";
|
||||
import { observeProcessStartIdentity } from "../../../src/process/ProcessOwnershipObservation.js";
|
||||
import {
|
||||
analysisCliErrorEnvelopeSchema,
|
||||
analysisErrorProjectionSchema,
|
||||
@@ -76,6 +77,66 @@ describe("documented process CLI workflow", () => {
|
||||
);
|
||||
});
|
||||
|
||||
describe("process CLI cleanup errors", () => {
|
||||
cliTest.skipIf(process.platform !== "darwin")(
|
||||
"reports the retained capture root when platform-binary finalization cannot establish ownership",
|
||||
async ({ cli }) => {
|
||||
const root = await createTestTempDirectory("rea-process-cli-cleanup-");
|
||||
const scenario = join(root, "scenario.json");
|
||||
const marker = join(root, "target.pid");
|
||||
await writeFile(
|
||||
scenario,
|
||||
JSON.stringify({
|
||||
executable: "/bin/sh",
|
||||
arguments: [
|
||||
"-c",
|
||||
'printf "%s" "$$" > "$1"; exec /bin/sleep 3',
|
||||
"capture-cleanup-fixture",
|
||||
marker,
|
||||
],
|
||||
timeout_ms: 500,
|
||||
idle_timeout_ms: 500,
|
||||
finalization_ms: 100,
|
||||
}),
|
||||
);
|
||||
const result = await cli.run({
|
||||
arguments: ["capture-process", scenario, "--json"],
|
||||
environment: { TMPDIR: root },
|
||||
});
|
||||
const targetPid = Number(await readFile(marker, "utf8"));
|
||||
expect(Number.isSafeInteger(targetPid) && targetPid > 0).toBe(true);
|
||||
// The fail-closed command deliberately leaves the platform binary alive.
|
||||
// Observe its bounded natural exit before the temporary fixture is removed.
|
||||
await expect
|
||||
.poll(() => observeProcessStartIdentity(targetPid), {
|
||||
timeout: 7_000,
|
||||
interval: 25,
|
||||
})
|
||||
.toBeUndefined();
|
||||
expect(result.exitCode, result.stdout || result.stderr).toBe(1);
|
||||
const retained = (await readdir(root)).filter((name) =>
|
||||
/^rea-process-(?!token-)/u.test(name),
|
||||
);
|
||||
expect(retained).toHaveLength(1);
|
||||
const retainedRoot = retained[0];
|
||||
if (retainedRoot === undefined) throw new Error("Missing retained root");
|
||||
expect(result.json).toMatchObject({
|
||||
code: "cleanup_incomplete",
|
||||
message: expect.stringContaining(
|
||||
"platform_binary_environment_withheld",
|
||||
),
|
||||
details: {
|
||||
resources: ["owned_process_group", join(root, retainedRoot)],
|
||||
cleanup_report: {
|
||||
temporary_root: { state: "unverified" },
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
CLI_INTEGRATION_TIMEOUT_MS,
|
||||
);
|
||||
});
|
||||
|
||||
describe("process CLI errors", () => {
|
||||
cliTest(
|
||||
"returns canonical JSON errors for missing and malformed process files",
|
||||
|
||||
Reference in new issue
Block a user