Files
rea/tests/acceptance/tools/inspectorFileUrlIdentity.test.ts
Kaoru0822andClaude Opus 5.5 710fad8acf feat(evidence): discover missing Evidence links without transferring records (#8633)
* feat(evidence): discover missing Evidence links without transferring records

An agent could only learn that a retained record's source was absent by reading
complete records or by having a downstream analysis refuse. The summary said
nothing about links, and complete-record retention was easy to mistake for
source closure.

- Add detail: links to get_evidence_bundle and CLI inspect-evidence-bundle:
  paged link rows with retained or missing status and no record payloads, for
  selected records against the whole session or file.
- Add retained-closure traversal, the walk composed workflows require, reporting
  complete or incomplete. Direct reads make no closure claim.
- Count direct and missing direct links in the summary.

Fixes #8454

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(evidence): count a repeated Evidence link once in every delivery

The Evidence schema does not deduplicate evidence_links, so the summary counted
a repeated link twice while link rows and counts reported it once. Both now read
one helper for a record's distinct direct links.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(evidence): validate link delivery against the advertised schemas

The test never called tools/list, so neither the shared validator nor the MCP
client checked its replies against what each profile advertises. A schema that
rejected the links result could have passed. Fetch the advertised definition and
validate every request and reply against it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 05:14:34 -08:00

135 lines
5.1 KiB
TypeScript

import { evidenceBundleSchema } from "../../../src/domain/evidenceBundle.js";
import { spawn } from "node:child_process";
import { once } from "node:events";
import { readFile, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { pathToFileURL } from "node:url";
import { expect, it, onTestFinished } from "vitest";
import { toolContract } from "../../../src/contracts/toolContracts.js";
import { connectLocalToolsMcp } from "../../fixtures/localToolsMcp.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
it("preserves file URL scheme spelling and script identity after real Inspector state changes", async () => {
const root = await createTestTempDirectory("rea-inspector-file-url-");
const source = join(root, "source #参数%.cjs");
const runner = join(root, "runner.cjs");
const sourceText = "globalThis.fileUrlProbe = true;\n";
await writeFile(source, sourceText);
await writeFile(
runner,
`
const { readFileSync } = require("node:fs");
const vm = require("node:vm");
const scripts = [];
process.on("message", (urls) => {
for (const filename of urls) {
const script = new vm.Script(readFileSync(process.argv[2], "utf8"), { filename });
scripts.push(script);
script.runInThisContext();
}
process.send({ loaded: scripts.length });
});
setInterval(() => {}, 1000);
`,
);
const child = spawn(
process.execPath,
["--inspect=127.0.0.1:0", runner, source],
{
stdio: ["ignore", "ignore", "pipe", "ipc"],
},
);
onTestFinished(async () => {
if (child.exitCode === null && child.signalCode === null) {
const exited = once(child, "exit");
child.kill("SIGTERM");
await exited;
}
});
let stderr = "";
if (child.stderr === null) throw new Error("Expected piped Inspector stderr");
child.stderr.on("data", (chunk: Buffer) => {
stderr += chunk.toString();
});
const address = /ws:\/\/127\.0\.0\.1:\d+\/[^\s]+/u;
await expect.poll(() => stderr, { timeout: 5000 }).toMatch(address);
const socket = address.exec(stderr)?.[0];
if (socket === undefined) throw new Error("Node omitted its Inspector URL");
const endpoint = `http://127.0.0.1:${new URL(socket).port}`;
const load = async (urls: readonly string[]) => {
const loaded = once(child, "message");
child.send(urls);
await loaded;
};
const originalUrl = pathToFileURL(source).href;
await load([originalUrl]);
const { call } = await connectLocalToolsMcp();
const listed = await call("list_javascript_runtime_targets", {
inspector_endpoint: endpoint,
});
expect(listed.isError, JSON.stringify(listed.content)).not.toBe(true);
const discovery = toolContract(
"list_javascript_runtime_targets",
).outputSchema.parse(listed.structuredContent);
const targetId = discovery.normalized_result.targets[0]?.target_id;
if (targetId === undefined) throw new Error("Node target was not listed");
const request = {
inspector_endpoint: endpoint,
target_id: targetId,
observation_ms: 25,
};
const observe = async () => {
const response = await call("observe_javascript_runtime", request);
expect(response.isError, JSON.stringify(response.content)).not.toBe(true);
return toolContract("observe_javascript_runtime").outputSchema.parse(
response.structuredContent,
);
};
const first = await observe();
const initialScript = first.normalized_result.scripts.items.find(
(script) => script.reported_url === originalUrl,
);
expect(initialScript).toMatchObject({
location: { kind: "file", file_path: source },
});
const addedUrls = [
originalUrl.replace(/^file:/u, "FILE:"),
`${originalUrl.replace(/^file:/u, "FiLe:")}?instance=2#selected`,
` \t${originalUrl}\t `,
];
await load(addedUrls);
const second = await observe();
const expectedUrls = [originalUrl, ...addedUrls];
const scripts = second.normalized_result.scripts.items.filter((script) =>
expectedUrls.includes(script.reported_url ?? ""),
);
expect(scripts.map((script) => script.reported_url)).toEqual(
expect.arrayContaining(expectedUrls),
);
expect(scripts).toHaveLength(4);
expect(new Set(scripts.map((script) => script.script_key)).size).toBe(4);
for (const script of scripts)
expect(script.location).toEqual({ kind: "file", file_path: source });
expect(
scripts.find((script) => script.reported_url === originalUrl)?.script_key,
).toBe(initialScript?.script_key);
const retained = await call("get_evidence_bundle", {});
expect(retained.isError, JSON.stringify(retained.content)).not.toBe(true);
const { records } = evidenceBundleSchema.parse(
toolContract("get_evidence_bundle").outputSchema.parse(
retained.structuredContent,
).result,
);
const original = records.find(
(record) => record.evidence_id === first.evidence_id,
);
if (original === undefined || !("normalized_result" in original))
throw new Error("Original Inspector Evidence was not retained");
expect(original.predicate_type).toBe(first.predicate_type);
expect(original.normalized_result).toEqual(first.normalized_result);
expect(await readFile(source, "utf8")).toBe(sourceText);
}, 30_000);