Files
reverse-engineering-anything/src/application/environment/environmentStateWriteRefusals.test.ts
T
morluto 63220edcc3 fix(environment)!: withhold clock writes without an anchor
Keep missing or ambiguous scenario request durations unknown, retain learned clock rules, and withhold plans that cannot generate an observed clock. Refuse such writes in older imported states before changing state or generators.

BREAKING CHANGE: The write-plan reason can now be unknown-clock; strict clients must accept the new output value.

Validated state, CLI and advertised MCP workflows, documentation, and focused clock and cleanup regressions.
2026-10-27 05:19:35 +08:00

326 lines
12 KiB
TypeScript

import { EnvironmentStateStore } from "./environmentStateStore.js";
import { environmentToolAnswer } from "./environmentToolAnswer.js";
import { projectAnalysisError } from "../../domain/analysisErrorProjection.js";
import type { StateWritePlan } from "../../domain/analysisView/environmentStateWriteRules.js";
import { describe, expect, it } from "vitest";
import type { TimedCaptureRow } from "../../../tests/fixtures/analysisView.js";
import {
servedFrom,
type Served,
} from "../../../tests/fixtures/environmentStateServing.js";
import {
FIRST_BOOK,
SECOND_BOOK,
renamedBook,
type Book,
} from "../../../tests/fixtures/environmentStateWrites.js";
import { MAX_JSON_DEPTH, type JsonValue } from "../../domain/jsonValue.js";
import type { ReplayArguments } from "./environmentReplay.js";
import { STATE_INTAKE_LIMIT_BYTES } from "./environmentStateDocument.js";
const DARK = { theme: "dark", meta: { n: 1 } };
const LIGHT = { theme: "light", meta: { n: 2 } };
/** One organisation's settings document, read, patched with both members and read again. */
const SETTINGS_ROWS: readonly TimedCaptureRow[] = [
["GET", "/api/orgs/1/settings", DARK],
["PATCH", "/api/orgs/1/settings", LIGHT, 200, LIGHT],
["GET", "/api/orgs/1/settings", LIGHT],
];
const READ_SETTINGS = "get_orgs_settings_by_id";
const PATCH_SETTINGS = "patch_orgs_settings_by_id";
/** Every refused call is a tool error naming its problem, and leaves the state as it was. */
const expectRefusedUnchanged = (
served: Served,
refused: readonly (readonly [string, ReplayArguments, string])[],
): void => {
const before = served.snapshot();
for (const [name, args, message] of refused) {
expect(served.refusal(name, args)).toMatchObject({
code: "invalid_request",
message: expect.stringContaining(message),
});
expect(served.snapshot()).toBe(before);
}
};
/** What data/get reports loads back through data/add byte for byte. */
const expectReloadsUnchanged = (served: Served): void => {
const before = served.snapshot();
expect(served.store.replace(JSON.parse(before)).ok).toBe(true);
expect(served.snapshot()).toBe(before);
};
const nested = (depth: number): JsonValue =>
Array.from({ length: depth }).reduce<JsonValue>((inner) => [inner], 1);
describe("served document writes", () => {
it("accepts only the members the capture sent, as the JSON types the document holds", () => {
const settings = servedFrom(SETTINGS_ROWS);
expect(settings.state.writes.get(PATCH_SETTINGS)).toMatchObject({
served: true,
effect: "document",
});
expectRefusedUnchanged(settings, [
[
PATCH_SETTINGS,
{ id: "1", body: { theme: 5, extra: true } },
"Member theme must be a JSON string",
],
[
PATCH_SETTINGS,
{ id: "1", body: { extra: true } },
"Member extra is not one the capture showed stored",
],
]);
const blue = { theme: "blue", meta: DARK.meta };
expect(
settings.answer(PATCH_SETTINGS, { id: "1", body: { theme: "blue" } }),
).toMatchObject({
body: blue,
state: { view: "document-write", path: "/api/orgs/1/settings" },
});
expect(settings.answer(READ_SETTINGS, { id: "1" }).body).toEqual(blue);
});
it("leaves a document write unserved when its recording changed a member's JSON type, which serving would refuse", () => {
const retyped = { theme: 5, meta: LIGHT.meta };
const settings = servedFrom([
["GET", "/api/orgs/1/settings", DARK],
["PATCH", "/api/orgs/1/settings", retyped, 200, retyped],
["GET", "/api/orgs/1/settings", retyped],
]);
expect(settings.state.writes.get(PATCH_SETTINGS)).toMatchObject({
served: false,
reason: "unexplained-change",
});
});
it("does not confirm a document write at another path, which serving refuses", () => {
const settings = servedFrom([
["GET", "/api/orgs/1/settings", DARK],
["PATCH", "/api/orgs/2/settings", LIGHT, 200, LIGHT],
["GET", "/api/orgs/1/settings", LIGHT],
]);
expect(settings.state.writes.get(PATCH_SETTINGS)).toMatchObject({
served: false,
});
});
it("changes the document only at the path the state holds it", () => {
const settings = servedFrom(SETTINGS_ROWS);
expectRefusedUnchanged(settings, [
[
PATCH_SETTINGS,
{ id: "2", body: { theme: "blue" } },
"holds this document only at /api/orgs/1/settings",
],
]);
expect(settings.answer(READ_SETTINGS, { id: "1" }).body).toEqual(DARK);
});
});
describe("values the state can load back", () => {
it("preserves nested prototype-named data through writes and reload", () => {
const settings = servedFrom(SETTINGS_ROWS);
const meta = JSON.parse(
'{"__proto__":{"n":1},"constructor":2,"prototype":3}',
);
expect(
settings.answer(PATCH_SETTINGS, { id: "1", body: { meta } }).body,
).toEqual({ theme: "dark", meta });
expect(settings.answer(READ_SETTINGS, { id: "1" }).body).toEqual({
theme: "dark",
meta,
});
expectReloadsUnchanged(settings);
});
it("refuses a body value data/add would refuse, whatever member carries it", () => {
const settings = servedFrom(SETTINGS_ROWS);
const unexact =
"Member meta would hold a value that is not exactly representable JSON";
expectRefusedUnchanged(settings, [
[PATCH_SETTINGS, { id: "1", body: { meta: { n: 1e20 } } }, unexact],
[
PATCH_SETTINGS,
{ id: "1", body: { meta: { n: nested(MAX_JSON_DEPTH) } } },
unexact,
],
]);
settings.answer(PATCH_SETTINGS, { id: "1", body: { meta: { n: 3 } } });
expectReloadsUnchanged(settings);
});
it("refuses an increment past the exact integers, so data/get still loads back", () => {
const versioned = (entry: Book, version: number): Book => ({
...entry,
version,
});
const last = Number.MAX_SAFE_INTEGER;
const edition = versioned(renamedBook(SECOND_BOOK, "Second Edition", 3), 2);
const books = servedFrom([
[
"GET",
"/api/books",
[versioned(FIRST_BOOK, last - 1), versioned(SECOND_BOOK, 1)],
],
["GET", "/api/books/1", versioned(FIRST_BOOK, last - 1)],
["GET", "/api/books/2", versioned(SECOND_BOOK, 1)],
["PATCH", "/api/books/2", edition, 200, { name: "Second Edition" }],
["GET", "/api/books/2", edition],
]);
const rename = (name: string) => ({ id: "1", body: { name } });
expect(
books.answer("patch_books_by_id", rename("Renamed")).body,
).toMatchObject({ version: last });
expectRefusedUnchanged(books, [
[
"patch_books_by_id",
rename("Renamed Again"),
"Field version would hold a value that is not exactly representable JSON",
],
]);
expectReloadsUnchanged(books);
});
});
describe("the state size limit", () => {
it("keeps the size data/get writes as each write changes the state", () => {
const books = servedFrom();
const settings = servedFrom(SETTINGS_ROWS);
const tracked = (served: Served) =>
expect(served.store.byteLength()).toBe(
Buffer.byteLength(served.snapshot(), "utf8"),
);
tracked(books);
books.answer("post_books", { body: { name: "Ünïcode Bøok" } });
tracked(books);
books.answer("patch_books_by_id", { id: "3", body: { name: "Short" } });
tracked(books);
books.answer("delete_books_by_id", { id: "1" });
books.answer("delete_books_by_id", { id: "2" });
books.answer("delete_books_by_id", { id: "3" });
tracked(books);
books.answer("post_books", { body: { name: "Only Book" } });
tracked(books);
settings.answer(PATCH_SETTINGS, { id: "1", body: { theme: "dark blue" } });
tracked(settings);
expect(settings.store.replace(JSON.parse(settings.snapshot())).ok).toBe(
true,
);
tracked(settings);
});
it("refuses a create that would take data/get past what data/add loads", () => {
const books = servedFrom();
// Each answer renders the book, so each stays under the reply body limit.
const create = { body: { name: "a".repeat(100_000) } };
let refused: ReturnType<Served["call"]> | undefined;
for (let made = 0; made < 200 && refused === undefined; made += 1) {
const result = books.call("post_books", create);
if (!result.ok) refused = result;
}
const before = books.snapshot();
expect(Buffer.byteLength(before, "utf8")).toBeLessThanOrEqual(
STATE_INTAKE_LIMIT_BYTES,
);
expect(refused?.ok === false && refused.error).toMatchObject({
resource: "capacity",
reportedLimits: { max_state_bytes: STATE_INTAKE_LIMIT_BYTES },
});
expect(books.refusal("post_books", create)).toMatchObject({
code: "resource_constraint",
});
expect(books.snapshot()).toBe(before);
expectReloadsUnchanged(books);
// A smaller state has room again.
books.answer("delete_books_by_id", { id: "3" });
expect(books.answer("post_books", create).status).toBe(201);
});
it("refuses a document that data/get would write past what data/add loads", () => {
const books = servedFrom();
const { rea_state: state } = books.store.snapshot();
// Without the members data/get adds back, the document fits only until they return.
const document = {
rea_state: {
version: state.version,
entities: state.entities.map(({ type, collection, id, fields }) => ({
type,
collection,
id,
fields: { ...fields },
})),
documents: state.documents,
},
};
const first = document.rea_state.entities[0]!.fields;
first["name"] = `${String(first["name"])}${"a".repeat(
STATE_INTAKE_LIMIT_BYTES -
Buffer.byteLength(JSON.stringify(document), "utf8"),
)}`;
expect(Buffer.byteLength(JSON.stringify(document), "utf8")).toBe(
STATE_INTAKE_LIMIT_BYTES,
);
const before = books.snapshot();
const replaced = books.store.replace(document);
expect(replaced.ok).toBe(false);
expect(!replaced.ok && replaced.error.issues).toEqual([
expect.objectContaining({
reason: "out_of_range",
maximum: STATE_INTAKE_LIMIT_BYTES,
}),
]);
expect(books.snapshot()).toBe(before);
});
});
it.each(["clock", "held-clock", "nested-clock"] as const)(
"refuses a legacy served %s plan with an unknown clock before changing state or generators",
(rule) => {
const fixture = servedFrom();
const original = fixture.state.writes.get("patch_books_by_id");
if (original === undefined || !original.served)
throw new Error("Expected served fixture update");
const format = { kind: "epoch", unit: "ms" } as const;
const plan: StateWritePlan = {
...original,
rules:
rule === "nested-clock"
? [{ field: "updated_at", rule, format, members: ["at"] }]
: [{ field: "updated_at", rule, format }],
};
const writes = new Map(fixture.state.writes).set(plan.tool, plan);
const state = { ...fixture.state, writeClock: null, writes };
const store = new EnvironmentStateStore(state);
const respond = environmentToolAnswer(fixture.source, store);
const tool = fixture.source.manifest.tools.find(
(candidate) => candidate.name === plan.tool,
);
const deletion = fixture.source.manifest.tools.find(
(candidate) => candidate.name === "delete_books_by_id",
);
if (tool === undefined || deletion === undefined)
throw new Error("Expected fixture tools");
const before = JSON.stringify(store.snapshot());
const pending = store.pendingWrite();
const result = respond(tool, { id: "1", body: { name: "Refused" } });
if (result.ok) throw new Error("Expected unknown clock refusal");
expect(result.error._tag).toBe("AnalysisInputError");
expect(projectAnalysisError(result.error)).toMatchObject({
code: "invalid_request",
message: expect.stringContaining("requires a known write clock"),
});
expect(JSON.stringify(store.snapshot())).toBe(before);
expect(store.pendingWrite()).toEqual(pending);
expect(store.state.writes.get(plan.tool)).toBe(plan);
// Independent writes keep working; the guard does not erase imported metadata.
expect(respond(deletion, { id: "1" }).ok).toBe(true);
expect(JSON.stringify(store.snapshot())).not.toBe(before);
},
);