mirror of
https://github.com/morluto/rea
synced 2026-10-26 06:16:17 -04:00
fix(flutter): keep completed inspection facts when cancelled during snapshot cleanup (#4766)
FlutterBuildProvider.execute checks the caller signal after its awaited snapshot-root removal. When the signal was aborted during a successful first removal, that check replaced the completed AnalysisExecution with a bare AnalysisCancelledError, so identify-flutter-build and inspect-dart-aot reported a typed cancellation without details.partial_observation and the shared CLI/MCP Evidence assembly never saw the facts. The post-cleanup check now keeps the caller's abort reason and retains a completed result as the cancellation's partial observation. The Evidence composition from executeProviderOperation moves into createAnalysisExecutionEvidence beside the provider contract, so the success path and the retained cancellation use one composer and carry identical Evidence. Failed results keep their existing classification. Fixes #4717 Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -9,9 +9,13 @@ import type { AnalysisError } from "../domain/analysisErrorBase.js";
|
||||
import type { AnalysisProfileCommitment } from "../domain/analysisProfile.js";
|
||||
import type { JsonValue } from "../domain/jsonValue.js";
|
||||
import type { Result } from "../domain/result.js";
|
||||
import type { EvidenceLocation } from "../domain/evidence.js";
|
||||
import type { EvidenceSubjectTarget } from "../domain/evidence.js";
|
||||
import { jsonValueSchema } from "../domain/jsonValue.js";
|
||||
import {
|
||||
createEvidence,
|
||||
type Evidence,
|
||||
type EvidenceLocation,
|
||||
type EvidenceSubjectTarget,
|
||||
} from "../domain/evidence.js";
|
||||
import { jsonObjectSchema, jsonValueSchema } from "../domain/jsonValue.js";
|
||||
import type { ProgressReporter } from "./ProgressReporter.js";
|
||||
import type { ProviderRejectionCode } from "../contracts/providerSelection.js";
|
||||
import type { ProcessLineageObservation } from "../process/ProcessOwnership.js";
|
||||
@@ -123,6 +127,26 @@ export function createAnalysisExecution(
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Compose one completed provider execution into its caller-visible Evidence,
|
||||
* so success and a late cancellation that retains it carry identical facts.
|
||||
*/
|
||||
export const createAnalysisExecutionEvidence = (
|
||||
operation: string,
|
||||
input: unknown,
|
||||
execution: AnalysisExecution,
|
||||
confidence: "observed" | "derived",
|
||||
): Evidence =>
|
||||
createEvidence(execution.subject ?? undefined, execution.provider, {
|
||||
operation,
|
||||
parameters: jsonObjectSchema.parse(input),
|
||||
result: execution.result,
|
||||
rawResult: execution.rawResult,
|
||||
limitations: execution.limitations,
|
||||
locations: execution.locations,
|
||||
confidence,
|
||||
});
|
||||
|
||||
/** Provider-neutral application capability for one analysis operation. */
|
||||
export interface AnalysisOperationPort {
|
||||
execute(
|
||||
|
||||
@@ -7,12 +7,12 @@ import {
|
||||
AnalysisCancelledError,
|
||||
AnalysisInputError,
|
||||
} from "../domain/analysisErrorCore.js";
|
||||
import { createEvidence, type Evidence } from "../domain/evidence.js";
|
||||
import { jsonObjectSchema } from "../domain/jsonValue.js";
|
||||
import type { Evidence } from "../domain/evidence.js";
|
||||
import { err, ok, type Result } from "../domain/result.js";
|
||||
import type {
|
||||
AnalysisExecution,
|
||||
ExecutionOptions,
|
||||
import {
|
||||
createAnalysisExecutionEvidence,
|
||||
type AnalysisExecution,
|
||||
type ExecutionOptions,
|
||||
} from "./AnalysisProvider.js";
|
||||
|
||||
/** Validated caller-selected request carrying its operation input. */
|
||||
@@ -55,19 +55,11 @@ export const executeProviderOperation = async <
|
||||
return err(new AnalysisInputError(operation, { cause: request.error }));
|
||||
const executed = await provider.execute(request.data, options);
|
||||
if (!executed.ok) return executed;
|
||||
const execution = executed.value;
|
||||
const evidence = createEvidence(
|
||||
execution.subject ?? undefined,
|
||||
execution.provider,
|
||||
{
|
||||
operation,
|
||||
parameters: jsonObjectSchema.parse(request.data.input),
|
||||
result: execution.result,
|
||||
rawResult: execution.rawResult,
|
||||
limitations: execution.limitations,
|
||||
locations: execution.locations,
|
||||
confidence,
|
||||
},
|
||||
const evidence = createAnalysisExecutionEvidence(
|
||||
operation,
|
||||
request.data.input,
|
||||
executed.value,
|
||||
confidence,
|
||||
);
|
||||
return completeProviderEvidence(evidence, options?.signal);
|
||||
};
|
||||
|
||||
@@ -12,6 +12,7 @@ import type {
|
||||
} from "../application/AnalysisProvider.js";
|
||||
import {
|
||||
createAnalysisExecution,
|
||||
createAnalysisExecutionEvidence,
|
||||
type AnalysisExecution,
|
||||
} from "../application/AnalysisProvider.js";
|
||||
import type { FlutterRequest } from "../domain/flutter/flutterBuildAnalysis.js";
|
||||
@@ -619,9 +620,23 @@ export class FlutterBuildProvider {
|
||||
} finally {
|
||||
if (snapshot !== undefined) await this.discardRoot(snapshot.root);
|
||||
}
|
||||
// Cancellation observed after the awaited snapshot removal still keeps
|
||||
// the completed facts, composed exactly as the uncancelled Evidence.
|
||||
if (signal?.aborted === true)
|
||||
return err(
|
||||
new AnalysisCancelledError(request.operation, { cause: signal.reason }),
|
||||
new AnalysisCancelledError(request.operation, {
|
||||
cause: signal.reason,
|
||||
...(result.ok
|
||||
? {
|
||||
partialObservation: createAnalysisExecutionEvidence(
|
||||
request.operation,
|
||||
request.input,
|
||||
result.value,
|
||||
"observed",
|
||||
),
|
||||
}
|
||||
: {}),
|
||||
}),
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -371,6 +371,48 @@ it.each(["close", "elf", "pool", "cleanup"] as const)(
|
||||
},
|
||||
);
|
||||
|
||||
it.each(["identify_flutter_build", "inspect_dart_aot"] as const)(
|
||||
"%s keeps completed Evidence when cancelled during successful snapshot removal",
|
||||
async (operation) => {
|
||||
const path = await buildApk("removal-cancellation.apk", [
|
||||
{ path: "lib/arm64-v8a/libapp.so", bytes: aotImage() },
|
||||
{ path: "lib/arm64-v8a/libflutter.so", bytes: libflutter() },
|
||||
]);
|
||||
const controlProvider = new FlutterBuildProvider();
|
||||
onTestFinished(() => controlProvider.close());
|
||||
const control = await new FlutterBuildAnalysisService(
|
||||
controlProvider,
|
||||
).execute(operation, { path });
|
||||
if (!control.ok) throw control.error;
|
||||
|
||||
const controller = new AbortController();
|
||||
const reason = new Error("caller interrupted snapshot removal");
|
||||
const removedRoots: string[] = [];
|
||||
const provider = new FlutterBuildProvider({
|
||||
removeRoot: async (root) => {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
removedRoots.push(root);
|
||||
controller.abort(reason);
|
||||
},
|
||||
});
|
||||
const result = await new FlutterBuildAnalysisService(provider).execute(
|
||||
operation,
|
||||
{ path },
|
||||
{ signal: controller.signal },
|
||||
);
|
||||
if (result.ok) throw new Error("cancellation must prevent success");
|
||||
expect(result.error).toBeInstanceOf(AnalysisCancelledError);
|
||||
expect(result.error.cause).toBe(reason);
|
||||
expect(result.error.partialObservation).toEqual(control.value);
|
||||
expect(removedRoots).toHaveLength(1);
|
||||
await expect(stat(removedRoots[0]!)).rejects.toMatchObject({
|
||||
code: "ENOENT",
|
||||
});
|
||||
await provider.close();
|
||||
expect(removedRoots).toHaveLength(1);
|
||||
},
|
||||
);
|
||||
|
||||
it("projects each payload shape through the owned snapshot", async () => {
|
||||
const hash = REFERENCE_HASH;
|
||||
const agreeBoth = await buildApk("agree.apk", [
|
||||
|
||||
Reference in New Issue
Block a user