chore: integrate registration and native capture clock fixes

This commit is contained in:
gensokyo-dev committed 2026-10-19 17:31:03 +00:00
commit 6892c8bc52
24 files changed
+420 -62

No files matched your search

@@ -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
@@ -0,0 +1,8 @@
export function assertJavaScriptApplicationInputPaths(
evidence: {
readonly parameters: Readonly<Record<string, unknown>>;
readonly subject: { readonly local_path: string } | null;
readonly normalized_result: { readonly input_path: string };
},
inputPath: string,
): Promise<void>;
@@ -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",
);
};
+2 -1
View File
@@ -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)
+2 -1
View File
@@ -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)
+2 -1
View File
@@ -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)
+2 -1
View File
@@ -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)
+2 -1
View File
@@ -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)
+5 -4
View File
@@ -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]
+2 -1
View File
@@ -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)
+2 -1
View File
@@ -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)
+2 -1
View File
@@ -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)
+2 -1
View File
@@ -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)
@@ -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
+2 -1
View File
@@ -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
@@ -3,6 +3,7 @@ import {
cleanupOwnedProcessGroup,
verifyNoTokenOwnedProcesses,
type ProcessOwnershipHost,
type ProcessRunTokenObservation,
type ProcessTableEntry,
} from "./ProcessOwnership.js";
import { ProcessOwnershipEvidenceOwner } from "./ProcessOwnershipEvidence.js";
@@ -109,6 +110,108 @@ it("waits for native absence after one signal and does not signal again", async
expect(fixture.signalGroups).toEqual([100]);
});
it("finishes the public cleanup sweep when the application timer is unavailable", async () => {
const unrelated: ProcessTableEntry = {
pid: 200,
parentPid: 1,
processGroupId: 200,
state: "S",
command: "transient-token-process",
};
let signaled = false;
let postSignalSnapshots = 0;
let unrelatedTokenReads = 0;
const fixture = makeHost(() => {
signaled = true;
fixture.replaceRows([unrelated]);
});
fixture.replaceRows([...initialRows, unrelated]);
const host: ProcessOwnershipHost = {
...fixture.host,
listProcesses: async () => {
if (!signaled) return fixture.rows();
postSignalSnapshots += 1;
if (postSignalSnapshots === 1) return fixture.rows();
fixture.replaceRows([]);
return [];
},
runTokens: async (entries) =>
new Map<number, ProcessRunTokenObservation>(
entries.map(({ pid }) => {
if (pid !== unrelated.pid)
return [pid, { state: "readable", runId: "sweep-run" }] as const;
if (!signaled)
return [pid, { state: "readable", runId: undefined }] as const;
unrelatedTokenReads += 1;
return [pid, { state: "readable", runId: "sweep-run" }] as const;
}),
),
};
const applicationTimer = vi
.spyOn(globalThis, "setTimeout")
.mockImplementation(() => {
throw new Error("application timer is unavailable");
});
try {
await expect(cleanupOwnedProcessGroup(ownership(), host)).resolves.toEqual({
cleaned: true,
signaled: true,
});
expect(unrelatedTokenReads).toBeGreaterThan(0);
expect(postSignalSnapshots).toBe(2);
expect(fixture.signalGroups).toEqual([100]);
expect(applicationTimer).not.toHaveBeenCalled();
} finally {
applicationTimer.mockRestore();
}
});
it("keeps an unreadable live group member fail-closed when the application timer is unavailable", async () => {
const fixture = makeHost(() => undefined);
let unreadableReads = 0;
const host: ProcessOwnershipHost = {
...fixture.host,
runTokens: async (entries) =>
new Map<number, ProcessRunTokenObservation>(
entries.map(({ pid }) => {
if (pid === 101) {
unreadableReads += 1;
return [
pid,
{
state: "unavailable",
reason: "EACCES: fixture permission denied",
},
] as const;
}
return [pid, { state: "readable", runId: "sweep-run" }] as const;
}),
),
};
const applicationTimer = vi
.spyOn(globalThis, "setTimeout")
.mockImplementation(() => {
throw new Error("application timer is unavailable");
});
try {
await expect(
cleanupOwnedProcessGroup(ownership(), host),
).resolves.toMatchObject({
cleaned: false,
reason: expect.stringContaining(
"process ownership token could not be read",
),
});
expect(unreadableReads).toBeGreaterThan(1);
expect(fixture.signalGroups).toEqual([]);
expect(applicationTimer).not.toHaveBeenCalled();
} finally {
applicationTimer.mockRestore();
}
});
it("preserves a moved same-generation helper as unresolved through the final deadline", async () => {
const child = initialRows[1];
if (child === undefined) throw new Error("fixture child is missing");
@@ -20,41 +20,6 @@ const observedIdentity = (entry: ProcessTableEntry, identity: string) => ({
},
});
it.each([null, "baseline-start"])(
"accepts native absence for a stale process-table row with baseline identity %s",
async (identity) => {
const row: ProcessTableEntry = {
pid: 908,
parentPid: 1,
processGroupId: 908,
state: "S",
command: "exited-process-table-helper",
};
const signalGroup = vi.fn();
const host: ProcessOwnershipHost = {
listProcesses: () => Promise.resolve([row]),
environment: () => Promise.resolve({}),
processIdentities: (entries) =>
Promise.resolve(
new Map(
entries.map(({ pid }) => [
pid,
{ state: "absent", reason: "not-in-snapshot" } as const,
]),
),
),
signalGroup,
};
await expect(
verifyNoTokenOwnedProcesses("run-token", host, [
{ pid: row.pid, identity },
]),
).resolves.toEqual({ cleaned: true, signaled: false });
expect(signalGroup).not.toHaveBeenCalled();
},
);
it.each([undefined, "run-token"])(
"validates a current generation with token %s when its baseline identity was unknown",
async (runId) => {
@@ -289,7 +289,7 @@ export const settleProcessCaptureJournal = async (
let observed = journal.length;
let lastChangeAt = monotonicProcessCaptureClock();
while (monotonicProcessCaptureClock() < deadline) {
await new Promise<void>((resolve) => setTimeout(resolve, 4));
await delay(4);
// A stalled event loop can leave timer and poll callbacks that were already
// due queued behind this wake-up. Drain them before judging silence so a
// trailing observation is not dropped by an early settle.
@@ -257,6 +257,22 @@ it("preserves the actionable Darwin ownership compiler prerequisite", () => {
});
});
it("settles the capture journal when the application timer is unavailable", async () => {
const applicationTimer = vi
.spyOn(globalThis, "setTimeout")
.mockImplementation(() => {
throw new Error("application timer is unavailable");
});
try {
await expect(
settleProcessCaptureJournal([], 0, 50),
).resolves.toBeUndefined();
expect(applicationTimer).not.toHaveBeenCalled();
} finally {
applicationTimer.mockRestore();
}
});
it("waits for terminal observations delivered after the exit callback", async () => {
const journal: Array<{
capture_order: number;
@@ -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,
@@ -11,6 +11,14 @@ import {
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { cliTest } from "../../support/cli/cliFixture.js";
// Registration configuration tests must not discover installed providers or
// plan provider-environment edits based on the machine running the suite.
const missingHopperAndGhidraEnvironment = (home: string) => ({
HOPPER_LAUNCHER_PATH: join(home, "missing-hopper"),
GHIDRA_INSTALL_DIR: join(home, "missing-ghidra"),
JAVA_HOME: join(home, "missing-java"),
});
const scenarios = [
{
client: "cursor",
@@ -75,7 +83,7 @@ cliTest.for([
USERPROFILE: home,
XDG_CONFIG_HOME: join(home, ".config"),
DSH_HOME: join(home, ".dsh"),
HOPPER_LAUNCHER_PATH: join(home, "missing-hopper"),
...missingHopperAndGhidraEnvironment(home),
};
const registration = (env: Readonly<Record<string, string>>) => {
const entry = clientRegistrationEntry(
@@ -194,6 +202,7 @@ for (const scenario of scenarios)
USERPROFILE: home,
XDG_CONFIG_HOME: join(home, ".config"),
DSH_HOME: join(home, ".dsh"),
...missingHopperAndGhidraEnvironment(home),
};
const registration = (env: Readonly<Record<string, string>>) => {
const server = {
@@ -324,11 +333,7 @@ cliTest(
HOME: home,
USERPROFILE: home,
XDG_CONFIG_HOME: join(home, ".config"),
// Select absent provider locations so installed tools cannot make this
// configuration merge test plan an unrelated provider-environment edit.
HOPPER_LAUNCHER_PATH: join(home, "missing-hopper"),
GHIDRA_INSTALL_DIR: join(home, "missing-ghidra"),
JAVA_HOME: join(home, "missing-java"),
...missingHopperAndGhidraEnvironment(home),
};
const run = (args: readonly string[]) =>
cli.run({ arguments: [...args, "--json"], cwd: home, environment });
@@ -405,7 +410,7 @@ cliTest(
XDG_CONFIG_HOME: join(home, ".config"),
CODEX_HOME: codexHome,
KIMI_CODE_HOME: kimiHome,
HOPPER_LAUNCHER_PATH: join(home, "missing-hopper"),
...missingHopperAndGhidraEnvironment(home),
REA_ANALYSIS_PROVIDER: "auto",
REA_GHIDRA_STARTUP_TIMEOUT_MS: "2700001",
REA_HOPPER_STARTUP_TIMEOUT_MS: "2800002",
@@ -503,7 +508,7 @@ for (const [client, provider] of [
XDG_CONFIG_HOME: join(home, ".config"),
CODEX_HOME: clientHome,
KIMI_CODE_HOME: clientHome,
HOPPER_LAUNCHER_PATH: join(home, "missing-hopper"),
...missingHopperAndGhidraEnvironment(home),
};
const setupArguments = [
"setup",
@@ -628,7 +633,7 @@ for (const invalidSetting of [
USERPROFILE: home,
XDG_CONFIG_HOME: join(home, ".config"),
KIMI_CODE_HOME: kimiHome,
HOPPER_LAUNCHER_PATH: join(home, "missing-hopper"),
...missingHopperAndGhidraEnvironment(home),
[invalidSetting.setting]: invalidSetting.value,
};
const result = await cli.run({
@@ -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");
});
@@ -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({
@@ -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);
},
);