Files
reverse-engineering-anything/src/domain/analysisView/environmentStateWrites.test.ts
T
538017d1d2 feat(environment): apply evidence-gated write effects to the served state (#5600)
* feat(environment): render more read tools from the state and describe responses

Keep main's environment serving and fold in coverage for non-entity reads,
response summaries, and MCP tool annotations.

Squashed rebase of #5352 onto main.

* fix(environment): keep rendered documents and quoted samples inside their budgets

A recorded body of exponential numbers can render past 256 KiB and turn a
working read into a state size error, so that document stays a recorded replay.
Description samples are measured after JSON quoting, and the replay-fallback
catalog check uses a tool count the longer response descriptions still separate.

Co-authored-by: morluto <morluto@users.noreply.github.com>

* feat(environment): apply evidence-gated write effects to the served state

Serve write tools whose later reads confirm the effect, and keep main's
state-file ownership and part-wise reply measurement.

Squashed rebase of #5600 onto the rebased #5352 tip.

* fix(environment): bound and validate write state history

* feat(environment): render more read tools from the state and describe responses

Keep main's environment serving and fold in coverage for non-entity reads,
response summaries, and MCP tool annotations.

Squashed rebase of #5352 onto main.

* fix(environment): keep rendered documents and quoted samples inside their budgets

A recorded body of exponential numbers can render past 256 KiB and turn a
working read into a state size error, so that document stays a recorded replay.
Description samples are measured after JSON quoting, and the replay-fallback
catalog check uses a tool count the longer response descriptions still separate.

Co-authored-by: morluto <morluto@users.noreply.github.com>

* feat(environment): apply evidence-gated write effects to the served state

Serve write tools whose later reads confirm the effect, and keep main's
state-file ownership and part-wise reply measurement.

Squashed rebase of #5600 onto the rebased #5352 tip.

* fix(environment): bound and validate write state history

* fix(environment): bound additional collection history

* fix(environment): credit sent fields, negative sequences, and encoded targets

A later overwrite copied surviving derived fields into the sent-member
claim, so a members-only check rejected an update whose sent field still
matched. Serving floored sequences at zero and stored missing history as
zero, so a collection sequence learned from negative ranks skipped the
next value. An absence after a delete forgot the resolved collection, and
an encoded read could not observe an update spelled with another
collection encoding.

Regression tests cover each case.

Co-authored-by: morluto <morluto@users.noreply.github.com>

* test(environment): cover resolved encoded update identities

* fix: validate timed update observations

* fix(environment): share write overlap and check clocks on earlier calls

Observation search skips a read begun before the write ended. That
comparison now uses the same overlap as the check path.

A clock learned on a later call was not checked against earlier calls
that kept the field. Serving would stamp those calls too, so the plan
stays unserved. A clock learned earlier is still refused when a later
call does not advance it.

Regression tests cover an overlapping empty update and a published_at
clock that one recorded call leaves unchanged.

Co-authored-by: morluto <morluto@users.noreply.github.com>

* fix: preserve partial update evidence

* fix(environment): let an unmodelled write explain only the list members that differ

A failed whole-list read now reports which members alone account for the
difference. An unmodelled write excuses the read's writes only when it came
after each such member's last check and pending writes, so an older change
elsewhere no longer hides a newer write the list shows undone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 7b6aab9d7191e600051693f2c192afcfc743d083)

* fix(environment): explain added fields, tie creates to serving's collection, and bound write descriptions

An update explains a field the entity lacked before when the observing
view had shown the entity, so a server-filled field is not dropped. A
create is tied to the held collection serving picks for its path. Write
descriptions and body refusals quote member names and sample them within
a byte bound. The request body is indexed once per explanation, so copied
and slugged fields no longer rescan it.

Replays 07aca54e onto the restacked branch. Its rule that a read begun
before a write ended teaches nothing is already here (fb6785dc), so only
its test is kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(environment): state how list reads weigh unmodelled writes, added update fields and quoted members

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(environment): keep the observing view helpers with the observations

