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>
This commit is contained in:
Edgar ArakelyanandClaude Opus 5.5 committed 2026-10-20 14:12:34 -07:00
1 parent df2bded16e
commit 2086da6cc7
11 files changed
+343 -52

No files matched your search

@@ -17,9 +17,13 @@ import {
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 { environmentToolDefinitions } from "./environmentTools.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));
@@ -355,3 +359,52 @@ describe("paged list lines", () => {
});
});
});
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" },
});
}
});
});