mirror of
https://github.com/morluto/rea.git
synced 2026-11-07 07:16:13 +00:00
test(environment): cover per-line list membership; document it
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
54d3d5e65a
commit
87e94d12ea
8 files changed
+129
-7
No files matched your search
@@ -422,7 +422,7 @@ Each read tool answers according to its view in the state:
|
||||
| View | Answer |
|
||||
| ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `item` | The entity the path arguments name, rendered into the recorded envelope of `envelope_ordinal`. Arguments match as replay matches them: each collection segment equals the held collection's segment as written or after percent-decoding, and the identity is the last argument percent-decoded, then as written; values are never re-split on `/`. Any entity the state holds answers, including one `data/add` loaded; query arguments only decide `replay.exact_match`. An entity the state does not hold answers the tool's latest recorded 400, 404 or 410 response with `state.view` `missing`, or is a tool error when none was recorded; one lacking a field every recording carried is a tool error; a body is never invented. |
|
||||
| `list` | When the arguments name one of the lines the view checked exactly (its latest recordings before the first related write; every path and query value equal to the recording's, and every recorded query parameter supplied), that recorded envelope with its array rendered from the state: every entity of the collection in state order for `membership: "all"`, or the recorded members still held. A call with no query argument to a `membership: "all"` view answers from a checked line of the collection its path arguments name that only pages its list, with `replay.exact_match` false. A member lacking a field every recording carried makes the call a tool error rather than a partial list. |
|
||||
| `list` | When the arguments name one of the lines the view checked exactly (its latest recordings before the first related write; every path and query value equal to the recording's, and every recorded query parameter supplied), that recorded envelope with its array rendered from the state: every entity of the collection in state order for `membership: "all"` or a line `all_record_ordinals` names, or the recorded members still held. A call with no query argument answers from such a line of the collection its path arguments name that only pages its list, with `replay.exact_match` false. A member lacking a field every recording carried makes the call a tool error rather than a partial list. |
|
||||
| `document` | When the arguments name the document's checked line exactly, the state's value for it; a tool error when the state holds none. |
|
||||
| `recorded` | The recording, as replay answers it. |
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
# Implementation plan
|
||||
|
||||
1. **Gate.** Judge each latest list recording on its own for paging-only query, totals, next page and state order;
|
||||
keep `all` when every line passes and otherwise name the passing lines in `all_record_ordinals`; reproduce each
|
||||
line with its own membership.
|
||||
2. **Counters.** Count `number`-of-items members as item totals.
|
||||
3. **Consumers.** One helper says whether a view lists all on a line; serving, the write gate's list checks and the
|
||||
insert retry use it, and an argument-less call may answer on any such line.
|
||||
4. **Tests and docs.** Domain and serving tests as the acceptance lists; the membership row of the environment-state
|
||||
docs and the list answer row of the agent-environment docs.
|
||||
@@ -0,0 +1,31 @@
|
||||
# Decide a list's whole-collection membership per line
|
||||
|
||||
Issue: https://github.com/morluto/rea/issues/4933
|
||||
|
||||
## Outcome
|
||||
|
||||
A list view reproduces its lines from the state with one of two memberships: `all`, every entity of the collection in
|
||||
state order, or `recorded`, the entities each recording listed. The view was `all` only when every checked line listed
|
||||
its whole collection. A list tool often also has a filtered line (`?library_id=…`, `?name=…`), and that one line kept
|
||||
every line of the tool to its recorded members. An entity the environment created then never appeared in a list,
|
||||
though the unfiltered line of the same tool had shown the application listing every entity.
|
||||
|
||||
Each checked line is now judged on its own. A recorded-membership view names, in `all_record_ordinals`, the checked
|
||||
lines that each meet the conditions `all` asks of every line: the line has no query or only paging parameters, lists
|
||||
exactly the entities of its collection in state order, and shows no total that differs from the number listed and no
|
||||
next page. A call answered on such a line lists every entity of the collection, the write gate checks a read on it
|
||||
against the whole collection, and a create's insert rule may be retried on it, as for an `all` view. Other lines keep
|
||||
their recorded members. Without such a line, or when every line is one, the view is unchanged and the member is absent,
|
||||
so existing states keep their values.
|
||||
|
||||
`numberOfElements` and similar number-of-items members (`number` with `elements`, `entries`, `items` or `results`)
|
||||
count the listed entities, so a whole line's counter follows the state as `totalElements` does.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- Domain: a tool with a whole line and a filtered line names the whole one; a create the whole line later lists is
|
||||
confirmed; a tool whose lines are all whole, or none, names none.
|
||||
- Serving: after a served create, the whole line lists the created entity with its counters, and the filtered line
|
||||
keeps its recorded members.
|
||||
- Measured: the six published environments compared with the application before and after, on the probes of the
|
||||
fidelity comparison and the held-out sessions recorded since.
|
||||
@@ -0,0 +1,9 @@
|
||||
# Tasks
|
||||
|
||||
- [x] Judge whole-collection membership per checked line.
|
||||
- [x] Count number-of-items members.
|
||||
- [x] Serve, check and place creates per line.
|
||||
- [x] Add domain and serving tests.
|
||||
- [x] Document per-line membership.
|
||||
- [x] Measure the six published environments before and after.
|
||||
- [x] Run static, formatting, duplication, documentation and test gates.
|
||||
@@ -579,12 +579,12 @@ Every entity cites its `record_ordinals`.
|
||||
|
||||
`views` lists one plan per read tool in tool order:
|
||||
|
||||
| Shape | Plan |
|
||||
| ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `item` | The entity object at `pointer` (the root, one member, or `/0` or `/<member>/0`), the ordered `fields` the view shows and the `required_fields` every recording had, and `envelope_ordinal`: the recording whose response, with the entity replaced, every item response shares |
|
||||
| `list` | The array at `pointer`, its `fields`, `membership` (`all`: every entity of the collection in state order, when each recording listed exactly that, had no query or only paging parameters (`limit`, `offset`, `page`, `per_page`, `perpage`, `page_size`, `pagesize`, `size`, `start`, `_start`, `_end`), no item-total member (`total`, `count`, `totalCount`, `item_count`, `totalcount`, `itemcount`, but not `total_pages` or `per_page`; a number or a string of digits) differing from the number listed, and no `has_more`, `next` or next-page token member set; `recorded`: the entities each recording listed) and `counters` that equal the number listed |
|
||||
| `document` | The latest initial value of a list tool whose responses hold no identified list, when its initial reads share one request line; held in `documents` with that `path`, whether or not the capture writes it, unless its value changed across any recorded write between those reads (`fields-differ`) |
|
||||
| `recorded` | A `reason` the tool replays its recordings instead |
|
||||
| Shape | Plan |
|
||||
| ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `item` | The entity object at `pointer` (the root, one member, or `/0` or `/<member>/0`), the ordered `fields` the view shows and the `required_fields` every recording had, and `envelope_ordinal`: the recording whose response, with the entity replaced, every item response shares |
|
||||
| `list` | The array at `pointer`, its `fields`, `membership` (`all`: every entity of the collection in state order, when each recording listed exactly that, had no query or only paging parameters (`limit`, `offset`, `page`, `per_page`, `perpage`, `page_size`, `pagesize`, `size`, `start`, `_start`, `_end`), no item-total member (`total`, `count`, `totalCount`, `item_count`, `totalcount`, `itemcount`, `numberOfElements`, but not `total_pages` or `per_page`; a number or a string of digits) differing from the number listed, and no `has_more`, `next` or next-page token member set; `recorded`: the entities each recording listed, except on the lines `all_record_ordinals` names, which each meet those conditions on their own and list every entity), and `counters` that equal the number listed |
|
||||
| `document` | The latest initial value of a list tool whose responses hold no identified list, when its initial reads share one request line; held in `documents` with that `path`, whether or not the capture writes it, unless its value changed across any recorded write between those reads (`fields-differ`) |
|
||||
| `recorded` | A `reason` the tool replays its recordings instead |
|
||||
|
||||
An item or list plan is kept only when it reproduces the latest initial
|
||||
recording of each request line from the state, comparing JSON values without
|
||||
|
||||
@@ -10,10 +10,12 @@ import {
|
||||
BOOK_ROWS,
|
||||
DELETE_NOT_FOUND,
|
||||
FIRST_BOOK,
|
||||
LIST_LINE_ROWS,
|
||||
SECOND_BOOK,
|
||||
THIRD_BOOK,
|
||||
book,
|
||||
bookClock,
|
||||
bookPage,
|
||||
renamedBook,
|
||||
type Book,
|
||||
} from "../../../tests/fixtures/environmentStateWrites.js";
|
||||
@@ -360,6 +362,21 @@ describe("paged list lines", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("list lines", () => {
|
||||
it("lists a created book on the line that lists every book, while a filtered line keeps its recorded members", () => {
|
||||
const books = servedFrom(LIST_LINE_ROWS);
|
||||
const created = books.answer("post_books", CREATE_NEW_BOOK).body as Book;
|
||||
expect(books.answer("get_books")).toMatchObject({
|
||||
body: bookPage([FIRST_BOOK, SECOND_BOOK, created]),
|
||||
state: { source: "state", membership: "all", listed: 3 },
|
||||
});
|
||||
expect(books.answer("get_books", { name: "First Book" })).toMatchObject({
|
||||
body: bookPage([FIRST_BOOK]),
|
||||
state: { source: "state", membership: "recorded", listed: 1 },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("path values", () => {
|
||||
const orgBook = (id: number, org: string, name: string): Book => ({
|
||||
id,
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
BOOK_ROWS,
|
||||
BOOK_WRITE_CLOCK,
|
||||
FIRST_BOOK,
|
||||
LIST_LINE_ROWS,
|
||||
SECOND_BOOK,
|
||||
THIRD_BOOK,
|
||||
book,
|
||||
@@ -490,3 +491,35 @@ describe("observation from a later read", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("list lines", () => {
|
||||
it("lists every entity on a line that lists its whole collection, though another line of the tool is filtered", () => {
|
||||
const state = stateOf(LIST_LINE_ROWS);
|
||||
expect(state.views.find((view) => view.tool === "get_books")).toMatchObject(
|
||||
{
|
||||
shape: "list",
|
||||
pointer: "/content",
|
||||
membership: "recorded",
|
||||
all_record_ordinals: [0],
|
||||
counters: ["numberOfElements", "totalElements"],
|
||||
checked_record_ordinals: [0, 1],
|
||||
},
|
||||
);
|
||||
// The whole list after the create shows the created book, which confirms it.
|
||||
expect(planOf(state, "post_books")).toMatchObject({
|
||||
served: true,
|
||||
checked_record_ordinals: [5, 6],
|
||||
});
|
||||
});
|
||||
|
||||
it("names no line when every line lists its whole collection, or none does", () => {
|
||||
const listOf = (rows: readonly TimedCaptureRow[]) =>
|
||||
stateOf(rows).views.find((view) => view.tool === "get_books");
|
||||
const whole = listOf(LIST_LINE_ROWS.filter((_, index) => index !== 1));
|
||||
expect(whole).toMatchObject({ membership: "all" });
|
||||
expect(whole).not.toHaveProperty("all_record_ordinals");
|
||||
expect(listOf(LIST_LINE_ROWS.slice(1))).not.toHaveProperty(
|
||||
"all_record_ordinals",
|
||||
);
|
||||
});
|
||||
});
|
||||
+22
@@ -60,6 +60,8 @@ 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 book a create recorded at exchange 4 made. */
|
||||
const THIRD_BOOK_AT_4 = book(3, "Third Book", bookClock(4));
|
||||
/** The first book after the recorded update at exchange 5. */
|
||||
export const RENAMED_BOOK = renamedBook(FIRST_BOOK, "Renamed Book", 5);
|
||||
|
||||
@@ -92,6 +94,26 @@ export const BOOK_ROWS: readonly TimedCaptureRow[] = [
|
||||
["POST", "/api/exports", { queued: true }, 202, { format: "csv" }],
|
||||
];
|
||||
|
||||
/** A page of books as a paging API wraps it, counting what it lists. */
|
||||
export const bookPage = (books: readonly Book[]) => ({
|
||||
content: [...books],
|
||||
totalElements: books.length,
|
||||
numberOfElements: books.length,
|
||||
});
|
||||
|
||||
/**
|
||||
* One list tool with a line that lists every book and a line filtered by name, item reads, then a
|
||||
* create the whole list confirms.
|
||||
*/
|
||||
export const LIST_LINE_ROWS: readonly TimedCaptureRow[] = [
|
||||
["GET", "/api/books", bookPage([FIRST_BOOK, SECOND_BOOK])],
|
||||
["GET", "/api/books?name=First%20Book", bookPage([FIRST_BOOK])],
|
||||
...BOOK_INIT_ROWS.slice(1),
|
||||
["POST", "/api/books", THIRD_BOOK_AT_4, 201, { name: "Third Book" }],
|
||||
["GET", "/api/books/3", THIRD_BOOK_AT_4],
|
||||
["GET", "/api/books", bookPage([FIRST_BOOK, SECOND_BOOK, THIRD_BOOK_AT_4])],
|
||||
];
|
||||
|
||||
/** 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";
|
||||
|
||||
|
||||
Reference in new issue
Block a user