test(environment): cover write plans, the write gate and served writes

Unit tests for clock, UUID, token, slug and delimited-token values, the field
rules that explain created and updated entities, response shapes, insert
rules and the shared apply functions; view tests for every plan reason, delete
confirmation by a 400, 404 or null read, learned insert rules, unmodelled
writes and observations from later reads; services tests for served creates,
updates and deletes, refusals that leave the state unchanged, deterministic
values across reset and load, missing entities, the catalog and paging-only
list lines; and an MCP boundary test that serves a capture through the built
CLI, applies a create and resets it. The capture fixture records request
bodies.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Edgar ArakelyanandClaude Opus 5.5 committed 2026-10-20 09:58:26 -07:00
1 parent 84291a05d7
commit cbb7b613f7
8 files changed
+2469 -3

No files matched your search

@@ -0,0 +1,399 @@
import { describe, expect, it } from "vitest";
import {
timedCaptureEvidence,
type TimedCaptureRow,
} from "../../../tests/fixtures/analysisView.js";
import {
environmentStateServeFixture,
environmentStateServeInputs,
} from "../../../tests/fixtures/environmentServe.js";
import {
BOOK_INIT_ROWS,
BOOK_NOT_FOUND,
BOOK_ROWS,
DELETE_NOT_FOUND,
FIRST_BOOK,
SECOND_BOOK,
THIRD_BOOK,
book,
bookClock,
renamedBook,
type Book,
} from "../../../tests/fixtures/environmentStateWrites.js";
import { projectAnalysisError } from "../../domain/analysisErrorProjection.js";
import type { ReplayArguments } from "./environmentReplay.js";
import { servedCatalog } from "./environmentState.js";
import { EnvironmentStateStore } from "./environmentStateStore.js";
import { environmentToolAnswer } from "./environmentToolAnswer.js";
import { environmentToolDefinitions } from "./environmentTools.js";
/** A served environment over the state derived from a capture of `rows`, answering tool calls as the server does. */
const servedFrom = (rows: readonly TimedCaptureRow[] = BOOK_ROWS) => {
const { source, state } = environmentStateServeInputs(
environmentStateServeFixture(timedCaptureEvidence(rows)),
);
const store = new EnvironmentStateStore(state);
const respond = environmentToolAnswer(source, store);
const call = (name: string, args: ReplayArguments) => {
const tool = source.manifest.tools.find(
(candidate) => candidate.name === name,
);
if (tool === undefined) throw new Error(`The manifest lacks ${name}`);
return respond(tool, args);
};
return {
source,
state,
store,
answer: (name: string, args: ReplayArguments = {}) => {
const result = call(name, args);
if (!result.ok) throw result.error;
return result.value;
},
refusal: (name: string, args: ReplayArguments = {}) => {
const result = call(name, args);
if (result.ok) throw new Error(`Expected ${name} to be refused`);
return projectAnalysisError(result.error);
},
/** What data/get reports, as text, so whole states compare byte for byte. */
snapshot: () => JSON.stringify(store.snapshot()),
};
};
/** The book the first served create of "New Book" makes: the next id, one second past the served clock's start. */
const NEW_BOOK = book(3, "New Book", bookClock(13));
const CREATE_NEW_BOOK = { body: { name: "New Book" } };
type Served = ReturnType<typeof servedFrom>;
const entityOf = (books: Served, id: string) =>
books.store.snapshot().rea_state.entities.find((entity) => entity.id === id);
/** Load what data/get reports back through data/add: the state must come back byte for byte. */
const expectReloadsUnchanged = (books: Served): void => {
const before = books.snapshot();
expect(books.store.replace(JSON.parse(before)).ok).toBe(true);
expect(books.snapshot()).toBe(before);
};
describe("served creates", () => {
it("creates a book with the next id, the served clock and a derived slug, which later reads show", () => {
const books = servedFrom();
const created = books.answer("post_books", CREATE_NEW_BOOK);
expect(created).toMatchObject({
status: 201,
content_type: "application/json",
state: {
source: "state",
view: "create",
type: "/api/books",
collection: "/api/books",
id: "3",
},
});
expect(created.body).toEqual(NEW_BOOK);
expect(books.answer("get_books_by_id", { id: "3" })).toMatchObject({
status: 200,
body: NEW_BOOK,
state: { source: "state", view: "item", id: "3" },
});
expect(books.answer("get_books")).toMatchObject({
body: [FIRST_BOOK, SECOND_BOOK, NEW_BOOK],
state: { source: "state", view: "list", membership: "all", listed: 3 },
});
});
it("reports a created book in data/get without record citations and loads it back unchanged", () => {
const books = servedFrom();
books.answer("post_books", CREATE_NEW_BOOK);
expect(entityOf(books, "3")).toEqual({
type: "/api/books",
collection: "/api/books",
id: "3",
fields: NEW_BOOK,
view_fields: [],
record_ordinals: [],
});
expectReloadsUnchanged(books);
expect(books.answer("get_books_by_id", { id: "3" }).body).toEqual(NEW_BOOK);
});
});
describe("served updates and deletes", () => {
const SECOND_EDITION = renamedBook(SECOND_BOOK, "Second Edition", 13);
const RENAME_SECOND = { id: "2", body: { name: "Second Edition" } };
it("changes only the sent members, the slug that follows them and the clock", () => {
const books = servedFrom();
expect(books.answer("patch_books_by_id", RENAME_SECOND)).toMatchObject({
status: 200,
body: SECOND_EDITION,
state: { source: "state", view: "update", id: "2" },
});
expect(books.answer("get_books_by_id", { id: "2" }).body).toEqual(
SECOND_EDITION,
);
expect(books.answer("get_books").body).toEqual([
FIRST_BOOK,
SECOND_EDITION,
]);
});
it("cites no records for an updated book in data/get, so it loads back unchanged", () => {
const books = servedFrom();
books.answer("patch_books_by_id", RENAME_SECOND);
expect(entityOf(books, "2")).toMatchObject({
fields: SECOND_EDITION,
record_ordinals: [],
});
expectReloadsUnchanged(books);
});
it("removes a deleted book; a later read answers the recorded 404 as a missing entity", () => {
const books = servedFrom();
expect(books.answer("delete_books_by_id", { id: "2" })).toMatchObject({
status: 204,
body: null,
state: { source: "state", view: "delete", id: "2" },
});
expect(books.answer("get_books_by_id", { id: "2" })).toMatchObject({
status: 404,
body: BOOK_NOT_FOUND,
state: {
source: "state",
view: "missing",
type: "/api/books",
collection: "/api/books",
id: "2",
},
});
expect(books.answer("get_books").body).toEqual([FIRST_BOOK]);
});
it("answers an update or delete of a missing book with a recorded 404, else refuses it", () => {
const books = servedFrom();
const before = books.snapshot();
// The update recorded no absence, so it answers the item view's; the delete answers its own.
expect(
books.answer("patch_books_by_id", { id: "99", body: { name: "Ghost" } }),
).toMatchObject({
status: 404,
body: BOOK_NOT_FOUND,
state: { view: "missing", id: "99" },
});
expect(books.answer("delete_books_by_id", { id: "99" })).toMatchObject({
status: 404,
body: DELETE_NOT_FOUND,
state: { view: "missing", id: "99" },
});
expect(books.snapshot()).toBe(before);
const unrecorded = servedFrom(
BOOK_ROWS.filter(([, , , status]) => status !== 404),
);
for (const [name, args] of [
["patch_books_by_id", { id: "99", body: { name: "Ghost" } }],
["delete_books_by_id", { id: "99" }],
] as const)
expect(unrecorded.refusal(name, args)).toMatchObject({
code: "invalid_request",
message: expect.stringContaining(
"holds no /api/books entity 99 in /api/books",
),
});
});
});
describe("refusals", () => {
it("refuses a body the capture does not support and leaves the state unchanged", () => {
const books = servedFrom();
const before = books.snapshot();
const refused: [string, ReplayArguments, string][] = [
[
"post_books",
{ body: { name: "Extra", color: "red" } },
"Member color is not one the capture showed stored",
],
["post_books", { body: {} }, "lacks required members name"],
[
"post_books",
{ body: { name: 42 } },
"Member name must be a JSON string",
],
["post_books", { body: ["New Book"] }, "takes a JSON object body"],
[
"patch_books_by_id",
{ id: "1", body: "New Book" },
"takes a JSON object body",
],
["patch_books_by_id", { id: "1", body: {} }, "sends none of the members"],
[
"patch_books_by_id",
{ id: "1", body: { title: "New" } },
"Member title is not one",
],
];
for (const [name, args, message] of refused) {
expect(books.refusal(name, args)).toMatchObject({
code: "invalid_request",
message: expect.stringContaining(message),
});
expect(books.snapshot()).toBe(before);
}
});
it("accepts a member the application stored differently only with the value the capture sent", () => {
const bound = (entry: Book, format: string): Book => ({ ...entry, format });
const books = servedFrom([
[
"GET",
"/api/books",
[bound(FIRST_BOOK, "PAPERBACK"), bound(SECOND_BOOK, "PAPERBACK")],
],
["GET", "/api/books/1", bound(FIRST_BOOK, "PAPERBACK")],
["GET", "/api/books/2", bound(SECOND_BOOK, "PAPERBACK")],
[
"POST",
"/api/books",
bound(THIRD_BOOK, "HARDCOVER"),
201,
{ name: "Third Book", format: "hardcover" },
],
["GET", "/api/books/3", bound(THIRD_BOOK, "HARDCOVER")],
]);
const before = books.snapshot();
expect(
books.refusal("post_books", {
body: { name: "Soft", format: "softcover" },
}),
).toMatchObject({
message: expect.stringContaining(
"Member format is accepted only with the value the capture sent",
),
});
expect(books.snapshot()).toBe(before);
expect(
books.answer("post_books", {
body: { name: "Hard", format: "hardcover" },
}).body,
).toMatchObject({ id: 3, name: "Hard", format: "HARDCOVER" });
});
it("generates the same values after a refused call as without it", () => {
const refusedFirst = servedFrom();
refusedFirst.refusal("post_books", { body: {} });
refusedFirst.refusal("patch_books_by_id", {
id: "1",
body: { title: "New" },
});
expect(refusedFirst.answer("post_books", CREATE_NEW_BOOK).body).toEqual(
servedFrom().answer("post_books", CREATE_NEW_BOOK).body,
);
});
});
describe("generated values", () => {
it("answers the same calls identically after a reset, never reusing a deleted id", () => {
const books = servedFrom();
const session = () => {
const answers = [
books.answer("post_books", CREATE_NEW_BOOK),
books.answer("patch_books_by_id", {
id: "3",
body: { name: "Newer Book" },
}),
books.answer("delete_books_by_id", { id: "3" }),
books.answer("post_books", { body: { name: "Another Book" } }),
books.answer("get_books"),
];
return { answers, text: JSON.stringify([answers, books.snapshot()]) };
};
const first = session();
books.store.reset();
expect(session().text).toBe(first.text);
expect(first.answers[3]?.body).toMatchObject({
id: 4,
name: "Another Book",
});
});
it("restarts the clock and the sequences when data/add loads a state", () => {
const books = servedFrom();
const initial: unknown = JSON.parse(books.snapshot());
const created = books.answer("post_books", CREATE_NEW_BOOK);
books.answer("post_books", { body: { name: "Another Book" } });
expect(books.store.replace(initial).ok).toBe(true);
// Had the sequence and clock run on, this create would take id 5 one second later.
expect(books.answer("post_books", CREATE_NEW_BOOK).body).toEqual(
created.body,
);
});
});
describe("served catalog", () => {
it("adds only the served write tools, saying what each does to the state", () => {
const { source, state } = servedFrom();
const catalog = servedCatalog(source, state, "books");
if (!catalog.ok) throw catalog.error;
const described = new Map(
catalog.value.tools.map((tool) => [tool.name, tool]),
);
// The export write targets no state type, so it is not served.
expect([...described.keys()].sort()).toEqual([
"delete_books_by_id",
"get_books",
"get_books_by_id",
"patch_books_by_id",
"post_books",
]);
expect(described.get("post_books")?.description).toContain(
"creates an entity of /api/books in the served environment state (body members name; required: name), modelled on the recorded calls",
);
expect(described.get("patch_books_by_id")?.description).toContain(
"changes the entity the path names in the served environment state (body members name; send at least one)",
);
expect(described.get("delete_books_by_id")?.description).toContain(
"removes the entity the path names from the served environment state",
);
expect(described.get("post_books")?.annotations.readOnlyHint).toBe(false);
expect(described.get("get_books")?.description).toContain(
"answers from the served environment state where the state backs this tool",
);
});
it("serves every manifest tool unchanged in replay mode", () => {
const { source } = servedFrom();
const replay = servedCatalog(source, undefined, "books");
const definitions = environmentToolDefinitions(source, "books");
if (!replay.ok || !definitions.ok) throw new Error("Expected a catalog");
expect(replay.value).toEqual({
tools: definitions.value,
dataPlane: "none",
});
expect(replay.value.tools.map(({ name }) => name)).toContain(
"post_exports",
);
});
});
describe("paged list lines", () => {
it("answers an argument-less list from the state on a line that only pages, and another page from its recording", () => {
const paged = servedFrom([
["GET", "/api/books?limit=50", [FIRST_BOOK, SECOND_BOOK]],
...BOOK_INIT_ROWS.slice(1),
["POST", "/api/books", THIRD_BOOK, 201, { name: "Third Book" }],
["GET", "/api/books/3", THIRD_BOOK],
["GET", "/api/books?limit=50", [FIRST_BOOK, SECOND_BOOK, THIRD_BOOK]],
]);
const created = paged.answer("post_books", CREATE_NEW_BOOK).body;
expect(paged.answer("get_books")).toMatchObject({
body: [FIRST_BOOK, SECOND_BOOK, created],
replay: { record_ordinal: 0, exact_match: false },
state: { source: "state", view: "list", listed: 3 },
});
expect(paged.answer("get_books", { limit: "10" })).toMatchObject({
body: [FIRST_BOOK, SECOND_BOOK],
state: { source: "recording", reason: "unrecorded-request" },
});
});
});
@@ -0,0 +1,509 @@
import { expect, it } from "vitest";
import type { JsonValue } from "../jsonValue.js";
import { err, ok } from "../result.js";
import type { JsonObject, StateEntity } from "./environmentStateRules.js";
import {
bodyProblem,
createdFields,
updatedEntity,
updatedFields,
valueAt,
writtenDocument,
writtenResponse,
} from "./environmentStateWriteApply.js";
import type {
FieldRule,
GeneratedRule,
StateWritePlan,
WriteBody,
WriteCall,
WriteResponse,
} from "./environmentStateWriteRules.js";
import type { ClockFormat } from "./environmentStateWriteValues.js";
const SECONDS: ClockFormat = {
kind: "iso",
separator: "T",
fraction: 0,
zone: "Z",
};
const NOW = "2026-03-04T05:06:07Z";
const callWith = (
body: JsonObject | undefined,
pathValues: Record<string, string> = {},
): WriteCall => ({
path: "/items",
pathValues: new Map(Object.entries(pathValues)),
body,
});
/** Generated values the store would supply, recording which rules asked. */
const generator = () => {
const asked: GeneratedRule[] = [];
const values = { identity: "id-9", sequence: 9, unique: "ref-9", clock: NOW };
const generate = (rule: GeneratedRule): JsonValue => {
asked.push(rule);
return values[rule.rule];
};
return { asked, generate };
};
const CONTRACT: WriteBody = {
accepted: ["title", "tags", "meta", "note"],
fixed: [
{ member: "priority", sent: "HIGH" },
{ member: "options", sent: { a: 1, b: 2 } },
],
required: ["title"],
identity: "id",
};
const RECORDED = { title: "A", tags: ["x"], meta: null, priority: "high" };
const LISTED =
"accepted members: title, tags, meta, note; members accepted only with their recorded value: priority, options; required: title";
const problemOf = (body: JsonObject | undefined): string | undefined =>
bodyProblem(CONTRACT, callWith(body), { entity: RECORDED, id: "42" });
it.each<[string, JsonObject]>([
[
"accepted and fixed members",
{ title: "B", priority: "HIGH", options: { b: 2, a: 1 } },
],
["the identity repeated", { id: "42", title: "B" }],
["the identity repeated as a number", { id: 42, title: "B" }],
["any type where the recorded value is null", { title: "B", meta: { x: 1 } }],
["any type where no value was recorded", { title: "B", note: 5 }],
])("accepts a body with %s", (_, body) => {
expect(problemOf(body)).toBeUndefined();
});
it.each<[string, JsonObject | undefined, string]>([
[
"an unknown member",
{ title: "B", color: "red" },
`Member color is not one the capture showed stored (${LISTED}).`,
],
[
"a fixed member with another value",
{ title: "B", priority: "LOW" },
`Member priority is accepted only with the value the capture sent, because the application stored it differently (${LISTED}).`,
],
[
"a number for a string",
{ title: 5 },
"Member title must be a JSON string, as the capture stored it.",
],
[
"null for a string",
{ title: null },
"Member title must be a JSON string, as the capture stored it.",
],
[
"an object for an array",
{ title: "B", tags: { x: true } },
"Member tags must be a JSON array, as the capture stored it.",
],
[
"a missing required member",
{ tags: ["y"] },
`The call lacks required members title (${LISTED}).`,
],
["no body", undefined, `The call lacks required members title (${LISTED}).`],
[
"another identity",
{ id: "43", title: "B" },
"Member id must repeat the target's identity 42.",
],
])("refuses a body with %s", (_, body, message) => {
expect(problemOf(body)).toBe(message);
});
it.each<[string, JsonValue | undefined]>([
["", { a: [{ b: 1 }, { b: [2, 3] }], "x/y": { "m~n": true }, "": "blank" }],
["/a/0/b", 1],
["/a/1/b/1", 3],
["/x~1y/m~0n", true],
["/", "blank"],
["/a/2/b", undefined],
["/a/first", undefined],
["/a/0/b/c", undefined],
["/missing", undefined],
])("reads the body value at %j", (pointer, expected) => {
const body = {
a: [{ b: 1 }, { b: [2, 3] }],
"x/y": { "m~n": true },
"": "blank",
};
expect(valueAt(body, pointer)).toEqual(expected);
});
const CREATE_RULES: FieldRule[] = [
{ field: "id", rule: "identity", generator: "token" },
{ field: "number", rule: "sequence", scope: "collection" },
{ field: "createdAt", rule: "clock", format: SECONDS },
{ field: "ref", rule: "unique" },
{ field: "title", rule: "echo" },
{ field: "summary", rule: "copy", from: "/meta/notes/1" },
{ field: "projectId", rule: "path", from: "project" },
{ field: "projectKey", rule: "path", from: "project" },
{ field: "slug", rule: "slug", from: "title" },
{ field: "url", rule: "template", from: "id" },
];
type Plan = Pick<StateWritePlan, "rules" | "body">;
const CREATE_PLAN: Plan = {
rules: CREATE_RULES,
body: {
accepted: ["title", "meta"],
fixed: [],
required: ["title", "meta"],
identity: null,
},
};
const CREATE_OBSERVED: JsonObject = {
id: "id-1",
number: 1,
createdAt: "2026-01-01T00:00:00Z",
ref: "ref-1",
title: "Old title",
summary: "old summary",
projectId: 7,
projectKey: "7",
slug: "old-title",
url: "https://x.test/items/id-1?v=id-10",
status: "open",
};
const NEW_ITEM = {
title: "New Title!",
meta: { notes: ["a", "the new summary"] },
};
it("creates an entity from generated values, the call and the observed constants", () => {
const { asked, generate } = generator();
const observed = structuredClone(CREATE_OBSERVED);
const result = createdFields(
CREATE_PLAN,
observed,
callWith(NEW_ITEM, { project: "42" }),
generate,
);
expect(result).toEqual(
ok({
id: "id-9",
number: 9,
createdAt: NOW,
ref: "ref-9",
title: "New Title!",
summary: "the new summary",
projectId: 42,
projectKey: "42",
slug: "new-title",
url: "https://x.test/items/id-9?v=id-10",
status: "open",
}),
);
expect(asked).toEqual(CREATE_RULES.slice(0, 4));
expect(observed).toEqual(CREATE_OBSERVED);
});
it.each<[string, Plan, WriteCall, string]>([
[
"no modelled body",
{ rules: CREATE_RULES, body: null },
callWith(NEW_ITEM, { project: "42" }),
"The tool has no modelled body.",
],
[
"a missing copy source",
CREATE_PLAN,
callWith({ title: "x", meta: { notes: ["only one"] } }, { project: "42" }),
"The call has no value at /meta/notes/1 in its body, which summary copies.",
],
[
"a missing path value",
CREATE_PLAN,
callWith(NEW_ITEM),
"The call names no project path value, which projectId takes.",
],
[
"a body problem",
CREATE_PLAN,
callWith({ ...NEW_ITEM, extra: 1 }, { project: "42" }),
"Member extra is not one the capture showed stored (accepted members: title, meta; members accepted only with their recorded value: none; required: title, meta).",
],
])("refuses a create with %s", (_, plan, call, message) => {
expect(
createdFields(plan, CREATE_OBSERVED, call, generator().generate),
).toEqual(err(message));
});
const UPDATE_PLAN: Plan = {
rules: [
{ field: "title", rule: "echo" },
{ field: "notes", rule: "echo" },
{ field: "status", rule: "constant" },
{ field: "slug", rule: "slug", from: "title" },
{ field: "version", rule: "increment" },
{ field: "revision", rule: "increment" },
{ field: "updatedAt", rule: "clock", format: SECONDS },
],
body: {
accepted: ["title", "notes"],
fixed: [{ member: "status", sent: "CLOSED" }],
required: [],
identity: "id",
},
};
const BEFORE: JsonObject = {
id: "42",
title: "Old Title",
notes: "n",
status: "open",
slug: "old-title",
version: 3,
revision: "9",
updatedAt: "2026-01-01T00:00:00Z",
color: "red",
};
const AFTER: JsonObject = {
...BEFORE,
title: "Recorded Title",
status: "closed",
slug: "recorded-title",
version: 4,
revision: "10",
};
const update = (body: JsonObject | undefined, plan = UPDATE_PLAN) => {
const { asked, generate } = generator();
const result = updatedFields(
plan,
{ before: BEFORE, observed: AFTER },
{ ...callWith(body, { item: "42" }), id: "42" },
generate,
);
return { asked, result };
};
const ADVANCED = { version: 4, revision: "10", updatedAt: NOW };
it("updates sent echoed members and derives the slug of a sent member", () => {
const { asked, result } = update({ id: "42", title: "New Title" });
expect(result).toEqual(
ok({ ...BEFORE, ...ADVANCED, title: "New Title", slug: "new-title" }),
);
expect(asked).toEqual([UPDATE_PLAN.rules[6]]);
});
it("applies a constant from the recorded update only when the call sends it", () => {
expect(update({ status: "CLOSED" }).result).toEqual(
ok({ ...BEFORE, ...ADVANCED, status: "closed" }),
);
expect(update({ notes: "m" }).result).toEqual(
ok({ ...BEFORE, ...ADVANCED, notes: "m" }),
);
});
it.each<[string, JsonObject | undefined, string]>([
[
"only the identity",
{ id: "42" },
"The call sends none of the members an update applies",
],
["no members", {}, "The call sends none of the members an update applies"],
[
"no body",
undefined,
"The call sends none of the members an update applies",
],
[
"another identity",
{ id: "43", title: "x" },
"Member id must repeat the target's identity 42.",
],
["a number for a string", { title: 5 }, "Member title must be a JSON string"],
[
"a fixed member with another value",
{ status: "OPEN" },
"Member status is accepted only with the value",
],
])("refuses an update with %s", (_, body, message) => {
const { result } = update(body);
expect(result.ok ? undefined : result.error).toContain(message);
});
it("refuses an update without a modelled body", () => {
expect(update({ title: "x" }, { ...UPDATE_PLAN, body: null }).result).toEqual(
err("The tool has no modelled body."),
);
});
const DOCUMENT = { settings: { theme: "dark", lang: "en" }, version: 1 };
const documentPlan = (
merge: "merge" | "replace",
pointer?: string,
): Pick<StateWritePlan, "merge" | "observation"> => ({
merge,
observation: pointer === undefined ? null : { ordinal: 1, pointer },
});
it.each<[string, ReturnType<typeof documentPlan>, JsonObject, JsonValue]>([
[
"merges into a member",
documentPlan("merge", "/settings"),
{ theme: "light" },
{ settings: { theme: "light", lang: "en" }, version: 1 },
],
[
"replaces a member",
documentPlan("replace", "/settings"),
{ theme: "light" },
{ settings: { theme: "light" }, version: 1 },
],
[
"merges into the root",
documentPlan("merge"),
{ version: 2 },
{ ...DOCUMENT, version: 2 },
],
[
"replaces the root",
documentPlan("replace", ""),
{ theme: "x" },
{ theme: "x" },
],
])("%s of a document", (_, plan, body, expected) => {
expect(writtenDocument(plan, DOCUMENT, callWith(body))).toEqual(ok(expected));
expect(DOCUMENT).toEqual({
settings: { theme: "dark", lang: "en" },
version: 1,
});
});
it("refuses a document write that does not reach an object", () => {
const refused = err("The call's body does not fit the document.");
const plan = (pointer: string) => documentPlan("merge", pointer);
expect(
writtenDocument(plan("/version"), DOCUMENT, callWith({ a: 1 })),
).toEqual(refused);
expect(
writtenDocument(plan("/settings"), DOCUMENT, callWith(undefined)),
).toEqual(refused);
});
const answer = (
shape: WriteResponse["shape"],
pointer: string | null,
): WriteResponse => ({ ordinal: 1, shape, pointer });
const WRITTEN = { id: "8", title: "New", extra: true };
it.each<[string, WriteResponse, JsonValue | undefined, JsonValue | undefined]>([
["no body", answer("empty", null), { id: "1" }, undefined],
["the recorded body", answer("recorded", null), { ok: true }, { ok: true }],
[
"no body when none was recorded",
answer("entity", ""),
undefined,
undefined,
],
["a numeric identity at the root", answer("identity", ""), 7, 8],
["a string identity at the root", answer("identity", ""), "7", "8"],
[
"a numeric identity in a member",
answer("identity", "/id"),
{ id: 7, ok: true },
{ id: 8, ok: true },
],
[
"the recorded fields of the entity",
answer("entity", "/data"),
{ data: { title: "Old", id: "1" }, page: 1 },
{ data: { title: "New", id: "8" }, page: 1 },
],
[
"the entity in a one-element array",
answer("entity", "/0"),
[{ id: "1" }],
[{ id: "8" }],
],
])("answers with %s", (_, response, recorded, expected) => {
expect(
writtenResponse(response, recorded, { fields: WRITTEN, id: "8" }),
).toEqual(ok(expected));
});
it("spells an identity that is not an integer as a string where a number was recorded", () => {
expect(
writtenResponse(
answer("identity", "/id"),
{ id: 7 },
{ fields: undefined, id: "abc" },
),
).toEqual(ok({ id: "abc" }));
});
it("refuses an entity answer the state cannot render", () => {
const recorded = { data: { id: "1", title: "Old", owner: "x" } };
expect(
writtenResponse(answer("entity", "/data"), recorded, {
fields: WRITTEN,
id: "8",
}),
).toEqual(
err(
"The state holds no complete entity for this response: it lacks owner.",
),
);
expect(
writtenResponse(answer("entity", "/data"), recorded, {
fields: undefined,
id: "8",
}),
).toEqual(err("The recorded response does not carry the entity."));
expect(
writtenResponse(answer("entity", "/missing"), recorded, {
fields: WRITTEN,
id: "8",
}),
).toEqual(err("The recorded response does not carry the entity."));
});
it("drops view-specific values of the fields an update changed and cites no records", () => {
const entity: StateEntity = {
type: "/items",
collection: "/items",
id: "1",
fields: { id: "1", title: "Old", status: "open", meta: { a: 1, b: 2 } },
view_fields: [
{
tool: "list_items",
fields: { title: "Old…", status: "OPEN", meta: "a,b" },
},
{ tool: "get_item", fields: { title: "OLD" } },
{ tool: "search_items", fields: { priority: "HIGH" } },
],
record_ordinals: [3, 5],
};
const snapshot = structuredClone(entity);
const fields = {
id: "1",
title: "New",
status: "open",
meta: { b: 2, a: 1 },
priority: "low",
};
expect(updatedEntity(entity, fields)).toEqual({
...snapshot,
fields,
record_ordinals: [],
view_fields: [
{ tool: "list_items", fields: { status: "OPEN", meta: "a,b" } },
],
});
expect(entity).toEqual(snapshot);
expect(updatedEntity(entity, entity.fields).view_fields).toEqual(
entity.view_fields,
);
});
@@ -0,0 +1,552 @@
import { expect, it } from "vitest";
import type { JsonValue } from "../jsonValue.js";
import type { JsonObject } from "./environmentStateRules.js";
import {
compareValues,
documentPointerOf,
explainCreate,
explainUpdate,
insertCandidates,
insertIndex,
responseShapeOf,
type CreateEvidence,
type UpdateEvidence,
} from "./environmentStateWriteExplain.js";
import {
classifyWrite,
pathValuesOf,
prefixRelated,
type FieldRule,
type InsertRule,
type WriteEffect,
type WriteResponse,
} from "./environmentStateWriteRules.js";
const START = Date.UTC(2026, 2, 4, 5, 6, 0);
const MINUTE = 60_000;
const UUID = "123e4567-e89b-42d3-a456-426614174000";
const OTHER_UUID = "00000000-0000-4000-8000-000000000000";
const created = (
entity: JsonObject,
body: JsonObject,
context: Partial<CreateEvidence> & {
readonly path?: Record<string, string>;
} = {},
): CreateEvidence => {
const { path = {}, ...rest } = context;
return {
entity,
call: { path: "/items", pathValues: new Map(Object.entries(path)), body },
idField: "id",
startMs: START,
collection: [],
type: [],
...rest,
};
};
const updated = (
before: JsonObject,
after: JsonObject,
body: JsonObject,
context: Partial<UpdateEvidence> = {},
): UpdateEvidence => ({
before,
after,
call: { path: "/items/42", pathValues: new Map([["item", "42"]]), body },
method: "PATCH",
idField: "id",
id: "42",
startMs: START,
...context,
});
const ruleFor = (
rules: readonly FieldRule[],
field: string,
): FieldRule | undefined => rules.find((rule) => rule.field === field);
it("explains created fields by echo, copy, path and slug, leaving constants unlisted", () => {
const notes = ["x", "drafted by the finance team"];
const result = explainCreate(
created(
{
id: 7,
title: "Quarterly report",
heading: "Quarterly report",
summary: "drafted by the finance team",
projectId: 42,
slug: "quarterly-report",
code: "abc",
status: "open",
},
{ title: "Quarterly report", meta: { notes }, label: "abc" },
{ path: { project: "42" } },
),
);
expect(result.rules).toEqual([
{ field: "id", rule: "identity", generator: "sequence" },
{ field: "title", rule: "echo" },
{ field: "heading", rule: "copy", from: "/title" },
{ field: "summary", rule: "copy", from: "/meta/notes/1" },
{ field: "projectId", rule: "path", from: "project" },
{ field: "slug", rule: "slug", from: "title" },
]);
expect(result.tied).toBe(false);
});
it("copies only body strings of eight or more characters", () => {
const short = explainCreate(
created({ id: 1, label: "seven77" }, { name: "seven77" }),
);
expect(ruleFor(short.rules, "label")).toBeUndefined();
const long = explainCreate(
created({ id: 1, label: "eight888" }, { name: { first: "eight888" } }),
);
expect(ruleFor(long.rules, "label")).toEqual({
field: "label",
rule: "copy",
from: "/name/first",
});
});
it.each<[string, JsonValue, number | undefined, boolean]>([
[
"ten minutes after",
new Date(START + 10 * MINUTE).toISOString(),
START,
true,
],
[
"ten minutes before",
new Date(START - 10 * MINUTE).toISOString(),
START,
true,
],
[
"just outside",
new Date(START + 10 * MINUTE + 1).toISOString(),
START,
false,
],
["untimed", new Date(START).toISOString(), undefined, false],
])("explains a clock %s the request start", (_, value, startMs, clock) => {
const result = explainCreate(created({ id: 1, at: value }, {}, { startMs }));
expect(ruleFor(result.rules, "at")).toEqual(
clock
? {
field: "at",
rule: "clock",
format: { kind: "iso", separator: "T", fraction: 3, zone: "Z" },
}
: undefined,
);
});
it("keeps an epoch clock's unit", () => {
const result = explainCreate(created({ id: 1, at: START / 1000 + 5 }, {}));
expect(ruleFor(result.rules, "at")).toEqual({
field: "at",
rule: "clock",
format: { kind: "epoch", unit: "s" },
});
});
it.each<[JsonValue, FieldRule | undefined]>([
[4, { field: "number", rule: "sequence", scope: "collection" }],
["4", { field: "number", rule: "sequence", scope: "collection" }],
[10, { field: "number", rule: "sequence", scope: "type" }],
[5, undefined],
])("explains the number %j by the sequence it continues", (value, rule) => {
const collection = [{ number: 1 }, { number: "3" }];
const evidence = created(
{ id: 1, number: value },
{},
{
collection,
type: [...collection, { number: 9 }, { number: "x" }],
},
);
expect(ruleFor(explainCreate(evidence).rules, "number")).toEqual(rule);
});
it.each<[string, JsonObject[], boolean]>([
["no other entity shares", [{ ref: OTHER_UUID }, {}], true],
["another entity shares", [{ ref: OTHER_UUID }, { ref: UUID }], false],
["the type holds no other entity", [], false],
])("explains a UUID as unique when %s it", (_, type, unique) => {
const result = explainCreate(created({ id: 1, ref: UUID }, {}, { type }));
expect(ruleFor(result.rules, "ref")).toEqual(
unique ? { field: "ref", rule: "unique" } : undefined,
);
});
it.each<[JsonValue, "sequence" | "uuid" | "token"]>([
[17, "sequence"],
["17", "sequence"],
[UUID, "uuid"],
["tok_AbC9", "token"],
])("generates the identity %j as a %s", (id, generator) => {
const result = explainCreate(
created({ key: id, title: "x" }, {}, { idField: "key" }),
);
expect(result.rules).toEqual([{ field: "key", rule: "identity", generator }]);
});
it("rebuilds strings that embed a generated value as a delimited token", () => {
const result = explainCreate(
created(
{
id: "abc123",
number: 4,
url: "https://x.test/items/abc123",
ref: "#4",
near: "abc1234 is not it",
year: "2024",
},
{},
{ collection: [{ number: 3 }] },
),
);
expect(result.rules).toEqual([
{ field: "id", rule: "identity", generator: "token" },
{ field: "number", rule: "sequence", scope: "collection" },
{ field: "url", rule: "template", from: "id" },
{ field: "ref", rule: "template", from: "number" },
]);
});
it("accepts echoed members and fixes the others at their sent values", () => {
const result = explainCreate(
created(
{ id: 1, title: "A", priority: "high" },
{ title: "A", priority: "HIGH", notify: true },
),
);
expect(result.body).toEqual({
accepted: ["title"],
fixed: [
{ member: "priority", sent: "HIGH" },
{ member: "notify", sent: true },
],
required: ["title", "priority", "notify"],
identity: null,
});
});
it.each<[string, JsonObject, boolean]>([
[
"a constant holds the path value delimited",
{ team: "/orgs/acme/teams" },
true,
],
["a constant holds it only inside a word", { team: "acmecorp" }, false],
["only a path field holds it", { org: "acme" }, false],
[
"a template keeps it beside the generated value",
{ url: "/orgs/acme/items/1" },
true,
],
])("ties a create to its collection only when %s", (_, fields, tied) => {
const result = explainCreate(
created({ id: 1, ...fields }, {}, { path: { org: "acme", empty: "" } }),
);
expect(result.tied).toBe(tied);
});
it("explains an update's sent members as echoes, constants or the identity", () => {
const result = explainUpdate(
updated(
{ id: "42", title: "Old", status: "open" },
{ id: "42", title: "New", status: "closed" },
{ id: 42, title: "New", status: "CLOSED", comment: "done" },
),
);
expect(result).toEqual({
rules: [
{ field: "title", rule: "echo" },
{ field: "status", rule: "constant" },
],
body: {
accepted: ["title"],
fixed: [
{ member: "status", sent: "CLOSED" },
{ member: "comment", sent: "done" },
],
required: [],
identity: "id",
},
unexplained: [],
});
});
it("explains unsent changes as a clock, an increment or a slug of an echoed member", () => {
const result = explainUpdate(
updated(
{
id: "42",
title: "Old Title",
slug: "old-title",
version: 3,
revision: "9",
updatedAt: "2026-01-01T00:00:00Z",
color: "red",
},
{
id: "42",
title: "New Title",
slug: "new-title",
version: 4,
revision: "10",
updatedAt: new Date(START + 1000).toISOString(),
color: "blue",
added: true,
},
{ title: "New Title" },
),
);
expect(result.rules).toEqual([
{ field: "title", rule: "echo" },
{ field: "slug", rule: "slug", from: "title" },
{ field: "version", rule: "increment" },
{ field: "revision", rule: "increment" },
{
field: "updatedAt",
rule: "clock",
format: { kind: "iso", separator: "T", fraction: 3, zone: "Z" },
},
]);
expect(result.unexplained).toEqual(["color"]);
});
it("leaves a slug of a transformed member, an untimed clock and a jump unexplained", () => {
const result = explainUpdate(
updated(
{ id: "42", title: "Old", slug: "old", at: "2026-01-01T00:00:00Z", n: 3 },
{
id: "42",
title: "new title",
slug: "new-title",
at: new Date(START).toISOString(),
n: 5,
},
{ title: "New Title" },
{ startMs: undefined },
),
);
expect(result.rules).toEqual([{ field: "title", rule: "constant" }]);
expect(result.unexplained).toEqual(["slug", "at", "n"]);
});
it.each<[string, string[]]>([
["PUT", ["title", "status"]],
["PATCH", []],
["POST", []],
])(
"requires the members a %s sent apart from the identity",
(method, required) => {
const after = { id: "42", title: "New", status: "closed" };
const body = { id: "42", title: "New", status: "closed" };
const result = explainUpdate(updated(after, after, body, { method }));
expect(result.body.required).toEqual(required);
expect(result.body.identity).toBe("id");
},
);
type Shaped = [WriteResponse["shape"], string | null];
it.each<[string, JsonValue | undefined, boolean, Shaped]>([
["no body", undefined, true, ["empty", null]],
["an empty response with a body", { id: "42" }, true, ["empty", null]],
["no recorded body", undefined, false, ["recorded", null]],
["the entity at the root", { id: "42", title: "x" }, false, ["entity", ""]],
[
"the entity in one member",
{ data: { id: 42 } },
false,
["entity", "/data"],
],
[
"the entity in a one-element array",
[{ id: "42" }],
false,
["entity", "/0"],
],
["the identity at the root", "42", false, ["identity", ""]],
["a numeric identity at the root", 42, false, ["identity", ""]],
[
"the identity in one member",
{ created: "42", ok: true },
false,
["identity", "/created"],
],
[
"the identity in an escaped member",
{ "a/b": 42 },
false,
["identity", "/a~1b"],
],
[
"the identity in two members",
{ a: "42", b: "42" },
false,
["recorded", null],
],
["no identity", { ok: true }, false, ["recorded", null]],
])("shapes a response carrying %s", (_, body, empty, [shape, pointer]) => {
expect(responseShapeOf(3, body, empty, { idField: "id", id: "42" })).toEqual({
ordinal: 3,
shape,
pointer,
});
});
it.each<[JsonValue | undefined, JsonValue | undefined, number | undefined]>([
[2, 10, -1],
["10", 9, 1],
["7", 7, 0],
["2026-03-04T05:00:00Z", "2026-03-04T06:30:00+02:00", 1],
[Date.UTC(2026, 2, 4, 6) / 1000, "2026-03-04T05:00:00Z", 1],
["apple", "banana", -1],
["Zebra", "apple", -1],
["b", "b", 0],
[5, "apple", undefined],
[true, false, undefined],
[undefined, "x", undefined],
[{ a: 1 }, { a: 1 }, undefined],
])("orders %j against %j as %j", (left, right, sign) => {
const order = compareValues(left, right);
expect(order === undefined ? undefined : Math.sign(order)).toBe(sign);
});
it("offers each strictly ordered field in its direction, then last and first", () => {
const members = [
{ id: 1, at: "2026-03-02T00:00:00Z", name: "b", tag: "x" },
{ id: 2, at: "2026-03-01T12:00:00+02:00", name: "a", tag: "x" },
{ id: "3", at: "2026-02-28T00:00:00Z", name: "c" },
];
expect(insertCandidates(members, ["id", "at", "name", "tag"])).toEqual([
{ rule: "sorted", field: "id", direction: "ascending" },
{ rule: "sorted", field: "at", direction: "descending" },
{ rule: "last" },
{ rule: "first" },
]);
const fallback = [{ rule: "last" }, { rule: "first" }];
expect(insertCandidates([{ id: 1 }], ["id"])).toEqual(fallback);
expect(insertCandidates([], ["id"])).toEqual(fallback);
});
const ASCENDING = [{ n: 1 }, { n: 3 }, { n: 5 }];
const DESCENDING = [{ n: 5 }, { n: 3 }, { n: 1 }];
const up: InsertRule = { rule: "sorted", field: "n", direction: "ascending" };
const down: InsertRule = {
rule: "sorted",
field: "n",
direction: "descending",
};
it.each<[string, InsertRule, JsonObject, number]>([
["first", { rule: "first" }, { n: 9 }, 0],
["last", { rule: "last" }, { n: 0 }, 3],
["ascending, between members", up, { n: 4 }, 2],
["ascending, before every member", up, { n: 0 }, 0],
["ascending, after every member", up, { n: 6 }, 3],
["ascending, after an equal member", up, { n: 3 }, 2],
["descending, between members", down, { n: 4 }, 1],
["descending, after every member", down, { n: 0 }, 3],
["sorted, with an incomparable value", up, { n: true }, 3],
["sorted, without the field", up, {}, 3],
])("inserts %s", (_, rule, fields, index) => {
const members = rule === down ? DESCENDING : ASCENDING;
expect(insertIndex(rule, members, fields)).toBe(index);
});
it.each<[string, JsonValue, JsonObject, string | undefined]>([
["the root", { theme: "dark", lang: "en" }, { theme: "light" }, ""],
[
"the member sharing most",
{ theme: "dark", settings: { theme: "dark", lang: "en" } },
{ theme: "light", lang: "fr" },
"/settings",
],
[
"the root on a tie",
{ theme: "d", s: { lang: "en" } },
{ theme: "x", lang: "y" },
"",
],
["an escaped member", { "a/b": { theme: "dark" } }, { theme: "x" }, "/a~1b"],
[
"nothing when none is shared",
{ a: 1, b: { c: 2 } },
{ theme: "x" },
undefined,
],
["nothing in an array", [{ theme: "dark" }], { theme: "x" }, undefined],
])("points a document write at %s", (_, value, body, pointer) => {
expect(documentPointerOf(value, body)).toBe(pointer);
});
it.each<[string, string, WriteEffect, string | null]>([
["POST", "/users", "create", "/users"],
["POST", "/users/", "create", "/users"],
["POST", "/users/{userId}/posts", "create", "/users/{userId}/posts"],
["PUT", "/users/{userId}", "update", "/users"],
["PATCH", "/users/{id}", "update", "/users"],
["POST", "/users/{id}", "update", "/users"],
["DELETE", "/users/{id}", "delete", "/users"],
[
"DELETE",
"/users/{userId}/posts/{postId}",
"delete",
"/users/{userId}/posts",
],
["GET", "/users/{id}", "none", null],
["DELETE", "/users", "none", null],
["PUT", "/users", "none", null],
["PUT", "/users/me", "none", null],
["POST", "/users/{id}/archive", "none", null],
["PUT", "/settings", "document", "get_settings"],
["PATCH", "/settings/", "document", "get_settings"],
["POST", "/settings", "document", "get_settings"],
["DELETE", "/settings", "none", null],
])("classifies %s %s as %s", (method, path, effect, target) => {
expect(
classifyWrite(
{ method, path_template: path },
new Set(["/users", "/users/{userId}/posts"]),
new Map([["/settings", "get_settings"]]),
),
).toEqual({ effect, target });
});
it.each<[string, string, Record<string, string> | undefined]>([
[
"/orgs/{org}/repos/{repo}",
"/orgs/acme/repos/rea",
{ org: "acme", repo: "rea" },
],
["/orgs/{org}", "/orgs/a%20b%2Fc/", { org: "a b/c" }],
["/orgs/{org}", "/orgs/%E0%A4%A", { org: "%E0%A4%A" }],
["/orgs", "/orgs", {}],
["/orgs/{org}", "/teams/acme", undefined],
["/orgs/{org}", "/orgs/acme/repos", undefined],
])("reads %s from %s", (template, path, values) => {
expect(pathValuesOf(template, path)).toEqual(
values === undefined ? undefined : new Map(Object.entries(values)),
);
});
it.each<[string, string, boolean]>([
["/users", "/users/{id}", true],
["/users/{id}/posts", "/users/42", true],
["/users/42/posts", "/users/{id}/posts/7", true],
["/", "/anything/at/all", true],
["/users/42", "/users/43", false],
["/users", "/teams/{id}", false],
["/{tenant}/users", "/acme/teams", false],
])("relates %s and %s by prefix: %s", (left, right, related) => {
expect(prefixRelated(left, right)).toBe(related);
expect(prefixRelated(right, left)).toBe(related);
});
@@ -0,0 +1,242 @@
import { expect, it } from "vitest";
import type { JsonValue } from "../jsonValue.js";
import {
containsDelimited,
delimitedOffsets,
formatClock,
integerOf,
isUuid,
readClock,
replaceDelimited,
seededToken,
seededUuid,
slugOf,
spelledLike,
type ClockFormat,
} from "./environmentStateWriteValues.js";
const WHOLE_SECOND = Date.UTC(2026, 2, 4, 5, 6, 7);
const WITH_MS = WHOLE_SECOND + 123;
const V4_UUID =
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u;
const UUID_EXEMPLAR = "123e4567-e89b-12d3-a456-426614174000";
const iso = (
separator: "T" | " ",
fraction: number,
zone: string,
): ClockFormat => ({ kind: "iso", separator, fraction, zone });
const seeds = (count: number): string[] =>
Array.from({ length: count }, (_, index) => `seed-${String(index)}`);
it.each<[string, number, ClockFormat]>([
["2026-03-04T05:06:07Z", WHOLE_SECOND, iso("T", 0, "Z")],
["2026-03-04 05:06:07.123Z", WITH_MS, iso(" ", 3, "Z")],
["2026-03-04T05:06:07.123456Z", WITH_MS, iso("T", 6, "Z")],
["2026-03-04T07:06:07.123+02:00", WITH_MS, iso("T", 3, "+02:00")],
["2026-03-03T23:36:07-0530", WHOLE_SECOND, iso("T", 0, "-0530")],
["2026-03-04T05:06:07", WHOLE_SECOND, iso("T", 0, "")],
["2024-02-29 00:00:00Z", Date.UTC(2024, 1, 29), iso(" ", 0, "Z")],
])("reads the ISO clock %s with its spelling", (text, ms, format) => {
expect(readClock(text)).toEqual({ ms, format });
});
it.each([
"2026-02-30T00:00:00Z",
"2026-02-29T00:00:00Z",
"2026-13-01T00:00:00Z",
"2026-03-04T24:00:00Z",
"2026-03-04T05:60:00Z",
"2026-03-04T05:06:60Z",
"2026-03-04",
"2026-03-04T05:06Z",
"2026-03-04T05:06:07+05",
"2026-03-04T05:06:07.1234567890Z",
"2026-03-04t05:06:07Z",
"next tuesday",
])("refuses %s as a clock", (text) => {
expect(readClock(text)).toBeUndefined();
});
it.each<[JsonValue | undefined, ReturnType<typeof readClock>]>([
[
WHOLE_SECOND / 1000,
{ ms: WHOLE_SECOND, format: { kind: "epoch", unit: "s" } },
],
[WITH_MS, { ms: WITH_MS, format: { kind: "epoch", unit: "ms" } }],
[999_999_999, undefined],
[99_999_999_999, undefined],
[10_000_000_000_000, undefined],
[WHOLE_SECOND / 1000 + 0.5, undefined],
[String(WHOLE_SECOND / 1000), undefined],
[null, undefined],
[undefined, undefined],
])("reads the epoch value %j", (value, expected) => {
expect(readClock(value)).toEqual(expected);
});
it.each<JsonValue>([
"2026-03-04T05:06:07Z",
"2026-03-04 05:06:07.123Z",
"2026-03-04T07:06:07.123+02:00",
"2026-03-03T23:36:07-0530",
"2026-03-04T05:06:07.1",
"2026-03-04T05:06:07.123000000",
WHOLE_SECOND / 1000,
WITH_MS,
])("formats %j back to its own spelling", (value) => {
const clock = readClock(value)!;
expect(formatClock(clock.ms, clock.format)).toBe(value);
});
it.each<[ClockFormat, JsonValue]>([
[iso("T", 6, "Z"), "2026-03-04T05:06:07.123000Z"],
[iso(" ", 9, ""), "2026-03-04 05:06:07.123000000"],
[iso("T", 1, "Z"), "2026-03-04T05:06:07.1Z"],
[iso("T", 0, "+02:00"), "2026-03-04T07:06:07+02:00"],
[iso("T", 3, "-0530"), "2026-03-03T23:36:07.123-0530"],
[{ kind: "epoch", unit: "s" }, WHOLE_SECOND / 1000],
[{ kind: "epoch", unit: "ms" }, WITH_MS],
])("formats an instant as %j", (format, expected) => {
expect(formatClock(WITH_MS, format)).toBe(expected);
});
it.each<[JsonValue | undefined, boolean]>([
[UUID_EXEMPLAR, true],
[UUID_EXEMPLAR.toUpperCase(), true],
[UUID_EXEMPLAR.replaceAll("-", ""), false],
[UUID_EXEMPLAR.slice(1), false],
[`g${UUID_EXEMPLAR.slice(1)}`, false],
[` ${UUID_EXEMPLAR}`, false],
[42, false],
[undefined, false],
])("isUuid(%j) is %s", (value, expected) => {
expect(isUuid(value)).toBe(expected);
});
it.each<[JsonValue | undefined, number | undefined]>([
[42, 42],
[-3, -3],
["42", 42],
["007", 7],
["123456789012345", 123_456_789_012_345],
["1234567890123456", undefined],
["-3", undefined],
["4.2", undefined],
[4.2, undefined],
[2 ** 53, undefined],
["", undefined],
[" 4", undefined],
[true, undefined],
[null, undefined],
[undefined, undefined],
])("integerOf(%j) is %j", (value, expected) => {
expect(integerOf(value)).toBe(expected);
});
it("spells an integer as the recorded value spells integers", () => {
expect(spelledLike("7", 8)).toBe("8");
expect(spelledLike(7, 8)).toBe(8);
expect(spelledLike(null, 8)).toBe(8);
});
it.each([
["Hello World", "hello-world"],
["Héllo, Wörld!", "hello-world"],
[" Crème brûlée -- recipe__v2 ", "creme-brulee-recipe-v2"],
["Fullwidth 2", "fullwidth-2"],
["already-a-slug", "already-a-slug"],
["--Edge--", "edge"],
["!!!", ""],
])("slugOf(%j) is %j", (text, slug) => {
expect(slugOf(text)).toBe(slug);
});
it.each<[string, string, number[]]>([
["/issues/2", "2", [8]],
["2026", "2", []],
["2 of 22, page 2", "2", [0, 14]],
["x2/2y", "2", []],
["é2", "2", []],
["https://x.test/items/abc123?ref=abc123", "abc123", [21, 32]],
["anything", "", []],
])("finds %j's delimited %j at %j", (text, token, offsets) => {
expect(delimitedOffsets(text, token)).toEqual(offsets);
});
it("replaces only delimited occurrences", () => {
expect(replaceDelimited("/issues/2 in 2026", "2", "9")).toBe(
"/issues/9 in 2026",
);
expect(replaceDelimited("a-2-b-2", "2", "10")).toBe("a-10-b-10");
expect(replaceDelimited("2026", "2", "9")).toBe("2026");
});
// Known bug: a rejected match skips its whole length, hiding an overlapping delimited one.
it("finds a delimited match that overlaps an undelimited one", () => {
expect(delimitedOffsets("11-1-1", "1-1")).toEqual([3]);
});
it("finds a delimited token in any string nested in objects and arrays", () => {
expect(
containsDelimited({ a: [1, { b: ["x", "see /issues/2"] }] }, "2"),
).toBe(true);
expect(containsDelimited("2", "2")).toBe(true);
expect(containsDelimited({ c: "2026", n: 2, d: null }, "2")).toBe(false);
expect(containsDelimited(["2026", true, [{ e: "x2" }]], "2")).toBe(false);
});
it("derives a deterministic version 4 UUID per seed", () => {
const first = seededUuid("seed-a", UUID_EXEMPLAR);
expect(first).toMatch(V4_UUID);
expect(seededUuid("seed-a", UUID_EXEMPLAR)).toBe(first);
const derived = seeds(64).map((seed) => seededUuid(seed, UUID_EXEMPLAR));
expect(new Set(derived).size).toBe(derived.length);
for (const uuid of derived) expect(uuid).toMatch(V4_UUID);
});
it("keeps upper case only when the recorded UUID is upper case", () => {
const upper = seededUuid("seed-a", UUID_EXEMPLAR.toUpperCase());
expect(upper).toBe(seededUuid("seed-a", UUID_EXEMPLAR).toUpperCase());
expect(upper).toMatch(/[A-F]/u);
const digits = "12345678-1234-4234-8234-123456789012";
expect(seededUuid("seed-a", digits)).toMatch(V4_UUID);
const mixed = "123e4567-E89B-12d3-a456-426614174000";
expect(seededUuid("seed-a", mixed)).toMatch(V4_UUID);
});
it.each<[string, RegExp]>([
["a1b2c3d4e5f6", /^[0-9a-f]{12}$/u],
["A1B2C3D4E5F6", /^[0-9A-F]{12}$/u],
["k9x2m7q4z1w8", /^[0-9a-z]{12}$/u],
["AbCdEfGhIjKl", /^[A-Za-z]{12}$/u],
["deadbeefcafe", /^[a-z]{12}$/u],
[
"tok_9Xa-7bQ.2",
/^[0-9A-Za-z]{3}_[0-9A-Za-z]{3}-[0-9A-Za-z]{3}\.[0-9A-Za-z]$/u,
],
["____", /^____$/u],
])("keeps the length and character classes of %j", (exemplar, shape) => {
for (const seed of seeds(16)) {
const token = seededToken(seed, exemplar);
expect(token).toHaveLength(exemplar.length);
expect(token).toMatch(shape);
expect(seededToken(seed, exemplar)).toBe(token);
}
});
it("draws tokens from the whole alphabet the recorded token uses", () => {
const characters = (exemplar: string): string =>
seeds(64)
.map((seed) => seededToken(seed, exemplar))
.join("");
expect(characters("a1b2c3d4e5f6")).toMatch(/^[0-9a-f]+$/u);
expect(characters("k9x2m7q4z1w8")).toMatch(/[g-z]/u);
const mixed = characters("AbCdEfGhIjKl");
expect(mixed).toMatch(/[a-z]/u);
expect(mixed).toMatch(/[A-Z]/u);
expect(seededToken("seed-a", "k9x2m7q4z1w8")).not.toBe(
seededToken("seed-b", "k9x2m7q4z1w8"),
);
});
@@ -0,0 +1,492 @@
import { describe, expect, it } from "vitest";
import type { JsonValue } from "../jsonValue.js";
import {
timedCaptureEvidence,
type TimedCaptureRow,
} from "../../../tests/fixtures/analysisView.js";
import {
BOOK_INIT_ROWS,
BOOK_NOT_FOUND,
BOOK_ROWS,
BOOK_WRITE_CLOCK,
FIRST_BOOK,
SECOND_BOOK,
THIRD_BOOK,
book,
bookClock,
environmentStateViewOf,
rawCaptureEvidence,
renamedBook,
type Book,
} from "../../../tests/fixtures/environmentStateWrites.js";
type StateView = ReturnType<typeof environmentStateViewOf>;
const ISO_SECONDS = {
kind: "iso",
separator: "T",
fraction: 0,
zone: "Z",
} as const;
const stateOf = (rows: readonly TimedCaptureRow[]): StateView =>
environmentStateViewOf(timedCaptureEvidence(rows));
const planOf = (state: StateView, tool: string) => {
const plan = state.writes.find((candidate) => candidate.tool === tool);
if (plan === undefined) throw new Error(`No write plan for ${tool}`);
return plan;
};
/** Each write tool's verdict: "served", else the reason it is not. */
const verdicts = (rows: readonly TimedCaptureRow[]) =>
Object.fromEntries(
stateOf(rows).writes.map((plan) => [
plan.tool,
plan.served ? "served" : plan.reason,
]),
);
/** A create of the third book whose response is `response`, then the given later exchanges. */
const created = (
response: JsonValue,
...later: TimedCaptureRow[]
): TimedCaptureRow[] => [
...BOOK_INIT_ROWS,
["POST", "/api/books", response, 201, { name: "Third Book" }],
...later,
];
const RENAMED_AT_3 = renamedBook(FIRST_BOOK, "Renamed Book", 3);
/** An update renaming the first book whose response is `response`, then the given later exchanges. */
const rename = (
response: Book = RENAMED_AT_3,
...later: TimedCaptureRow[]
): TimedCaptureRow[] => [
...BOOK_INIT_ROWS,
["PATCH", "/api/books/1", response, 200, { name: "Renamed Book" }],
...later,
];
describe("served write plans", () => {
it("serves the create, update and delete that later reads confirm, citing those reads", () => {
const state = stateOf(BOOK_ROWS);
expect(state.write_clock).toBe(BOOK_WRITE_CLOCK);
expect(planOf(state, "post_books")).toEqual({
tool: "post_books",
effect: "create",
target: "/api/books",
served: true,
reason: null,
exemplar_ordinal: 3,
observation: { ordinal: 3, pointer: "" },
response: { ordinal: 3, shape: "entity", pointer: "" },
body: {
accepted: ["name"],
fixed: [],
required: ["name"],
identity: null,
},
rules: [
{ field: "id", rule: "identity", generator: "sequence" },
{ field: "name", rule: "echo" },
{ field: "slug", rule: "slug", from: "name" },
{ field: "created_at", rule: "clock", format: ISO_SECONDS },
{ field: "updated_at", rule: "clock", format: ISO_SECONDS },
],
collection: null,
insert: { rule: "sorted", field: "id", direction: "ascending" },
merge: null,
checked_record_ordinals: [4, 9],
contradicted_by: null,
});
// PATCH requires no member; the slug follows the sent name and the update stamps its clock.
expect(planOf(state, "patch_books_by_id")).toMatchObject({
effect: "update",
served: true,
exemplar_ordinal: 5,
body: { accepted: ["name"], required: [] },
rules: [
{ field: "name", rule: "echo" },
{ field: "slug", rule: "slug", from: "name" },
{ field: "updated_at", rule: "clock", format: ISO_SECONDS },
],
checked_record_ordinals: [6, 9],
});
expect(planOf(state, "delete_books_by_id")).toMatchObject({
effect: "delete",
served: true,
exemplar_ordinal: 7,
response: { ordinal: 7, shape: "empty", pointer: null },
checked_record_ordinals: [8, 9],
});
expect(planOf(state, "post_exports")).toMatchObject({
effect: "none",
target: null,
served: false,
reason: "no-state-target",
});
});
it("starts no clock on a record-order timeline, where no value is generated by a clock", () => {
const state = environmentStateViewOf(
rawCaptureEvidence(
created(THIRD_BOOK, ["GET", "/api/books/3", THIRD_BOOK]),
{
mimeType: "application/json",
timed: false,
},
),
);
expect(state.timeline).toBe("record-order");
expect(state.write_clock).toBeNull();
const plan = planOf(state, "post_books");
expect(plan).toMatchObject({ served: true, checked_record_ordinals: [4] });
// Without start times the clock fields keep the recorded values as constants.
expect(plan.rules.map(({ field }) => field)).toEqual([
"id",
"name",
"slug",
]);
});
});
describe("unserved write plans", () => {
it("names a write to no state type and a tool with no successful call", () => {
expect(
verdicts([
...BOOK_INIT_ROWS,
["POST", "/api/session", { token: "t0" }, 200, { user: "ada" }],
["POST", "/api/books", { errors: ["name"] }, 422, { name: "" }],
["DELETE", "/api/books/9", BOOK_NOT_FOUND, 404],
]),
).toEqual({
post_session: "no-state-target",
post_books: "no-call",
delete_books_by_id: "no-call",
});
});
it("refuses a create whose request body cannot enter the state", () => {
/** The create of the third book sending `sent`, or no body, then a read that shows it. */
const sending = (...sent: [] | [JsonValue]): TimedCaptureRow[] => [
...BOOK_INIT_ROWS,
["POST", "/api/books", THIRD_BOOK, 201, ...sent],
["GET", "/api/books/3", THIRD_BOOK],
];
const recorded = (mimeType: string, text: string) =>
environmentStateViewOf(
rawCaptureEvidence(sending(text), { mimeType, timed: true }),
);
for (const state of [
stateOf(sending()),
stateOf(sending([{ name: "Third Book" }])),
recorded("application/x-www-form-urlencoded", "name=Third+Book"),
recorded("application/json", '{"__proto__":{},"name":"Third Book"}'),
recorded(
"application/json",
'{"name":"Third Book","copies":12345678901234567890}',
),
])
expect(planOf(state, "post_books")).toMatchObject({
served: false,
reason: "body-unusable",
exemplar_ordinal: null,
});
});
it("reports no observation when neither the response nor a read before the next write shows the entity", () => {
expect(verdicts(created({ ok: true }))).toEqual({
post_books: "no-observation",
});
// The read of the created book comes after the next write; the update names a book never held.
expect(
verdicts(
created(
"",
[
"PATCH",
"/api/books/7",
{ id: 7, name: "Seventh" },
200,
{ name: "Seventh" },
],
["GET", "/api/books/3", THIRD_BOOK],
),
),
).toEqual({
post_books: "no-observation",
patch_books_by_id: "no-observation",
});
});
it("reports an update that changed an unsent field no clock, increment or slug explains", () => {
const changed = { ...RENAMED_AT_3, slug: "first-book-v2" };
expect(verdicts(rename(changed, ["GET", "/api/books/1", changed]))).toEqual(
{
patch_books_by_id: "unexplained-change",
},
);
});
it("names the first later read that contradicts a write", () => {
const other = renamedBook(RENAMED_AT_3, "Other Name", 3);
const plan = planOf(
stateOf(
rename(
RENAMED_AT_3,
["GET", "/api/books/1", other],
["GET", "/api/books/1", other],
),
),
"patch_books_by_id",
);
expect(plan).toMatchObject({
served: false,
reason: "contradicted",
contradicted_by: 4,
checked_record_ordinals: [],
});
});
it("blames a related write when a book no write touched changed after it", () => {
const plan = planOf(
stateOf(
created(
THIRD_BOOK,
["GET", "/api/books/3", THIRD_BOOK],
[
"GET",
"/api/books/2",
renamedBook(SECOND_BOOK, "Second Edition", 5),
],
),
),
"post_books",
);
// The create's own read confirmed it, but the changed sibling outweighs that.
expect(plan).toMatchObject({
served: false,
reason: "side-effect",
checked_record_ordinals: [4],
contradicted_by: null,
});
});
it("leaves a write that no later read checks unconfirmed", () => {
expect(planOf(stateOf(created(THIRD_BOOK)), "post_books")).toMatchObject({
served: false,
reason: "unconfirmed",
exemplar_ordinal: 3,
checked_record_ordinals: [],
});
});
});
describe("delete confirmation", () => {
it("confirms a delete by a later 404, 400 or JSON null item read", () => {
const absences: [body: JsonValue, status: number][] = [
[BOOK_NOT_FOUND, 404],
[{ detail: "Invalid book id." }, 400],
[null, 200],
];
for (const [body, status] of absences)
expect(
planOf(
stateOf([
...BOOK_INIT_ROWS,
["DELETE", "/api/books/2", "", 204],
["GET", "/api/books/2", body, status],
]),
"delete_books_by_id",
),
).toMatchObject({ served: true, checked_record_ordinals: [4] });
});
it("withholds a delete whose recorded answer shows fields no initial entity holds", () => {
const answered = (previous: JsonValue) =>
planOf(
stateOf([
...BOOK_INIT_ROWS,
["DELETE", "/api/books/2", { deleted: true, previous }, 200],
["GET", "/api/books/2", BOOK_NOT_FOUND, 404],
]),
"delete_books_by_id",
);
// An edit view of the book: the read views never showed its draft field.
expect(answered({ ...SECOND_BOOK, draft: false })).toMatchObject({
served: false,
reason: "incomplete-entities",
checked_record_ordinals: [4],
});
expect(answered(SECOND_BOOK)).toMatchObject({
served: true,
response: { shape: "entity", pointer: "/previous" },
});
});
});
describe("insert rules", () => {
it("learns where a create inserts from the recorded collection order", () => {
// Listed by name, while neither identities nor clocks are in order.
const shelf = [
book(3, "Alpha", "2026-10-01T10:00:00Z"),
book(1, "Delta", "2026-10-01T09:00:00Z"),
book(2, "Zulu", "2026-10-01T11:00:00Z"),
];
const mike = book(4, "Mike", bookClock(6));
const state = stateOf([
["GET", "/api/books", shelf],
...shelf.map((entry): TimedCaptureRow => [
"GET",
`/api/books/${String(entry.id)}`,
entry,
]),
["GET", "/api/books", shelf],
["GET", "/api/books", shelf],
["POST", "/api/books", mike, 201, { name: "Mike" }],
["GET", "/api/books", [shelf[0]!, shelf[1]!, mike, shelf[2]!]],
]);
expect(planOf(state, "post_books")).toMatchObject({
served: true,
insert: { rule: "sorted", field: "name", direction: "ascending" },
checked_record_ordinals: [7],
});
});
it("takes the position a later list shows when the recorded order admits several", () => {
const zed = book(3, "Zed", bookClock(3));
const plan = planOf(
stateOf(
created(zed, ["GET", "/api/books", [zed, FIRST_BOOK, SECOND_BOOK]]),
),
"post_books",
);
expect(plan).toMatchObject({
served: true,
insert: { rule: "first" },
checked_record_ordinals: [4],
});
});
});
describe("unmodelled writes", () => {
const shelved = (entry: Book, archived = false): Book => ({
...entry,
archived,
});
/** The rename, an archive the gate does not model when `archive` is set, then a read of both changes. */
const archivedAfterRename = (archive: boolean) =>
stateOf([
["GET", "/api/books", [shelved(FIRST_BOOK), shelved(SECOND_BOOK)]],
["GET", "/api/books/1", shelved(FIRST_BOOK)],
["GET", "/api/books/2", shelved(SECOND_BOOK)],
[
"PATCH",
"/api/books/1",
shelved(RENAMED_AT_3),
200,
{ name: "Renamed Book" },
],
...(archive
? [
[
"POST",
"/api/books/1/archive",
{ archived: true },
200,
{},
] as TimedCaptureRow,
]
: []),
["GET", "/api/books/1", shelved(RENAMED_AT_3, true)],
]);
it("confirms an update on the members it sent when an unmodelled write also changed the entity", () => {
const state = archivedAfterRename(true);
expect(planOf(state, "patch_books_by_id")).toMatchObject({
served: true,
checked_record_ordinals: [5],
contradicted_by: null,
});
expect(planOf(state, "post_books_archive_by_id")).toMatchObject({
effect: "none",
reason: "no-state-target",
});
// With no unmodelled write the same read contradicts the update.
expect(
planOf(archivedAfterRename(false), "patch_books_by_id"),
).toMatchObject({
reason: "contradicted",
contradicted_by: 4,
});
});
it("does not blame a modelled write for a check that fails after an unmodelled write", () => {
const changedSibling = renamedBook(SECOND_BOOK, "Second Edition", 4);
const reindex: TimedCaptureRow = [
"POST",
"/api/jobs/reindex",
{ queued: true },
202,
{},
];
const list: TimedCaptureRow = [
"GET",
"/api/books",
[RENAMED_AT_3, changedSibling],
];
expect(
planOf(stateOf(rename(RENAMED_AT_3, list)), "patch_books_by_id"),
).toMatchObject({
reason: "contradicted",
contradicted_by: 4,
});
expect(
planOf(stateOf(rename(RENAMED_AT_3, reindex, list)), "patch_books_by_id"),
).toMatchObject({ reason: "unconfirmed", contradicted_by: null });
// A create wrote every field, so after an unmodelled write a differing read checks none of them.
const reslugged = { ...THIRD_BOOK, slug: "third-book-1" };
const read: TimedCaptureRow = ["GET", "/api/books/3", reslugged];
expect(
planOf(stateOf(created(THIRD_BOOK, read)), "post_books"),
).toMatchObject({
reason: "contradicted",
contradicted_by: 4,
});
expect(
planOf(stateOf(created(THIRD_BOOK, reindex, read)), "post_books"),
).toMatchObject({
reason: "unconfirmed",
contradicted_by: null,
});
});
});
describe("observation from a later read", () => {
it("observes a created entity in the first later item read when the response names only its identity", () => {
const later: TimedCaptureRow[] = [
["GET", "/api/books/3", THIRD_BOOK],
["GET", "/api/books", [FIRST_BOOK, SECOND_BOOK, THIRD_BOOK]],
];
const bare = planOf(stateOf(created({ id: 3 }, ...later)), "post_books");
// The read that showed the entity does not check the create; the later list does.
expect(bare).toMatchObject({
served: true,
exemplar_ordinal: 3,
observation: { ordinal: 4, pointer: "" },
response: { ordinal: 3, shape: "entity", pointer: "" },
checked_record_ordinals: [5],
});
const named = planOf(
stateOf(created({ created_id: 3 }, ...later)),
"post_books",
);
expect(named).toMatchObject({
served: true,
observation: { ordinal: 4, pointer: "" },
response: { ordinal: 3, shape: "identity", pointer: "/created_id" },
checked_record_ordinals: [5],
});
});
});
@@ -0,0 +1,97 @@
import {
Client,
StreamableHTTPClientTransport,
} from "@modelcontextprotocol/client";
import { describe, expect } from "vitest";
import { timedCaptureEvidence } from "../../fixtures/analysisView.js";
import {
environmentStateServeFixture,
writeEnvironmentServeFiles,
} from "../../fixtures/environmentServe.js";
import {
BOOK_NOT_FOUND,
BOOK_ROWS,
book,
bookClock,
} from "../../fixtures/environmentStateWrites.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { startListeningProcess } from "../../support/mcp/streamableHttpProcess.js";
import { processTest } from "../../support/process/processFixture.js";
const HTTP_BOUNDARY_TIMEOUT_MS = 90_000;
/** The book the first served create of "Boundary Book" makes: one second past the served clock's start. */
const BOUNDARY_BOOK = book(3, "Boundary Book", bookClock(13));
describe("rea environment-serve write tools", () => {
processTest(
"applies a served create to the state the next read answers from, until data/reset",
async ({ processes }) => {
const root = await createTestTempDirectory("rea-environment-writes-mcp-");
const files = await writeEnvironmentServeFiles(
root,
environmentStateServeFixture(timedCaptureEvidence(BOOK_ROWS)),
);
const serve = [files.manifestPath, "--evidence", files.capturePath];
const started = await startListeningProcess(processes, [
"environment-serve",
...serve,
"--port",
"0",
]);
const client = new Client({ name: "environment-writes", version: "1" });
await client.connect(new StreamableHTTPClientTransport(started.url));
try {
const { tools } = await client.listTools();
const names = tools.map(({ name }) => name);
expect(names).toEqual(
expect.arrayContaining(["post_books", "patch_books_by_id"]),
);
// The capture's export write targets no state type, so only confirmed writes are listed.
expect(names).not.toContain("post_exports");
expect(
tools.find(({ name }) => name === "post_books")?.annotations,
).toMatchObject({
readOnlyHint: false,
});
const created = await client.callTool({
name: "post_books",
arguments: { body: { name: "Boundary Book" } },
});
expect(created.isError).toBeFalsy();
expect(created.structuredContent).toMatchObject({
status: 201,
body: BOUNDARY_BOOK,
state: { source: "state", view: "create", id: "3" },
});
const read = () =>
client.callTool({ name: "get_books_by_id", arguments: { id: "3" } });
expect((await read()).structuredContent).toMatchObject({
status: 200,
body: BOUNDARY_BOOK,
state: { source: "state", view: "item", id: "3" },
});
const reset = await fetch(new URL("/agentenv", started.url.origin), {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
jsonrpc: "2.0",
id: "reset",
method: "data/reset",
}),
});
await expect(reset.json()).resolves.toMatchObject({ result: {} });
// The initial state holds no third book, so the read answers the recorded absence.
expect((await read()).structuredContent).toMatchObject({
status: 404,
body: BOOK_NOT_FOUND,
state: { source: "state", view: "missing", id: "3" },
});
} finally {
await client.close();
}
},
HTTP_BOUNDARY_TIMEOUT_MS,
);
});
+15 -3
View File
@@ -314,12 +314,13 @@ export const environmentManifestCaptureFixture = (
export const environmentStateCaptureFixture = () =>
environmentManifestCaptureFixture(environmentStateHar().log.entries);
/** One synthetic exchange: method, path, JSON response body (a string is recorded as written) and status. */
/** One synthetic exchange: method, path, JSON response body (a string is recorded as written), status and JSON request body. */
export type TimedCaptureRow = readonly [
method: string,
path: string,
body?: JsonValue,
status?: number,
request?: JsonValue,
];
/** Capture Evidence of HAR entries one second apart on one origin, as the environment-state view orders them. */
@@ -328,12 +329,23 @@ export const timedCaptureEvidence = (
): Evidence =>
analysisViewCaptureEvidence(
environmentManifestCaptureFixture(
rows.map(([method, path, body = null, status = 200], index) => ({
rows.map(([method, path, body = null, status = 200, sent], index) => ({
startedDateTime: new Date(
Date.parse("2026-10-17T18:00:00.000Z") + index * 1000,
).toISOString(),
time: 5,
request: { method, url: `http://app.example${path}` },
request: {
method,
url: `http://app.example${path}`,
...(sent === undefined
? {}
: {
postData: {
mimeType: "application/json",
text: JSON.stringify(sent),
},
}),
},
response: {
status,
content: {
+163
View File
@@ -0,0 +1,163 @@
import {
projectAnalysisView,
type AnalysisViewResult,
} from "../../src/domain/analysisView/analysisView.js";
import type { Evidence } from "../../src/domain/evidence.js";
import type { JsonValue } from "../../src/domain/jsonValue.js";
import {
analysisViewCaptureEvidence,
environmentManifestCaptureFixture,
type TimedCaptureRow,
} from "./analysisView.js";
/** When the first synthetic exchange starts; exchange `i` starts `i` seconds later, as in `timedCaptureEvidence`. */
const CAPTURE_START_MS = Date.parse("2026-10-17T18:00:00.000Z");
/** The instant `seconds` after the first exchange, spelled as the books application spells clock values. */
export const bookClock = (seconds: number): string =>
new Date(CAPTURE_START_MS + seconds * 1000)
.toISOString()
.replace(".000Z", "Z");
/** Lower case with every run of other characters joined by one `-`, as the books application slugs names. */
const slugOf = (name: string): string =>
name
.toLowerCase()
.replace(/[^a-z0-9]+/gu, "-")
.replace(/^-|-$/gu, "");
/** A book as the application shows it. */
export type Book = { [field: string]: JsonValue };
/** A book whose slug follows its name. */
export const book = (
id: number,
name: string,
created: string,
updated = created,
): Book => ({
id,
name,
slug: slugOf(name),
created_at: created,
updated_at: updated,
});
/** A book renamed by an update recorded at exchange `seconds`: its slug and update clock follow. */
export const renamedBook = (
entity: Book,
name: string,
seconds: number,
): Book => ({
...entity,
name,
slug: slugOf(name),
updated_at: bookClock(seconds),
});
export const FIRST_BOOK = book(1, "First Book", "2026-10-01T09:00:00Z");
export const SECOND_BOOK = book(2, "Second Book", "2026-10-01T10:00:00Z");
/** The book the recorded create at exchange 3 made. */
export const THIRD_BOOK = book(3, "Third Book", bookClock(3));
/** The first book after the recorded update at exchange 5. */
export const RENAMED_BOOK = renamedBook(FIRST_BOOK, "Renamed Book", 5);
/** What the item read answered for a book the application does not hold. */
export const BOOK_NOT_FOUND = { detail: "Not found." };
/** What the delete answered for a book the application does not hold. */
export const DELETE_NOT_FOUND = { detail: "No Book matches the given query." };
/** Reads before any write: the whole list, then each book; the initial state comes from these. */
export const BOOK_INIT_ROWS: readonly TimedCaptureRow[] = [
["GET", "/api/books", [FIRST_BOOK, SECOND_BOOK]],
["GET", "/api/books/1", FIRST_BOOK],
["GET", "/api/books/2", SECOND_BOOK],
];
/**
* A create, an update and a delete, each confirmed by later reads; a delete of a book the
* application did not hold; and a write to no state type.
*/
export const BOOK_ROWS: readonly TimedCaptureRow[] = [
...BOOK_INIT_ROWS,
["POST", "/api/books", THIRD_BOOK, 201, { name: "Third Book" }],
["GET", "/api/books/3", THIRD_BOOK],
["PATCH", "/api/books/1", RENAMED_BOOK, 200, { name: "Renamed Book" }],
["GET", "/api/books/1", RENAMED_BOOK],
["DELETE", "/api/books/3", "", 204],
["GET", "/api/books/3", BOOK_NOT_FOUND, 404],
["GET", "/api/books", [RENAMED_BOOK, SECOND_BOOK]],
["DELETE", "/api/books/9", DELETE_NOT_FOUND, 404],
["POST", "/api/exports", { queued: true }, 202, { format: "csv" }],
];
/** The end of the last exchange of BOOK_ROWS, to the next whole second: where the served clock starts. */
export const BOOK_WRITE_CLOCK = "2026-10-17T18:00:12.000Z";
/**
* Capture Evidence like `timedCaptureEvidence`, but each request body is recorded under the
* given media type (a string as written, anything else as JSON), and the exchanges may carry
* no start times.
*/
export const rawCaptureEvidence = (
rows: readonly TimedCaptureRow[],
request: { readonly mimeType: string; readonly timed: boolean },
): Evidence =>
analysisViewCaptureEvidence(
environmentManifestCaptureFixture(
rows.map(([method, path, body = null, status = 200, sent], index) => ({
...(request.timed
? {
startedDateTime: new Date(
CAPTURE_START_MS + index * 1000,
).toISOString(),
time: 5,
}
: {}),
request: {
method,
url: `http://app.example${path}`,
...(sent === undefined
? {}
: {
postData: {
mimeType: request.mimeType,
text: typeof sent === "string" ? sent : JSON.stringify(sent),
},
}),
},
response: {
status,
content: {
mimeType: "application/json",
text: typeof body === "string" ? body : JSON.stringify(body),
},
},
})),
),
);
type EnvironmentStateView = Extract<
AnalysisViewResult,
{ kind: "environment-state" }
>;
/** The environment-state view a capture projects with no opt-in template rules. */
export const environmentStateViewOf = (
capture: Evidence,
): EnvironmentStateView => {
const result = projectAnalysisView(
{
evidenceId: capture.evidence_id,
operation: capture.operation,
normalizedResult: capture.normalized_result,
subject: capture.subject,
limitations: [],
parameters: capture.parameters,
},
{ kind: "environment-state", template_rules: [] },
);
if (!result.ok || result.value.kind !== "environment-state")
throw new Error("Expected an environment state");
return result.value;
};