mirror of
https://github.com/morluto/rea.git
synced 2026-11-14 10:43:30 +00:00
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:
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" } });
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user