test(environment): cover per-line list membership; document it

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Edgar ArakelyanandClaude Opus 5.5 committed 2026-10-20 20:47:33 -07:00
1 parent 54d3d5e65a
commit 87e94d12ea
8 files changed
+129 -7

No files matched your search

+1 -1
View File
@@ -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.
+6 -6
View File
@@ -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
View File
@@ -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";