fix(process): identify unknown live processes in cleanup diagnostics

This commit is contained in:
morluto
2026-10-09 00:29:08 +08:00
parent bfb7776006
commit eb89a16d9b
6 changed files with 49 additions and 6 deletions
+1 -1
View File
@@ -208,7 +208,7 @@ describe("process environment permission diagnostics", () => {
),
).resolves.toMatchObject({
cleaned: false,
reason: `process ownership token could not be read for 1 live process(es): environment_errno_${code}=1`,
reason: `process ownership token could not be read for 1 live process(es): environment_errno_${code}=1; live candidates 900=environment_errno_${code}`,
failures: [{ pid: 900, reason: "environment-unreadable", diagnostic }],
});
expect(signalGroup).not.toHaveBeenCalled();
+7
View File
@@ -193,6 +193,13 @@ describe("opaque process neighbors during cleanup", () => {
cleaned: false,
reason: expect.stringContaining("environment_unavailable=1"),
});
if (result.cleaned)
throw new Error("Expected unknown ownership to fail closed");
expect(result.reason).toContain(
"live candidates 900=environment_unavailable",
);
expect(result.reason).not.toContain("unowned");
expect(result.reason).not.toContain("run-token");
expect(signalGroup.mock.calls).toEqual([[100, "SIGKILL"]]);
expect(signalGroup).not.toHaveBeenCalledWith(900, "SIGKILL");
});
+7 -1
View File
@@ -566,9 +566,15 @@ const cleanupValidationFailure = (
.sort(([left], [right]) => left.localeCompare(right))
.map(([category, count]) => `${category}=${String(count)}`)
.join(", ");
const candidates = unreadable
.map(
({ pid, diagnostic }) =>
`${String(pid)}=${sanitizedTokenReadFailure(diagnostic)}`,
)
.sort((left, right) => left.localeCompare(right));
return {
cleaned: false,
reason: `process ownership token could not be read for ${String(unreadable.length)} live process(es): ${breakdown}`,
reason: `process ownership token could not be read for ${String(unreadable.length)} live process(es): ${breakdown}; live candidates ${candidates.join(", ")}`,
failures,
...unverifiedEntry,
};
+1 -1
View File
@@ -304,7 +304,7 @@ it("retains observations and both causes when process cleanup is unverifiable",
owned_process_group: {
state: "unverified" as const,
reason:
"process ownership token could not be read for 1 live process(es): environment_unavailable=1",
"process ownership token could not be read for 1 live process(es): environment_unavailable=1; live candidates 900=environment_unavailable",
unverified_processes: [
{ pid: 900, reason: "platform_binary_environment_withheld" },
],
+25
View File
@@ -15,6 +15,7 @@ import {
projectProcessCliError,
} from "../../../src/application/process/ProcessCli.js";
import { createEvidence } from "../../../src/domain/evidence.js";
import { ProcessCaptureError } from "../../../src/process/capture/ProcessCaptureError.js";
import { PROCESS_PROVIDER } from "../../../src/application/process/ProcessEvidence.js";
import { INVESTIGATION_EXAMPLES } from "../../../src/contracts/investigationExamples.js";
@@ -229,6 +230,30 @@ describe("process CLI environment key diagnostics", () => {
});
describe("process CLI evidence validation", () => {
it("preserves sanitized unknown-process IDs and reasons through CLI projection", () => {
const reason =
"process ownership token could not be read for 1 live process(es): environment_unavailable=1; live candidates 900=environment_unavailable";
const projected = projectProcessCliError(
new ProcessCaptureError("cleanup incomplete", {
reason: "cleanup_incomplete",
cleanupResources: ["owned_process_group"],
cleanupReport: {
owned_process_group: { state: "unverified", reason },
terminal_renderer: { state: "cleaned", reason: null },
temporary_root: { state: "cleaned", reason: null },
},
}),
);
expect(projected).toMatchObject({
code: "cleanup_incomplete",
details: {
cleanup_report: {
owned_process_group: { reason },
},
},
});
});
it("captures the minimal executable-and-arguments scenario", async () => {
const root = await fixture();
const scenario = join(root, "scenario.json");
@@ -46,12 +46,14 @@ const captureObservations = (
const partial = result.error.partialObservation;
expect(result.error.reason).toBe("cleanup_incomplete");
expect(report?.owned_process_group.state).toBe("unverified");
const [summary, breakdown] = (report?.owned_process_group.reason ?? "").split(
": ",
);
const [summary, diagnostics] = (
report?.owned_process_group.reason ?? ""
).split(": ");
expect(summary).toMatch(
/^process ownership token could not be read for [1-9][0-9]* live process\(es\)$/u,
);
const [breakdown, liveCandidates] =
diagnostics?.split("; live candidates ") ?? [];
const categories = breakdown?.split(", ") ?? [];
expect(categories.length).toBeGreaterThan(0);
const expectedCategory =
@@ -59,6 +61,9 @@ const captureObservations = (
? /^environment_errno_(?:EACCES|EPERM)=[1-9][0-9]*$/u
: /^environment_unavailable=[1-9][0-9]*$/u;
for (const category of categories) expect(category).toMatch(expectedCategory);
expect(liveCandidates).toMatch(
/^(?:[1-9][0-9]*=(?:environment_unavailable|environment_errno_(?:EACCES|EPERM)))(?:, [1-9][0-9]*=(?:environment_unavailable|environment_errno_(?:EACCES|EPERM)))*$/u,
);
expect(report?.terminal_renderer.state).toBe("cleaned");
expect(report?.temporary_root.state).toBe("cleaned");
if (partial === undefined || !("capture" in partial))