Files
reverse-engineering-anything/src/application/environment/environmentStateWriteAnswer.test.ts
T
Edgar ArakelyanandClaude Opus 5.5 2086da6cc7 fix(environment): make the write gate and serving agree on lines, rules, inserts and documents
Five places where the gate approved a write serving would answer
differently:
- list and document reads are matched to checked lines as serving
  matches calls (path and query values decoded, the query in any
  order), so a reordered query no longer hides a contradiction;
- an increment or slug applies to every call, so a plan is
  unexplained-change when one call changes such a field and another
  leaves it as it was, in either order;
- an item read confirming a create no longer stops a later list from
  choosing another insert candidate; only a list check fixes the rule;
- the gate runs serving's document body check (accepted members, JSON
  types from the held and initial document), so a recorded write that
  serving would refuse leaves the plan unserved;
- a served create decodes its path values from the collection the call
  names, as the gate decoded the recorded path, whichever spelling the
  call used.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 14:12:34 -07:00

411 lines
14 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
servedFrom,
type Served,
} from "../../../tests/fixtures/environmentStateServing.js";
import {
BOOK_INIT_ROWS,
BOOK_NOT_FOUND,
BOOK_ROWS,
DELETE_NOT_FOUND,
FIRST_BOOK,
SECOND_BOOK,
THIRD_BOOK,
book,
bookClock,
renamedBook,
type Book,
} from "../../../tests/fixtures/environmentStateWrites.js";
import type { TimedCaptureRow } from "../../../tests/fixtures/analysisView.js";
import type { ReplayArguments } from "./environmentReplay.js";
import { servedCatalog } from "./environmentState.js";
import {
environmentToolArguments,
environmentToolDefinitions,
} from "./environmentTools.js";
/** The book the first served create of "New Book" makes: the next id, one second past the served clock's start. */
const NEW_BOOK = book(3, "New Book", bookClock(13));
const CREATE_NEW_BOOK = { body: { name: "New Book" } };
const entityOf = (books: Served, id: string) =>
books.store.snapshot().rea_state.entities.find((entity) => entity.id === id);
/** Load what data/get reports back through data/add: the state must come back byte for byte. */
const expectReloadsUnchanged = (books: Served): void => {
const before = books.snapshot();
expect(books.store.replace(JSON.parse(before)).ok).toBe(true);
expect(books.snapshot()).toBe(before);
};
describe("served creates", () => {
it("creates a book with the next id, the served clock and a derived slug, which later reads show", () => {
const books = servedFrom();
const created = books.answer("post_books", CREATE_NEW_BOOK);
expect(created).toMatchObject({
status: 201,
content_type: "application/json",
state: {
source: "state",
view: "create",
type: "/api/books",
collection: "/api/books",
id: "3",
},
});
expect(created.body).toEqual(NEW_BOOK);
expect(books.answer("get_books_by_id", { id: "3" })).toMatchObject({
status: 200,
body: NEW_BOOK,
state: { source: "state", view: "item", id: "3" },
});
expect(books.answer("get_books")).toMatchObject({
body: [FIRST_BOOK, SECOND_BOOK, NEW_BOOK],
state: { source: "state", view: "list", membership: "all", listed: 3 },
});
});
it("reports a created book in data/get without record citations and loads it back unchanged", () => {
const books = servedFrom();
books.answer("post_books", CREATE_NEW_BOOK);
expect(entityOf(books, "3")).toEqual({
type: "/api/books",
collection: "/api/books",
id: "3",
fields: NEW_BOOK,
view_fields: [],
record_ordinals: [],
});
expectReloadsUnchanged(books);
expect(books.answer("get_books_by_id", { id: "3" }).body).toEqual(NEW_BOOK);
});
});
describe("served updates and deletes", () => {
const SECOND_EDITION = renamedBook(SECOND_BOOK, "Second Edition", 13);
const RENAME_SECOND = { id: "2", body: { name: "Second Edition" } };
it("changes only the sent members, the slug that follows them and the clock", () => {
const books = servedFrom();
expect(books.answer("patch_books_by_id", RENAME_SECOND)).toMatchObject({
status: 200,
body: SECOND_EDITION,
state: { source: "state", view: "update", id: "2" },
});
expect(books.answer("get_books_by_id", { id: "2" }).body).toEqual(
SECOND_EDITION,
);
expect(books.answer("get_books").body).toEqual([
FIRST_BOOK,
SECOND_EDITION,
]);
});
it("cites no records for an updated book in data/get, so it loads back unchanged", () => {
const books = servedFrom();
books.answer("patch_books_by_id", RENAME_SECOND);
expect(entityOf(books, "2")).toMatchObject({
fields: SECOND_EDITION,
record_ordinals: [],
});
expectReloadsUnchanged(books);
});
it("removes a deleted book; a later read answers the recorded 404 as a missing entity", () => {
const books = servedFrom();
expect(books.answer("delete_books_by_id", { id: "2" })).toMatchObject({
status: 204,
body: null,
state: { source: "state", view: "delete", id: "2" },
});
expect(books.answer("get_books_by_id", { id: "2" })).toMatchObject({
status: 404,
body: BOOK_NOT_FOUND,
state: {
source: "state",
view: "missing",
type: "/api/books",
collection: "/api/books",
id: "2",
},
});
expect(books.answer("get_books").body).toEqual([FIRST_BOOK]);
});
it("answers an update or delete of a missing book with a recorded 404, else refuses it", () => {
const books = servedFrom();
const before = books.snapshot();
// The update recorded no absence, so it answers the item view's; the delete answers its own.
expect(
books.answer("patch_books_by_id", { id: "99", body: { name: "Ghost" } }),
).toMatchObject({
status: 404,
body: BOOK_NOT_FOUND,
state: { view: "missing", id: "99" },
});
expect(books.answer("delete_books_by_id", { id: "99" })).toMatchObject({
status: 404,
body: DELETE_NOT_FOUND,
state: { view: "missing", id: "99" },
});
expect(books.snapshot()).toBe(before);
const unrecorded = servedFrom(
BOOK_ROWS.filter(([, , , status]) => status !== 404),
);
for (const [name, args] of [
["patch_books_by_id", { id: "99", body: { name: "Ghost" } }],
["delete_books_by_id", { id: "99" }],
] as const)
expect(unrecorded.refusal(name, args)).toMatchObject({
code: "invalid_request",
message: expect.stringContaining(
"holds no /api/books entity 99 in /api/books",
),
});
});
});
describe("refusals", () => {
it("refuses a body the capture does not support and leaves the state unchanged", () => {
const books = servedFrom();
const before = books.snapshot();
const refused: [string, ReplayArguments, string][] = [
[
"post_books",
{ body: { name: "Extra", color: "red" } },
"Member color is not one the capture showed stored",
],
["post_books", { body: {} }, "lacks required members name"],
[
"post_books",
{ body: { name: 42 } },
"Member name must be a JSON string",
],
["post_books", { body: ["New Book"] }, "takes a JSON object body"],
[
"patch_books_by_id",
{ id: "1", body: "New Book" },
"takes a JSON object body",
],
["patch_books_by_id", { id: "1", body: {} }, "sends none of the members"],
[
"patch_books_by_id",
{ id: "1", body: { title: "New" } },
"Member title is not one",
],
];
for (const [name, args, message] of refused) {
expect(books.refusal(name, args)).toMatchObject({
code: "invalid_request",
message: expect.stringContaining(message),
});
expect(books.snapshot()).toBe(before);
}
});
it("accepts a member the application stored differently only with the value the capture sent", () => {
const bound = (entry: Book, format: string): Book => ({ ...entry, format });
const books = servedFrom([
[
"GET",
"/api/books",
[bound(FIRST_BOOK, "PAPERBACK"), bound(SECOND_BOOK, "PAPERBACK")],
],
["GET", "/api/books/1", bound(FIRST_BOOK, "PAPERBACK")],
["GET", "/api/books/2", bound(SECOND_BOOK, "PAPERBACK")],
[
"POST",
"/api/books",
bound(THIRD_BOOK, "HARDCOVER"),
201,
{ name: "Third Book", format: "hardcover" },
],
["GET", "/api/books/3", bound(THIRD_BOOK, "HARDCOVER")],
]);
const before = books.snapshot();
expect(
books.refusal("post_books", {
body: { name: "Soft", format: "softcover" },
}),
).toMatchObject({
message: expect.stringContaining(
"Member format is accepted only with the value the capture sent",
),
});
expect(books.snapshot()).toBe(before);
expect(
books.answer("post_books", {
body: { name: "Hard", format: "hardcover" },
}).body,
).toMatchObject({ id: 3, name: "Hard", format: "HARDCOVER" });
});
it("generates the same values after a refused call as without it", () => {
const refusedFirst = servedFrom();
refusedFirst.refusal("post_books", { body: {} });
refusedFirst.refusal("patch_books_by_id", {
id: "1",
body: { title: "New" },
});
expect(refusedFirst.answer("post_books", CREATE_NEW_BOOK).body).toEqual(
servedFrom().answer("post_books", CREATE_NEW_BOOK).body,
);
});
});
describe("generated values", () => {
it("answers the same calls identically after a reset, never reusing a deleted id", () => {
const books = servedFrom();
const session = () => {
const answers = [
books.answer("post_books", CREATE_NEW_BOOK),
books.answer("patch_books_by_id", {
id: "3",
body: { name: "Newer Book" },
}),
books.answer("delete_books_by_id", { id: "3" }),
books.answer("post_books", { body: { name: "Another Book" } }),
books.answer("get_books"),
];
return { answers, text: JSON.stringify([answers, books.snapshot()]) };
};
const first = session();
books.store.reset();
expect(session().text).toBe(first.text);
expect(first.answers[3]?.body).toMatchObject({
id: 4,
name: "Another Book",
});
});
it("restarts the clock and the sequences when data/add loads a state", () => {
const books = servedFrom();
const initial: unknown = JSON.parse(books.snapshot());
const created = books.answer("post_books", CREATE_NEW_BOOK);
books.answer("post_books", { body: { name: "Another Book" } });
expect(books.store.replace(initial).ok).toBe(true);
// Had the sequence and clock run on, this create would take id 5 one second later.
expect(books.answer("post_books", CREATE_NEW_BOOK).body).toEqual(
created.body,
);
});
});
describe("served catalog", () => {
it("adds only the served write tools, saying what each does to the state", () => {
const { source, state } = servedFrom();
const catalog = servedCatalog(source, state, "books");
if (!catalog.ok) throw catalog.error;
const described = new Map(
catalog.value.tools.map((tool) => [tool.name, tool]),
);
// The export write targets no state type, so it is not served.
expect([...described.keys()].sort()).toEqual([
"delete_books_by_id",
"get_books",
"get_books_by_id",
"patch_books_by_id",
"post_books",
]);
expect(described.get("post_books")?.description).toContain(
"creates an entity of /api/books in the served environment state (body members name; required: name), modelled on the recorded calls",
);
expect(described.get("patch_books_by_id")?.description).toContain(
"changes the entity the path names in the served environment state (body members name; send at least one)",
);
expect(described.get("delete_books_by_id")?.description).toContain(
"removes the entity the path names from the served environment state",
);
expect(described.get("post_books")?.annotations.readOnlyHint).toBe(false);
expect(described.get("get_books")?.description).toContain(
"answers from the served environment state where the state backs this tool",
);
});
it("serves every manifest tool unchanged in replay mode", () => {
const { source } = servedFrom();
const replay = servedCatalog(source, undefined, "books");
const definitions = environmentToolDefinitions(source, "books");
if (!replay.ok || !definitions.ok) throw new Error("Expected a catalog");
expect(replay.value).toEqual({
tools: definitions.value,
dataPlane: "none",
});
expect(replay.value.tools.map(({ name }) => name)).toContain(
"post_exports",
);
});
});
describe("paged list lines", () => {
it("answers an argument-less list from the state on a line that only pages, and another page from its recording", () => {
const paged = servedFrom([
["GET", "/api/books?limit=50", [FIRST_BOOK, SECOND_BOOK]],
...BOOK_INIT_ROWS.slice(1),
["POST", "/api/books", THIRD_BOOK, 201, { name: "Third Book" }],
["GET", "/api/books/3", THIRD_BOOK],
["GET", "/api/books?limit=50", [FIRST_BOOK, SECOND_BOOK, THIRD_BOOK]],
]);
const created = paged.answer("post_books", CREATE_NEW_BOOK).body;
expect(paged.answer("get_books")).toMatchObject({
body: [FIRST_BOOK, SECOND_BOOK, created],
replay: { record_ordinal: 0, exact_match: false },
state: { source: "state", view: "list", listed: 3 },
});
expect(paged.answer("get_books", { limit: "10" })).toMatchObject({
body: [FIRST_BOOK, SECOND_BOOK],
state: { source: "recording", reason: "unrecorded-request" },
});
});
});
describe("path values", () => {
const orgBook = (id: number, org: string, name: string): Book => ({
id,
org,
name,
});
const ALPHA = orgBook(1, "my org1", "Alpha");
const BETA = orgBook(2, "acme2", "Beta");
const GAMMA = orgBook(3, "my org1", "Gamma");
const DELTA = orgBook(4, "acme2", "Delta");
/** Books under two organisations, one whose name the paths percent-encode, and a create under each. */
const ROWS: readonly TimedCaptureRow[] = [
["GET", "/api/orgs/my%20org1/books", [ALPHA]],
["GET", "/api/orgs/acme2/books", [BETA]],
["GET", "/api/orgs/my%20org1/books/1", ALPHA],
["GET", "/api/orgs/acme2/books/2", BETA],
["POST", "/api/orgs/my%20org1/books", GAMMA, 201, { name: "Gamma" }],
["GET", "/api/orgs/my%20org1/books", [ALPHA, GAMMA]],
["POST", "/api/orgs/acme2/books", DELTA, 201, { name: "Delta" }],
["GET", "/api/orgs/acme2/books", [BETA, DELTA]],
];
it("stores a parent path value decoded, as the gate learned it, whichever spelling the call uses", () => {
for (const spelling of ["my%20org1", "my org1"]) {
const books = servedFrom(ROWS);
const tool = books.source.manifest.tools.find(
(candidate) => candidate.method === "POST",
)!;
expect(books.state.writes.get(tool.name)).toMatchObject({
served: true,
rules: expect.arrayContaining([
expect.objectContaining({ field: "org", rule: "path" }),
]),
});
const [org] = environmentToolArguments(tool).path.values();
books.answer(tool.name, { [org!]: spelling, body: { name: "Epsilon" } });
const created = books.store
.snapshot()
.rea_state.entities.find(
(entity) => entity.fields["name"] === "Epsilon",
);
expect(created).toMatchObject({
collection: "/api/orgs/my%20org1/books",
fields: { org: "my org1" },
});
}
});
});