chore: integrate main registration isolation and native lifecycle clocks
Retain main’s provider-independent registration fixtures and stronger native lifecycle controls. The earlier shared-host EACCES failure remains recorded; no permission error is suppressed. Assisted-by: Reimu (gensokyo-skills:reimu-incident-triage)
This commit is contained in:
commit
2a42096e15
24 files changed
+421
-28
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",
|
||||
);
|
||||
};
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import { performance } from "node:perf_hooks";
|
||||
import { promisify } from "node:util";
|
||||
import { setTimeout as delay } from "node:timers/promises";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
import {
|
||||
errorMessage,
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
);
|
||||
Reference in new issue
Block a user