fix(environment): own JSON fields across capture, state and replay

This commit is contained in:
morluto committed 2026-10-26 18:27:30 +08:00
1 parent cc5a96933a
commit e54bf1006e
57 files changed
+1019 -389

No files matched your search

@@ -1,5 +1,6 @@
import { createHash } from "node:crypto";
import { timedCaptureEvidence } from "../../fixtures/analysisView.js";
import { jsonObjectSchema } from "../../../src/domain/jsonValue.js";
import {
Client,
@@ -57,6 +58,36 @@ const connect = async (url: URL) => {
return client;
};
processTest(
"replays recorded own JSON keys without interpreting inherited members as fields",
async ({ processes }) => {
const body = JSON.parse(
'{"id":1,"__proto__":{"source":"recorded"},"nested":{"constructor":null,"__proto__":[false,0,""]}}',
);
const fixture = environmentStateServeFixture(
timedCaptureEvidence([["GET", "/api/books/1", body]]),
);
const started = await serveReplayOnly(processes, {}, fixture);
const client = await connect(started.url);
try {
const response = await client.callTool({
name: "get_books_by_id",
arguments: { id: "1" },
});
expect(response.isError).toBeFalsy();
expect(response.structuredContent).toMatchObject({ body });
const replayed = jsonObjectSchema.parse(
jsonObjectSchema.parse(response.structuredContent).body,
);
expect(Object.hasOwn(replayed, "__proto__")).toBe(true);
expect(Object.hasOwn(replayed, "constructor")).toBe(false);
} finally {
await client.close();
}
},
HTTP_BOUNDARY_TIMEOUT_MS,
);
processTest(
"validates repeated query arrays through the advertised MCP contract",
async ({ processes }) => {
@@ -32,12 +32,137 @@ import { unsupportedInputLiteralSchemaError } from "../../fixtures/mcpClientSche
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { startListeningProcess } from "../../support/mcp/streamableHttpProcess.js";
import { processTest } from "../../support/process/processFixture.js";
import {
jsonObjectSchema,
type JsonValue,
} from "../../../src/domain/jsonValue.js";
const HTTP_BOUNDARY_TIMEOUT_MS = 90_000;
/** The book the first served create of "Boundary Book" makes: one second past the served clock's start. */
const BOUNDARY_BOOK = book(3, "Boundary Book", bookClock(13));
processTest(
"preserves own JSON keys through capture, state reload and computed writes",
async ({ processes }) => {
const nested = JSON.parse(
'{"__proto__":{"recorded":true},"constructor":null,"values":[false,0,""]}',
) as JsonValue;
const captured = (id: number, name: string, updated = bookClock(id)) => {
const { slug, ...fields } = book(id, name, bookClock(id), updated);
if (typeof slug !== "string") throw new Error("Fixture book has no slug");
return { ...fields, ["__proto__"]: slug, nested };
};
const first = captured(1, "First Book");
const second = captured(2, "Second Book");
const third = captured(3, "Third Book", bookClock(3));
const renamed = captured(1, "Renamed Book", bookClock(5));
const rows: readonly TimedCaptureRow[] = [
["GET", "/api/books", [first, second]],
["GET", "/api/books/1", first],
["GET", "/api/books/2", second],
["POST", "/api/books", third, 201, { name: "Third Book" }],
["GET", "/api/books/3", third],
["PATCH", "/api/books/1", renamed, 200, { name: "Renamed Book" }],
["GET", "/api/books/1", renamed],
];
const fixture = environmentStateServeFixture(timedCaptureEvidence(rows));
expect(fixture.state.normalized_result).toMatchObject({
entities: expect.arrayContaining([
expect.objectContaining({ id: "1", fields: { ...first } }),
]),
});
const root = await createTestTempDirectory("rea-own-keys-state-mcp-");
const files = await writeEnvironmentServeFiles(root, fixture);
const started = await startListeningProcess(processes, [
"environment-serve",
files.manifestPath,
"--evidence",
files.capturePath,
"--port",
"0",
]);
type StateDocument = {
rea_state: {
entities: Array<{ id: string; fields: Record<string, JsonValue> }>;
[key: string]: unknown;
};
};
const rpc = async (method: string, params?: unknown) =>
(
await fetch(new URL("/agentenv", started.url.origin), {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }),
})
).json() as Promise<{
result?: { parts?: Array<{ data: StateDocument }> };
error?: { code: number };
}>;
const document = (await rpc("data/get")).result?.parts?.[0]?.data;
if (document === undefined)
throw new Error("Missing served state document");
const held = document.rea_state.entities.find(({ id }) => id === "1");
if (held === undefined) throw new Error("Missing initial book");
expect(Object.hasOwn(held.fields, "__proto__")).toBe(true);
expect(held.fields.nested).toEqual(nested);
expect(Object.hasOwn(held.fields, "constructor")).toBe(false);
await expect(
rpc("data/add", { parts: [{ kind: "data", data: document }] }),
).resolves.toMatchObject({ result: {} });
const reloaded = (await rpc("data/get")).result?.parts?.[0]?.data;
expect(reloaded).toEqual(document);
const invalid = {
rea_state: {
...document.rea_state,
["__proto__"]: "unexpected structural field",
},
};
await expect(
rpc("data/add", { parts: [{ kind: "data", data: invalid }] }),
).resolves.toMatchObject({ error: { code: -32602 } });
delete held.fields["__proto__"];
await expect(
rpc("data/add", { parts: [{ kind: "data", data: document }] }),
).resolves.toMatchObject({ result: {} });
const client = new Client({ name: "own-keys-writes", version: "1" });
await client.connect(new StreamableHTTPClientTransport(started.url));
try {
for (const [tool, args, name, id] of [
[
"patch_books_by_id",
{ id: "1", body: { name: "Updated Book" } },
"Updated Book",
"1",
],
["post_books", { body: { name: "Created Book" } }, "Created Book", "3"],
] as const) {
const written = await client.callTool({ name: tool, arguments: args });
expect(written.isError).toBeFalsy();
const expected = {
name,
["__proto__"]: name.toLowerCase().replaceAll(" ", "-"),
nested,
};
expect(written.structuredContent).toMatchObject({ body: expected });
const read = await client.callTool({
name: "get_books_by_id",
arguments: { id },
});
expect(read.structuredContent).toMatchObject({ body: expected });
const body = jsonObjectSchema.parse(
jsonObjectSchema.parse(read.structuredContent).body,
);
expect(Object.hasOwn(body, "__proto__")).toBe(true);
expect(Object.hasOwn(body, "constructor")).toBe(false);
}
} finally {
await client.close();
}
},
HTTP_BOUNDARY_TIMEOUT_MS,
);
processTest(
"keeps create response clocks separate from enriched item reads through the SDK",
async ({ processes }) => {