mirror of
https://github.com/morluto/rea.git
synced 2026-10-31 11:56:12 +00:00
Build an agent environment directly from retained capture_browser_scenario Evidence through the existing manifest, serve and export commands. Preserve request ordinals, source pointers, redaction markers, unavailable bodies, provenance and the default stdio MCP transport. Followups admit copied/transformed text before body validation or decoding, avoid request-text allocation in replay, normalize portable file inventories, select dependent CI lanes, retain the author's redaction/body-budget corrections, and count body coverage over represented requests. Validation: full pinned-toolchain static gate, source and public CLI/MCP regressions, canonical catalog checks, actual Chromium interactive and live GitHub API captures with exact HTTP replay and stdio parity, the generated non-root Docker image, and its smoke task through real agent-env 0.9.1334. Verification scopes, resource measurements and platform limits are recorded in the PR and feature verification document. Co-authored-by: gensokyo-dev <gensokyo-dev@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
197 lines
7.1 KiB
TypeScript
197 lines
7.1 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import {
|
|
analysisViewCaptureEvidence,
|
|
analysisViewCaptureFixture,
|
|
environmentManifestCaptureFixture,
|
|
} from "../../../tests/fixtures/analysisView.js";
|
|
import {
|
|
environmentServeFixture,
|
|
environmentServeSource,
|
|
} from "../../../tests/fixtures/environmentServe.js";
|
|
import { projectAnalysisError } from "../../domain/analysisErrorProjection.js";
|
|
import { inspectAnalysisView } from "../analysisView/AnalysisViewService.js";
|
|
import { loadEnvironmentSource } from "./environmentSource.js";
|
|
|
|
const refusal = (manifest: unknown, evidence: unknown) => {
|
|
const loaded = loadEnvironmentSource(manifest, evidence);
|
|
if (loaded.ok) throw new Error("Expected the pair to be refused");
|
|
return projectAnalysisError(loaded.error);
|
|
};
|
|
|
|
describe("loadEnvironmentSource", () => {
|
|
it("authenticates the pair and indexes every cited record as a replayable exchange", () => {
|
|
const fixture = environmentServeFixture();
|
|
const source = environmentServeSource(fixture);
|
|
expect(source.manifest.kind).toBe("environment-manifest");
|
|
expect(source.manifest.name).toBe("grafana-example");
|
|
expect(source.evidenceId).toBe(fixture.capture.evidence_id);
|
|
expect(source.manifestEvidenceId).toBe(fixture.manifest.evidence_id);
|
|
const cited = new Set(
|
|
source.manifest.tools.flatMap((tool) => tool.record_ordinals),
|
|
);
|
|
expect(new Set(source.exchanges.keys())).toEqual(cited);
|
|
const users = source.manifest.tools.find(
|
|
(tool) => tool.name === "get_users_by_id",
|
|
);
|
|
const user = source.exchanges.get(users?.record_ordinals[0] ?? -1);
|
|
expect(user).toMatchObject({
|
|
path: "/api/users/2",
|
|
query: null,
|
|
status: 200,
|
|
contentType: "application/json",
|
|
});
|
|
expect(user?.bodyText).toContain('"login":"probe"');
|
|
const search = source.manifest.tools.find(
|
|
(tool) => tool.name === "get_search",
|
|
);
|
|
expect(source.exchanges.get(search?.record_ordinals[0] ?? -1)?.query).toBe(
|
|
"?query=probe&type=dash-db",
|
|
);
|
|
});
|
|
|
|
it("refuses capture Evidence in a format other than HAR before pairing it", () => {
|
|
const fixture = environmentServeFixture();
|
|
for (const format of ["saz", "mitmproxy"] as const) {
|
|
const evidence = analysisViewCaptureEvidence({
|
|
...analysisViewCaptureFixture(),
|
|
format,
|
|
});
|
|
expect(refusal(fixture.manifest, evidence)).toMatchObject({
|
|
code: "invalid_request",
|
|
details: {
|
|
issues: [
|
|
{
|
|
path: ["evidence", "normalized_result", "format"],
|
|
expected: "har",
|
|
},
|
|
],
|
|
},
|
|
});
|
|
}
|
|
});
|
|
|
|
it("refuses a manifest whose parent is not the supplied evidence with a typed invalid request", () => {
|
|
const fixture = environmentServeFixture();
|
|
const other = analysisViewCaptureEvidence(analysisViewCaptureFixture());
|
|
const projected = refusal(fixture.manifest, other);
|
|
expect(projected).toMatchObject({
|
|
code: "invalid_request",
|
|
category: "invalid_input",
|
|
details: {
|
|
operation: "environment-serve",
|
|
issues: [
|
|
{
|
|
path: ["evidence", "evidence_id"],
|
|
reason: "invalid_value",
|
|
expected: fixture.capture.evidence_id,
|
|
},
|
|
],
|
|
},
|
|
});
|
|
expect(projected.message).toContain("different capture");
|
|
});
|
|
|
|
it("refuses manifests of another view kind, non-capture evidence and non-Evidence JSON", () => {
|
|
const fixture = environmentServeFixture();
|
|
const surface = inspectAnalysisView({
|
|
source: { kind: "inline", evidence: fixture.capture },
|
|
view: { kind: "api-surface" },
|
|
});
|
|
if (!surface.ok) throw surface.error;
|
|
expect(refusal(surface.value, fixture.capture)).toMatchObject({
|
|
code: "invalid_request",
|
|
details: {
|
|
issues: [
|
|
{
|
|
path: ["manifest", "normalized_result", "kind"],
|
|
expected: "environment-manifest",
|
|
},
|
|
],
|
|
},
|
|
});
|
|
expect(refusal(fixture.manifest, fixture.manifest)).toMatchObject({
|
|
code: "invalid_request",
|
|
details: {
|
|
issues: [
|
|
{
|
|
path: ["evidence", "operation"],
|
|
expected: "inspect_web_network_capture or capture_browser_scenario",
|
|
},
|
|
],
|
|
},
|
|
});
|
|
const malformed = refusal({ evidence_id: "ev_x" }, fixture.capture);
|
|
expect(malformed.code).toBe("invalid_request");
|
|
expect(malformed.details?.issues).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({ path: expect.arrayContaining(["manifest"]) }),
|
|
]),
|
|
);
|
|
});
|
|
|
|
it("indexes nothing for a manifest that cites no records and leaves tampered captures to authentication", () => {
|
|
const fixture = environmentServeFixture();
|
|
const capture = analysisViewCaptureEvidence({
|
|
...analysisViewCaptureFixture(),
|
|
records: [],
|
|
total_records: 0,
|
|
});
|
|
const manifest = inspectAnalysisView({
|
|
source: { kind: "inline", evidence: capture },
|
|
view: { kind: "environment-manifest" },
|
|
});
|
|
if (!manifest.ok) throw manifest.error;
|
|
const empty = loadEnvironmentSource(manifest.value, capture);
|
|
if (!empty.ok) throw empty.error;
|
|
expect(empty.value.exchanges.size).toBe(0);
|
|
// Removing records changes the capture's semantic identity, so the pair is
|
|
// refused as an unauthenticated evidence file before any ordinal is looked up.
|
|
const tampered = structuredClone(fixture.capture);
|
|
const result = tampered.normalized_result;
|
|
if (typeof result !== "object" || result === null || Array.isArray(result))
|
|
throw new Error("Capture result must be an object");
|
|
result.records = [];
|
|
expect(refusal(fixture.manifest, tampered)).toMatchObject({
|
|
code: "invalid_request",
|
|
details: {
|
|
issues: expect.arrayContaining([
|
|
expect.objectContaining({
|
|
path: expect.arrayContaining(["evidence"]),
|
|
}),
|
|
]),
|
|
},
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("loadEnvironmentSource recorded request lines", () => {
|
|
it("keeps the delimiter of an explicit empty query in the recorded exchange", () => {
|
|
const capture = environmentManifestCaptureFixture();
|
|
const record = capture.records.find((entry) =>
|
|
JSON.stringify(entry.reported).includes("/api/users/2"),
|
|
);
|
|
if (record === undefined) throw new Error("Fixture lacks /api/users/2");
|
|
const reported = structuredClone(record.reported) as {
|
|
request: { url: string };
|
|
};
|
|
reported.request.url = `${reported.request.url}?`;
|
|
record.reported = reported;
|
|
const evidence = analysisViewCaptureEvidence(capture);
|
|
const manifest = inspectAnalysisView({
|
|
source: { kind: "inline", evidence },
|
|
view: { kind: "environment-manifest" },
|
|
});
|
|
if (!manifest.ok) throw manifest.error;
|
|
const loaded = loadEnvironmentSource(manifest.value, evidence);
|
|
if (!loaded.ok) throw loaded.error;
|
|
const exchanges = [...loaded.value.exchanges.values()];
|
|
expect(
|
|
exchanges.find((entry) => entry.path === "/api/users/2")?.query,
|
|
).toBe("?");
|
|
expect(
|
|
exchanges.find((entry) => entry.path === "/api/users/3")?.query,
|
|
).toBeNull();
|
|
});
|
|
});
|