676 lines
22 KiB
TypeScript
676 lines
22 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import {
|
|
timedCaptureEvidence,
|
|
analysisViewCaptureEvidence,
|
|
type TimedCaptureRow,
|
|
} from "../../../tests/fixtures/analysisView.js";
|
|
import {
|
|
environmentStateServeFixture,
|
|
environmentStateServeInputs,
|
|
} from "../../../tests/fixtures/environmentServe.js";
|
|
import { servedCatalog } from "./environmentState.js";
|
|
import { servedFrom } from "../../../tests/fixtures/environmentStateServing.js";
|
|
import {
|
|
BOOK_NOT_FOUND,
|
|
FIRST_BOOK,
|
|
SECOND_BOOK,
|
|
book,
|
|
bookClock,
|
|
renamedBook,
|
|
offsetListed,
|
|
} from "../../../tests/fixtures/environmentStateWrites.js";
|
|
|
|
import { webNetworkCaptureSchema } from "../../domain/webNetworkCapture.js";
|
|
|
|
const archived = (value: boolean) => ({ ...FIRST_BOOK, archived: value });
|
|
|
|
describe("bodyless action evidence", () => {
|
|
it.each<{
|
|
name: string;
|
|
postData: unknown;
|
|
bodySize?: unknown;
|
|
served?: boolean;
|
|
shape?: boolean;
|
|
}>([
|
|
{
|
|
name: "form",
|
|
postData: {
|
|
mimeType: "application/x-www-form-urlencoded",
|
|
text: "toggle=false",
|
|
},
|
|
},
|
|
{
|
|
name: "malformed JSON",
|
|
postData: { mimeType: "application/json", text: "{" },
|
|
},
|
|
{
|
|
name: "oversized JSON",
|
|
postData: {
|
|
mimeType: "application/json",
|
|
text: JSON.stringify({ note: "x".repeat(256 * 1024) }),
|
|
},
|
|
},
|
|
{ name: "unretained body", postData: undefined },
|
|
{
|
|
name: "omitted body with zero size",
|
|
postData: undefined,
|
|
bodySize: 0,
|
|
served: true,
|
|
},
|
|
{ name: "explicit null with zero size", postData: null, bodySize: 0 },
|
|
{
|
|
name: "params-only with zero size",
|
|
postData: {
|
|
mimeType: "application/json",
|
|
params: [{ name: "archived", value: "true" }],
|
|
},
|
|
bodySize: 0,
|
|
},
|
|
{
|
|
name: "empty text with nonempty params",
|
|
postData: {
|
|
mimeType: "application/json",
|
|
text: "",
|
|
params: [{ name: "archived", value: "true" }],
|
|
},
|
|
bodySize: 0,
|
|
},
|
|
{
|
|
name: "explicit empty body with zero size",
|
|
postData: { mimeType: "application/json", text: "", params: [] },
|
|
bodySize: 0,
|
|
served: true,
|
|
},
|
|
{
|
|
name: "invalid size with retained JSON",
|
|
postData: { mimeType: "application/json", text: '{"archived":true}' },
|
|
bodySize: "unknown",
|
|
shape: true,
|
|
},
|
|
])(
|
|
"preserves literal request-body availability for repeated actions: $name",
|
|
({ postData, bodySize = 12, served = false, shape = false }) => {
|
|
const capture = timedCaptureEvidence([
|
|
["GET", "/api/books", [archived(false), SECOND_BOOK]],
|
|
["GET", "/api/books/1", archived(false)],
|
|
["GET", "/api/books/2", SECOND_BOOK],
|
|
["POST", "/api/books/1/archive", archived(true)],
|
|
["GET", "/api/books/1", archived(true)],
|
|
["POST", "/api/books/1/archive", archived(true)],
|
|
["GET", "/api/books/1", archived(true)],
|
|
]);
|
|
const result = capture.normalized_result as {
|
|
records: {
|
|
reported: { request: { postData?: unknown; bodySize?: unknown } };
|
|
}[];
|
|
};
|
|
const request = result.records[5]!.reported.request;
|
|
request.bodySize = bodySize;
|
|
if (postData === undefined) delete request.postData;
|
|
else request.postData = postData;
|
|
if (shape) {
|
|
result.records[3]!.reported.request.bodySize = bodySize;
|
|
result.records[3]!.reported.request.postData = postData;
|
|
}
|
|
const { source, state } = environmentStateServeInputs(
|
|
environmentStateServeFixture(
|
|
analysisViewCaptureEvidence(
|
|
webNetworkCaptureSchema.parse(capture.normalized_result),
|
|
),
|
|
),
|
|
);
|
|
expect(state.writes.get("post_books_archive_by_id")).toMatchObject({
|
|
served,
|
|
});
|
|
const tool = source.manifest.tools.find(
|
|
({ name }) => name === "post_books_archive_by_id",
|
|
);
|
|
expect(tool).toBeDefined();
|
|
if (shape)
|
|
expect(tool?.body_shape).toMatchObject({
|
|
properties: { archived: "boolean" },
|
|
});
|
|
else if (bodySize === 0) expect(tool?.body_shape).toBeNull();
|
|
},
|
|
);
|
|
});
|
|
|
|
it("keeps a stored array-of-object field as an ordinary update body", () => {
|
|
const original = { ...FIRST_BOOK, tags: [{ id: 7, name: "Old" }] };
|
|
const second = { ...SECOND_BOOK, tags: [{ id: 8, name: "Other" }] };
|
|
const changed = { ...original, tags: [{ id: 9, name: "Recorded" }] };
|
|
const books = servedFrom([
|
|
["GET", "/api/books", [original, second]],
|
|
["GET", "/api/books/1", original],
|
|
["GET", "/api/books/2", second],
|
|
["PATCH", "/api/books/1", changed, 200, { tags: changed.tags }],
|
|
["GET", "/api/books/1", changed],
|
|
["GET", "/api/books", [changed, second]],
|
|
]);
|
|
expect(books.state.writes.get("patch_books_by_id")).toMatchObject({
|
|
served: true,
|
|
});
|
|
expect(books.state.writes.get("patch_books_by_id")?.body).not.toHaveProperty(
|
|
"envelope",
|
|
);
|
|
const tags = [
|
|
{ id: 10, name: "Caller" },
|
|
{ id: 11, name: "Second tag" },
|
|
];
|
|
expect(
|
|
books.answer("patch_books_by_id", { id: "2", body: { tags } }).body,
|
|
).toMatchObject({ id: 2, tags });
|
|
});
|
|
|
|
const TOKENS = ["Abcdef0123456789A", "Abcdef0123456789B", "Abcdef0123456789C"];
|
|
|
|
it("withholds a generated create token copied into a nested constant", () => {
|
|
const first = { ...FIRST_BOOK, version: TOKENS[0]! };
|
|
const second = { ...SECOND_BOOK, version: TOKENS[1]! };
|
|
const third = {
|
|
...book(3, "Created", bookClock(3)),
|
|
version: TOKENS[2]!,
|
|
metadata: { versions: [{ value: TOKENS[2]! }] },
|
|
};
|
|
const books = servedFrom([
|
|
["GET", "/api/books", [first, second]],
|
|
["GET", "/api/books/1", first],
|
|
["GET", "/api/books/2", second],
|
|
["POST", "/api/books", third, 201, { name: "Created" }],
|
|
["GET", "/api/books/3", third],
|
|
]);
|
|
expect(books.state.writes.get("post_books")).toMatchObject({
|
|
served: false,
|
|
reason: "unexplained-change",
|
|
});
|
|
});
|
|
|
|
it("withholds an update token already present in a nested unchanged field", () => {
|
|
const first = {
|
|
...FIRST_BOOK,
|
|
version: TOKENS[0]!,
|
|
metadata: { versions: [TOKENS[1]!] },
|
|
};
|
|
const second = { ...SECOND_BOOK, version: TOKENS[2]!, metadata: {} };
|
|
const changed = { ...first, version: TOKENS[1]! };
|
|
const books = servedFrom([
|
|
["GET", "/api/books", [first, second]],
|
|
["GET", "/api/books/1", first],
|
|
["GET", "/api/books/2", second],
|
|
["PATCH", "/api/books/1", changed, 200, {}],
|
|
["GET", "/api/books/1", changed],
|
|
["GET", "/api/books", [changed, second]],
|
|
]);
|
|
expect(books.state.writes.get("patch_books_by_id")).toMatchObject({
|
|
served: false,
|
|
});
|
|
});
|
|
|
|
it.each(["2", "02"])(
|
|
"withholds a bulk acknowledgement with string count %s",
|
|
(deleted) => {
|
|
const rows: TimedCaptureRow[] = [
|
|
[
|
|
"GET",
|
|
"/api/books",
|
|
[
|
|
{ ...FIRST_BOOK, id: 10 },
|
|
{ ...SECOND_BOOK, id: 20 },
|
|
],
|
|
],
|
|
["GET", "/api/books/10", { ...FIRST_BOOK, id: 10 }],
|
|
["GET", "/api/books/20", { ...SECOND_BOOK, id: 20 }],
|
|
["DELETE", "/api/books", { deleted }, 200, { ids: [10, 20] }],
|
|
["GET", "/api/books/10", BOOK_NOT_FOUND, 404],
|
|
["GET", "/api/books/20", BOOK_NOT_FOUND, 404],
|
|
["GET", "/api/books", []],
|
|
];
|
|
expect(servedFrom(rows).state.writes.get("delete_books")).toMatchObject({
|
|
served: false,
|
|
reason: "body-unusable",
|
|
});
|
|
},
|
|
);
|
|
|
|
const nestedClockRows = (edit: {
|
|
[key: string]: string;
|
|
}): TimedCaptureRow[] => {
|
|
const first = {
|
|
...FIRST_BOOK,
|
|
edit: {
|
|
at: FIRST_BOOK.created_at!,
|
|
seen_at: FIRST_BOOK.created_at!,
|
|
by: "alice",
|
|
},
|
|
};
|
|
const second = {
|
|
...SECOND_BOOK,
|
|
edit: {
|
|
at: SECOND_BOOK.created_at!,
|
|
seen_at: SECOND_BOOK.created_at!,
|
|
by: "alice",
|
|
},
|
|
};
|
|
const third = {
|
|
...book(3, "Third", bookClock(3)),
|
|
edit: { at: bookClock(3), seen_at: bookClock(3), by: "alice" },
|
|
};
|
|
const fourth = { ...book(4, "Fourth", bookClock(5)), edit };
|
|
return [
|
|
["GET", "/api/books", [first, second]],
|
|
["GET", "/api/books/1", first],
|
|
["GET", "/api/books/2", second],
|
|
["POST", "/api/books", third, 201, { name: "Third" }],
|
|
["GET", "/api/books/3", third],
|
|
["POST", "/api/books", fourth, 201, { name: "Fourth" }],
|
|
["GET", "/api/books/4", fourth],
|
|
["GET", "/api/books", [first, second, third, fourth]],
|
|
];
|
|
};
|
|
|
|
it.each<{ name: string; edit: { [key: string]: string } }>([
|
|
{
|
|
name: "changed editor",
|
|
edit: { at: bookClock(5), seen_at: bookClock(5), by: "bob" },
|
|
},
|
|
{
|
|
name: "added member",
|
|
edit: {
|
|
at: bookClock(5),
|
|
seen_at: bookClock(5),
|
|
by: "alice",
|
|
role: "admin",
|
|
},
|
|
},
|
|
{ name: "omitted member", edit: { at: bookClock(5), seen_at: bookClock(5) } },
|
|
{
|
|
name: "different clock values",
|
|
edit: { at: bookClock(5), seen_at: bookClock(6), by: "alice" },
|
|
},
|
|
])(
|
|
"withholds creates whose nested-clock renderer cannot reproduce a later $name",
|
|
({ edit }) => {
|
|
expect(
|
|
servedFrom(nestedClockRows(edit)).state.writes.get("post_books"),
|
|
).toMatchObject({ served: false, reason: "unexplained-change" });
|
|
},
|
|
);
|
|
|
|
it("serves successive creates whose complete nested-clock objects are reproducible", () => {
|
|
const books = servedFrom(
|
|
nestedClockRows({ at: bookClock(5), seen_at: bookClock(5), by: "alice" }),
|
|
);
|
|
expect(books.state.writes.get("post_books")).toMatchObject({ served: true });
|
|
const result = books.answer("post_books", { body: { name: "Caller" } });
|
|
expect(result.body).toMatchObject({ name: "Caller", edit: { by: "alice" } });
|
|
const edit = (result.body as { edit: { at: string; seen_at: string } }).edit;
|
|
expect(edit.at).toBe(edit.seen_at);
|
|
});
|
|
|
|
it.each([0, 1])(
|
|
"checks the complete nested-clock update for a %s-second member difference",
|
|
(seconds) => {
|
|
const first = {
|
|
...FIRST_BOOK,
|
|
edit: { at: bookClock(-8), seen_at: bookClock(-8), by: "alice" },
|
|
};
|
|
const second = {
|
|
...SECOND_BOOK,
|
|
edit: { at: bookClock(-7), seen_at: bookClock(-7), by: "alice" },
|
|
};
|
|
const changed = {
|
|
...renamedBook(first, "Renamed", 3),
|
|
edit: { at: bookClock(3), seen_at: bookClock(3 + seconds), by: "alice" },
|
|
};
|
|
const books = servedFrom([
|
|
["GET", "/api/books", [first, second]],
|
|
["GET", "/api/books/1", first],
|
|
["GET", "/api/books/2", second],
|
|
["PATCH", "/api/books/1", changed, 200, { name: "Renamed" }],
|
|
["GET", "/api/books/1", changed],
|
|
["GET", "/api/books", [changed, second]],
|
|
]);
|
|
expect(books.state.writes.get("patch_books_by_id")).toMatchObject({
|
|
served: seconds === 0,
|
|
});
|
|
if (seconds === 0) {
|
|
const result = books.answer("patch_books_by_id", {
|
|
id: "2",
|
|
body: { name: "Caller" },
|
|
});
|
|
expect(result.body).toMatchObject({ edit: { by: "alice" } });
|
|
const edit = (result.body as { edit: { at: string; seen_at: string } })
|
|
.edit;
|
|
expect(edit.at).toBe(edit.seen_at);
|
|
}
|
|
},
|
|
);
|
|
|
|
it.each([
|
|
{ bulk: false, unequal: false },
|
|
{ bulk: true, unequal: false },
|
|
{ bulk: false, unequal: true },
|
|
{ bulk: true, unequal: true },
|
|
])(
|
|
"validates the write response before its stored clock spelling (bulk=$bulk, unequal=$unequal)",
|
|
({ bulk, unequal }) => {
|
|
const edit = (seconds: number, fraction: string) => ({
|
|
at: new Date(bookClock(seconds)).toISOString().replace(".000Z", fraction),
|
|
seen_at: new Date(bookClock(seconds))
|
|
.toISOString()
|
|
.replace(".000Z", fraction),
|
|
by: "alice",
|
|
});
|
|
const first = { ...FIRST_BOOK, edit: edit(-8, ".000123Z") };
|
|
const second = { ...SECOND_BOOK, edit: edit(-7, ".000123Z") };
|
|
const changed = {
|
|
...renamedBook(first, "Renamed", 3),
|
|
edit: {
|
|
...edit(3, ".000Z"),
|
|
seen_at: new Date(bookClock(unequal ? 4 : 3)).toISOString(),
|
|
},
|
|
};
|
|
const read = { ...changed, edit: edit(3, ".000123Z") };
|
|
const books = servedFrom([
|
|
["GET", "/api/books", [first, second]],
|
|
["GET", "/api/books/1", first],
|
|
["GET", "/api/books/2", second],
|
|
[
|
|
"PATCH",
|
|
bulk ? "/api/books" : "/api/books/1",
|
|
bulk ? [changed] : changed,
|
|
200,
|
|
bulk ? [{ id: 1, name: "Renamed" }] : { name: "Renamed" },
|
|
],
|
|
["GET", "/api/books/1", read],
|
|
["GET", "/api/books", [read, second]],
|
|
]);
|
|
const name = bulk ? "patch_books" : "patch_books_by_id";
|
|
expect(books.state.writes.get(name)).toMatchObject({ served: !unequal });
|
|
if (!unequal) {
|
|
const result = books.answer(
|
|
name,
|
|
bulk
|
|
? { body: [{ id: 2, name: "Caller" }] }
|
|
: { id: "2", body: { name: "Caller" } },
|
|
);
|
|
const entity = (bulk ? (result.body as unknown[])[0] : result.body) as {
|
|
name: string;
|
|
edit: { at: string; seen_at: string; by: string };
|
|
};
|
|
expect(entity).toMatchObject({ name: "Caller", edit: { by: "alice" } });
|
|
expect(entity.edit.at).toBe(entity.edit.seen_at);
|
|
expect(entity.edit.at).toMatch(/\.\d{3}Z$/u);
|
|
}
|
|
},
|
|
);
|
|
|
|
it("keeps a create's nested reference pinned when an unobserved call varies its id", () => {
|
|
const card = (id: number, name: string, collection_id: number) => ({
|
|
id,
|
|
name,
|
|
collection_id,
|
|
collection: {
|
|
id: collection_id,
|
|
name: collection_id === 8 ? "Ledger" : "Other",
|
|
},
|
|
});
|
|
const first = card(1, "First", 8);
|
|
const second = card(2, "Second", 9);
|
|
const third = card(3, "Third", 8);
|
|
const fourth = card(4, "Fourth", 9);
|
|
const cards = servedFrom([
|
|
["GET", "/api/cards", [first, second]],
|
|
["GET", "/api/cards/1", first],
|
|
["GET", "/api/cards/2", second],
|
|
["POST", "/api/cards", third, 201, { name: "Third", collection_id: 8 }],
|
|
["GET", "/api/cards/3", third],
|
|
["POST", "/api/cards", fourth, 201, { name: "Fourth", collection_id: 9 }],
|
|
]);
|
|
const plan = cards.state.writes.get("post_cards");
|
|
expect(plan?.body?.fixed).toContainEqual({
|
|
member: "collection_id",
|
|
sent: 8,
|
|
});
|
|
expect(plan).toMatchObject({ served: false });
|
|
const catalog = servedCatalog(cards.source, cards.state, "cards");
|
|
if (!catalog.ok) throw catalog.error;
|
|
expect(catalog.value.tools.map(({ name }) => name)).not.toContain(
|
|
"post_cards",
|
|
);
|
|
});
|
|
|
|
it("refuses a create's unsupported dependent state even when an unobserved call varied it", () => {
|
|
const post = (id: number, status: string, published_at: string | null) => ({
|
|
id,
|
|
title: `Post ${String(id)}`,
|
|
status,
|
|
published_at,
|
|
});
|
|
const first = post(1, "published", "2026-10-01T09:00:00Z");
|
|
const second = post(2, "published", "2026-10-01T10:00:00Z");
|
|
const draft = post(3, "draft", null);
|
|
const posts = servedFrom([
|
|
["GET", "/api/posts", [first, second]],
|
|
["GET", "/api/posts/1", first],
|
|
["GET", "/api/posts/2", second],
|
|
["POST", "/api/posts", draft, 201, { title: "Post 3", status: "draft" }],
|
|
["GET", "/api/posts/3", draft],
|
|
["POST", "/api/posts", "", 204, { title: "Post 4", status: "published" }],
|
|
]);
|
|
expect(posts.state.writes.get("post_posts")).toMatchObject({ served: true });
|
|
const before = posts.snapshot();
|
|
expect(
|
|
posts.refusal("post_posts", {
|
|
body: { title: "Caller", status: "published" },
|
|
}),
|
|
).toMatchObject({ code: "invalid_request" });
|
|
expect(posts.snapshot()).toBe(before);
|
|
expect(
|
|
posts.answer("post_posts", {
|
|
body: { title: "Caller", status: "draft" },
|
|
}).body,
|
|
).toMatchObject({ title: "Caller", status: "draft", published_at: null });
|
|
});
|
|
|
|
it("drops a write's confirmation when its read view later disproves learned array sorting", () => {
|
|
const tagged = (book: typeof FIRST_BOOK, tags: string[]) => ({
|
|
...book,
|
|
tags,
|
|
});
|
|
const first = tagged(FIRST_BOOK, ["alpha", "beta"]);
|
|
const second = tagged(SECOND_BOOK, ["gamma", "omega"]);
|
|
const written = tagged(SECOND_BOOK, ["zeta", "delta"]);
|
|
const books = servedFrom([
|
|
["GET", "/api/books", [first, second]],
|
|
["GET", "/api/books/1", first],
|
|
["GET", "/api/books/2", second],
|
|
["PATCH", "/api/books/2", written, 200, { tags: ["zeta", "delta"] }],
|
|
["GET", "/api/books/2", tagged(SECOND_BOOK, ["delta", "zeta"])],
|
|
["GET", "/api/books/2", written],
|
|
]);
|
|
expect(books.state.views.get("get_books_by_id")).toMatchObject({
|
|
shape: "recorded",
|
|
});
|
|
expect(books.state.writes.get("patch_books_by_id")).toMatchObject({
|
|
served: false,
|
|
checked_record_ordinals: [],
|
|
});
|
|
});
|
|
|
|
it("reloads an unchanged snapshot after a view with its own clock spellings disproves sorting", () => {
|
|
const first = { ...FIRST_BOOK, tags: ["alpha", "beta"] };
|
|
const second = { ...SECOND_BOOK, tags: ["gamma", "omega"] };
|
|
const written = { ...second, tags: ["zeta", "delta"] };
|
|
const rows: TimedCaptureRow[] = [
|
|
["GET", "/api/books", [first, second].map(offsetListed)],
|
|
["GET", "/api/books/1", first],
|
|
["GET", "/api/books/2", second],
|
|
["PATCH", "/api/books/2", written, 200, { tags: written.tags }],
|
|
[
|
|
"GET",
|
|
"/api/books",
|
|
[first, { ...written, tags: ["delta", "zeta"] }].map(offsetListed),
|
|
],
|
|
];
|
|
const consistent = servedFrom(rows);
|
|
expect(consistent.state.entities[0]?.view_fields).toContainEqual({
|
|
tool: "get_books",
|
|
fields: {
|
|
created_at: offsetListed(first).created_at,
|
|
updated_at: offsetListed(first).updated_at,
|
|
},
|
|
});
|
|
expect(consistent.store.replace(consistent.store.snapshot()).ok).toBe(true);
|
|
|
|
const books = servedFrom([
|
|
...rows,
|
|
["GET", "/api/books", [first, written].map(offsetListed)],
|
|
]);
|
|
expect(books.state.views.get("get_books")).toMatchObject({
|
|
shape: "recorded",
|
|
});
|
|
const snapshot = books.store.snapshot();
|
|
expect(books.store.replace(snapshot)).toMatchObject({ ok: true });
|
|
expect(books.store.snapshot()).toEqual(snapshot);
|
|
expect(books.answer("get_books_by_id", { id: "1" }).body).toEqual(first);
|
|
});
|
|
|
|
it.each([
|
|
{ wrong: false, mixed: false },
|
|
{ wrong: true, mixed: false },
|
|
{ wrong: false, mixed: true },
|
|
{ wrong: true, mixed: true },
|
|
])(
|
|
"keeps contradictions after retiring a sorted list while an item proof survives (wrong=$wrong, mixed=$mixed)",
|
|
({ wrong, mixed }) => {
|
|
const first = { ...FIRST_BOOK, tags: ["alpha", "beta"] };
|
|
const second = { ...SECOND_BOOK, tags: ["gamma", "omega"] };
|
|
const written = { ...second, tags: ["zeta", "delta"] };
|
|
const later = { ...first, tags: ["theta", "epsilon"] };
|
|
const books = servedFrom([
|
|
["GET", "/api/books", [first, second]],
|
|
["GET", "/api/books/1", first],
|
|
["GET", "/api/books/2", second],
|
|
["PATCH", "/api/books/2", written, 200, { tags: written.tags }],
|
|
["GET", "/api/books/2", { ...written, tags: ["delta", "zeta"] }],
|
|
["PATCH", "/api/books/1", later, 200, { tags: later.tags }],
|
|
[
|
|
"GET",
|
|
"/api/books",
|
|
[
|
|
{ ...later, tags: wrong ? ["wrong", "values"] : later.tags },
|
|
mixed ? { ...written, tags: ["delta", "zeta"] } : written,
|
|
],
|
|
],
|
|
]);
|
|
expect(books.state.views.get("get_books")).toMatchObject({
|
|
shape: "recorded",
|
|
});
|
|
expect(books.state.writes.get("patch_books_by_id")).toMatchObject({
|
|
served: !wrong,
|
|
checked_record_ordinals: [4],
|
|
contradicted_by: wrong ? 6 : null,
|
|
});
|
|
},
|
|
);
|
|
|
|
it("drops state with no surviving view while preserving another type and snapshot reload", () => {
|
|
const first = { ...FIRST_BOOK, tags: ["alpha", "beta"] };
|
|
const second = { ...SECOND_BOOK, tags: ["gamma", "omega"] };
|
|
const written = { ...second, tags: ["zeta", "delta"] };
|
|
const authors = [
|
|
{ id: 1, name: "Alice" },
|
|
{ id: 2, name: "Bob" },
|
|
];
|
|
const books = servedFrom([
|
|
["GET", "/api/books", [first, second]],
|
|
["GET", "/api/authors", authors],
|
|
["PATCH", "/api/books/2", written, 200, { tags: written.tags }],
|
|
["GET", "/api/books", [first, { ...written, tags: ["delta", "zeta"] }]],
|
|
["GET", "/api/books", [first, written]],
|
|
]);
|
|
expect(books.state.views.get("get_books")).toMatchObject({
|
|
shape: "recorded",
|
|
});
|
|
expect(books.state.types.map((type) => type.collection_template)).toEqual([
|
|
"/api/authors",
|
|
]);
|
|
expect(books.state.entities.map((entity) => entity.type)).toEqual([
|
|
"/api/authors",
|
|
"/api/authors",
|
|
]);
|
|
expect(books.store.replace(books.store.snapshot()).ok).toBe(true);
|
|
expect(books.answer("get_authors").body).toEqual(authors);
|
|
});
|
|
|
|
it.each([false, true])(
|
|
"checks later writes through a retired sorted view without adding proof (wrong values: %s)",
|
|
(wrong) => {
|
|
const first = { ...FIRST_BOOK, tags: ["alpha", "beta"] };
|
|
const second = { ...SECOND_BOOK, tags: ["gamma", "omega"] };
|
|
const written = { ...second, tags: ["zeta", "delta"] };
|
|
const later = { ...first, tags: ["theta", "epsilon"] };
|
|
const books = servedFrom([
|
|
["GET", "/api/books", [first, second]],
|
|
["GET", "/api/books/1", first],
|
|
["GET", "/api/books/2", second],
|
|
["PATCH", "/api/books/2", written, 200, { tags: written.tags }],
|
|
["GET", "/api/books/2", { ...written, tags: ["delta", "zeta"] }],
|
|
["GET", "/api/books", [first, written]],
|
|
["PATCH", "/api/books/1", later, 200, { tags: later.tags }],
|
|
[
|
|
"GET",
|
|
"/api/books",
|
|
[
|
|
{ ...later, tags: wrong ? ["wrong", "values"] : later.tags },
|
|
{ ...written, tags: ["delta", "zeta"] },
|
|
],
|
|
],
|
|
]);
|
|
expect(books.state.views.get("get_books")).toMatchObject({
|
|
shape: "recorded",
|
|
});
|
|
expect(books.state.writes.get("patch_books_by_id")).toMatchObject({
|
|
served: !wrong,
|
|
checked_record_ordinals: [4],
|
|
contradicted_by: wrong ? 7 : null,
|
|
});
|
|
},
|
|
);
|
|
|
|
it("withholds varied create states whose dependent fields still copy the first create", () => {
|
|
const first = { ...FIRST_BOOK, status: "draft", closed_at: null };
|
|
const second = {
|
|
...SECOND_BOOK,
|
|
status: "closed",
|
|
closed_at: SECOND_BOOK.created_at!,
|
|
};
|
|
const third = {
|
|
...book(3, "Third", "2026-10-01T10:00:00Z"),
|
|
status: "closed",
|
|
closed_at: SECOND_BOOK.created_at!,
|
|
};
|
|
const fourth = {
|
|
...book(4, "Fourth", bookClock(4)),
|
|
status: "draft",
|
|
closed_at: null,
|
|
};
|
|
const fifth = {
|
|
...book(5, "Fifth", bookClock(6)),
|
|
status: "closed",
|
|
closed_at: bookClock(6),
|
|
};
|
|
const books = servedFrom([
|
|
["GET", "/api/books", [first, second, third]],
|
|
["GET", "/api/books/1", first],
|
|
["GET", "/api/books/2", second],
|
|
["GET", "/api/books/3", third],
|
|
["POST", "/api/books", fourth, 201, { name: "Fourth", status: "draft" }],
|
|
["GET", "/api/books/4", fourth],
|
|
["POST", "/api/books", fifth, 201, { name: "Fifth", status: "closed" }],
|
|
]);
|
|
expect(books.state.writes.get("post_books")).toMatchObject({ served: false });
|
|
});
|