import { OmittedFields } from "./environmentStateWriteOmitted.js"; import { checkedLines, servedLine } from "./environmentStateCheckedLines.js"; import { describe, expect, it, vi } from "vitest"; import { timedCaptureEvidence, type TimedCaptureRow, } from "../../../tests/fixtures/analysisView.js"; import { BOOK_INIT_ROWS, BOOK_NOT_FOUND, FIRST_BOOK, SECOND_BOOK, THIRD_BOOK, book, bookClock, environmentStateViewOf, renamedBook, type Book, } from "../../../tests/fixtures/environmentStateWrites.js"; import type { JsonValue } from "../jsonValue.js"; import { ok } from "../result.js"; import { checkItemRead, itemTarget, SimulatedState, } from "./environmentStateWriteChecks.js"; import { UnseenEntities } from "./environmentStateWriteAdoption.js"; import type { StateItemView } from "./environmentStateRules.js"; import { laterObservation, type ObservationSearch, } from "./environmentStateWriteObserve.js"; import { updatedFields } from "./environmentStateWriteApply.js"; import { retainFixed, type EffectContext, } from "./environmentStateWriteEffects.js"; /** The plan the gate gives `tool` over a capture of `rows`. */ const gated = (rows: readonly TimedCaptureRow[], tool: string) => { const { writes } = environmentStateViewOf(timedCaptureEvidence(rows)); const plan = writes.find((candidate) => candidate.tool === tool); if (plan === undefined) throw new Error(`The gate planned no ${tool}`); return plan; }; const RENAMED = renamedBook(FIRST_BOOK, "Renamed Book", 3); /** A book whose constant names organisation 1, created at exchange `seconds`. */ const tiedToOrgOne = (id: number, name: string, seconds: number): Book => ({ ...book(id, name, bookClock(seconds)), org: "/orgs/1", }); /** * Two organisations, a create tied to the first by a constant that names it, then the same constant * recorded under the second. `confirmOwn` also reads the created book back from the first. */ const tiedCreateRows = (confirmOwn: boolean): TimedCaptureRow[] => { const alpha = { ...book(1, "Alpha", "2026-10-01T09:00:00Z"), org: "/orgs/1" }; const beta = { ...book(10, "Beta", "2026-10-01T10:00:00Z"), org: "/orgs/2" }; const made = tiedToOrgOne(2, "Made", 4); const other = tiedToOrgOne(11, "Other", confirmOwn ? 6 : 5); const rows: TimedCaptureRow[] = [ ["GET", "/api/orgs/1/books", [alpha]], ["GET", "/api/orgs/2/books", [beta]], ["GET", "/api/orgs/1/books/1", alpha], ["GET", "/api/orgs/2/books/10", beta], ["POST", "/api/orgs/1/books", made, 201, { name: "Made" }], ]; if (confirmOwn) rows.push(["GET", "/api/orgs/1/books/2", made]); rows.push( ["POST", "/api/orgs/2/books", other, 201, { name: "Other" }], ["GET", "/api/orgs/2/books/11", other], ); return rows; }; /** A book whose constant names organisation `acme1`, created at exchange `seconds`. */ const tiedToAcme = (id: number, name: string, seconds: number): Book => ({ ...book(id, name, bookClock(seconds)), team: "/orgs/uid/acme1", }); /** The create plan the gate gives a capture of `rows` when the segment after `uid` is a parameter. */ const acmeCreate = (rows: readonly TimedCaptureRow[]) => { const { writes } = environmentStateViewOf(timedCaptureEvidence(rows), [ "after-uid-literal", ]); const plan = writes.find((candidate) => candidate.effect === "create"); if (plan === undefined) throw new Error("The gate planned no create"); return plan; }; describe("reads compared as the served environment answers them", () => { /** Item reads in a `{data, revision}` envelope, a rename, then a read recorded at `revision`. */ const renamedInEnvelope = (revision: string): TimedCaptureRow[] => [ ["GET", "/api/books", [FIRST_BOOK, SECOND_BOOK]], ["GET", "/api/books/1", { data: FIRST_BOOK, revision: "r1" }], ["GET", "/api/books/2", { data: SECOND_BOOK, revision: "r1" }], ["PATCH", "/api/books/1", RENAMED, 200, { name: "Renamed Book" }], ["GET", "/api/books/1", { data: RENAMED, revision }], ]; it("contradicts a write when an item read's envelope differs from the one served", () => { expect(gated(renamedInEnvelope("r2"), "patch_books_by_id")).toMatchObject({ served: false, reason: "contradicted", contradicted_by: 4, }); expect(gated(renamedInEnvelope("r1"), "patch_books_by_id")).toMatchObject({ served: true, checked_record_ordinals: [4], }); }); /** A list in a `{items, revision}` envelope, a create, then the list recorded at `revision`. */ const createdInEnvelope = (revision: string): TimedCaptureRow[] => [ ["GET", "/api/books", { items: [FIRST_BOOK, SECOND_BOOK], revision: "r1" }], ["GET", "/api/books/1", FIRST_BOOK], ["GET", "/api/books/2", SECOND_BOOK], ["POST", "/api/books", THIRD_BOOK, 201, { name: "Third Book" }], [ "GET", "/api/books", { items: [FIRST_BOOK, SECOND_BOOK, THIRD_BOOK], revision }, ], ]; it("contradicts a write when a list's envelope differs from its served line", () => { expect(gated(createdInEnvelope("r2"), "post_books")).toMatchObject({ served: false, reason: "contradicted", contradicted_by: 4, }); expect(gated(createdInEnvelope("r1"), "post_books")).toMatchObject({ served: true, checked_record_ordinals: [4], }); }); /** A filtered list of both books, a delete of the second, then the filtered list recorded with `listed`. */ const deletedFromFiltered = (listed: readonly Book[]): TimedCaptureRow[] => [ ["GET", "/api/books/1", FIRST_BOOK], ["GET", "/api/books/2", SECOND_BOOK], ["GET", "/api/books?shelf=a", [FIRST_BOOK, SECOND_BOOK]], ["DELETE", "/api/books/2", "", 204], ["GET", "/api/books?shelf=a", [...listed]], ]; it("contradicts a delete that a filtered list still shows, and confirms one it drops", () => { expect( gated( deletedFromFiltered([FIRST_BOOK, SECOND_BOOK]), "delete_books_by_id", ), ).toMatchObject({ served: false, reason: "contradicted", contradicted_by: 4, }); expect( gated(deletedFromFiltered([FIRST_BOOK]), "delete_books_by_id"), ).toMatchObject({ served: true, checked_record_ordinals: [4] }); }); /** A delete confirmed by an item read, then the filtered list recorded with its query reordered. */ const deletedThenReordered = (listed: readonly Book[]): TimedCaptureRow[] => [ ["GET", "/api/books/1", FIRST_BOOK], ["GET", "/api/books/2", SECOND_BOOK], ["GET", "/api/books?shelf=a&sort=id", [FIRST_BOOK, SECOND_BOOK]], ["DELETE", "/api/books/2", "", 204], ["GET", "/api/books/2", BOOK_NOT_FOUND, 404], ["GET", "/api/books?sort=id&shelf=a", [...listed]], ]; it("checks a list read on a checked line however its query is ordered, as serving matches it", () => { // The item read already checked the delete, so a list still showing the book marks it a side effect. expect( gated( deletedThenReordered([FIRST_BOOK, SECOND_BOOK]), "delete_books_by_id", ), ).toMatchObject({ served: false, reason: "side-effect" }); expect( gated(deletedThenReordered([FIRST_BOOK]), "delete_books_by_id"), ).toMatchObject({ served: true, checked_record_ordinals: [4] }); }); }); describe("update constants", () => { const paper = (entry: Book, format = "PAPER"): Book => ({ ...entry, format }); const RENAMED_PAPER = paper(RENAMED); const HARDCOVER = paper( { ...SECOND_BOOK, updated_at: bookClock(5) }, "HARDCOVER", ); /** A rename that keeps the format, then a format change the application stores in capitals. */ const ROWS: TimedCaptureRow[] = [ ["GET", "/api/books", [paper(FIRST_BOOK), paper(SECOND_BOOK)]], ["GET", "/api/books/1", paper(FIRST_BOOK)], ["GET", "/api/books/2", paper(SECOND_BOOK)], ["PATCH", "/api/books/1", RENAMED_PAPER, 200, { name: "Renamed Book" }], ["GET", "/api/books/1", RENAMED_PAPER], ["PATCH", "/api/books/2", HARDCOVER, 200, { format: "hardcover" }], ["GET", "/api/books/2", HARDCOVER], ]; it("stores the value the call that showed a constant recorded, as serving applies the plan", () => { const plan = gated(ROWS, "patch_books_by_id"); expect(plan).toMatchObject({ served: true, observation: { ordinal: 3, pointer: "" }, body: { fixed: [{ member: "format", sent: "hardcover" }] }, checked_record_ordinals: [4, 6], }); // Serving reads the plan's observation, the first update's answer, which still shows PAPER. const served = updatedFields( plan, { before: paper(SECOND_BOOK), observed: RENAMED_PAPER }, { path: "/api/books/2", pathValues: new Map([["id", "2"]]), body: { format: "hardcover" }, id: "2", }, () => ok(bookClock(9)), ); expect(served).toEqual( ok(paper({ ...SECOND_BOOK, updated_at: bookClock(9) }, "HARDCOVER")), ); }); }); describe("write examples", () => { it("takes no modelled effect or exemplar from a redirected write", () => { const rows: TimedCaptureRow[] = [ ...BOOK_INIT_ROWS, ["DELETE", "/api/books/2", "", 302], ["GET", "/api/books/2", SECOND_BOOK], ["DELETE", "/api/books/2", "", 204], ["GET", "/api/books/2", BOOK_NOT_FOUND, 404], ]; expect(gated(rows, "delete_books_by_id")).toMatchObject({ served: true, exemplar_ordinal: 5, checked_record_ordinals: [6], contradicted_by: null, }); }); }); describe("update rules learned from a later call", () => { const versioned = (entry: Book, version: number): Book => ({ ...entry, version, }); /** Two renames, of the first and the second book, each answered at the version given. */ const renames = (first: number, second: number): TimedCaptureRow[] => { const firstAfter = versioned( renamedBook(FIRST_BOOK, "Renamed Book", 3), first, ); const secondAfter = versioned( renamedBook(SECOND_BOOK, "Second Edition", 5), second, ); return [ [ "GET", "/api/books", [versioned(FIRST_BOOK, 1), versioned(SECOND_BOOK, 1)], ], ["GET", "/api/books/1", versioned(FIRST_BOOK, 1)], ["GET", "/api/books/2", versioned(SECOND_BOOK, 1)], ["PATCH", "/api/books/1", firstAfter, 200, { name: "Renamed Book" }], ["GET", "/api/books/1", firstAfter], ["PATCH", "/api/books/2", secondAfter, 200, { name: "Second Edition" }], ["GET", "/api/books/2", secondAfter], ]; }; it("leaves an update unserved when one call increments a field another left as it was", () => { for (const [first, second] of [ [1, 2], [2, 1], ] as const) expect(gated(renames(first, second), "patch_books_by_id")).toMatchObject({ served: false, reason: "unexplained-change", }); expect(gated(renames(2, 2), "patch_books_by_id")).toMatchObject({ served: true, rules: expect.arrayContaining([{ field: "version", rule: "increment" }]), checked_record_ordinals: [4, 6], }); }); }); describe("slug rules learned from a later call", () => { /** A patch of the first book sending `name`, answered with `answered`, then a rename of the second. */ const renames = (name: string, answered: Book): TimedCaptureRow[] => { const second = renamedBook(SECOND_BOOK, "Second Edition", 5); return [ ...BOOK_INIT_ROWS, ["PATCH", "/api/books/1", answered, 200, { name }], ["GET", "/api/books/1", answered], ["PATCH", "/api/books/2", second, 200, { name: "Second Edition" }], ["GET", "/api/books/2", second], ]; }; it("serves a slug rule an earlier call agrees with, and not one it contradicts", () => { const kept = { ...FIRST_BOOK, updated_at: bookClock(3) }; expect( gated(renames("First Book", kept), "patch_books_by_id"), ).toMatchObject({ served: true, rules: expect.arrayContaining([ { field: "slug", rule: "slug", from: "name" }, ]), }); // The application renamed the first book without following its slug. expect( gated( renames("Renamed Book", { ...kept, name: "Renamed Book" }), "patch_books_by_id", ), ).toMatchObject({ served: false, reason: "unexplained-change" }); }); }); describe("clock rules learned across calls", () => { it("serves a clock only when every recorded call changed that field", () => { const oldFirst = "2026-10-01T09:00:00Z"; const oldSecond = "2026-10-01T10:00:00Z"; const described = ( entry: Book, description: string, publishedAt: string, ): Book => ({ ...entry, description, published_at: publishedAt }); const first = described(FIRST_BOOK, "original", oldFirst); const second = described(SECOND_BOOK, "other", oldSecond); const published = described( renamedBook(FIRST_BOOK, "Renamed Book", 3), "original", bookClock(3), ); /** The first call stamps `published_at`; `later` is the second call's answer. */ const rows = (later: Book, sent: Book): TimedCaptureRow[] => [ ["GET", "/api/books", [first, second]], ["GET", "/api/books/1", first], ["GET", "/api/books/2", second], ["PATCH", "/api/books/1", published, 200, { name: "Renamed Book" }], ["GET", "/api/books/1", published], ["PATCH", "/api/books/2", later, 200, sent], ["GET", "/api/books/2", later], ]; // The description edit stamps `updated_at` and leaves `published_at` as it was. const edited = described( { ...SECOND_BOOK, updated_at: bookClock(5) }, "revised", oldSecond, ); expect( gated(rows(edited, { description: "revised" }), "patch_books_by_id"), ).toMatchObject({ served: false, reason: "unexplained-change" }); const stamped = described( renamedBook(SECOND_BOOK, "Second Edition", 5), "other", bookClock(5), ); const plan = gated( rows(stamped, { name: "Second Edition" }), "patch_books_by_id", ); expect(plan).toMatchObject({ served: true, checked_record_ordinals: [4, 6], }); expect(plan.rules).toContainEqual({ field: "published_at", rule: "clock", format: { kind: "iso", separator: "T", fraction: 0, zone: "Z" }, }); }); it("leaves a clock unserved when an earlier call kept the field a later call stamped", () => { const oldFirst = "2026-10-01T09:00:00Z"; const oldSecond = "2026-10-01T10:00:00Z"; const described = ( entry: Book, description: string, publishedAt: string, ): Book => ({ ...entry, description, published_at: publishedAt }); const first = described(FIRST_BOOK, "original", oldFirst); const second = described(SECOND_BOOK, "other", oldSecond); const edited = described( { ...SECOND_BOOK, updated_at: bookClock(3) }, "revised", oldSecond, ); const published = described( renamedBook(FIRST_BOOK, "Renamed Book", 5), "original", bookClock(5), ); const rows: TimedCaptureRow[] = [ ["GET", "/api/books", [first, second]], ["GET", "/api/books/1", first], ["GET", "/api/books/2", second], ["PATCH", "/api/books/2", edited, 200, { description: "revised" }], ["GET", "/api/books/2", edited], ["PATCH", "/api/books/1", published, 200, { name: "Renamed Book" }], ["GET", "/api/books/1", published], ]; expect(gated(rows, "patch_books_by_id")).toMatchObject({ served: false, reason: "unexplained-change", }); }); }); describe("checked lines", () => { /** A list page that echoes its own request path, as some APIs do. */ const page = (path: string, items: JsonValue[]): JsonValue => ({ path, items, }); const PLAIN = "/api/orgs/acme1/books"; const ENCODED = "/api/orgs/%61cme1/books"; const OTHER = "/api/orgs/beta2/books"; /** Three organisations' books under one templated tool, two spelling one organisation differently, then deletes each confirmed by its list. */ const ROWS: TimedCaptureRow[] = [ ["GET", PLAIN, page(PLAIN, [FIRST_BOOK])], ["GET", ENCODED, page(ENCODED, [SECOND_BOOK])], ["GET", OTHER, page(OTHER, [THIRD_BOOK])], ["GET", `${PLAIN}/1`, FIRST_BOOK], ["GET", `${ENCODED}/2`, SECOND_BOOK], ["GET", `${OTHER}/3`, THIRD_BOOK], ["DELETE", `${ENCODED}/2`, "", 204], ["GET", ENCODED, page(ENCODED, [])], ["DELETE", `${OTHER}/3`, "", 204], ["GET", OTHER, page(OTHER, [])], ]; it("keeps a line's path spelling, as replay prefers the verbatim one", () => { const view = environmentStateViewOf(timedCaptureEvidence(ROWS), [ "sibling-variance", ]); expect(view.views).toContainEqual( expect.objectContaining({ shape: "list", checked_record_ordinals: [0, 1, 2], }), ); expect( view.writes.find((candidate) => candidate.effect === "delete"), ).toMatchObject({ served: true, checked_record_ordinals: [7, 9] }); }); /** Organisations listed only under encoded or other spellings, a delete in each, then a list of the first spelled plainly. */ const deletedThenPlain = (listed: readonly Book[]): TimedCaptureRow[] => { const third = "/api/orgs/gamma3/books"; const fourth = book(4, "Fourth Book", "2026-10-01T11:00:00Z"); return [ ["GET", ENCODED, [SECOND_BOOK]], ["GET", OTHER, [THIRD_BOOK]], ["GET", third, [fourth]], ["GET", `${ENCODED}/2`, SECOND_BOOK], ["GET", `${OTHER}/3`, THIRD_BOOK], ["DELETE", `${ENCODED}/2`, "", 204], ["GET", `${ENCODED}/2`, BOOK_NOT_FOUND, 404], ["DELETE", `${OTHER}/3`, "", 204], ["GET", `${OTHER}/3`, BOOK_NOT_FOUND, 404], ["GET", PLAIN, [...listed]], ]; }; it("checks a read spelled plainly against the encoded line serving answers it from", () => { const deleteOf = (listed: readonly Book[]) => environmentStateViewOf(timedCaptureEvidence(deletedThenPlain(listed)), [ "sibling-variance", ]).writes.find((candidate) => candidate.effect === "delete"); // Serving lists the encoded collection for the plain argument, which no longer holds the book. expect(deleteOf([SECOND_BOOK])).toMatchObject({ served: false, reason: "contradicted", contradicted_by: 9, }); expect(deleteOf([])).toMatchObject({ served: true, checked_record_ordinals: [6, 8, 9], }); }); const TWICE = "/api/orgs/%61%63me1/books"; const shelved = (entry: Book, shelf: string): Book => ({ ...entry, shelf }); const ENCODED_LINE: TimedCaptureRow = [ "GET", ENCODED, [shelved(FIRST_BOOK, "a"), shelved(SECOND_BOOK, "b")], ]; const TWICE_LINE: TimedCaptureRow = [ "GET", TWICE, [shelved(THIRD_BOOK, "a"), shelved(RENAMED, "b")], ]; const SHELF_LINE: TimedCaptureRow = [ "GET", `${ENCODED}?shelf=a`, [shelved(FIRST_BOOK, "a")], ]; /** Lines of one organisation spelled two encoded ways, deletes each whole list confirms, then a plain read of new query values still listing the deleted book. */ const deletedThenQueried = ( lines: readonly TimedCaptureRow[], read: string, ): TimedCaptureRow[] => [ ...lines, ["GET", OTHER, [shelved(SECOND_BOOK, "a")]], ["DELETE", `${TWICE}/1`, "", 204], ["DELETE", `${OTHER}/2`, "", 204], ["GET", TWICE, [shelved(THIRD_BOOK, "a")]], ["GET", OTHER, []], ["GET", read, [shelved(RENAMED, "b")]], ]; const deleteOf = (rows: readonly TimedCaptureRow[]) => environmentStateViewOf(timedCaptureEvidence(rows), [ "sibling-variance", ]).writes.find((candidate) => candidate.effect === "delete"); it("checks a read on new query values against the line serving ranks first by the values it shares", () => { const rows = deletedThenQueried( [ ENCODED_LINE, TWICE_LINE, SHELF_LINE, ["GET", `${TWICE}?shelf=b`, [shelved(RENAMED, "b")]], ["GET", `${ENCODED}?limit=1`, [shelved(FIRST_BOOK, "a")]], ], `${PLAIN}?shelf=b&limit=5`, ); // Serving lists the twice-encoded collection, whose `shelf=b` line the read shares a value with. expect(deleteOf(rows)).toMatchObject({ served: false, contradicted_by: 10, }); }); it("checks a read on new query values against the line serving takes first in record order", () => { const rows = deletedThenQueried( [TWICE_LINE, ENCODED_LINE, SHELF_LINE], `${PLAIN}?shelf=b`, ); // No line shares a value, so serving lists the collection of the line recorded first. expect(deleteOf(rows)).toMatchObject({ served: false, contradicted_by: 8, }); }); }); describe("insert rules", () => { it.each([false, true])( "uses the successful retry's page coverage, keeping an unseen update pending (later read: %s)", (laterRead) => { const beta = { id: 1, name: "Beta", note: "first" }; const zulu = { id: 2, name: "Zulu", note: "original" }; const patched = { ...zulu, note: "updated" }; const alpha = { id: 3, name: "Alpha", note: "created" }; const rows: TimedCaptureRow[] = [ ["GET", "/api/books?limit=5", [beta, zulu]], ["GET", "/api/books?limit=1", [beta]], ["GET", "/api/books/1", beta], ["GET", "/api/books/2", zulu], ["PATCH", "/api/books/2", patched, 200, { note: "updated" }], ["POST", "/api/books", alpha, 201, { name: "Alpha", note: "created" }], // The initial page includes Zulu. Retrying the create in name order // replaces it with Alpha, so the passing page proves nothing of Zulu. ["GET", "/api/books?limit=2", [alpha, beta]], ]; if (laterRead) rows.push(["GET", "/api/books/2", patched]); expect(gated(rows, "post_books")).toMatchObject({ served: true, insert: { rule: "sorted", field: "name", direction: "ascending" }, checked_record_ordinals: [6], }); expect(gated(rows, "patch_books_by_id")).toMatchObject({ served: laterRead, ...(laterRead ? {} : { reason: "unconfirmed" }), checked_record_ordinals: laterRead ? [7] : [], }); }, ); it("still learns where a list places a create after an item read confirmed it", () => { const alpha = book(3, "Alpha", bookClock(3)); const rows: TimedCaptureRow[] = [ ...BOOK_INIT_ROWS, ["POST", "/api/books", alpha, 201, { name: "Alpha" }], ["GET", "/api/books/3", alpha], ["GET", "/api/books", [alpha, FIRST_BOOK, SECOND_BOOK]], ]; expect(gated(rows, "post_books")).toMatchObject({ served: true, insert: { rule: "sorted", field: "name", direction: "ascending" }, checked_record_ordinals: [4, 5], }); }); }); describe("reads the served answer no longer matches", () => { it("contradicts an update when a later read's body no longer holds the entity", () => { const rows: TimedCaptureRow[] = [ ["GET", "/api/books", [FIRST_BOOK, SECOND_BOOK]], ["GET", "/api/books/1", { data: FIRST_BOOK }], ["GET", "/api/books/2", { data: SECOND_BOOK }], ["PATCH", "/api/books/1", RENAMED, 200, { name: "Renamed Book" }], ["GET", "/api/books/1", { data: null }], ["GET", "/api/books/1", { data: RENAMED }], ]; // Serving still answers the renamed book, so the read that shows none is a check it fails, not one it skips. expect(gated(rows, "patch_books_by_id")).toMatchObject({ served: false, reason: "contradicted", contradicted_by: 4, }); }); it("confirms no create by the absence of the entity a later delete removed", () => { const rows: TimedCaptureRow[] = [ ...BOOK_INIT_ROWS, ["POST", "/api/books", THIRD_BOOK, 201, { name: "Third Book" }], ["DELETE", "/api/books/3", "", 204], ["GET", "/api/books/3", BOOK_NOT_FOUND, 404], ["GET", "/api/books", [FIRST_BOOK, SECOND_BOOK]], ]; expect(gated(rows, "post_books")).toMatchObject({ served: false, reason: "unconfirmed", checked_record_ordinals: [], }); expect(gated(rows, "delete_books_by_id")).toMatchObject({ served: true, checked_record_ordinals: [5, 6], }); }); it("confirms a create that lacks a field every item recording carried only when its own read leaves the field out too", () => { const detailed = (entry: Book): Book => ({ ...entry, detail: "x" }); const rows = (readBack: Book): TimedCaptureRow[] => [ ["GET", "/api/books", [FIRST_BOOK, SECOND_BOOK]], ["GET", "/api/books/1", detailed(FIRST_BOOK)], ["GET", "/api/books/2", detailed(SECOND_BOOK)], ["POST", "/api/books", THIRD_BOOK, 201, { name: "Third Book" }], ["GET", "/api/books/3", readBack], ]; // The application omits the field for this book, so serving need not invent it. expect(gated(rows(THIRD_BOOK), "post_books")).toMatchObject({ served: true, }); // The read shows the field the create's own answer left out: the state cannot render it. expect(gated(rows(detailed(THIRD_BOOK)), "post_books")).toMatchObject({ served: false, }); }); }); describe("updates whose effect later writes erase", () => { const versioned = (entry: Book, version: number): Book => ({ ...entry, version, }); it("keeps an update confirmed when a later one rewrites only part of what it changed", () => { const again = versioned(renamedBook(FIRST_BOOK, "Again Book", 4), 2); const rows: TimedCaptureRow[] = [ [ "GET", "/api/books", [versioned(FIRST_BOOK, 1), versioned(SECOND_BOOK, 1)], ], ["GET", "/api/books/1", versioned(FIRST_BOOK, 1)], ["GET", "/api/books/2", versioned(SECOND_BOOK, 1)], [ "PATCH", "/api/books/1", versioned(RENAMED, 2), 200, { name: "Renamed Book" }, ], ["PUT", "/api/books/1", again, 200, { name: "Again Book" }], ["GET", "/api/books/1", again], ]; // The PUT keeps the version the PATCH incremented, so the read still shows the PATCH's effect. expect(gated(rows, "patch_books_by_id")).toMatchObject({ served: true, checked_record_ordinals: [5], }); }); it("confirms no update whose changes later writes overwrote between them", () => { const renamed = versioned(renamedBook(FIRST_BOOK, "Again Book", 4), 2); const final = versioned(renamedBook(FIRST_BOOK, "Again Book", 5), 9); const rows: TimedCaptureRow[] = [ [ "GET", "/api/books", [versioned(FIRST_BOOK, 1), versioned(SECOND_BOOK, 1)], ], ["GET", "/api/books/1", versioned(FIRST_BOOK, 1)], ["GET", "/api/books/2", versioned(SECOND_BOOK, 1)], [ "PATCH", "/api/books/1", versioned(RENAMED, 2), 200, { name: "Renamed Book" }, ], ["PUT", "/api/books/1", renamed, 200, { name: "Again Book" }], ["POST", "/api/books/1", final, 200, { version: 9 }], ["GET", "/api/books/1", final], ]; // The PUT overwrote the name, slug and clock, the POST the version: nothing of the PATCH is left to read. expect(gated(rows, "patch_books_by_id")).toMatchObject({ served: false, checked_record_ordinals: [], }); expect(gated(rows, "put_books_by_id")).toMatchObject({ checked_record_ordinals: [6], }); }); it("confirms no update whose changes a recorded outcome replaced", () => { const described = ( entry: Book, name: string, at: number, text: string, ): Book => ({ ...renamedBook(entry, name, at), description: text, }); const final = described(FIRST_BOOK, "Other Book", 7, "z"); const rows: TimedCaptureRow[] = [ [ "GET", "/api/books", [ { ...FIRST_BOOK, description: "d" }, { ...SECOND_BOOK, description: "d" }, ], ], ["GET", "/api/books/1", { ...FIRST_BOOK, description: "d" }], ["GET", "/api/books/2", { ...SECOND_BOOK, description: "d" }], [ "PUT", "/api/books/2", described(SECOND_BOOK, "Second Book", 3, "x"), 200, { description: "x" }, ], [ "PATCH", "/api/books/1", described(FIRST_BOOK, "Renamed Book", 4, "d"), 200, { name: "Renamed Book" }, ], // Without a body the PUT is not modelled: its recorded outcome becomes the book's values. ["PUT", "/api/books/1", described(FIRST_BOOK, "Other Book", 5, "d")], [ "PATCH", "/api/books/1", described(FIRST_BOOK, "Other Book", 6, "y"), 200, { description: "y" }, ], ["PUT", "/api/books/1", final, 200, { description: "z" }], ["GET", "/api/books/1", final], ]; expect(gated(rows, "patch_books_by_id")).toMatchObject({ served: false, checked_record_ordinals: [], }); }); }); describe("reads as serving answers them", () => { it("checks a read with no query on the line serving answers it from, a whole list's paging line", () => { const rows: TimedCaptureRow[] = [ ["GET", "/api/books?limit=50", [FIRST_BOOK, SECOND_BOOK]], ["GET", "/api/books/1", FIRST_BOOK], ["GET", "/api/books/2", SECOND_BOOK], ["POST", "/api/books", THIRD_BOOK, 201, { name: "Third Book" }], ["GET", "/api/books", [FIRST_BOOK, SECOND_BOOK, THIRD_BOOK]], ]; expect(gated(rows, "post_books")).toMatchObject({ served: true, checked_record_ordinals: [4], }); }); it("checks nothing by a line whose query repeats a name, which no call can name", () => { const rows: TimedCaptureRow[] = [ ["GET", "/api/books?tag=a&tag=b", [FIRST_BOOK]], ["GET", "/api/books/1", FIRST_BOOK], ["GET", "/api/books/2", SECOND_BOOK], ["PATCH", "/api/books/1", RENAMED, 200, { name: "Renamed Book" }], ["GET", "/api/books?tag=a&tag=b", [RENAMED]], ]; expect(gated(rows, "patch_books_by_id")).toMatchObject({ checked_record_ordinals: [], }); }); it("checks nothing by a read made while a write to its targets was still running", () => { const rows: TimedCaptureRow[] = [ ["GET", "/api/books", [FIRST_BOOK, SECOND_BOOK]], ["GET", "/api/books/1", FIRST_BOOK], ["GET", "/api/books/2", SECOND_BOOK], // The create runs from second 3 to 4.5; the list at second 4 still shows the books before it. ["POST", "/api/books", THIRD_BOOK, 201, { name: "Third Book" }, 1500], ["GET", "/api/books", [FIRST_BOOK, SECOND_BOOK]], ["GET", "/api/books", [FIRST_BOOK, SECOND_BOOK, THIRD_BOOK]], ]; expect(gated(rows, "post_books")).toMatchObject({ served: true, checked_record_ordinals: [5], contradicted_by: null, }); }); it("refuses an update a later call stores otherwise than the plan echoed", () => { const formatted = (at: number, format: string): Book => ({ ...renamedBook(FIRST_BOOK, "First Book", at), format, }); const rows: TimedCaptureRow[] = [ [ "GET", "/api/books", [ { ...FIRST_BOOK, format: "EBOOK" }, { ...SECOND_BOOK, format: "EBOOK" }, ], ], ["GET", "/api/books/1", { ...FIRST_BOOK, format: "EBOOK" }], ["GET", "/api/books/2", { ...SECOND_BOOK, format: "EBOOK" }], [ "PATCH", "/api/books/1", formatted(3, "PAPER"), 200, { format: "PAPER" }, ], ["GET", "/api/books/1", formatted(3, "PAPER")], [ "PATCH", "/api/books/1", formatted(5, "HARDCOVER"), 200, { format: "hardcover" }, ], ["GET", "/api/books/1", formatted(5, "HARDCOVER")], ]; // One call echoed the format, the other stored another value: no one plan serves both. expect(gated(rows, "patch_books_by_id")).toMatchObject({ served: false, reason: "unexplained-change", }); }); it("learns no update's effect from a read made while the update was still running", () => { const full = (description: string) => ({ id: 1, name: "First", description, }); const summary = [ { id: 1, name: "First" }, { id: 2, name: "Second" }, ]; const rows: TimedCaptureRow[] = [ ["GET", "/api/books", summary], ["GET", "/api/books/1", full("Old desc")], ["GET", "/api/books/2", { id: 2, name: "Second", description: "Other" }], // The update runs from second 3 to 4.5; the read at second 4 still shows the old description. [ "PATCH", "/api/books/1", "", 204, { name: "First", description: "New desc" }, 1500, ], ["GET", "/api/books/1", full("Old desc")], ["GET", "/api/books", summary], ]; expect(gated(rows, "patch_books_by_id")).toMatchObject({ served: false, observation: null, }); }); it("explains a field an update adds that the observing view had shown the entity without", () => { const rows: TimedCaptureRow[] = [ [ "GET", "/api/books", [ { id: 1, name: "A" }, { id: 2, name: "B" }, ], ], ["GET", "/api/books/1", { id: 1, name: "A" }], ["GET", "/api/books/2", { id: 2, name: "B", edited_at: bookClock(0) }], ["PATCH", "/api/books/1", "", 204, { name: "Z" }], ["GET", "/api/books/1", { id: 1, name: "Z", edited_at: bookClock(3) }], [ "GET", "/api/books", [ { id: 1, name: "Z" }, { id: 2, name: "B" }, ], ], ]; const plan = gated(rows, "patch_books_by_id"); expect(plan).toMatchObject({ served: true }); expect(plan.rules).toContainEqual( expect.objectContaining({ field: "edited_at", rule: "clock" }), ); }); }); describe("what a read shows of a write", () => { it("confirms no update by a read that shows none of the fields it changed", () => { const rows: TimedCaptureRow[] = [ [ "GET", "/api/books", [ { id: 1, name: "A" }, { id: 2, name: "B" }, ], ], ["GET", "/api/books/1", { id: 1, name: "A", description: "x" }], ["GET", "/api/books/2", { id: 2, name: "B", description: "y" }], [ "PATCH", "/api/books/1", { id: 1, name: "A", description: "new" }, 200, { description: "new" }, ], // The list passes, but it shows no description. [ "GET", "/api/books", [ { id: 1, name: "A" }, { id: 2, name: "B" }, ], ], ]; expect(gated(rows, "patch_books_by_id")).toMatchObject({ served: false, reason: "unconfirmed", checked_record_ordinals: [], }); }); it("lets a read a create was observed through still contradict another write", () => { const rows: TimedCaptureRow[] = [ ["GET", "/api/books", [FIRST_BOOK, SECOND_BOOK]], ["GET", "/api/books/1", FIRST_BOOK], ["GET", "/api/books/2", SECOND_BOOK], ["PATCH", "/api/books/1", RENAMED, 200, { name: "Renamed Book" }], ["GET", "/api/books/1", RENAMED], // The create answers with its id alone, so the next list shows its entity; that list shows book 1 unrenamed. ["POST", "/api/books", { id: 3 }, 201, { name: "Third Book" }], ["GET", "/api/books", [FIRST_BOOK, SECOND_BOOK, THIRD_BOOK]], ]; expect(gated(rows, "patch_books_by_id")).toMatchObject({ served: false, contradicted_by: 6, }); }); it("confirms no document write a later replacement overwrote", () => { const rows: TimedCaptureRow[] = [ ["GET", "/api/settings", { theme: "light", lang: "en" }], [ "PATCH", "/api/settings", { theme: "dark", lang: "en" }, 200, { theme: "dark" }, ], [ "PUT", "/api/settings", { theme: "blue", lang: "fr" }, 200, { theme: "blue", lang: "fr" }, ], ["GET", "/api/settings", { theme: "blue", lang: "fr" }], ]; expect(gated(rows, "patch_settings")).toMatchObject({ served: false, checked_record_ordinals: [], }); expect(gated(rows, "put_settings")).toMatchObject({ served: true, checked_record_ordinals: [3], }); }); it("confirms a document write whose path encodes a parent the plain read holds", () => { const rows: TimedCaptureRow[] = [ ["GET", "/api/orgs/uid/acme1/settings", { theme: "light", lang: "en" }], [ "PATCH", "/api/orgs/uid/%61cme1/settings", { theme: "dark", lang: "en" }, 200, { theme: "dark" }, ], ["GET", "/api/orgs/uid/acme1/settings", { theme: "dark", lang: "en" }], ]; const { writes } = environmentStateViewOf(timedCaptureEvidence(rows), [ "after-uid-literal", ]); expect(writes.find((plan) => plan.effect === "document")).toMatchObject({ served: true, checked_record_ordinals: [2], }); }); }); describe("document writes later writes overwrite", () => { it("confirms no document replacement a later merge overwrote", () => { const rows: TimedCaptureRow[] = [ ["GET", "/api/settings", { theme: "light", lang: "en" }], [ "PUT", "/api/settings", { theme: "dark", lang: "fr" }, 200, { theme: "dark", lang: "fr" }, ], [ "PATCH", "/api/settings", { theme: "blue", lang: "de" }, 200, { theme: "blue", lang: "de" }, ], ["GET", "/api/settings", { theme: "blue", lang: "de" }], ]; expect(gated(rows, "put_settings")).toMatchObject({ served: false, checked_record_ordinals: [], }); expect(gated(rows, "patch_settings")).toMatchObject({ served: true, checked_record_ordinals: [3], }); }); it("confirms no document merge a later replacement of its object overwrote", () => { const rows: TimedCaptureRow[] = [ ["GET", "/api/settings", { prefs: { lang: "en" }, theme: "light" }], [ "PATCH", "/api/settings", { prefs: { lang: "fr" }, theme: "light" }, 200, { prefs: { lang: "fr" } }, ], // The replacement holds `lang`, so it replaces all of `prefs`, which the merge wrote. [ "PUT", "/api/settings", { prefs: { lang: "de" }, theme: "light" }, 200, { lang: "de" }, ], ["GET", "/api/settings", { prefs: { lang: "de" }, theme: "light" }], ]; expect(gated(rows, "patch_settings")).toMatchObject({ served: false, checked_record_ordinals: [], }); expect(gated(rows, "put_settings")).toMatchObject({ served: true, checked_record_ordinals: [3], }); }); }); describe("reads named as serving names them", () => { const orgBook = (id: number, org: string, name: string): Book => ({ id, org, name, }); it("contradicts an update by a read spelled plainly of the encoded collection serving answers it from", () => { const alpha = orgBook(1, "acme1", "Alpha"); const renamed = orgBook(1, "acme1", "Renamed"); const beta = orgBook(2, "beta2", "Beta"); const renamedBeta = orgBook(2, "beta2", "Renamed Beta"); const rows: TimedCaptureRow[] = [ ["GET", "/api/orgs/%61cme1/books", [alpha]], ["GET", "/api/orgs/beta2/books", [beta]], ["GET", "/api/orgs/%61cme1/books/1", alpha], ["GET", "/api/orgs/beta2/books/2", beta], ["PATCH", "/api/orgs/%61cme1/books/1", renamed, 200, { name: "Renamed" }], [ "PATCH", "/api/orgs/beta2/books/2", renamedBeta, 200, { name: "Renamed Beta" }, ], // Serving answers this plain spelling with the renamed book, which the recording shows absent. ["GET", "/api/orgs/acme1/books/1", null], ["GET", "/api/orgs/%61cme1/books/1", renamed], ["GET", "/api/orgs/beta2/books/2", renamedBeta], ]; const { writes } = environmentStateViewOf(timedCaptureEvidence(rows), [ "sibling-variance", ]); expect(writes.find((plan) => plan.effect === "update")).toMatchObject({ served: false, contradicted_by: 6, }); }); it("confirms a delete by a plain absence of an entity held under the encoded collection", () => { const alpha = orgBook(1, "acme1", "Alpha"); const beta = orgBook(2, "beta2", "Beta"); const rows: TimedCaptureRow[] = [ ["GET", "/api/orgs/%61cme1/books", [alpha]], ["GET", "/api/orgs/beta2/books", [beta]], ["GET", "/api/orgs/%61cme1/books/1", alpha], ["GET", "/api/orgs/beta2/books/2", beta], ["DELETE", "/api/orgs/acme1/books/1", "", 204], ["GET", "/api/orgs/acme1/books/1", BOOK_NOT_FOUND, 404], ["DELETE", "/api/orgs/beta2/books/2", "", 204], ["GET", "/api/orgs/beta2/books/2", BOOK_NOT_FOUND, 404], ]; const { writes } = environmentStateViewOf(timedCaptureEvidence(rows), [ "sibling-variance", ]); expect(writes.find((plan) => plan.effect === "delete")).toMatchObject({ served: true, checked_record_ordinals: expect.arrayContaining([5]), }); }); it("learns a plain-path update from a later read under the encoded collection", () => { const alpha = orgBook(1, "acme1", "Alpha"); const renamed = orgBook(1, "acme1", "Renamed"); const beta = orgBook(2, "beta2", "Beta"); const renamedBeta = orgBook(2, "beta2", "Renamed Beta"); const rows: TimedCaptureRow[] = [ ["GET", "/api/orgs/%61cme1/books", [alpha]], ["GET", "/api/orgs/beta2/books", [beta]], ["GET", "/api/orgs/%61cme1/books/1", alpha], ["GET", "/api/orgs/beta2/books/2", beta], ["PATCH", "/api/orgs/acme1/books/1", "", 204, { name: "Renamed" }], ["GET", "/api/orgs/%61cme1/books/1", renamed], ["GET", "/api/orgs/%61cme1/books/1", renamed], [ "PATCH", "/api/orgs/beta2/books/2", renamedBeta, 200, { name: "Renamed Beta" }, ], ["GET", "/api/orgs/beta2/books/2", renamedBeta], ]; const { writes } = environmentStateViewOf(timedCaptureEvidence(rows), [ "sibling-variance", ]); expect(writes.find((plan) => plan.effect === "update")).toMatchObject({ served: true, observation: { ordinal: 5 }, checked_record_ordinals: expect.arrayContaining([6]), }); }); it("checks a line whose query repeats a name with one value, which a call still names", () => { const rows: TimedCaptureRow[] = [ ["GET", "/api/books?tag=a&tag=a", [FIRST_BOOK]], ["GET", "/api/books/1", FIRST_BOOK], ["GET", "/api/books/2", SECOND_BOOK], ["PATCH", "/api/books/1", RENAMED, 200, { name: "Renamed Book" }], // The list still shows the old name, though the item read after it shows the rename. ["GET", "/api/books?tag=a&tag=a", [FIRST_BOOK]], ["GET", "/api/books/1", RENAMED], ]; expect(gated(rows, "patch_books_by_id")).toMatchObject({ served: false, contradicted_by: 4, }); }); it("ties a create named plainly to the encoded collection serving creates it in", () => { const linked = (id: number, org: string, name: string) => ({ id, name, href: `/orgs/${org}/books/${String(id)}`, }); const rows: TimedCaptureRow[] = [ ["GET", "/api/orgs/%61cme1/books", [linked(1, "acme1", "Alpha")]], ["GET", "/api/orgs/beta2/books", [linked(2, "beta2", "Beta")]], ["GET", "/api/orgs/%61cme1/books/1", linked(1, "acme1", "Alpha")], ["GET", "/api/orgs/beta2/books/2", linked(2, "beta2", "Beta")], [ "POST", "/api/orgs/acme1/books", linked(3, "acme1", "Gamma"), 201, { name: "Gamma" }, ], ["GET", "/api/orgs/acme1/books/3", linked(3, "acme1", "Gamma")], [ "POST", "/api/orgs/beta2/books", linked(4, "beta2", "Delta"), 201, { name: "Delta" }, ], ]; const { writes } = environmentStateViewOf(timedCaptureEvidence(rows), [ "sibling-variance", ]); expect(writes.find((plan) => plan.effect === "create")).toMatchObject({ served: true, collection: "/api/orgs/%61cme1/books", }); }); }); describe("encoded parent spellings", () => { const type = "/api/orgs/uid/{uid}/books"; const collection = "/api/orgs/uid/acme1/books"; /** A state holding one book under its parent's plain spelling. */ const plainlyHeld = () => new SimulatedState( [ { type, collection, id: "1", fields: { id: 1, name: "Alpha" }, view_fields: [], record_ordinals: [], }, ], [], ); it("names the plainly held entity from a path that encodes its parent", () => { expect( itemTarget(plainlyHeld(), type, { path: "/api/orgs/uid/%61cme1/books/1", }), ).toEqual({ collection, id: "1" }); }); it("lists the type's entities only when the path's own collection holds no such id", () => { const state = plainlyHeld(); const listed = vi.spyOn(state, "ofType"); expect( itemTarget(state, type, { path: "/api/orgs/uid/acme1/books/1" }), ).toEqual({ collection, id: "1" }); expect(listed).not.toHaveBeenCalled(); expect( itemTarget(state, type, { path: "/api/orgs/uid/%61cme1/books/1" }), ).toEqual({ collection, id: "1" }); expect(listed).toHaveBeenCalledTimes(1); }); it("answers an encoded read from the plain checked line", () => { const tool = { name: "get_orgs_books_by_uid", method: "GET", path_template: "/api/orgs/uid/{uid}/books", record_ordinals: [0], }; const record = { ordinal: 0, method: "GET", path: "/api/orgs/uid/acme1/books", query: undefined, status: 200, start: 0, end: 0, }; const lines = checkedLines({ tools: [tool], records: [record], views: new Map([ [ tool.name, { tool: tool.name, type: "/api/orgs/uid/{uid}/books", shape: "list", pointer: "", fields: ["id"], required_fields: ["id"], checked_record_ordinals: [0], membership: "all", counters: [], }, ], ]), }); expect( servedLine(lines.get(tool.name), { ...record, ordinal: 1, path: "/api/orgs/uid/%61cme1/books", }), ).toEqual({ ordinal: 0, collection: "/api/orgs/uid/acme1/books", named: true, }); }); it("confirms an update whose path encodes a parent the plain reads hold", () => { const alpha = { id: 1, org: "acme1", name: "Alpha" }; const renamed = { id: 1, org: "acme1", name: "Renamed" }; const rows: TimedCaptureRow[] = [ ["GET", "/api/orgs/uid/acme1/books", [alpha]], ["GET", "/api/orgs/uid/acme1/books/1", alpha], [ "PATCH", "/api/orgs/uid/%61cme1/books/1", renamed, 200, { name: "Renamed" }, ], ["GET", "/api/orgs/uid/acme1/books/1", renamed], ]; const { writes } = environmentStateViewOf(timedCaptureEvidence(rows), [ "after-uid-literal", ]); expect(writes.find((plan) => plan.effect === "update")).toMatchObject({ served: true, checked_record_ordinals: [3], }); }); }); describe("writes named as serving names them", () => { const orgBook = ( id: number, org: string, name: string, archived = false, ) => ({ id, org, name, archived, }); const ALPHA = orgBook(1, "acme1", "Alpha"); const BETA = orgBook(2, "beta2", "Beta"); const RENAMED_BETA = orgBook(2, "beta2", "Renamed Beta"); const INIT_ROWS: readonly TimedCaptureRow[] = [ ["GET", "/api/orgs/%61cme1/books", [ALPHA]], ["GET", "/api/orgs/beta2/books", [BETA]], ["GET", "/api/orgs/%61cme1/books/1", ALPHA], ["GET", "/api/orgs/beta2/books/2", BETA], ]; const RENAME_BETA: readonly TimedCaptureRow[] = [ [ "PATCH", "/api/orgs/beta2/books/2", RENAMED_BETA, 200, { name: "Renamed Beta" }, ], ["GET", "/api/orgs/beta2/books/2", RENAMED_BETA], ]; const CREATE_DELTA: readonly TimedCaptureRow[] = [ [ "POST", "/api/orgs/beta2/books", orgBook(4, "beta2", "Delta"), 201, { name: "Delta" }, ], ]; const planned = (rows: readonly TimedCaptureRow[], effect: string) => environmentStateViewOf(timedCaptureEvidence(rows), [ "sibling-variance", ]).writes.find((plan) => plan.effect === effect); it("contradicts a create named plainly by a list of the encoded collection it went to that omits it", () => { const gamma = orgBook(3, "acme1", "Gamma"); const rows: TimedCaptureRow[] = [ ...INIT_ROWS, ["POST", "/api/orgs/acme1/books", gamma, 201, { name: "Gamma" }], ["GET", "/api/orgs/acme1/books/3", gamma], // Serving lists the create in the encoded collection, which the recording shows without it. ["GET", "/api/orgs/%61cme1/books", [ALPHA]], ...CREATE_DELTA, ]; expect(planned(rows, "create")).toMatchObject({ served: false, contradicted_by: 6, }); }); it("leaves unknown the entity a plainly named action changed under the encoded collection", () => { const archived = orgBook(1, "acme1", "Alpha", true); const rows: TimedCaptureRow[] = [ ...INIT_ROWS, ["POST", "/api/orgs/acme1/books/1/archive", archived, 200], // The rename's answer also shows the archive, which no rule of the rename explains. [ "PATCH", "/api/orgs/acme1/books/1", orgBook(1, "acme1", "Renamed", true), 200, { name: "Renamed" }, ], ...RENAME_BETA, ]; expect(planned(rows, "update")).toMatchObject({ served: true, reason: null, }); }); it("checks no list of the encoded collection after a plainly named write at the collection", () => { const gamma = orgBook(3, "acme1", "Gamma"); const rows: TimedCaptureRow[] = [ ...INIT_ROWS, ["DELETE", "/api/orgs/acme1/books", "", 204], ["POST", "/api/orgs/acme1/books", gamma, 201, { name: "Gamma" }], ["GET", "/api/orgs/acme1/books/3", gamma], // The cleared collection now lists only the create, which the state alone cannot show. ["GET", "/api/orgs/%61cme1/books", [gamma]], ...CREATE_DELTA, ]; expect(planned(rows, "create")).toMatchObject({ served: true, contradicted_by: null, }); }); it("checks no read under the encoded collection after a plainly named write above it", () => { const rows: TimedCaptureRow[] = [ ...INIT_ROWS, [ "PATCH", "/api/orgs/%61cme1/books/1", orgBook(1, "acme1", "Renamed"), 200, { name: "Renamed" }, ], ["DELETE", "/api/orgs/acme1", "", 204], // The organisation's books went with it, which the state alone cannot show. ["GET", "/api/orgs/%61cme1/books/1", BOOK_NOT_FOUND, 404], ...RENAME_BETA, ]; expect(planned(rows, "update")).toMatchObject({ served: true, contradicted_by: null, }); }); }); describe("resolved encoded item observations", () => { it("matches the resolved raw ID when the collection alias and response ID are encoded", () => { const collection = "/api/orgs/%61cme1/books"; const state = new SimulatedState( [ { type: "/api/orgs/{org_id}/books", collection, id: "%61", fields: { id: "%61", name: "Before" }, view_fields: [], record_ordinals: [], }, ], [], ); const write = { ordinal: 0, method: "PATCH", path: "/api/orgs/acme1/books/%61", query: undefined, status: 204, start: 0, end: 0, }; const target = itemTarget(state, "/api/orgs/{org_id}/books", write); const itemView = { tool: "get_books_by_id", type: "/api/orgs/{org_id}/books", shape: "item" as const, pointer: "", fields: ["id", "name"], required_fields: ["id"], checked_record_ordinals: [1], envelope_ordinal: 1, }; const search: ObservationSearch = { records: [ write, { ordinal: 1, method: "GET", path: "/api/orgs/acme1/books/%61", query: undefined, status: 200, start: 1, end: 1, }, ], from: 0, viewOf: () => itemView, withBody: (_ordinal, use) => use({ id: "%61", name: "After" }), queryOf: () => undefined, }; expect(target).toEqual({ collection, id: "%61" }); expect( laterObservation(search, { type: itemView.type, ...target, idField: "id", held: () => true, isWrite: (record) => record.method !== "GET", sameItem: (record) => { const resolved = itemTarget(state, itemView.type, record); return ( resolved.collection === target.collection && resolved.id === target.id ); }, }), ).toEqual({ ordinal: 1, pointer: "", object: { id: "%61", name: "After" }, id: "%61", }); }); }); describe("what a write's own answer pins down", () => { it("observes a create that answers with its id alone only through a read of that id", () => { const fourth = book(4, "Fourth Book", "2026-10-01T11:00:00Z"); const rows: TimedCaptureRow[] = [ ["GET", "/api/books", [FIRST_BOOK, SECOND_BOOK]], ["GET", "/api/books/1", FIRST_BOOK], ["GET", "/api/books/2", SECOND_BOOK], ["POST", "/api/books", { id: 3 }, 201, { name: "Third Book" }], // A book the capture never listed is not the one the create made. ["GET", "/api/books/4", fourth], ["GET", "/api/books/4", fourth], ["GET", "/api/books/3", THIRD_BOOK], ["GET", "/api/books/3", THIRD_BOOK], ]; expect(gated(rows, "post_books")).toMatchObject({ observation: { ordinal: 6, pointer: "" }, checked_record_ordinals: [7], }); }); it("keeps an update served when a later call is the first to send the path's id in its body", () => { const renamedSecond = renamedBook(SECOND_BOOK, "Renamed Second", 5); const rows: TimedCaptureRow[] = [ ["GET", "/api/books", [FIRST_BOOK, SECOND_BOOK]], ["GET", "/api/books/1", FIRST_BOOK], ["GET", "/api/books/2", SECOND_BOOK], ["PATCH", "/api/books/1", RENAMED, 200, { name: "Renamed Book" }], ["GET", "/api/books/1", RENAMED], [ "PATCH", "/api/books/2", renamedSecond, 200, { id: 2, name: "Renamed Second" }, ], ["GET", "/api/books/2", renamedSecond], ]; expect(gated(rows, "patch_books_by_id")).toMatchObject({ served: true, body: { identity: "id" }, }); }); }); it("counts the sent values a plan keeps for fixed members against the view's allowance", () => { let retained = 0; const context = { io: { retain: (bytes: number) => (retained += bytes) }, } as unknown as EffectContext; retainFixed(context, [{ sent: "x".repeat(10_000) }]); expect(retained).toBeGreaterThan(20_000); retained = 0; retainFixed(context, []); expect(retained).toBe(0); }); describe("what a check leaves pending", () => { const summary = (books: readonly Book[]): JsonValue => books.map(({ id, name }) => ({ id: id ?? null, name: name ?? null })); const described = (id: number, name: string, description: string) => ({ id, name, description, }); it("keeps an update a passing read showed nothing of for a later read to confirm", () => { const rows: TimedCaptureRow[] = [ ["GET", "/api/books", summary([FIRST_BOOK, SECOND_BOOK])], ["GET", "/api/books/1", described(1, "First Book", "x")], ["GET", "/api/books/2", described(2, "Second Book", "y")], [ "PATCH", "/api/books/1", described(1, "First Book", "new"), 200, { description: "new" }, ], // The list passes but shows no description; the item read does. ["GET", "/api/books", summary([FIRST_BOOK, SECOND_BOOK])], ["GET", "/api/books/1", described(1, "First Book", "new")], ]; expect(gated(rows, "patch_books_by_id")).toMatchObject({ served: true, checked_record_ordinals: [5], }); }); it("checks only a partial update's fields that remain after a later overwrite", () => { const patched = described(1, "Patched", "survives"); const replaced = described(1, "Replaced", "survives"); const changedByOtherAction = described(1, "Archived", "survives"); const rows: TimedCaptureRow[] = [ ["GET", "/api/books", summary([FIRST_BOOK, SECOND_BOOK])], ["GET", "/api/books/1", described(1, "First Book", "original")], ["GET", "/api/books/2", described(2, "Second Book", "other")], [ "PATCH", "/api/books/1", patched, 200, { name: "Patched", description: "survives" }, ], ["PUT", "/api/books/1", replaced, 200, { name: "Replaced" }], ["POST", "/api/books/1/archive", changedByOtherAction, 204, {}], ["GET", "/api/books/1", changedByOtherAction], ]; expect(gated(rows, "patch_books_by_id")).toMatchObject({ served: true, checked_record_ordinals: [6], contradicted_by: null, }); }); it("checks a surviving sent member after a derived field was bumped by an unmodelled write", () => { const entry = ( name: string, description: string, version: number, ): Book => ({ id: 1, name, description, version }); const other = { id: 2, name: "B", description: "y", version: 1 }; const rows: TimedCaptureRow[] = [ ["GET", "/api/books", [entry("A", "x", 1), other]], ["GET", "/api/books/1", entry("A", "x", 1)], ["GET", "/api/books/2", other], [ "PATCH", "/api/books/1", entry("A2", "new", 2), 200, { name: "A2", description: "new" }, ], ["PUT", "/api/books/1", entry("A3", "new", 2), 200, { name: "A3" }], ["POST", "/api/books/1/archive", { archived: true }, 204, {}], // The read still shows the description the patch sent. The version bump is not that patch. ["GET", "/api/books/1", entry("A3", "new", 9)], ]; expect(gated(rows, "patch_books_by_id")).toMatchObject({ served: true, rules: expect.arrayContaining([{ field: "version", rule: "increment" }]), checked_record_ordinals: [6], contradicted_by: null, }); }); it("neither resynchronises nor checks an entity by a read begun while a write to it still ran", () => { const rows: TimedCaptureRow[] = [ ["GET", "/api/books", [FIRST_BOOK, SECOND_BOOK]], ["GET", "/api/books/1", FIRST_BOOK], ["GET", "/api/books/2", SECOND_BOOK], // The rename runs from second 3 to 5.5, and an unmodelled action leaves book 1 unknown. ["PATCH", "/api/books/1", RENAMED, 200, { name: "Renamed Book" }, 2500], ["POST", "/api/books/1/archive", null, 204, {}], ["GET", "/api/books/1", FIRST_BOOK], ["GET", "/api/books/1", RENAMED], ]; expect(gated(rows, "patch_books_by_id")).toMatchObject({ served: true, checked_record_ordinals: [6], contradicted_by: null, }); }); it("learns an update from a read begun after the write, not from one that overlapped it", () => { const named = (name: string) => ({ id: 1, name }); const other = { id: 2, name: "B" }; const rows: TimedCaptureRow[] = [ ["GET", "/api/books", [named("A"), other]], ["GET", "/api/books/1", named("A")], ["GET", "/api/books/2", other], // The patch runs from second 3 to 5.5 and returns no entity. A read begun while it still runs shows the old name. ["PATCH", "/api/books/1", "", 204, { name: "Z" }, 2500], ["GET", "/api/books/1", named("A")], ["GET", "/api/books/1", named("A")], ["GET", "/api/books/1", named("Z")], ["GET", "/api/books", [named("Z"), other]], ]; expect(gated(rows, "patch_books_by_id")).toMatchObject({ served: true, observation: { ordinal: 6, pointer: "" }, body: { accepted: ["name"], fixed: [] }, rules: [{ field: "name", rule: "echo" }], checked_record_ordinals: [7], contradicted_by: null, }); }); it("checks nothing by a range read the collection has outgrown", () => { const rows: TimedCaptureRow[] = [ ["GET", "/api/books?_start=0&_end=2", [FIRST_BOOK, SECOND_BOOK]], ["GET", "/api/books/1", FIRST_BOOK], ["GET", "/api/books/2", SECOND_BOOK], ["POST", "/api/books", THIRD_BOOK, 201, { name: "Third Book" }], // The application still answers the range with two books. ["GET", "/api/books?_start=0&_end=2", [FIRST_BOOK, SECOND_BOOK]], ["GET", "/api/books/3", THIRD_BOOK], ]; expect(gated(rows, "post_books")).toMatchObject({ served: true, checked_record_ordinals: [5], }); }); }); describe("what a later call must follow", () => { it("rejects a later create whose unique field repeats a value already held", () => { const existing = { id: "123e4567-e89b-42d3-a456-426614174000", ref: "223e4567-e89b-42d3-a456-426614174000", name: "Existing", }; const first = { id: "323e4567-e89b-42d3-a456-426614174000", ref: "423e4567-e89b-42d3-a456-426614174000", name: "First", }; const duplicate = { id: "523e4567-e89b-42d3-a456-426614174000", ref: first.ref, name: "Second", }; const rows: TimedCaptureRow[] = [ ["GET", "/api/items", [existing]], ["GET", `/api/items/${existing.id}`, existing], ["POST", "/api/items", first, 201, { name: "First" }], ["GET", `/api/items/${first.id}`, first], ["GET", "/api/items", [existing, first]], ["POST", "/api/items", duplicate, 201, { name: "Second" }], ["GET", `/api/items/${duplicate.id}`, duplicate], ["GET", "/api/items", [existing, first, duplicate]], ]; expect(gated(rows, "post_items")).toMatchObject({ served: false, reason: "unexplained-change", }); }); it("refuses a create whose later call does not follow the sequence it learned", () => { const ranked = (at: Book, rank: number) => ({ ...at, rank }); const third = ranked(book(3, "Third Book", bookClock(3)), 3); const fourth = ranked(book(4, "Fourth Book", bookClock(4)), 3); const rows: TimedCaptureRow[] = [ ["GET", "/api/books", [ranked(FIRST_BOOK, 1), ranked(SECOND_BOOK, 2)]], ["GET", "/api/books/1", ranked(FIRST_BOOK, 1)], ["GET", "/api/books/2", ranked(SECOND_BOOK, 2)], ["POST", "/api/books", third, 201, { name: "Third Book" }], // Serving ranks the next create 4; the application recorded 3 again. ["POST", "/api/books", fourth, 201, { name: "Fourth Book" }], [ "GET", "/api/books", [ranked(FIRST_BOOK, 1), ranked(SECOND_BOOK, 2), third, fourth], ], ]; expect(gated(rows, "post_books")).toMatchObject({ served: false, reason: "unexplained-change", }); }); it("keeps a document write a later replacement of another member left in place", () => { const rows: TimedCaptureRow[] = [ ["GET", "/api/settings", { theme: "light", prefs: { lang: "en" } }], [ "PATCH", "/api/settings", { theme: "dark", prefs: { lang: "en" } }, 200, { theme: "dark" }, ], // The replacement holds `lang`, so it replaces `prefs` alone. [ "PUT", "/api/settings", { theme: "dark", prefs: { lang: "fr" } }, 200, { lang: "fr" }, ], ["GET", "/api/settings", { theme: "dark", prefs: { lang: "fr" } }], ]; expect(gated(rows, "patch_settings")).toMatchObject({ served: true, checked_record_ordinals: [3], }); expect(gated(rows, "put_settings")).toMatchObject({ served: true, checked_record_ordinals: [3], }); }); it("applies a later document replacement at the plan's pointer, though it shares no member with the document", () => { const rows: TimedCaptureRow[] = [ ["GET", "/api/settings", { theme: "light", lang: "en" }], [ "PUT", "/api/settings", { theme: "dark", lang: "fr" }, 200, { theme: "dark", lang: "fr" }, ], ["PUT", "/api/settings", { theme: "blue" }, 200, { theme: "blue" }], ["PUT", "/api/settings", { lang: "de" }, 200, { lang: "de" }], ["GET", "/api/settings", { lang: "de" }], ]; expect(gated(rows, "put_settings")).toMatchObject({ served: true, checked_record_ordinals: [4], }); }); it("finds an update's entity as serving does when the write spells the collection plainly", () => { const alpha = { id: 1, org: "acme1", name: "Alpha" }; const beta = { id: 2, org: "beta2", name: "Beta" }; const renamed = { ...alpha, name: "Renamed" }; const renamedBeta = { ...beta, name: "Renamed Beta" }; const rows: TimedCaptureRow[] = [ ["GET", "/api/orgs/%61cme1/books", [alpha]], ["GET", "/api/orgs/beta2/books", [beta]], ["GET", "/api/orgs/%61cme1/books/1", alpha], ["GET", "/api/orgs/beta2/books/2", beta], ["PATCH", "/api/orgs/acme1/books/1", renamed, 200, { name: "Renamed" }], [ "PATCH", "/api/orgs/beta2/books/2", renamedBeta, 200, { name: "Renamed Beta" }, ], ["GET", "/api/orgs/%61cme1/books/1", renamed], ["GET", "/api/orgs/beta2/books/2", renamedBeta], ]; const { writes } = environmentStateViewOf(timedCaptureEvidence(rows), [ "sibling-variance", ]); expect(writes.find((plan) => plan.effect === "update")).toMatchObject({ served: true, checked_record_ordinals: [6, 7], }); }); }); describe("what a write's effect keeps", () => { it("keeps an entity member a later write's other member does not cover, though both names look like pointers", () => { const flat = (id: number, prefs: string, lang: string) => ({ id, name: `Book ${String(id)}`, "/prefs": prefs, "/prefs/lang": lang, }); const rows: TimedCaptureRow[] = [ ["GET", "/api/books", [flat(1, "x", "en"), flat(2, "x", "en")]], ["GET", "/api/books/1", flat(1, "x", "en")], ["GET", "/api/books/2", flat(2, "x", "en")], [ "PATCH", "/api/books/1", flat(1, "x", "fr"), 200, { "/prefs/lang": "fr" }, ], ["PUT", "/api/books/1", flat(1, "y", "fr"), 200, { "/prefs": "y" }], ["GET", "/api/books/1", flat(1, "y", "fr")], ]; expect(gated(rows, "patch_books_by_id")).toMatchObject({ served: true, checked_record_ordinals: [5], }); }); const subtitled = (id: number, name: string, subtitle?: string) => subtitle === undefined ? { id, name } : { id, name, subtitle }; const summaries = (first: string): TimedCaptureRow => [ "GET", "/api/books", [subtitled(1, first), subtitled(2, "B")], ]; it("refuses an update a later read shows dropped a field serving would keep", () => { const rows: TimedCaptureRow[] = [ summaries("A"), ["GET", "/api/books/1", subtitled(1, "A", "s")], ["GET", "/api/books/2", subtitled(2, "B", "t")], ["PATCH", "/api/books/1", "", 204, { name: "Z" }], ["GET", "/api/books/1", subtitled(1, "Z")], summaries("Z"), ]; expect(gated(rows, "patch_books_by_id")).toMatchObject({ served: false, reason: "unexplained-change", }); }); it("keeps an update whose partial response leaves out a field a later read still shows", () => { const rows: TimedCaptureRow[] = [ summaries("A"), ["GET", "/api/books/1", subtitled(1, "A", "s")], ["GET", "/api/books/2", subtitled(2, "B", "t")], ["PATCH", "/api/books/1", subtitled(1, "Z"), 200, { name: "Z" }], ["GET", "/api/books/1", subtitled(1, "Z", "s")], ]; expect(gated(rows, "patch_books_by_id")).toMatchObject({ served: true, checked_record_ordinals: [4], }); }); }); describe("what an unmodelled write explains", () => { it("weighs an unmodelled write against a tool's call on the entity a read checks, not its latest call elsewhere", () => { const tagged = (id: number, name: string, tag: string) => ({ id, name, tag, }); const rows: TimedCaptureRow[] = [ ["GET", "/api/books", [tagged(1, "A", "x"), tagged(2, "B", "x")]], ["GET", "/api/books/1", tagged(1, "A", "x")], ["GET", "/api/books/2", tagged(2, "B", "x")], ["PATCH", "/api/books/1", tagged(1, "A2", "x"), 200, { name: "A2" }], ["POST", "/api/exports", { queued: true }, 202, { format: "csv" }], ["PATCH", "/api/books/2", tagged(2, "B2", "x"), 200, { name: "B2" }], // The export came after book 1's rename, so it may explain the changed tag; the rename itself shows. ["GET", "/api/books/1", tagged(1, "A2", "y")], ]; expect(gated(rows, "patch_books_by_id")).toMatchObject({ served: true, contradicted_by: null, checked_record_ordinals: [6], }); }); it("lets an unmodelled write explain a list's change to an entity a tool made before it, whatever the tool did after", () => { const fourth = book(4, "Fourth Book", bookClock(5)); const rows: TimedCaptureRow[] = [ ["GET", "/api/books", [FIRST_BOOK, SECOND_BOOK]], ["GET", "/api/books/1", FIRST_BOOK], ["GET", "/api/books/2", SECOND_BOOK], ["POST", "/api/books", THIRD_BOOK, 201, { name: "Third Book" }], ["POST", "/api/exports", { queued: true }, 202, { format: "csv" }], ["POST", "/api/books", fourth, 201, { name: "Fourth Book" }], ["GET", "/api/books/4", fourth], // The export came after the third book was made, so it may explain the third book's other name. [ "GET", "/api/books", [FIRST_BOOK, SECOND_BOOK, { ...THIRD_BOOK, name: "Retitled" }, fourth], ], ]; expect(gated(rows, "post_books")).toMatchObject({ served: true, contradicted_by: null, }); }); it("weighs no unmodelled write against a call whose effect a later write overwrote", () => { const named = (id: number, name: string) => ({ ...book(id, name, bookClock(0)), id, }); const rows: TimedCaptureRow[] = [ ["GET", "/api/books", [named(1, "A"), named(2, "B")]], ["GET", "/api/books/1", named(1, "A")], ["GET", "/api/books/2", named(2, "B")], ["PATCH", "/api/books/1", named(1, "A2"), 200, { name: "A2" }], ["POST", "/api/exports", { queued: true }, 202, { format: "csv" }], // The PUT overwrites the patch's only change on book 1. ["PUT", "/api/books/1", named(1, "A3"), 200, { name: "A3" }], ["PATCH", "/api/books/2", named(2, "B2"), 200, { name: "B2" }], ["GET", "/api/books/2", named(2, "B2")], // The export came before book 2's patch, which the list shows undone. ["GET", "/api/books", [named(1, "A3"), named(2, "B")]], ]; expect(gated(rows, "patch_books_by_id")).toMatchObject({ served: false, contradicted_by: 8, }); }); it("contradicts by a failed item read the writes no unmodelled write came after, checking only the others' members", () => { const enveloped = (data: Record, revision: string) => ({ data, revision, }); const full = (name: string, description: string) => ({ id: 1, name, description, }); const summary = [ { id: 1, name: "A" }, { id: 2, name: "B" }, ]; const rows: TimedCaptureRow[] = [ ["GET", "/api/books", summary], ["GET", "/api/books/1", enveloped(full("A", "x"), "r1")], [ "GET", "/api/books/2", enveloped({ id: 2, name: "B", description: "y" }, "r1"), ], ["PATCH", "/api/books/1", full("A", "new"), 200, { description: "new" }], ["GET", "/api/books", summary], ["POST", "/api/exports", { queued: true }, 202, { format: "csv" }], // The rename sends the name alone, so the earlier description patch stays pending. ["PUT", "/api/books/1", full("Z", "new"), 200, { name: "Z" }], // The rename shows, but the envelope serving would answer with does not. ["GET", "/api/books/1", enveloped(full("Z", "new"), "r2")], ]; expect(gated(rows, "put_books_by_id")).toMatchObject({ served: false, contradicted_by: 7, }); }); it("contradicts by a list a write made after an unmodelled one, though the tool's earlier change elsewhere came before it", () => { const named = (id: number, name: string) => ({ ...book(id, name, bookClock(0)), id, }); const rows: TimedCaptureRow[] = [ ["GET", "/api/books", [named(1, "A"), named(2, "B")]], ["GET", "/api/books/1", named(1, "A")], ["GET", "/api/books/2", named(2, "B")], ["PATCH", "/api/books/1", named(1, "A2"), 200, { name: "A2" }], ["POST", "/api/exports", { queued: true }, 202, { format: "csv" }], ["PATCH", "/api/books/2", named(2, "B2"), 200, { name: "B2" }], ["GET", "/api/books/2", named(2, "B2")], // Only book 2 differs, and its patch came after the export. ["GET", "/api/books", [named(1, "A2"), named(2, "B")]], ]; expect(gated(rows, "patch_books_by_id")).toMatchObject({ served: false, contradicted_by: 7, }); }); it("contradicts a write made after an unmodelled one, though an earlier pending write came before it", () => { const summary = (name: string) => [ { id: 1, name }, { id: 2, name: "B" }, ]; const full = (name: string, description: string) => ({ id: 1, name, description, }); const rows: TimedCaptureRow[] = [ ["GET", "/api/books", summary("A")], ["GET", "/api/books/1", full("A", "x")], ["GET", "/api/books/2", { id: 2, name: "B", description: "y" }], ["PATCH", "/api/books/1", full("A", "new"), 200, { description: "new" }], ["GET", "/api/books", summary("A")], ["POST", "/api/exports", { queued: true }, 202, { format: "csv" }], [ "PUT", "/api/books/1", full("Z", "new"), 200, { name: "Z", description: "new" }, ], ["GET", "/api/books/1", full("Z", "new")], // The export came before the rename, so it cannot explain the list showing the old name. ["GET", "/api/books", summary("A")], ]; expect(gated(rows, "put_books_by_id")).toMatchObject({ served: false, contradicted_by: 8, }); }); }); describe("reads and calls on an entity an unmodelled write left unknown", () => { const named = (name: string) => ({ id: 1, name }); const other = { id: 2, name: "B" }; const ARCHIVE: TimedCaptureRow = [ "POST", "/api/books/1/archive", null, 204, {}, ]; it("keeps the entity unknown through a failed read that is no absence, for a later read to check", () => { const rows: TimedCaptureRow[] = [ ["GET", "/api/books", [named("A"), other]], ["GET", "/api/books/1", named("A")], ["GET", "/api/books/2", other], ["PATCH", "/api/books/1", named("Z"), 200, { name: "Z" }], ARCHIVE, // The application was briefly unavailable, so this answer shows nothing of the book. ["GET", "/api/books/1", { error: "unavailable" }, 503], ["GET", "/api/books/1", named("Z")], ]; expect(gated(rows, "patch_books_by_id")).toMatchObject({ served: true, checked_record_ordinals: [6], contradicted_by: null, }); }); it("confirms no update of a volatile field by a read that shows the entity again, as no other read does", () => { const seen = (id: number, count: number) => ({ id, name: id === 1 ? "A" : "B", seen: count, }); const rows = (archive: boolean): TimedCaptureRow[] => [ ["GET", "/api/books", [seen(1, 1), seen(2, 1)]], ["GET", "/api/books/1", seen(1, 1)], // The same read again shows another count, so the count changes with no write. ["GET", "/api/books/1", seen(1, 2)], ["GET", "/api/books/2", seen(2, 1)], ["PATCH", "/api/books/1", seen(1, 5), 200, { seen: 5 }], ...(archive ? [ARCHIVE] : []), ["GET", "/api/books/1", seen(1, 9)], ]; for (const archive of [false, true]) expect(gated(rows(archive), "patch_books_by_id")).toMatchObject({ served: false, reason: "unconfirmed", checked_record_ordinals: [], }); }); it("adds nothing to an update's plan from a call it cannot check", () => { const rows: TimedCaptureRow[] = [ ["GET", "/api/books", [named("A"), other]], ["GET", "/api/books/1", named("A")], ["GET", "/api/books/2", other], ["PATCH", "/api/books/1", named("Z"), 200, { name: "Z" }], ["GET", "/api/books/1", named("Z")], ARCHIVE, // The application stored this name otherwise than sent; a call on a known book would conflict with the echo. ["PATCH", "/api/books/1", named("Y!"), 200, { name: "y" }], ]; expect(gated(rows, "patch_books_by_id")).toMatchObject({ served: true, body: { accepted: ["name"], fixed: [] }, rules: [{ field: "name", rule: "echo" }], checked_record_ordinals: [4], }); }); }); describe("volatile fields", () => { it("confirms no write through a volatile field a read renders from itself", () => { const seen = (id: number, name: string, at: string) => ({ id, name, seen: at, }); const rows: TimedCaptureRow[] = [ ["GET", "/api/books", [seen(1, "A", "t1"), seen(2, "B", "t1")]], ["GET", "/api/books/1", seen(1, "A", "t1")], // A repeated read with no write between shows `seen` changing on its own, so it is volatile. ["GET", "/api/books/1", seen(1, "A", "t2")], ["GET", "/api/books/2", seen(2, "B", "t1")], ["PATCH", "/api/books/1", seen(1, "A", "t3"), 200, { seen: "t3" }], ["POST", "/api/exports", { queued: true }, 202, { format: "csv" }], // The export may explain the new name, and `seen` renders from the read, so nothing shows the patch. ["GET", "/api/books/1", seen(1, "Z", "t9")], ]; expect(gated(rows, "patch_books_by_id")).toMatchObject({ served: false, checked_record_ordinals: [], }); }); }); describe("entities an unmodelled write made", () => { const IMPORT: TimedCaptureRow = [ "POST", "/api/imports", { queued: true }, 202, { source: "csv" }, ]; const RENAME: TimedCaptureRow = [ "PATCH", "/api/books/1", renamedBook(FIRST_BOOK, "Renamed Book", 3), 200, { name: "Renamed Book" }, ]; const deletedAfterRead = (before: readonly TimedCaptureRow[]) => [ ...BOOK_INIT_ROWS, ...before, ["GET", "/api/books/3", THIRD_BOOK] as TimedCaptureRow, ["DELETE", "/api/books/3", "", 204] as TimedCaptureRow, ["GET", "/api/books/3", BOOK_NOT_FOUND, 404] as TimedCaptureRow, ]; it("serves a delete of a book an unmodelled import made, which a later item read showed", () => { // The rename ends the initial reads, so the third book is no initial one. expect( gated(deletedAfterRead([RENAME, IMPORT]), "delete_books_by_id"), ).toMatchObject({ served: true, checked_record_ordinals: [7] }); }); it("takes a book the state never held only when a write the gate does not model came first", () => { expect( gated(deletedAfterRead([RENAME, IMPORT]), "delete_books_by_id"), ).toMatchObject({ served: true, checked_record_ordinals: [7] }); // Only a modelled rename came first, so nothing explains the third book and the delete finds none. expect( gated(deletedAfterRead([RENAME]), "delete_books_by_id"), ).toMatchObject({ served: false, checked_record_ordinals: [], }); }); it("takes a book an import may have made though it was redirected or went unanswered", () => { for (const status of [303, 0]) { const unresolved: TimedCaptureRow = [ "POST", "/api/imports", { queued: true }, status, { source: "csv" }, ]; expect( gated(deletedAfterRead([RENAME, unresolved]), "delete_books_by_id"), ).toMatchObject({ served: true, checked_record_ordinals: [7] }); } }); it("counts an entity it takes against the view's allowance", () => { let retained = 0; const read = { ...THIRD_BOOK, notes: "x".repeat(10_000) }; const unseen = new UnseenEntities([], { state: new SimulatedState([], []), io: { withBody: (_ordinal, use) => use(read), retain: (bytes) => (retained += bytes), }, lastCheck: new Map(), idField: () => "id", }); unseen.unplanned(0); retained = 0; const view = { shape: "item", type: "/api/books", pointer: "" }; const record = { ordinal: 1, method: "GET", path: "/api/books/3", query: undefined, status: 200, start: 1, end: 1, }; expect( unseen.adopt(view as unknown as StateItemView, { record, index: 1 }), ).toMatchObject({ id: "3", fields: read }); expect(retained).toBeGreaterThan(20_000); }); it("finds no read's target among the held entities until an unplanned write came before the read", () => { const state = new SimulatedState([], []); const targets = vi.spyOn(state, "ofType"); const unseen = new UnseenEntities([], { state, io: { withBody: (_ordinal, use) => use(THIRD_BOOK), retain: () => 0 }, lastCheck: new Map(), idField: () => "id", }); const view = { shape: "item", type: "/api/books", pointer: "" }; const read = (index: number) => ({ record: { ordinal: index, method: "GET", path: "/api/books/3", query: undefined, status: 200, start: index, end: index, }, index, }); const adopt = (index: number) => unseen.adopt(view as unknown as StateItemView, read(index)); expect(adopt(1)).toBeUndefined(); // A write after the read cannot have made what it shows either. unseen.unplanned(2); expect(adopt(1)).toBeUndefined(); expect(targets).not.toHaveBeenCalled(); expect(adopt(3)).toMatchObject({ id: "3" }); }); it("takes back no book a modelled delete removed, so a list showing it again confirms no delete", () => { expect( gated(deletedAfterRead([RENAME, IMPORT]), "delete_books_by_id"), ).toMatchObject({ served: true }); const rows: TimedCaptureRow[] = [ ...BOOK_INIT_ROWS, ["POST", "/api/books", THIRD_BOOK, 201, { name: "Third Book" }], ["GET", "/api/books/3", THIRD_BOOK], ["DELETE", "/api/books/3", "", 204], IMPORT, // The import put the deleted book back; the copy must not take it as one the import made. ["GET", "/api/books/3", THIRD_BOOK], ["GET", "/api/books", [FIRST_BOOK, SECOND_BOOK, THIRD_BOOK]], ]; expect(gated(rows, "delete_books_by_id")).toMatchObject({ served: false, checked_record_ordinals: [], }); }); }); describe("an unmodelled write under another collection spelling", () => { it("keeps a confirmed update when the write spells the held collection plainly", () => { const acme = (name: string, status: string) => ({ id: 1, org: "acme1", name, status, }); const beta = (name: string) => ({ id: 2, org: "beta2", name, status: "open", }); const rows: TimedCaptureRow[] = [ ["GET", "/api/orgs/%61cme1/books", [acme("Alpha", "open")]], ["GET", "/api/orgs/beta2/books", [beta("Beta")]], ["GET", "/api/orgs/%61cme1/books/1", acme("Alpha", "open")], ["GET", "/api/orgs/beta2/books/2", beta("Beta")], [ "PATCH", "/api/orgs/acme1/books/1", acme("Renamed", "open"), 200, { name: "Renamed" }, ], [ "PATCH", "/api/orgs/beta2/books/2", beta("Renamed Beta"), 200, { name: "Renamed Beta" }, ], ["GET", "/api/orgs/%61cme1/books/1", acme("Renamed", "open")], ["GET", "/api/orgs/beta2/books/2", beta("Renamed Beta")], // Book 1 is held under the encoded collection. This archive spells it plainly. ["POST", "/api/orgs/acme1/books/1/archive", null, 204, {}], // The response shows the status the archive changed. The patch sent only the name. [ "PATCH", "/api/orgs/%61cme1/books/1", acme("Retitled", "archived"), 200, { name: "Retitled" }, ], ]; const { writes } = environmentStateViewOf(timedCaptureEvidence(rows), [ "sibling-variance", ]); expect(writes.find((plan) => plan.effect === "update")).toMatchObject({ served: true, reason: null, checked_record_ordinals: [6, 7], }); }); }); describe("timed write observations and generated clocks", () => { it("does not learn an update from a read that began before the write completed", () => { const changed = { ...FIRST_BOOK, name: "Changed", slug: "changed" }; const rows: TimedCaptureRow[] = [ ...BOOK_INIT_ROWS, [ "PATCH", "/api/books/1", { name: "Changed" }, 200, { name: "Changed" }, 2_000, ], // This read starts before the PATCH ends and still shows the old value. ["GET", "/api/books/1", FIRST_BOOK], ["GET", "/api/books/1", changed], ["GET", "/api/books/1", changed], ]; expect(gated(rows, "patch_books_by_id")).toMatchObject({ served: true, observation: { ordinal: 5 }, checked_record_ordinals: [6], }); }); it("refuses a later update that leaves a learned clock unchanged", () => { const afterFirst = { ...FIRST_BOOK, name: "First update", updated_at: bookClock(4), }; const afterSecond = { ...afterFirst, name: "Second update" }; const rows: TimedCaptureRow[] = [ ...BOOK_INIT_ROWS, ["PATCH", "/api/books/1", afterFirst, 200, { name: "First update" }], ["GET", "/api/books/1", afterFirst], [ "PATCH", "/api/books/1", { id: 1, name: "Second update" }, 200, { name: "Second update" }, ], ["GET", "/api/books/1", afterSecond], ]; expect(gated(rows, "patch_books_by_id")).toMatchObject({ served: false, reason: "unexplained-change", }); }); it("keeps a later update whose regenerated clock advances", () => { const afterFirst = { ...FIRST_BOOK, name: "First update", updated_at: bookClock(4), }; const afterSecond = { ...afterFirst, name: "Second update", updated_at: bookClock(6), }; const rows: TimedCaptureRow[] = [ ...BOOK_INIT_ROWS, [ "PATCH", "/api/books/1", { name: "First update" }, 200, { name: "First update" }, ], ["GET", "/api/books/1", afterFirst], [ "PATCH", "/api/books/1", { id: 1, name: "Second update" }, 200, { name: "Second update" }, ], ["GET", "/api/books/1", afterSecond], ["GET", "/api/books/1", afterSecond], ]; expect(gated(rows, "patch_books_by_id")).toMatchObject({ served: true, checked_record_ordinals: [7], }); }); it("leaves a later update unknown when neither its answer nor a read before the next write shows its learned clock", () => { const summary = ({ id, name }: Book): Book => ({ id: id!, name: name! }); const renamed = renamedBook(FIRST_BOOK, "Renamed Book", 3); const retitled = renamedBook(renamed, "Retitled Book", 5); const rows = (answer: JsonValue, status: number): TimedCaptureRow[] => [ ["GET", "/api/books", [FIRST_BOOK, SECOND_BOOK]], ["GET", "/api/books/1", summary(FIRST_BOOK)], ["GET", "/api/books/2", summary(SECOND_BOOK)], ["PATCH", "/api/books/1", renamed, 200, { name: "Renamed Book" }], ["GET", "/api/books", [renamed, SECOND_BOOK]], ["PATCH", "/api/books/1", answer, status, { name: "Retitled Book" }], // The item view shows no clock; the list shows the one the second call stamped. ["GET", "/api/books/1", summary(retitled)], ["GET", "/api/books", [retitled, SECOND_BOOK]], ]; expect(gated(rows("", 204), "patch_books_by_id")).toMatchObject({ served: true, rules: expect.arrayContaining([ expect.objectContaining({ field: "updated_at", rule: "clock" }), ]), checked_record_ordinals: [4], contradicted_by: null, }); // An answer that shows the clock observes the call, so the list checks it. expect(gated(rows(retitled, 200), "patch_books_by_id")).toMatchObject({ served: true, checked_record_ordinals: [4, 6, 7], }); }); }); describe("partial update observations", () => { it("keeps write-response fields when the item view omits them", () => { const first: Book = { ...FIRST_BOOK, description: "First description" }; const second: Book = { ...SECOND_BOOK, description: "Second description" }; const itemSummary = (entry: Book): Book => ({ id: entry.id!, name: entry.name!, }); const renamed = { ...first, name: "Renamed first", slug: "renamed-first", updated_at: bookClock(3), }; const rows: TimedCaptureRow[] = [ ["GET", "/api/books", [first, second]], ["GET", "/api/books/1", itemSummary(first)], ["GET", "/api/books/2", itemSummary(second)], ["PATCH", "/api/books/1", renamed, 200, { name: "Renamed first" }], ["GET", "/api/books/1", itemSummary(renamed)], [ "PATCH", "/api/books/2", { id: 2, name: second.name!, description: "Revised description" }, 200, { description: "Revised description" }, ], // This item view covers only id/name, not description or the learned clock. ["GET", "/api/books/2", itemSummary(second)], ]; expect(gated(rows, "patch_books_by_id")).toMatchObject({ served: true, reason: null, checked_record_ordinals: [4], rules: [ { field: "name", rule: "echo" }, { field: "slug", rule: "slug", from: "name" }, { field: "updated_at", rule: "clock" }, { field: "description", rule: "echo" }, ], }); }); it("uses read coverage without dropping fields shown by the write response", () => { const first: Book = { ...FIRST_BOOK, description: "First description" }; const second: Book = { ...SECOND_BOOK, description: "Second description" }; const itemSummary = (entry: Book): Book => ({ id: entry.id!, name: entry.name!, updated_at: entry.updated_at!, }); const renamed = { ...first, name: "Renamed first", slug: "renamed-first", updated_at: bookClock(3), }; const revised = { id: 2, name: second.name!, description: "Revised description", }; const revisedSummary = { id: 2, name: second.name!, updated_at: bookClock(6), }; const rows: TimedCaptureRow[] = [ ["GET", "/api/books", [first, second]], ["GET", "/api/books/1", itemSummary(first)], ["GET", "/api/books/2", itemSummary(second)], ["PATCH", "/api/books/1", renamed, 200, { name: "Renamed first" }], ["GET", "/api/books/1", itemSummary(renamed)], [ "PATCH", "/api/books/2", revised, 200, { description: "Revised description" }, ], // The item view covers the clock, but not the description returned by PATCH. ["GET", "/api/books/2", revisedSummary], ]; expect(gated(rows, "patch_books_by_id")).toMatchObject({ served: true, reason: null, checked_record_ordinals: [4], rules: [ { field: "name", rule: "echo" }, { field: "slug", rule: "slug", from: "name" }, { field: "updated_at", rule: "clock" }, { field: "description", rule: "echo" }, ], }); }); }); describe("what later calls must keep consistent", () => { it("refuses an update one call sends an ignored id with and another the path's id", () => { const renamed = (at: Book, name: string) => ({ ...at, name }); const rows: TimedCaptureRow[] = [ ...BOOK_INIT_ROWS, [ "PATCH", "/api/books/1", renamed(FIRST_BOOK, "B"), 200, { id: 999, name: "B" }, ], ["GET", "/api/books/1", renamed(FIRST_BOOK, "B")], [ "PATCH", "/api/books/2", renamed(SECOND_BOOK, "C"), 200, { id: 2, name: "C" }, ], ["GET", "/api/books/2", renamed(SECOND_BOOK, "C")], ]; expect(gated(rows, "patch_books_by_id")).toMatchObject({ served: false, reason: "unexplained-change", }); }); it("refuses a create whose later call records a clock in another format", () => { const epoch = (iso: string) => Math.floor(Date.parse(iso) / 1000); const fourth = { ...book(4, "Fourth Book", bookClock(5)), created_at: epoch(bookClock(5)), updated_at: epoch(bookClock(5)), }; const rows: TimedCaptureRow[] = [ ...BOOK_INIT_ROWS, ["POST", "/api/books", THIRD_BOOK, 201, { name: "Third Book" }], ["GET", "/api/books/3", THIRD_BOOK], ["POST", "/api/books", fourth, 201, { name: "Fourth Book" }], ["GET", "/api/books/4", fourth], ]; expect(gated(rows, "post_books")).toMatchObject({ served: false, reason: "unexplained-change", }); }); it("names no entity by a delete whose path id decodes to it only twice, as serving would not", () => { const uid = (key: string) => ({ uid: key, name: key }); const rows: TimedCaptureRow[] = [ ["GET", "/api/books", [uid("book1"), uid("book2")]], ["GET", "/api/books/uid/book1", uid("book1")], ["GET", "/api/books/uid/book2", uid("book2")], ["DELETE", "/api/books/uid/book%2531", "", 204], ["GET", "/api/books/uid/book1", BOOK_NOT_FOUND, 404], ]; const { writes } = environmentStateViewOf(timedCaptureEvidence(rows), [ "after-uid-literal", ]); expect(writes.find((plan) => plan.effect === "delete")).toMatchObject({ served: false, checked_record_ordinals: [], }); }); it("does not let a later empty ?page=2 page contradict a create", () => { const alpha = { id: 1, name: "Alpha", org: "1" }; const beta = { id: 2, name: "Beta", org: "2" }; const rows: TimedCaptureRow[] = [ ["GET", "/api/orgs/1/books", [alpha]], ["GET", "/api/orgs/1/books/1", alpha], ["GET", "/api/orgs/2/books?page=2", []], ["POST", "/api/orgs/2/books", beta, 201, { name: "Beta" }], ["GET", "/api/orgs/2/books/2", beta], ["GET", "/api/orgs/2/books?page=2", []], ]; const { writes } = environmentStateViewOf(timedCaptureEvidence(rows)); expect(writes.find((plan) => plan.effect === "create")).toMatchObject({ served: true, checked_record_ordinals: [4], }); }); it.each(["offset", "start", "_start"])( "does not let a later empty ?%s=1 page contradict a create", (name) => { const alpha = { id: 1, name: "Alpha", org: "1" }; const beta = { id: 2, name: "Beta", org: "2" }; const rows: TimedCaptureRow[] = [ ["GET", "/api/orgs/1/books", [alpha]], ["GET", "/api/orgs/1/books/1", alpha], ["GET", `/api/orgs/2/books?${name}=1`, []], ["POST", "/api/orgs/2/books", beta, 201, { name: "Beta" }], ["GET", "/api/orgs/2/books/2", beta], ["GET", `/api/orgs/2/books?${name}=1`, []], ]; const { writes } = environmentStateViewOf(timedCaptureEvidence(rows)); expect(writes.find((plan) => plan.effect === "create")).toMatchObject({ served: true, checked_record_ordinals: [4], }); }, ); it("checks nothing by a whole line recorded empty under a zero limit once a create fills its collection", () => { const alpha = { id: 1, name: "Alpha", org: "1" }; const beta = { id: 2, name: "Beta", org: "2" }; const rows: TimedCaptureRow[] = [ ["GET", "/api/orgs/1/books", [alpha]], ["GET", "/api/orgs/1/books/1", alpha], ["GET", "/api/orgs/2/books?limit=0", []], ["POST", "/api/orgs/2/books", beta, 201, { name: "Beta" }], ["GET", "/api/orgs/2/books/2", beta], // An empty recording does not show that zero lists every entity. ["GET", "/api/orgs/2/books?limit=0", []], ]; const { writes } = environmentStateViewOf(timedCaptureEvidence(rows)); expect(writes.find((plan) => plan.effect === "create")).toMatchObject({ served: true, checked_record_ordinals: [4], }); }); }); describe("creates tied to one collection", () => { it("does not let a later create in another collection confirm the plan", () => { expect(gated(tiedCreateRows(false), "post_orgs_books_by_id")).toMatchObject( { served: false, reason: "unconfirmed", collection: "/api/orgs/1/books", checked_record_ordinals: [], }, ); }); it("still confirms the plan from a read of the collection it is tied to", () => { expect(gated(tiedCreateRows(true), "post_orgs_books_by_id")).toMatchObject({ served: true, collection: "/api/orgs/1/books", checked_record_ordinals: [5], }); }); const acmeRows = ( laterPath: string, heldEncoded: boolean, ): TimedCaptureRow[] => { const alpha = { ...book(1, "Alpha", "2026-10-01T09:00:00Z"), team: "/orgs/uid/acme1", }; const made = tiedToAcme(2, "Made", 2); const other = tiedToAcme(3, "Other", heldEncoded ? 5 : 3); const rows: TimedCaptureRow[] = [ ["GET", "/api/orgs/uid/acme1/books", [alpha]], ["GET", "/api/orgs/uid/acme1/books/1", alpha], ]; if (heldEncoded) { const encoded = { ...book(10, "Encoded", "2026-10-01T10:00:00Z"), team: "/orgs/uid/acme1", }; rows.push( ["GET", "/api/orgs/uid/%61cme1/books", [encoded]], ["GET", "/api/orgs/uid/%61cme1/books/10", encoded], ); } rows.push( ["POST", "/api/orgs/uid/acme1/books", made, 201, { name: "Made" }], ["POST", laterPath, other, 201, { name: "Other" }], ["GET", "/api/orgs/uid/acme1/books/3", other], ); return rows; }; it("confirms a tied create whose later call spells the held collection encoded", () => { expect( acmeCreate(acmeRows("/api/orgs/uid/%61cme1/books", false)), ).toMatchObject({ served: true, collection: "/api/orgs/uid/acme1/books", checked_record_ordinals: [4], }); }); it("does not treat an encoded spelling as the tied collection when that spelling is held itself", () => { // Serving keeps the verbatim encoded collection, outside the plain tie. The plain read of the // entity that create did not add is related to the exemplar, so the plan is a side effect. expect( acmeCreate(acmeRows("/api/orgs/uid/%61cme1/books", true)), ).toMatchObject({ served: false, reason: "side-effect", collection: "/api/orgs/uid/acme1/books", checked_record_ordinals: [], }); }); it("chooses among emptied spellings of a tied collection in the order serving remembers them", () => { const first = "/api/orgs/uid/%61cme1/books"; const second = "/api/orgs/uid/a%63me1/books"; const alpha = { ...book(1, "Alpha", "2026-10-01T09:00:00Z"), team: "/orgs/uid/acme1", }; const beta = { ...book(10, "Beta", "2026-10-01T10:00:00Z"), team: "/orgs/uid/acme1", }; const made = tiedToAcme(2, "Made", 4); const other = tiedToAcme(3, "Other", 8); // The second spelling empties before the first, but serving still offers the first spelling first. const rows: TimedCaptureRow[] = [ ["GET", first, [alpha]], ["GET", second, [beta]], ["GET", `${first}/1`, alpha], ["GET", `${second}/10`, beta], ["POST", second, made, 201, { name: "Made" }], ["DELETE", `${second}/10`, "", 204], ["DELETE", `${second}/2`, "", 204], ["DELETE", `${first}/1`, "", 204], ["POST", "/api/orgs/uid/acme1/books", other, 201, { name: "Other" }], ["GET", "/api/orgs/uid/acme1/books/3", other], ]; expect(acmeCreate(rows)).toMatchObject({ collection: second, checked_record_ordinals: [], }); }); it("keeps a tied collection ahead of another spelling once its first entity is gone, as serving orders them", () => { const first = "/api/orgs/uid/%61cme1/books"; const second = "/api/orgs/uid/a%63me1/books"; const alpha = { ...book(1, "Alpha", "2026-10-01T09:00:00Z"), team: "/orgs/uid/acme1", }; const beta = { ...book(10, "Beta", "2026-10-01T10:00:00Z"), team: "/orgs/uid/acme1", }; const made = tiedToAcme(2, "Made", 4); const other = tiedToAcme(3, "Other", 6); // Serving places the created book beside its collection, so the first spelling still leads after its first book goes. const rows: TimedCaptureRow[] = [ ["GET", first, [alpha]], ["GET", second, [beta]], ["GET", `${first}/1`, alpha], ["GET", `${second}/10`, beta], ["POST", first, made, 201, { name: "Made" }], ["DELETE", `${first}/1`, "", 204], ["POST", "/api/orgs/uid/acme1/books", other, 201, { name: "Other" }], ["GET", "/api/orgs/uid/acme1/books/3", other], ]; expect(acmeCreate(rows)).toMatchObject({ served: true, collection: first, checked_record_ordinals: [7], }); }); }); describe("path fields of a create", () => { const held = "/api/orgs/uid/%2561cme1/books"; /** A create held under a doubly encoded parent, then one whose call spells the parent once encoded, read back with `org`. */ const rowsWith = (org: string): TimedCaptureRow[] => { const alpha = { ...book(1, "Alpha", "2026-10-01T09:00:00Z"), org: "%61cme1", }; const made = { ...book(2, "Made", bookClock(2)), org: "%61cme1" }; const other = { ...book(3, "Other", bookClock(3)), org }; return [ ["GET", held, [alpha]], ["GET", `${held}/1`, alpha], ["POST", held, made, 201, { name: "Made" }], ["POST", "/api/orgs/uid/%61cme1/books", other, 201, { name: "Other" }], ["GET", `${held}/3`, other], ]; }; const planOf = (rows: TimedCaptureRow[]) => environmentStateViewOf(timedCaptureEvidence(rows), [ "after-uid-literal", ]).writes.find((candidate) => candidate.effect === "create"); it("contradicts a later create whose read shows a parent serving would not write", () => { expect(planOf(rowsWith("acme1"))).toMatchObject({ served: false, checked_record_ordinals: [], contradicted_by: 4, }); }); it("confirms a later create whose read shows the parent decoded from the collection serving selects", () => { expect(planOf(rowsWith("%61cme1"))).toMatchObject({ served: true, checked_record_ordinals: [4], }); }); }); describe("path fields of a create tied to its collection", () => { const held = "/api/orgs/uid/%61cme1/books"; /** A tied create in the held encoded collection, then one spelled double-encoded whose parent the application stored as `org`. */ const parentRows = (org: string): TimedCaptureRow[] => { const alpha = { ...book(1, "Alpha", "2026-10-01T09:00:00Z"), team: "/orgs/uid/acme1", org: "acme1", }; const made = { ...tiedToAcme(2, "Made", 2), org: "acme1" }; const other = { ...tiedToAcme(3, "Other", 3), org }; return [ ["GET", held, [alpha]], ["GET", `${held}/1`, alpha], ["POST", held, made, 201, { name: "Made" }], ["POST", "/api/orgs/uid/%2561cme1/books", other, 201, { name: "Other" }], ["GET", `${held}/3`, other], ]; }; it("contradicts a later create whose read shows a parent serving would not write", () => { // Serving selects the held collection and decodes the parent from it, not from the call's spelling. expect(acmeCreate(parentRows("%61cme1"))).toMatchObject({ served: false, collection: held, rules: expect.arrayContaining([ expect.objectContaining({ field: "org", rule: "path" }), ]), checked_record_ordinals: [], contradicted_by: 4, }); }); it("confirms a later create whose read shows the parent decoded from the selected collection", () => { expect(acmeCreate(parentRows("acme1"))).toMatchObject({ served: true, collection: held, checked_record_ordinals: [4], }); }); }); it("withholds a create when a sibling read through an encoded collection reveals an extra change", () => { const alpha = { id: 1, name: "Alpha" }; const beta = { id: 2, name: "Beta" }; const gamma = { id: 3, name: "Gamma" }; const delta = { id: 4, name: "Delta" }; const rows: TimedCaptureRow[] = [ ["GET", "/api/orgs/%61cme1/books", [alpha]], ["GET", "/api/orgs/beta2/books", [beta]], ["GET", "/api/orgs/%61cme1/books/1", alpha], ["GET", "/api/orgs/beta2/books/2", beta], ["POST", "/api/orgs/acme1/books", gamma, 201, { name: "Gamma" }], ["GET", "/api/orgs/acme1/books/3", gamma], ["GET", "/api/orgs/%61cme1/books/1", { ...alpha, name: "Changed" }], ["POST", "/api/orgs/beta2/books", delta, 201, { name: "Delta" }], ]; const state = environmentStateViewOf(timedCaptureEvidence(rows), [ "sibling-variance", ]); expect(state.writes.find((plan) => plan.effect === "create")).toMatchObject({ served: false, reason: "side-effect", }); }); it("learns omitted fields using the raw ID resolved by the item owner", () => { const state = new SimulatedState( [ { type: "/api/books", collection: "/api/books", id: "%63", fields: { id: "%63", name: "Created" }, view_fields: [], record_ordinals: [], }, ], [], ); const view = { tool: "get_books_by_id", type: "/api/books", shape: "item" as const, pointer: "", fields: ["id", "name", "detail"], required_fields: ["id", "name", "detail"], checked_record_ordinals: [0], envelope_ordinal: 0, }; const record = { ordinal: 1, method: "GET", path: "/api/books/%63", query: undefined, status: 200, start: 1, end: 1, }; const target = itemTarget(state, view.type, record); expect(target.id).toBe("%63"); const read = { body: { id: "%63", name: "Created" }, base: { id: "seed", name: "Seed", detail: "x" }, volatile: new Set(), idField: "id", collection: target.collection, itemId: target.id, }; const omitted = new OmittedFields(() => {}); const outcome = omitted.check(state, view, record, read, (loosened) => checkItemRead(state, loosened, record, read), ); expect(outcome).toMatchObject({ pass: true, omitted: ["detail"] }); if (outcome === undefined) throw new Error("Expected a counted read"); omitted.keep(view, outcome); expect(omitted.views([view])[0]).toMatchObject({ required_fields: ["id", "name"], }); });