mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
fix(environment): own JSON fields across capture, state and replay
This commit is contained in:
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 }) => {
|
||||
|
||||
Reference in new issue
Block a user