feat(analysis-view): project rich captures from Evidence files and template path pairs (#5293)

Project large local Evidence captures through the authenticated file-source boundary and add guarded opt-in sibling-pair path inference. Preserve request cancellation, resource ownership and file-relative validation diagnostics.

Validated relevant source and compiled CLI/MCP/environment boundaries, static/build/docs/catalog gates, and a real isolated Gitea 28.2.0 capture through CLI, default stdio MCP and HTTP replay. Current-head automated review passed; unrelated long-running platform jobs may still be pending.

Co-authored-by: Edgar Arakelyan <edgar.arakelyan@scale.com>
Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
Co-authored-by: gensokyo-dev <gensokyo-dev@users.noreply.github.com>
This commit is contained in:
authored and GitHub committed 2026-10-20 10:33:52 +08:00
1 parent 5fa4defcbc
commit f93a0335a8
28 files changed
+1227 -66

No files matched your search

@@ -1,10 +1,13 @@
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import {
analysisViewCaptureFixture,
analysisViewCaptureEvidence,
analysisViewObjectDenseCapture,
environmentManifestCaptureFixture,
} from "../../../tests/fixtures/analysisView.js";
import { describe, expect, it } from "vitest";
import { describe, expect, it, vi } from "vitest";
import { JAVASCRIPT_APPLICATION_EVIDENCE_EXAMPLE } from "../../contracts/javascript/javascriptRuntimeReconciliationExample.js";
import { createEvidence, parseEvidence } from "../../domain/evidence.js";
@@ -19,6 +22,7 @@ import {
import {
inspectAnalysisView,
inspectAnalysisViewValidated,
inspectAnalysisViewWithFiles,
summarizeRetainedAnalysis,
} from "./AnalysisViewService.js";
import type { Evidence } from "../../domain/evidence.js";
@@ -27,6 +31,16 @@ import { err, ok } from "../../domain/result.js";
import { EvidenceLedger } from "../investigation/EvidenceLedger.js";
import { WEB_NETWORK_CAPTURE_LIMITS } from "../../domain/webNetworkCapture.js";
import { isImmutableJsonSnapshot } from "../../domain/immutableJson.js";
import { createTestTempDirectory } from "../../../tests/fixtures/temporaryDirectory.js";
import {
SCENARIO_EXCHANGES,
scenarioCaptureEvidence,
scenarioCaptureFixture,
} from "../../../tests/fixtures/browserScenarioCapture.js";
import { readEvidenceFileSource } from "./evidenceFileSource.js";
import { projectAnalysisError } from "../../domain/analysisErrorProjection.js";
vi.mock("./evidenceFileSource.js", { spy: true });
it.each(["layout", "javascript"])(
"carries the retained %s source snapshot into repeatable shared views",
@@ -860,3 +874,221 @@ it("admits raw_result and parameters before authenticating a portable capture",
});
}
});
const manifestView = { kind: "environment-manifest" } as const;
const writeEvidenceFile = async (value: unknown): Promise<string> => {
const root = await createTestTempDirectory("rea-analysis-view-file-");
const path = join(root, "capture.json");
await writeFile(path, JSON.stringify(value));
return path;
};
describe("inspectAnalysisViewWithFiles", () => {
const view = manifestView;
it("projects an Evidence file exactly as the same Evidence inline", async () => {
const parent = analysisViewCaptureEvidence(
environmentManifestCaptureFixture(),
);
const fromFile = await inspectAnalysisViewWithFiles({
source: { kind: "evidence-file", path: await writeEvidenceFile(parent) },
view,
});
const inline = inspectAnalysisView({
source: { kind: "inline", evidence: parent },
view,
});
if (!fromFile.ok) throw fromFile.error;
if (!inline.ok) throw inline.error;
expect(fromFile.value.normalized_result).toEqual(
inline.value.normalized_result,
);
expect(fromFile.value.parameters).toEqual(inline.value.parameters);
});
it("accepts from a file a capture whose response text exceeds the inline budget", async () => {
const capture = environmentManifestCaptureFixture();
const reported = capture.records[0]!.reported as {
response: { content: { text: string } };
};
// Inline admission charges about 14 bytes per character; this body alone overruns it.
reported.response.content.text = "a".repeat(
Math.ceil(WEB_NETWORK_CAPTURE_LIMITS.outputBytes / 14) + 1024,
);
const parent = analysisViewCaptureEvidence(capture);
expect(
inspectAnalysisView({
source: { kind: "inline", evidence: parent },
view,
}),
).toMatchObject({
ok: false,
error: { _tag: "AnalysisResourceConstraintError", resource: "memory" },
});
const fromFile = await inspectAnalysisViewWithFiles({
source: { kind: "evidence-file", path: await writeEvidenceFile(parent) },
view,
});
if (!fromFile.ok) throw fromFile.error;
expect(fromFile.value.parameters).toMatchObject({
parent_evidence_id: parent.evidence_id,
});
});
it("refuses a relative path, a missing file and JSON that is not Evidence", async () => {
expect(
await inspectAnalysisViewWithFiles({
source: { kind: "evidence-file", path: "capture.json" },
view,
}),
).toMatchObject({ ok: false, error: { _tag: "AnalysisInputError" } });
const missing = join(
await createTestTempDirectory("rea-analysis-view-missing-"),
"absent.json",
);
expect(
await inspectAnalysisViewWithFiles({
source: { kind: "evidence-file", path: missing },
view,
}),
).toMatchObject({ ok: false, error: { _tag: "EvidenceFileError" } });
const notEvidence = await inspectAnalysisViewWithFiles({
source: {
kind: "evidence-file",
path: await writeEvidenceFile({ hello: 1 }),
},
view,
});
expect(notEvidence).toMatchObject({
ok: false,
error: { _tag: "EvidenceFileError", reason: "wrong-kind" },
});
if (notEvidence.ok) throw new Error("Expected invalid Evidence file");
expect(projectAnalysisError(notEvidence.error)).toMatchObject({
details: {
content_issues: expect.arrayContaining([
{
path: ["evidence_id"],
reason: "missing_argument",
expected: "string",
},
]),
},
});
});
it("returns typed cancellation for a cancelled file request", async () => {
const controller = new AbortController();
controller.abort();
const parent = analysisViewCaptureEvidence(
environmentManifestCaptureFixture(),
);
expect(
await inspectAnalysisViewWithFiles(
{
source: {
kind: "evidence-file",
path: await writeEvidenceFile(parent),
},
view,
},
undefined,
{ signal: controller.signal },
),
).toMatchObject({ ok: false, error: { _tag: "AnalysisCancelledError" } });
});
it("returns typed cancellation when a running file request is aborted", async () => {
const controller = new AbortController();
const parent = analysisViewCaptureEvidence(
environmentManifestCaptureFixture(),
);
const path = await writeEvidenceFile(parent);
const running = inspectAnalysisViewWithFiles(
{ source: { kind: "evidence-file", path }, view },
undefined,
{ signal: controller.signal },
);
// Queued before the request yields after its synchronous work, so it is seen even if the read already finished.
setImmediate(() => controller.abort());
expect(await running).toMatchObject({
ok: false,
error: { _tag: "AnalysisCancelledError" },
});
});
it("leaves file sources to the file-aware entry point", () => {
expect(
inspectAnalysisView({
source: { kind: "evidence-file", path: "/tmp/capture.json" },
view,
}),
).toMatchObject({
ok: false,
error: { _tag: "AnalysisCapabilityUnavailableError" },
});
});
});
describe("inspectAnalysisViewWithFiles scenario parents and cancellation", () => {
const view = manifestView;
it("accepts from a file a scenario capture whose response bodies exceed the inline budget", async () => {
const blob = "a".repeat(
Math.ceil(WEB_NETWORK_CAPTURE_LIMITS.outputBytes / 14) + 1024,
);
const parent = scenarioCaptureEvidence(
scenarioCaptureFixture(
SCENARIO_EXCHANGES.map((exchange, index) =>
index === 1 ? { ...exchange, body: `{"blob":"${blob}"}` } : exchange,
),
),
);
expect(
inspectAnalysisView({
source: { kind: "inline", evidence: parent },
view,
}),
).toMatchObject({
ok: false,
error: { _tag: "AnalysisResourceConstraintError", resource: "memory" },
});
const fromFile = await inspectAnalysisViewWithFiles({
source: { kind: "evidence-file", path: await writeEvidenceFile(parent) },
view,
});
if (!fromFile.ok) throw fromFile.error;
expect(fromFile.value.parameters).toMatchObject({
parent_evidence_id: parent.evidence_id,
});
});
it("reports cancellation rather than the authentication failure of a file aborted while it is authenticated", async () => {
const controller = new AbortController();
const parent = analysisViewCaptureEvidence(
environmentManifestCaptureFixture(),
);
const path = await writeEvidenceFile({
...parent,
evidence_id: `ev_${"0".repeat(64)}`,
});
const { readEvidenceFileSource: read } = await vi.importActual<
typeof import("./evidenceFileSource.js")
>("./evidenceFileSource.js");
// The abort arrives once the read has finished, while the stale ID is being authenticated.
vi.mocked(readEvidenceFileSource).mockImplementationOnce(
async (...args) => {
const loaded = await read(...args);
controller.abort();
return loaded;
},
);
expect(
await inspectAnalysisViewWithFiles(
{ source: { kind: "evidence-file", path }, view },
undefined,
{ signal: controller.signal },
),
).toMatchObject({ ok: false, error: { _tag: "AnalysisCancelledError" } });
});
});