mirror of
https://github.com/morluto/rea.git
synced 2026-10-29 19:05:45 +00:00
fix(environment): make the write gate and serving agree on lines, rules, inserts and documents
Five places where the gate approved a write serving would answer differently: - list and document reads are matched to checked lines as serving matches calls (path and query values decoded, the query in any order), so a reordered query no longer hides a contradiction; - an increment or slug applies to every call, so a plan is unexplained-change when one call changes such a field and another leaves it as it was, in either order; - an item read confirming a create no longer stops a later list from choosing another insert candidate; only a list check fixes the rule; - the gate runs serving's document body check (accepted members, JSON types from the held and initial document), so a recorded write that serving would refuse leaves the plan unserved; - a served create decodes its path values from the collection the call names, as the gate decoded the recorded path, whichever spelling the call used. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
df2bded16e
commit
2086da6cc7
11 files changed
+343
-52
No files matched your search
@@ -10,6 +10,7 @@ import {
|
||||
FIRST_BOOK,
|
||||
SECOND_BOOK,
|
||||
THIRD_BOOK,
|
||||
book,
|
||||
bookClock,
|
||||
environmentStateViewOf,
|
||||
renamedBook,
|
||||
@@ -99,6 +100,29 @@ describe("reads compared as the served environment answers them", () => {
|
||||
gated(deletedFromFiltered([FIRST_BOOK]), "delete_books_by_id"),
|
||||
).toMatchObject({ served: true, checked_record_ordinals: [4] });
|
||||
});
|
||||
|
||||
/** A delete confirmed by an item read, then the filtered list recorded with its query reordered. */
|
||||
const deletedThenReordered = (listed: readonly Book[]): TimedCaptureRow[] => [
|
||||
["GET", "/api/books/1", FIRST_BOOK],
|
||||
["GET", "/api/books/2", SECOND_BOOK],
|
||||
["GET", "/api/books?shelf=a&sort=id", [FIRST_BOOK, SECOND_BOOK]],
|
||||
["DELETE", "/api/books/2", "", 204],
|
||||
["GET", "/api/books/2", BOOK_NOT_FOUND, 404],
|
||||
["GET", "/api/books?sort=id&shelf=a", [...listed]],
|
||||
];
|
||||
|
||||
it("checks a list read on a checked line however its query is ordered, as serving matches it", () => {
|
||||
// The item read already checked the delete, so a list still showing the book marks it a side effect.
|
||||
expect(
|
||||
gated(
|
||||
deletedThenReordered([FIRST_BOOK, SECOND_BOOK]),
|
||||
"delete_books_by_id",
|
||||
),
|
||||
).toMatchObject({ served: false, reason: "side-effect" });
|
||||
expect(
|
||||
gated(deletedThenReordered([FIRST_BOOK]), "delete_books_by_id"),
|
||||
).toMatchObject({ served: true, checked_record_ordinals: [4] });
|
||||
});
|
||||
});
|
||||
|
||||
describe("update constants", () => {
|
||||
@@ -162,3 +186,67 @@ describe("write examples", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("update rules learned from a later call", () => {
|
||||
const versioned = (entry: Book, version: number): Book => ({
|
||||
...entry,
|
||||
version,
|
||||
});
|
||||
/** Two renames, of the first and the second book, each answered at the version given. */
|
||||
const renames = (first: number, second: number): TimedCaptureRow[] => {
|
||||
const firstAfter = versioned(
|
||||
renamedBook(FIRST_BOOK, "Renamed Book", 3),
|
||||
first,
|
||||
);
|
||||
const secondAfter = versioned(
|
||||
renamedBook(SECOND_BOOK, "Second Edition", 5),
|
||||
second,
|
||||
);
|
||||
return [
|
||||
[
|
||||
"GET",
|
||||
"/api/books",
|
||||
[versioned(FIRST_BOOK, 1), versioned(SECOND_BOOK, 1)],
|
||||
],
|
||||
["GET", "/api/books/1", versioned(FIRST_BOOK, 1)],
|
||||
["GET", "/api/books/2", versioned(SECOND_BOOK, 1)],
|
||||
["PATCH", "/api/books/1", firstAfter, 200, { name: "Renamed Book" }],
|
||||
["GET", "/api/books/1", firstAfter],
|
||||
["PATCH", "/api/books/2", secondAfter, 200, { name: "Second Edition" }],
|
||||
["GET", "/api/books/2", secondAfter],
|
||||
];
|
||||
};
|
||||
|
||||
it("leaves an update unserved when one call increments a field another left as it was", () => {
|
||||
for (const [first, second] of [
|
||||
[1, 2],
|
||||
[2, 1],
|
||||
] as const)
|
||||
expect(gated(renames(first, second), "patch_books_by_id")).toMatchObject({
|
||||
served: false,
|
||||
reason: "unexplained-change",
|
||||
});
|
||||
expect(gated(renames(2, 2), "patch_books_by_id")).toMatchObject({
|
||||
served: true,
|
||||
rules: expect.arrayContaining([{ field: "version", rule: "increment" }]),
|
||||
checked_record_ordinals: [4, 6],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("insert rules", () => {
|
||||
it("still learns where a list places a create after an item read confirmed it", () => {
|
||||
const alpha = book(3, "Alpha", bookClock(3));
|
||||
const rows: TimedCaptureRow[] = [
|
||||
...BOOK_INIT_ROWS,
|
||||
["POST", "/api/books", alpha, 201, { name: "Alpha" }],
|
||||
["GET", "/api/books/3", alpha],
|
||||
["GET", "/api/books", [alpha, FIRST_BOOK, SECOND_BOOK]],
|
||||
];
|
||||
expect(gated(rows, "post_books")).toMatchObject({
|
||||
served: true,
|
||||
insert: { rule: "sorted", field: "name", direction: "ascending" },
|
||||
checked_record_ordinals: [4, 5],
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user