shownBefore and the item-or-list fields check move to the observe module,
keeping the write effects within the line limit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(environment): resolve unmodelled writes onto the held collection

An unmodelled path kept the request's collection spelling. An archive under
the plain spelling of a collection held encoded marked only that plain
collection unknown, so a later update was blamed for the archive's fields
and dropped. Name that entity the way a modelled write does.

Co-authored-by: morluto <morluto@users.noreply.github.com>

* fix(environment): confirm no write through volatile fields or overwritten replacements, and bound the served clock

A members-only check no longer compares volatile fields, which render
from the read itself. A document replacement credits the members it
wrote or removed at its object, so a later merge that overwrites them
takes its confirmation away. A recorded time past the range of a date
gives no served clock instead of throwing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(environment): leave unknown entities unchecked by failed reads, volatile fields and unchecked calls

A failed item read that is no absence leaves an unknown entity unknown, so
a later usable read still checks its writes. A read showing an unknown
entity again checks no write on a volatile field. An update of an unknown
entity takes its recorded outcome and adds nothing to a built plan. A later
update that does not answer with its entity is observed only by a read
whose view shows every learned clock; without one it is unmodelled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(environment): fill a create's path fields from the collection serving selects

The gate built a later create's path fields from the recorded request path,
while serving fills them from the held collection it selects for that path,
so a read showing the other spelling's parent could confirm a create serving
cannot reproduce. Rules still learn from the recorded path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(environment): name unmodelled writes as modelled ones, count fields updates write, and keep later members' types

An unmodelled write below an item path names its entity through itemTarget,
and one at or above a collection resolves the held collection, as modelled
writes do, so an encoded spelling no longer hides the entity it changed.
An update's answer counts the fields its rules write, other than increments,
as available, so a field only the update writes no longer makes it
incomplete-entities. A member a later call first stores keeps that call's
JSON type in the body's kinds, which the body check falls back to.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(environment): let a document replacement overwrite everything under its object

A replacement claims only the members it wrote or removed, but it now
overwrites every earlier effect under its object, so a merge that wrote
the object it replaces loses its confirmation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(environment): start the served clock from the records whose times a date can hold

A record timed past the range of a date no longer leaves the served clock
without a start, which the store read as zero and stamped writes in 1970;
the clock starts after the latest record a date can hold.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: morluto <morluto@users.noreply.github.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 14:15:35 +08:00

544 lines
17 KiB
TypeScript

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,
kinds: [],
},
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,
// The final list follows the delete of book 3, so it shows nothing of the create.
checked_record_ordinals: [4],
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 the clock from the records a date can hold when one lies past its range", () => {
const state = stateOf([
["GET", "/api/books", [FIRST_BOOK]],
["GET", "/api/books/1", FIRST_BOOK],
[
"PATCH",
"/api/books/1",
RENAMED_AT_3,
200,
{ name: "Renamed Book" },
9_000_000_000_000_000,
],
]);
// The second read ends at 18:00:01.005, so the clock starts at the next whole second.
expect(state.write_clock).toBe("2026-10-17T18:00:02.000Z");
});
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("update answers", () => {
it("serves an update whose recorded answer shows a field only the update writes", () => {
const summary = (name: string) => [
{ id: 1, name },
{ id: 2, name: "B" },
];
// No read showed a note before the update sent one, which its answer echoes.
const plan = planOf(
stateOf([
["GET", "/api/books", summary("A")],
["GET", "/api/books/1", { id: 1, name: "A" }],
["GET", "/api/books/2", { id: 2, name: "B" }],
[
"PATCH",
"/api/books/1",
{ id: 1, name: "Z", note: "n" },
200,
{ name: "Z", note: "n" },
],
["GET", "/api/books", summary("Z")],
]),
"patch_books_by_id",
);
expect(plan).toMatchObject({
served: true,
reason: null,
response: { shape: "entity", pointer: "" },
checked_record_ordinals: [4],
});
});
});
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],
});
});
});