fix(analysis): retain artifact admission cancellation reasons

This commit is contained in:
morluto committed 2026-10-18 08:52:55 +08:00
1 parent a8170163b9
commit 52b73ee857
6 files changed
+132 -41

No files matched your search

@@ -80,10 +80,11 @@ it.each([
it("retains a completed observation on late cancellation and keeps output errors primary", async () => {
const report = await observedReport();
const controller = new AbortController();
const reason = { request: "late PE cancellation" };
const service = new PeResourcesService({
identity: PE_RESOURCES_PROVIDER,
inspect: () => {
controller.abort();
controller.abort(reason);
return Promise.resolve(ok(report));
},
});
@@ -96,6 +97,7 @@ it("retains a completed observation on late cancellation and keeps output errors
error: { _tag: "AnalysisCancelledError" },
});
if (!late.ok) {
expect(late.error.cause).toBe(reason);
expect(parseEvidence(late.error.partialObservation)).toMatchObject({
operation: "inspect_pe_resources",
subject: { architecture: "x86" },
@@ -119,3 +121,40 @@ it("retains a completed observation on late cancellation and keeps output errors
expect(invalid.ok).toBe(false);
if (!invalid.ok) expect(invalid.error).toBeInstanceOf(AnalysisOutputError);
});
it.each([
null,
"caller cancellation",
{ request: "opaque caller value" },
new Error("caller failure"),
new AnalysisOutputError("caller_operation", "caller-owned cleanup", {
cleanup: {
reason: "unresolved caller resource",
resources: ["caller resource"],
},
}),
])(
"preserves an opaque admission cancellation reason without invoking the reader: %j",
async (reason) => {
const controller = new AbortController();
controller.abort(reason);
let calls = 0;
const service = new PeResourcesService({
identity: PE_RESOURCES_PROVIDER,
inspect: async () => {
calls += 1;
throw new Error("cancelled admission must not invoke the reader");
},
});
const result = await service.inspect(null, { signal: controller.signal });
expect(result.ok).toBe(false);
if (result.ok) throw new Error("expected cancelled admission");
expect(result.error).toBeInstanceOf(AnalysisCancelledError);
expect(result.error.cause).toBe(reason);
expect(result.error.cleanupIncomplete).toBe(false);
expect(result.error.partialObservation).toBeUndefined();
expect(calls).toBe(0);
if (reason instanceof AnalysisOutputError)
expect(reason.cleanupIncomplete).toBe(true);
},
);
@@ -37,7 +37,9 @@ export class PeResourcesService {
): Promise<Result<Evidence, AnalysisError>> {
const operation = "inspect_pe_resources";
if (options?.signal?.aborted)
return err(new AnalysisCancelledError(operation));
return err(
new AnalysisCancelledError(operation, { cause: options.signal.reason }),
);
const input = inspectPeResourcesInputSchema.safeParse(rawInput);
if (!input.success)
return err(
+40 -1
View File
@@ -153,12 +153,13 @@ it("refuses reader identity changes and invalid original source ranges", async (
it("honors cancellation before reading and after a completed observation", async () => {
const controller = new AbortController();
const reason = { request: "late Go cancellation" };
let calls = 0;
const service = new GoBinaryService({
identity,
inspect: () => {
calls += 1;
controller.abort();
controller.abort(reason);
return Promise.resolve(ok(report));
},
});
@@ -169,6 +170,7 @@ it("honors cancellation before reading and after a completed observation", async
expect(completed.ok).toBe(false);
if (!completed.ok) {
expect(completed.error).toBeInstanceOf(AnalysisCancelledError);
expect(completed.error.cause).toBe(reason);
expect(parseEvidence(completed.error.partialObservation)).toMatchObject({
operation: "inspect_go_binary",
normalized_result: report,
@@ -199,3 +201,40 @@ it("keeps malformed provider output ahead of a late cancellation", async () => {
expect(result.ok).toBe(false);
if (!result.ok) expect(result.error).toBeInstanceOf(AnalysisOutputError);
});
it.each([
null,
"caller cancellation",
{ request: "opaque caller value" },
new Error("caller failure"),
new AnalysisOutputError("caller_operation", "caller-owned cleanup", {
cleanup: {
reason: "unresolved caller resource",
resources: ["caller resource"],
},
}),
])(
"preserves an opaque admission cancellation reason without invoking the reader: %j",
async (reason) => {
const controller = new AbortController();
controller.abort(reason);
let calls = 0;
const service = new GoBinaryService({
identity,
inspect: async () => {
calls += 1;
throw new Error("cancelled admission must not invoke the reader");
},
});
const result = await service.inspect(null, { signal: controller.signal });
expect(result.ok).toBe(false);
if (result.ok) throw new Error("expected cancelled admission");
expect(result.error).toBeInstanceOf(AnalysisCancelledError);
expect(result.error.cause).toBe(reason);
expect(result.error.cleanupIncomplete).toBe(false);
expect(result.error.partialObservation).toBeUndefined();
expect(calls).toBe(0);
if (reason instanceof AnalysisOutputError)
expect(reason.cleanupIncomplete).toBe(true);
},
);
+3 -1
View File
@@ -45,7 +45,9 @@ export class GoBinaryService {
options?: ExecutionOptions,
): Promise<Result<Evidence, AnalysisError>> {
if (options?.signal?.aborted)
return err(new AnalysisCancelledError(OPERATION));
return err(
new AnalysisCancelledError(OPERATION, { cause: options.signal.reason }),
);
const input = inspectGoBinaryInputSchema.safeParse(rawInput);
if (!input.success)
return err(
+43 -36
View File
@@ -115,10 +115,11 @@ it("rejects a cycle claim the package dependencies do not contain", async () =>
it("retains completed Rust Evidence on late cancellation and keeps output failure precedence", async () => {
const controller = new AbortController();
const reason = { request: "late Rust cancellation" };
const service = new RustBinaryService({
identity,
inspect: () => {
controller.abort();
controller.abort(reason);
return Promise.resolve(ok(report));
},
});
@@ -154,8 +155,14 @@ it("retains completed Rust Evidence on late cancellation and keeps output failur
error: { _tag: "AnalysisOutputError" },
});
expect(cancelled.ok).toBe(false);
if (!cancelled.ok)
if (!cancelled.ok) {
expect(cancelled.error).toBeInstanceOf(AnalysisCancelledError);
expect(cancelled.error.cause).toBe(reason);
expect(parseEvidence(cancelled.error.partialObservation)).toMatchObject({
operation: "inspect_rust_binary",
normalized_result: report,
});
}
});
it.each([
@@ -178,39 +185,39 @@ it.each([
if (!result.ok) expect(result.error).toBeInstanceOf(AnalysisOutputError);
});
it("retains a completed report on late cancellation and keeps output errors primary", async () => {
const controller = new AbortController();
const completed = await new RustBinaryService({
identity,
inspect: () => {
controller.abort();
return Promise.resolve(ok(report));
it.each([
null,
"caller cancellation",
{ request: "opaque caller value" },
new Error("caller failure"),
new AnalysisOutputError("caller_operation", "caller-owned cleanup", {
cleanup: {
reason: "unresolved caller resource",
resources: ["caller resource"],
},
}).inspect({ path: report.artifact.path }, { signal: controller.signal });
expect(completed.ok).toBe(false);
if (!completed.ok) {
expect(completed.error).toBeInstanceOf(AnalysisCancelledError);
expect(parseEvidence(completed.error.partialObservation)).toMatchObject({
operation: "inspect_rust_binary",
normalized_result: report,
}),
])(
"preserves an opaque admission cancellation reason without invoking the reader: %j",
async (reason) => {
const controller = new AbortController();
controller.abort(reason);
let calls = 0;
const service = new RustBinaryService({
identity,
inspect: async () => {
calls += 1;
throw new Error("cancelled admission must not invoke the reader");
},
});
}
const malformed = {
...report,
artifact: { ...report.artifact, path: "/artifacts/other" },
};
const outputController = new AbortController();
const invalid = await new RustBinaryService({
identity,
inspect: () => {
outputController.abort();
return Promise.resolve(ok(malformed));
},
}).inspect(
{ path: report.artifact.path },
{ signal: outputController.signal },
);
expect(invalid.ok).toBe(false);
if (!invalid.ok) expect(invalid.error).toBeInstanceOf(AnalysisOutputError);
});
const result = await service.inspect(null, { signal: controller.signal });
expect(result.ok).toBe(false);
if (result.ok) throw new Error("expected cancelled admission");
expect(result.error).toBeInstanceOf(AnalysisCancelledError);
expect(result.error.cause).toBe(reason);
expect(result.error.cleanupIncomplete).toBe(false);
expect(result.error.partialObservation).toBeUndefined();
expect(calls).toBe(0);
if (reason instanceof AnalysisOutputError)
expect(reason.cleanupIncomplete).toBe(true);
},
);
+3 -1
View File
@@ -46,7 +46,9 @@ export class RustBinaryService {
options?: ExecutionOptions,
): Promise<Result<Evidence, AnalysisError>> {
if (options?.signal?.aborted)
return err(new AnalysisCancelledError(OPERATION));
return err(
new AnalysisCancelledError(OPERATION, { cause: options.signal.reason }),
);
const input = inspectRustBinaryInputSchema.safeParse(rawInput);
if (!input.success)
return err(