diff --git a/src/cli/javascriptApplicationAnalysis.ts b/src/cli/javascriptApplicationAnalysis.ts index 06a6be6612..d26bdbabbb 100644 --- a/src/cli/javascriptApplicationAnalysis.ts +++ b/src/cli/javascriptApplicationAnalysis.ts @@ -5,6 +5,7 @@ import { createProgressReporter } from "../application/ProgressReporter.js"; import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js"; import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js"; import { analysisErrorWithCleanupFailure } from "../domain/analysisErrorCleanup.js"; +import { completeProviderEvidence } from "../application/ProviderOperationExecution.js"; import { projectAnalysisError } from "../domain/analysisErrorProjection.js"; import type { JsonValue } from "../domain/jsonValue.js"; import { ProviderCleanupError } from "../domain/providerCleanupError.js"; @@ -133,19 +134,26 @@ export const runCliJavaScriptApplicationAnalysis = async ( { reason: cleanup.reason }, { cause, operation: "analyze_javascript_application" }, ); + const completed = result.ok + ? completeProviderEvidence(result.value, signal) + : result; + if (completed.ok) failure.retainPartialObservation(completed.value); return cliError( - result.ok + completed.ok ? failure : analysisErrorWithCleanupFailure( - result.error, + completed.error, failure, "analyze_javascript_application", ), ); } - const finalized = result.ok - ? result - : { ok: false as const, error: cliError(result.error) }; + const completed = result.ok + ? completeProviderEvidence(result.value, signal) + : result; + const finalized = completed.ok + ? completed + : { ok: false as const, error: cliError(completed.error) }; return delivery === undefined ? finalized.ok ? finalized.value diff --git a/tests/boundary/cli/cliJavaScriptResourceClose.test.ts b/tests/boundary/cli/cliJavaScriptResourceClose.test.ts index 90644a7001..a3410760eb 100644 --- a/tests/boundary/cli/cliJavaScriptResourceClose.test.ts +++ b/tests/boundary/cli/cliJavaScriptResourceClose.test.ts @@ -1,10 +1,14 @@ -import { afterEach, expect, it } from "vitest"; +import { writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { afterEach, expect, it, vi } from "vitest"; import { ArtifactResourceScope } from "../../../src/artifacts/ArtifactResourceScope.js"; import { closeCliJavaScriptResources, + CliJavaScriptAnalysisHost, runCliJavaScriptApplicationAnalysis, } from "../../../src/cli/javascriptApplicationAnalysis.js"; +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; const originalClose = ArtifactResourceScope.prototype.close; @@ -17,6 +21,80 @@ afterEach(async () => { const openCliScope = (): Promise => runCliJavaScriptApplicationAnalysis({}, {}); +it.each([false, true])( + "retains completed JavaScript Evidence when final cleanup fails (cancelled=%s)", + async (cancelled) => { + const root = await createTestTempDirectory("cli-js-cleanup-evidence-"); + await writeFile(join(root, "index.js"), "export const answer = 42;\n"); + const host = new CliJavaScriptAnalysisHost(); + const controller = new AbortController(); + const retry = vi + .spyOn(host.scope(), "retryCleanup") + .mockImplementationOnce(async () => { + if (cancelled) controller.abort("stop after analysis"); + throw new Error("cleanup failed"); + }); + try { + const result = await runCliJavaScriptApplicationAnalysis( + { input_path: root }, + {}, + controller.signal, + undefined, + host, + ); + expect(result).toMatchObject({ + code: "cleanup_incomplete", + details: { + partial_observation: { + operation: "analyze_javascript_application", + normalized_result: { input_path: root }, + }, + }, + }); + if (cancelled) + expect(result).toMatchObject({ + details: { diagnostics: { primary_error: { code: "cancelled" } } }, + }); + } finally { + retry.mockRestore(); + await host.close(); + } + }, +); + +it("retains completed JavaScript Evidence when cancellation arrives during final cleanup", async () => { + const root = await createTestTempDirectory("cli-js-cancel-evidence-"); + await writeFile(join(root, "index.js"), "export const answer = 42;\n"); + const host = new CliJavaScriptAnalysisHost(); + const controller = new AbortController(); + const retry = vi + .spyOn(host.scope(), "retryCleanup") + .mockImplementationOnce(async () => { + controller.abort("stop after analysis"); + }); + try { + const result = await runCliJavaScriptApplicationAnalysis( + { input_path: root }, + {}, + controller.signal, + undefined, + host, + ); + expect(result).toMatchObject({ + code: "cancelled", + details: { + partial_observation: { + operation: "analyze_javascript_application", + normalized_result: { input_path: root }, + }, + }, + }); + } finally { + retry.mockRestore(); + await host.close(); + } +}); + it("retries JavaScript worker shutdown when the first close fails", async () => { let calls = 0; ArtifactResourceScope.prototype.close = function (