diff --git a/docs/javascript-artifact-reconstruction.md b/docs/javascript-artifact-reconstruction.md index df14f67e4..9d61a7b1d 100644 --- a/docs/javascript-artifact-reconstruction.md +++ b/docs/javascript-artifact-reconstruction.md @@ -50,6 +50,12 @@ The equivalent MCP input is: `input_path` must be absolute. `format` accepts `auto`, `asar`, or `directory`. `integrity_policy` accepts `fail` or `record-and-continue`; both CLI routes and the MCP tool default to `fail`. +Evidence parameters retain the caller-selected `input_path`, including a +symlink alias. Both CLI routes anchor relative inputs to their working +directory without resolving symlinks. The subject and normalized result use +the resolved path for artifact identity; the selected path remains a separate +request fact. Older Evidence can omit this parameter; retained records are +validated without resolving their paths against the live filesystem. The result retains the canonical local path, root artifact digest, artifact manifest and graph commitments, JavaScript Application Graph, static Electron summary, reconstruction statistics, and explicit limitations. It does diff --git a/scripts/lib/javascript-application-paths.d.mts b/scripts/lib/javascript-application-paths.d.mts new file mode 100644 index 000000000..4181c5b81 --- /dev/null +++ b/scripts/lib/javascript-application-paths.d.mts @@ -0,0 +1,8 @@ +export function assertJavaScriptApplicationInputPaths( + evidence: { + readonly parameters: Readonly>; + readonly subject: { readonly local_path: string } | null; + readonly normalized_result: { readonly input_path: string }; + }, + inputPath: string, +): Promise; diff --git a/scripts/lib/javascript-application-paths.mjs b/scripts/lib/javascript-application-paths.mjs new file mode 100644 index 000000000..0a3926df5 --- /dev/null +++ b/scripts/lib/javascript-application-paths.mjs @@ -0,0 +1,25 @@ +import assert from "node:assert/strict"; +import { realpath } from "node:fs/promises"; + +/** Verify caller provenance separately from the resolved artifact identity. */ +export const assertJavaScriptApplicationInputPaths = async ( + evidence, + inputPath, +) => { + const canonicalPath = await realpath(inputPath); + assert.equal( + evidence.parameters.input_path, + inputPath, + "JavaScript Evidence must retain the caller's input_path", + ); + assert.equal( + evidence.subject?.local_path, + canonicalPath, + "JavaScript Evidence subject must identify the resolved artifact", + ); + assert.equal( + evidence.normalized_result.input_path, + canonicalPath, + "JavaScript analysis result must identify the resolved artifact", + ); +}; diff --git a/scripts/verify-claude-client.mjs b/scripts/verify-claude-client.mjs index 2c0d79bc0..9a7298920 100644 --- a/scripts/verify-claude-client.mjs +++ b/scripts/verify-claude-client.mjs @@ -11,6 +11,7 @@ import { toolContract, } from "../dist/contracts/toolContracts.js"; import { exec } from "./lib/verify-package-core.mjs"; +import { assertJavaScriptApplicationInputPaths } from "./lib/javascript-application-paths.mjs"; import { expandMcpSessionStatus, sortVerifierNames, @@ -210,7 +211,7 @@ const fixture = await createAnthropicModelFixture({ const evidence = toolContract( "analyze_javascript_application", ).outputSchema.parse(observed); - assert.equal(evidence.subject.local_path, target); + await assertJavaScriptApplicationInputPaths(evidence, target); const shape = evidence.normalized_result.graph.nodes .flatMap((n) => n.observations) .map((o) => o.properties) diff --git a/scripts/verify-commandcode-client.mjs b/scripts/verify-commandcode-client.mjs index 71e97deab..50ba09eac 100644 --- a/scripts/verify-commandcode-client.mjs +++ b/scripts/verify-commandcode-client.mjs @@ -10,6 +10,7 @@ import { toolContract, } from "../dist/contracts/toolContracts.js"; import { exec } from "./lib/verify-package-core.mjs"; +import { assertJavaScriptApplicationInputPaths } from "./lib/javascript-application-paths.mjs"; import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs"; import { createOpenAiModelFixture } from "./verify/clients/model-fixture.mjs"; @@ -228,7 +229,7 @@ const fixture = await createOpenAiModelFixture({ const evidence = toolContract( "analyze_javascript_application", ).outputSchema.parse(JSON.parse(last)); - assert.equal(evidence.subject.local_path, target); + await assertJavaScriptApplicationInputPaths(evidence, target); const shapes = evidence.normalized_result.graph.nodes .flatMap((node) => node.observations) .map((observation) => observation.properties) diff --git a/scripts/verify-copilot-client.mjs b/scripts/verify-copilot-client.mjs index 98ed62efb..a2b0e5a62 100644 --- a/scripts/verify-copilot-client.mjs +++ b/scripts/verify-copilot-client.mjs @@ -10,6 +10,7 @@ import { toolContract, } from "../dist/contracts/toolContracts.js"; import { exec } from "./lib/verify-package-core.mjs"; +import { assertJavaScriptApplicationInputPaths } from "./lib/javascript-application-paths.mjs"; import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs"; import { createOpenAiModelFixture } from "./verify/clients/model-fixture.mjs"; @@ -175,7 +176,7 @@ const fixture = await createOpenAiModelFixture({ const evidence = toolContract( "analyze_javascript_application", ).outputSchema.parse(observed); - assert.equal(evidence.subject.local_path, target); + await assertJavaScriptApplicationInputPaths(evidence, target); const shape = evidence.normalized_result.graph.nodes .flatMap((n) => n.observations) .map((o) => o.properties) diff --git a/scripts/verify-deepseek-client.mjs b/scripts/verify-deepseek-client.mjs index 42aae272b..b8d8dd740 100644 --- a/scripts/verify-deepseek-client.mjs +++ b/scripts/verify-deepseek-client.mjs @@ -11,6 +11,7 @@ import { toolContract, } from "../dist/contracts/toolContracts.js"; import { exec, pathExists } from "./lib/verify-package-core.mjs"; +import { assertJavaScriptApplicationInputPaths } from "./lib/javascript-application-paths.mjs"; import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs"; import { createOpenAiModelFixture } from "./verify/clients/model-fixture.mjs"; @@ -183,7 +184,7 @@ const fixture = await createOpenAiModelFixture({ const evidence = toolContract( "analyze_javascript_application", ).outputSchema.parse(found); - assert.equal(evidence.subject.local_path, target); + await assertJavaScriptApplicationInputPaths(evidence, target); const shape = evidence.normalized_result.graph.nodes .flatMap((node) => node.observations) .map((observation) => observation.properties) diff --git a/scripts/verify-gemini-client.mjs b/scripts/verify-gemini-client.mjs index 52c9aee08..97b099545 100644 --- a/scripts/verify-gemini-client.mjs +++ b/scripts/verify-gemini-client.mjs @@ -15,6 +15,7 @@ import { toolContract, } from "../dist/contracts/toolContracts.js"; import { exec } from "./lib/verify-package-core.mjs"; +import { assertJavaScriptApplicationInputPaths } from "./lib/javascript-application-paths.mjs"; import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs"; import { createGeminiModelFixture } from "./verify/clients/gemini-model-fixture.mjs"; @@ -307,7 +308,7 @@ const fixture = await createGeminiModelFixture({ const evidence = toolContract( "analyze_javascript_application", ).outputSchema.parse(observed); - assert.equal(evidence.subject.local_path, target); + await assertJavaScriptApplicationInputPaths(evidence, target); const shape = evidence.normalized_result.graph.nodes .flatMap((n) => n.observations) .map((o) => o.properties) diff --git a/scripts/verify-grok-client.mjs b/scripts/verify-grok-client.mjs index e1c066770..befe2de8e 100644 --- a/scripts/verify-grok-client.mjs +++ b/scripts/verify-grok-client.mjs @@ -10,6 +10,7 @@ import { toolContract, } from "../dist/contracts/toolContracts.js"; import { exec } from "./lib/verify-package-core.mjs"; +import { assertJavaScriptApplicationInputPaths } from "./lib/javascript-application-paths.mjs"; import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs"; import { createOpenAiModelFixture } from "./verify/clients/model-fixture.mjs"; @@ -121,11 +122,11 @@ const call = (name, args) => { ], }; }; -const verifyEvidence = (bytes) => { +const verifyEvidence = async (bytes) => { const evidence = toolContract( "analyze_javascript_application", ).outputSchema.parse(JSON.parse(bytes.toString("utf8"))); - assert.equal(evidence.subject.local_path, target); + await assertJavaScriptApplicationInputPaths(evidence, target); const shapes = evidence.normalized_result.graph.nodes .flatMap((node) => node.observations) .map((observation) => observation.properties) @@ -291,7 +292,7 @@ const fixture = await createOpenAiModelFixture({ const marker = "Full output written to: "; const start = last.indexOf(marker); if (start < 0) { - verifyEvidence(Buffer.from(last, "utf8")); + await verifyEvidence(Buffer.from(last, "utf8")); finalSeen = true; return { role: "assistant", content: "REA_CLIENT_COMPATIBILITY_OK" }; } @@ -310,7 +311,7 @@ const fixture = await createOpenAiModelFixture({ "Unexpected native MCP spill path", ); retainedArtifact = file; - expectedProjection = verifyEvidence(await readFile(file)); + expectedProjection = await verifyEvidence(await readFile(file)); spillQueryRequested = true; return call("run_terminal_command", { command: [process.execPath, "-e", recoveryCode, file] diff --git a/scripts/verify-native-agent-client.mjs b/scripts/verify-native-agent-client.mjs index af2ab0cec..d4ba0c8b4 100644 --- a/scripts/verify-native-agent-client.mjs +++ b/scripts/verify-native-agent-client.mjs @@ -9,6 +9,7 @@ import { toolContract, } from "../dist/contracts/toolContracts.js"; import { exec } from "./lib/verify-package-core.mjs"; +import { assertJavaScriptApplicationInputPaths } from "./lib/javascript-application-paths.mjs"; import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs"; import { createOpenAiModelFixture } from "./verify/clients/model-fixture.mjs"; @@ -169,7 +170,7 @@ const validateEvidence = async (value) => { const evidence = toolContract( "analyze_javascript_application", ).outputSchema.parse(value); - assert.equal(evidence.subject.local_path, target); + await assertJavaScriptApplicationInputPaths(evidence, target); const shape = evidence.normalized_result.graph.nodes .flatMap((n) => n.observations) .map((o) => o.properties) diff --git a/scripts/verify-omp-client.mjs b/scripts/verify-omp-client.mjs index ccf9f2ad0..f107ce9b0 100644 --- a/scripts/verify-omp-client.mjs +++ b/scripts/verify-omp-client.mjs @@ -9,6 +9,7 @@ import { toolContract, } from "../dist/contracts/toolContracts.js"; import { exec } from "./lib/verify-package-core.mjs"; +import { assertJavaScriptApplicationInputPaths } from "./lib/javascript-application-paths.mjs"; import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs"; import { createOpenAiModelFixture } from "./verify/clients/model-fixture.mjs"; @@ -156,7 +157,7 @@ const fixture = await createOpenAiModelFixture({ const evidence = toolContract( "analyze_javascript_application", ).outputSchema.parse(JSON.parse(evidenceReply.content)); - assert.equal(evidence.subject.local_path, target); + await assertJavaScriptApplicationInputPaths(evidence, target); const shape = evidence.normalized_result.graph.nodes .flatMap((node) => node.observations) .map((observation) => observation.properties) diff --git a/scripts/verify-opencode-client.mjs b/scripts/verify-opencode-client.mjs index b1006f8ba..2a3e4c050 100644 --- a/scripts/verify-opencode-client.mjs +++ b/scripts/verify-opencode-client.mjs @@ -10,6 +10,7 @@ import { toolContract, } from "../dist/contracts/toolContracts.js"; import { exec } from "./lib/verify-package-core.mjs"; +import { assertJavaScriptApplicationInputPaths } from "./lib/javascript-application-paths.mjs"; import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs"; import { createOpenAiModelFixture } from "./verify/clients/model-fixture.mjs"; @@ -167,7 +168,7 @@ const fixture = await createOpenAiModelFixture({ const evidence = toolContract( "analyze_javascript_application", ).outputSchema.parse(observed); - assert.equal(evidence.subject.local_path, target); + await assertJavaScriptApplicationInputPaths(evidence, target); const shape = evidence.normalized_result.graph.nodes .flatMap((n) => n.observations) .map((o) => o.properties) diff --git a/scripts/verify-qwen-client.mjs b/scripts/verify-qwen-client.mjs index fff4338b7..50001103c 100644 --- a/scripts/verify-qwen-client.mjs +++ b/scripts/verify-qwen-client.mjs @@ -12,6 +12,7 @@ import { join, resolve, sep } from "node:path"; import { fileURLToPath } from "node:url"; import { createHash } from "node:crypto"; import { exec } from "./lib/verify-package-core.mjs"; +import { assertJavaScriptApplicationInputPaths } from "./lib/javascript-application-paths.mjs"; import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs"; import { TOOL_CONTRACTS, @@ -195,7 +196,7 @@ const fixture = await createOpenAiModelFixture({ "analyze_javascript_application", ).outputSchema.parse(JSON.parse(artifact.toString("utf8"))); assert(extractEvidence(evidence)); - assert.equal(evidence.subject.local_path, target); + await assertJavaScriptApplicationInputPaths(evidence, target); const shape = evidence.normalized_result.graph.nodes .flatMap((n) => n.observations) .map((o) => o.properties) diff --git a/src/application/javascript/JavaScriptApplicationEvidence.ts b/src/application/javascript/JavaScriptApplicationEvidence.ts index c604949ef..f90c872bd 100644 --- a/src/application/javascript/JavaScriptApplicationEvidence.ts +++ b/src/application/javascript/JavaScriptApplicationEvidence.ts @@ -89,6 +89,7 @@ const applicationObservation = ( const parameters = ( input: AnalyzeJavaScriptApplicationInput, ): EvidenceObservation["parameters"] => ({ + input_path: input.input_path, format: input.format, integrity_policy: input.integrity_policy, ...(input.max_semantic_nodes === undefined diff --git a/src/cli/electronCommands.ts b/src/cli/electronCommands.ts index 8cc6550ba..9995a4e8c 100644 --- a/src/cli/electronCommands.ts +++ b/src/cli/electronCommands.ts @@ -22,6 +22,7 @@ import { } from "../cliObservationOptions.js"; import { runCliJavaScriptApplicationAnalysis } from "./javascriptApplicationAnalysis.js"; import { withCommandCancellation } from "./commandCancellation.js"; +import { absoluteCallerPath } from "../filesystem/callerPath.js"; import { finishAnalysisCommand } from "./finishAnalysisCommand.js"; /** Register CLI equivalents of the Electron MCP tools. */ @@ -279,7 +280,7 @@ const registerJavaScriptApplicationCommand = ( logCliCommand(logger, CLI_COMMANDS.analyzeJavaScriptApplication, () => runCliJavaScriptApplicationAnalysis( { - input_path: args.path, + input_path: absoluteCallerPath(args.path), format: options.artifactFormat, integrity_policy: options.integrityPolicy, ...(options.maxSemanticNodes === undefined diff --git a/tests/acceptance/applications/javascriptAnalysisLimits.test.ts b/tests/acceptance/applications/javascriptAnalysisLimits.test.ts index b0f180402..593b89ae9 100644 --- a/tests/acceptance/applications/javascriptAnalysisLimits.test.ts +++ b/tests/acceptance/applications/javascriptAnalysisLimits.test.ts @@ -53,6 +53,7 @@ cliTest.for(cases)( expect(response.exitCode, response.stdout + response.stderr).toBe(0); const evidence = parseEvidence(response.json); expect(evidence.parameters).toEqual({ + input_path: root, format: "auto", integrity_policy: "fail", ...controls, @@ -81,6 +82,7 @@ it("preserves MCP analysis limits in retained Evidence and an exported bundle ro }) .parse(response.structuredContent).normalized_result; expected.set(parent_evidence_id, { + input_path: root, format: "auto", integrity_policy: "fail", ...controls, @@ -169,6 +171,7 @@ cliTest( (record) => record.operation === "analyze_javascript_application", ); expect(partial?.parameters).toEqual({ + input_path: root, format: "auto", integrity_policy: "fail", max_heap_mb: 128, diff --git a/tests/boundary/filesystem/javascriptClientVerifier.test.ts b/tests/boundary/filesystem/javascriptClientVerifier.test.ts new file mode 100644 index 000000000..8fef4141d --- /dev/null +++ b/tests/boundary/filesystem/javascriptClientVerifier.test.ts @@ -0,0 +1,55 @@ +import { mkdir, realpath, symlink, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { expect, it } from "vitest"; + +import { assertJavaScriptApplicationInputPaths } from "../../../scripts/lib/javascript-application-paths.mjs"; +import { javascriptApplicationAnalysisResultSchema } from "../../../src/domain/javascript/javascriptApplicationAnalysis.js"; +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; +import { analyzeJavaScriptApplication } from "../../support/javascriptApplicationScope.js"; + +it("checks caller and canonical paths independently in real JavaScript analysis Evidence", async () => { + const root = await createTestTempDirectory("rea-js-client-paths-"); + const directory = join(root, "app α测试"); + const inputPath = join(root, "selected app"); + await mkdir(directory); + await writeFile(join(directory, "main.js"), "export const observed = 7;\n"); + await symlink( + directory, + inputPath, + process.platform === "win32" ? "junction" : "dir", + ); + const canonicalPath = await realpath(inputPath); + const result = await analyzeJavaScriptApplication({ input_path: inputPath }); + if (!result.ok) throw result.error; + const evidence = { + ...result.value, + normalized_result: javascriptApplicationAnalysisResultSchema.parse( + result.value.normalized_result, + ), + }; + + await expect( + assertJavaScriptApplicationInputPaths(evidence, inputPath), + ).resolves.toBeUndefined(); + + for (const parameters of [{}, { input_path: canonicalPath }]) + await expect( + assertJavaScriptApplicationInputPaths( + { ...evidence, parameters }, + inputPath, + ), + ).rejects.toThrow("must retain the caller's input_path"); + + await expect( + assertJavaScriptApplicationInputPaths( + { ...evidence, subject: { local_path: inputPath } }, + inputPath, + ), + ).rejects.toThrow("subject must identify the resolved artifact"); + await expect( + assertJavaScriptApplicationInputPaths( + { ...evidence, normalized_result: { input_path: inputPath } }, + inputPath, + ), + ).rejects.toThrow("result must identify the resolved artifact"); +}); diff --git a/tests/boundary/javascript/javascriptApplicationCancellation.test.ts b/tests/boundary/javascript/javascriptApplicationCancellation.test.ts index cecc0db28..2283006b1 100644 --- a/tests/boundary/javascript/javascriptApplicationCancellation.test.ts +++ b/tests/boundary/javascript/javascriptApplicationCancellation.test.ts @@ -1,5 +1,5 @@ import { setImmediate } from "node:timers"; -import { mkdir, writeFile } from "node:fs/promises"; +import { mkdir, realpath, symlink, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { expect, it, onTestFinished } from "vitest"; import { z } from "zod"; @@ -16,7 +16,16 @@ import { analyzeJavaScriptApplication } from "../../support/javascriptApplicatio it.each(["during cleanup", "on the next control turn"] as const)( "retains the final source when cancellation arrives %s after analysis", async (timing) => { - const root = await createTestTempDirectory("rea-js-cancel-handoff-"); + const parent = await createTestTempDirectory("rea-js-cancel-handoff-"); + const root = join(parent, "physical"); + const selected = join(parent, "selected"); + await mkdir(root); + await symlink( + root, + selected, + process.platform === "win32" ? "junction" : "dir", + ); + const resolved = await realpath(root); await writeFile( join(root, "main.js"), 'import { readFile } from "node:fs/promises"; export { readFile };\n', @@ -35,13 +44,22 @@ it.each(["during cleanup", "on the next control turn"] as const)( const scope = new CancellingCleanupScope(); onTestFinished(() => scope.close()); const result = await analyzeOwnedApplication( - { input_path: root, format: "directory" }, + { input_path: selected, format: "directory" }, scope, { signal: controller.signal }, ); if (result.ok) throw new Error("Cancelled handoff must not succeed"); const projected = projectAnalysisError(result.error); expect(projected.code).toBe("cancelled"); + expect(projected).toMatchObject({ + details: { + partial_observation: { + parameters: { input_path: selected }, + subject: { local_path: resolved }, + normalized_result: { input_path: resolved }, + }, + }, + }); const partial = z .object({ details: z.object({ diff --git a/tests/boundary/javascript/javascriptSelectedPathEvidence.test.ts b/tests/boundary/javascript/javascriptSelectedPathEvidence.test.ts new file mode 100644 index 000000000..d5372238e --- /dev/null +++ b/tests/boundary/javascript/javascriptSelectedPathEvidence.test.ts @@ -0,0 +1,142 @@ +import { + mkdir, + readFile, + realpath, + rm, + symlink, + writeFile, +} from "node:fs/promises"; +import { join } from "node:path"; +import { expect } from "vitest"; + +import { parseEvidence } from "../../../src/domain/evidence.js"; +import { parseEvidenceBundle } from "../../../src/domain/evidenceBundle.js"; +import { parseApplicationGraphEvidence } from "../../../src/application/javascript/JavaScriptApplicationEvidenceGraph.js"; +import { JAVASCRIPT_APPLICATION_EVIDENCE_EXAMPLE } from "../../../src/contracts/javascript/javascriptRuntimeReconciliationExample.js"; +import { connectLocalToolsMcp } from "../../fixtures/localToolsMcp.js"; +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; +import { cliTest } from "../../support/cli/cliFixture.js"; + +cliTest( + "retains selected JavaScript path separately from resolved identity", + async ({ cli }) => { + const root = await createTestTempDirectory("rea-js-selected-path-"); + const physicalPath = join(root, "physical-app"); + const selectedPath = join(root, "selected-app"); + await mkdir(physicalPath); + await writeFile(join(physicalPath, "main.js"), "export const value = 7;\n"); + await symlink( + physicalPath, + selectedPath, + process.platform === "win32" ? "junction" : "dir", + ); + const resolvedPath = await realpath(physicalPath); + expect(await realpath(selectedPath)).toBe(resolvedPath); + + const cliResponse = await cli.run({ + arguments: [ + "analyze-javascript-application", + selectedPath, + "--artifact-format", + "directory", + "--json", + ], + environment: { REA_LOG_LEVEL: "silent" }, + }); + expect(cliResponse.exitCode, cliResponse.stderr).toBe(0); + const cliEvidence = parseEvidence(cliResponse.json); + + for (const command of ["analyze", "analyze-javascript-application"]) { + const relativeResponse = await cli.run({ + arguments: [command, "./selected-app", "--json"], + cwd: root, + environment: { REA_LOG_LEVEL: "silent" }, + }); + expect(relativeResponse.exitCode, relativeResponse.stderr).toBe(0); + const relativeEvidence = parseEvidence(relativeResponse.json); + expect(relativeEvidence.parameters.input_path, command).toBe( + selectedPath, + ); + expect(relativeEvidence.subject).toEqual(cliEvidence.subject); + expect(relativeEvidence.normalized_result).toEqual( + cliEvidence.normalized_result, + ); + } + + const { call } = await connectLocalToolsMcp({ REA_LOG_LEVEL: "silent" }); + const aliasResponse = await call("analyze_javascript_application", { + input_path: selectedPath, + format: "directory", + integrity_policy: "fail", + }); + expect( + aliasResponse.isError, + JSON.stringify(aliasResponse.content), + ).not.toBe(true); + const aliasEvidence = parseEvidence(aliasResponse.structuredContent); + const physicalResponse = await call("analyze_javascript_application", { + input_path: physicalPath, + format: "directory", + integrity_policy: "fail", + }); + expect( + physicalResponse.isError, + JSON.stringify(physicalResponse.content), + ).not.toBe(true); + const physicalEvidence = parseEvidence(physicalResponse.structuredContent); + + for (const evidence of [cliEvidence, aliasEvidence]) { + expect(evidence.parameters.input_path).toBe(selectedPath); + expect(evidence.subject?.local_path).toBe(resolvedPath); + expect(evidence.normalized_result).toMatchObject({ + input_path: resolvedPath, + }); + expect(parseApplicationGraphEvidence(evidence).ok).toBe(true); + } + expect(cliEvidence.normalized_result).toEqual( + aliasEvidence.normalized_result, + ); + expect(physicalEvidence.parameters.input_path).toBe(physicalPath); + expect(physicalEvidence.subject?.local_path).toBe(resolvedPath); + expect(physicalEvidence.normalized_result).toEqual( + aliasEvidence.normalized_result, + ); + expect(physicalEvidence.subject?.digest.sha256).toBe( + aliasEvidence.subject?.digest.sha256, + ); + expect(physicalEvidence.evidence_id).not.toBe(aliasEvidence.evidence_id); + const bundlePath = join(root, "retained-evidence.json"); + expect( + (await call("export_evidence_bundle", { path: bundlePath })).isError, + ).not.toBe(true); + await rm(selectedPath); + await rm(physicalPath, { recursive: true }); + const bundle = parseEvidenceBundle( + JSON.parse(await readFile(bundlePath, "utf8")), + ); + const retained = bundle.records.find( + (record) => record.evidence_id === aliasEvidence.evidence_id, + ); + expect(retained?.parameters.input_path).toBe(selectedPath); + for (const evidence of [ + cliEvidence, + aliasEvidence, + physicalEvidence, + retained, + ]) + expect( + parseApplicationGraphEvidence(JSON.parse(JSON.stringify(evidence))).ok, + ).toBe(true); + expect((await call("close_binary", {})).isError).not.toBe(true); + expect( + (await call("import_evidence_bundle", { path: bundlePath })).isError, + ).not.toBe(true); + + expect( + JAVASCRIPT_APPLICATION_EVIDENCE_EXAMPLE.parameters, + ).not.toHaveProperty("input_path"); + expect( + parseApplicationGraphEvidence(JAVASCRIPT_APPLICATION_EVIDENCE_EXAMPLE).ok, + ).toBe(true); + }, +);