Files
rea/src/domain/analysisView/EnvironmentStateWriteSimulation.test.ts
T
edccb430c4 feat(environment): compare an environment with probe captures of its application (#5699)
* chore: restack environment fidelity onto main

Keep probe comparison, paging, and tied-create behavior on main after #5600. Capture parsing keeps field-path errors and records whether a probe kept request and response bodies.

Co-authored-by: morluto <morluto@users.noreply.github.com>

* fix(environment): keep served collection order with the fidelity checks

The tied-create collection order lives with the checks that match encoded parents. The write simulation reads field checks from that attribution, and taint uses the same segment match.

Co-authored-by: morluto <morluto@users.noreply.github.com>

* fix(environment): translate an object's keys only when it is keyed by learned ids

Differ.object mapped every key through the probe's id map, so a field named
like a learned id was compared with another field, and a real rename could
pass as a mapped key. Keys now translate only for an object keyed by
learned ids as learning reads one: a key both sides share needs every key
evidently an id, and otherwise each entry is an entity or each key evidently
an id. Any other object compares field by field.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: morluto <morluto@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
2026-10-22 21:52:01 +08:00

2816 lines
97 KiB
TypeScript

import { checkedLines, servedLine } from "./environmentStateCheckedLines.js";
import { describe, expect, it } 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 { itemTarget, SimulatedState } from "./environmentStateWriteChecks.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 no create whose entity lacks a field every recording of its item view carried", () => {
const detailed = (entry: Book): Book => ({ ...entry, detail: "x" });
const rows: 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", THIRD_BOOK],
];
expect(gated(rows, "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", () => {
it("names the plainly held entity from a path that encodes its parent", () => {
const type = "/api/orgs/uid/{uid}/books";
const collection = "/api/orgs/uid/acme1/books";
const state = new SimulatedState(
[
{
type,
collection,
id: "1",
fields: { id: 1, name: "Alpha" },
view_fields: [],
record_ordinals: [],
},
],
[],
);
expect(
itemTarget(state, type, { path: "/api/orgs/uid/%61cme1/books/1" }),
).toEqual({ collection, id: "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" }),
};
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<string, JsonValue>, 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("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],
});
});
});