fix(python): preserve pyc file outcomes and cancelled Evidence

This commit is contained in:
morluto committed 2026-10-22 22:55:22 +08:00
1 parent 2846cc0570
commit 8761d4251f
4 files changed
+115 -23

No files matched your search

@@ -2,6 +2,7 @@ import { expect, it } from "vitest";
import {
AnalysisInputError,
AnalysisCancelledError,
AnalysisOutputError,
} from "../../domain/analysisErrorCore.js";
import type { PythonPyc } from "../../domain/python/pythonPyc.js";
import { ok } from "../../domain/result.js";
@@ -105,12 +106,13 @@ it("records the selected artifact and the header plus marshal ranges", async ()
]);
});
it("discards an observation if the caller cancels while the provider completes", async () => {
it("retains completed pyc Evidence and the opaque caller reason on late cancellation", async () => {
const controller = new AbortController();
const reason = new AnalysisInputError("opaque caller metadata");
const service = new PythonPycService({
identity,
inspect: () => {
controller.abort();
controller.abort(reason);
return Promise.resolve(ok(report));
},
});
@@ -119,5 +121,54 @@ it("discards an observation if the caller cancels while the provider completes",
{ signal: controller.signal },
);
expect(result.ok).toBe(false);
if (!result.ok) expect(result.error).toBeInstanceOf(AnalysisCancelledError);
if (result.ok) throw new Error("Expected cancellation");
expect(result.error).toBeInstanceOf(AnalysisCancelledError);
expect(result.error.cause).toBe(reason);
expect(result.error.partialObservation).toMatchObject({
operation: "inspect_python_pyc",
normalized_result: report,
subject: { local_path: report.artifact.path },
});
});
it("retains a pre-admission cancellation reason without calling the pyc provider", async () => {
const reason = new Error("caller stopped before pyc admission");
let calls = 0;
const service = new PythonPycService({
identity,
inspect: async () => {
calls++;
return ok(report);
},
});
const result = await service.inspect(
{ path: report.artifact.path },
{ signal: AbortSignal.abort(reason) },
);
if (result.ok) throw new Error("Expected cancellation");
expect(result.error).toBeInstanceOf(AnalysisCancelledError);
expect(result.error.cause).toBe(reason);
expect(result.error.partialObservation).toBeUndefined();
expect(calls).toBe(0);
});
it("preserves malformed provider output instead of relabeling it as a late abort", async () => {
const controller = new AbortController();
const service = new PythonPycService({
identity,
inspect: async () => {
controller.abort(new Error("later caller stop"));
return ok({
...report,
artifact: { ...report.artifact, path: "/different.pyc" },
});
},
});
const result = await service.inspect(
{ path: report.artifact.path },
{ signal: controller.signal },
);
if (result.ok) throw new Error("Expected output failure");
expect(result.error).toBeInstanceOf(AnalysisOutputError);
expect(result.error.partialObservation).toBeUndefined();
});
+7 -5
View File
@@ -14,7 +14,8 @@ import {
pythonPycSchema,
} from "../../domain/python/pythonPyc.js";
import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js";
import { err, ok, type Result } from "../../domain/result.js";
import { err, type Result } from "../../domain/result.js";
import { completeProviderEvidence } from "../ProviderOperationExecution.js";
const OPERATION = "inspect_python_pyc";
@@ -31,7 +32,9 @@ export class PythonPycService {
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 = inspectPythonPycInputSchema.safeParse(rawInput);
if (!input.success)
return err(
@@ -51,8 +54,6 @@ export class PythonPycService {
);
const result = await this.provider.inspect(input.data, options);
if (!result.ok) return result;
if (options?.signal?.aborted)
return err(new AnalysisCancelledError(OPERATION));
const report = pythonPycSchema.safeParse(result.value);
if (
!report.success ||
@@ -68,7 +69,7 @@ export class PythonPycService {
),
);
const value = report.data;
return ok(
return completeProviderEvidence(
createEvidence(
{
path: value.artifact.path,
@@ -97,6 +98,7 @@ export class PythonPycService {
],
},
),
options?.signal,
);
}
}