feat(environment): serve writes whose calls send different members

Recordings now vary what a type's two creates and updates send, and REA
withheld writes where a later call left out what the first sent. An update
plan's required members narrow to those every call sent; a member one create
sent and another left for the application to number is a sequence that
takes the value a call sends; a PUT's left-out member the application
cleared becomes that member's default; and an update's clock is held as
the read after it spells it. The skill spaces creates (skill 54).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Edgar ArakelyanandClaude Opus 5.5 committed 2026-10-27 10:46:37 -07:00
1 parent 7d01a5d5f3
commit 6300ec804a
18 files changed
+475 -17

No files matched your search

@@ -2,7 +2,7 @@
name: reverse-engineer-anything
description: Reverse engineer native, managed, Electron/JavaScript, packaged, firmware, and browser targets with REA, and build agent environments (AgentEnv bundles) of web applications from a live instance or a capture. Use shipped-artifact or requested runtime evidence to explain features, compare versions, decompile code, or guide a reconstruction. Skip REA for ordinary source-repository architecture analysis.
metadata:
version: "53"
version: "54"
---
# REA
@@ -153,8 +153,9 @@ client with it. Match `wait_for` locators to exactly one element.
with varied free text. After each write, and before the next one, read the
object back by id and then its list: create 1, read 1, list, create 2,
read 2, list; never several writes before their reads.
4. Leave more than a second between a create and its first edit
(`wait_for_timeout` with `duration_ms: 1100`).
4. Leave more than a second between two creates and between a create and its
first edit (`wait_for_timeout` with `duration_ms: 1100`), so lists sorted
by creation time show them in order.
5. Update both objects and read each back, changing the fields a person
would change (a name, an email address, a status, a due date) rather than
only a description: a served update accepts only the members a recorded
@@ -0,0 +1,9 @@
# Implementation plan
1. **Update members.** `refinedBody` keeps only the required members a later update also sent.
2. **Numbered unless sent.** `sent` on sequence rules and their schema; the create merge adopts it for an echoed member
and a sequence from a call that left the member out; serving and replay take the sent value when there is one.
3. **Cleared by a PUT.** `explainUpdate` records a `PUT`'s left-out member cleared to an empty value as a default;
update refinement keeps defaults from later calls; `updatedFields` applies them to members a call leaves out.
4. **Update clocks.** `restampedFields` respells clocks from the first later read as it does nested clocks.
5. **Skill, tests and docs.**
@@ -0,0 +1,50 @@
# Serve writes whose calls send different members
Issue: https://github.com/morluto/rea/issues/4933
## Outcome
A fresh recording round of seven applications, main against #7332, showed the recording skill now varies what the two
creates and two updates of a type send, as it asks. Where the second call left out a member the first sent, REA
withheld the write:
- **Updates.** An update plan required every member its first call sent and never narrowed that, so EspoCRM's second
contact update, which changed only an email address, was refused in both recordings of the branch.
- **Members numbered when left out.** Linkding stores a bundle's `order` as sent, or one past the others when a call
leaves it out. One create showed the first and another the second, which the create merge took as a disagreement.
The same recordings and the 130 earlier captures showed two more update gaps:
- **Whole-object PUTs.** Paperless clears a tag's, correspondent's or document type's `owner` when a `PUT` leaves it
out, and Homebox an item's `assetId`, so those updates were `unexplained-change`.
- **Update clocks read at another precision.** Vikunja answers a task update with `done_at` to the nanosecond while its
reads spell it to the second, so the read after the update contradicted it.
The round also found two creates of EspoCRM contacts in one second, which a list sorted newest first shows in an order
the application breaks ties in. The skill now spaces creates.
## Behaviour
- **Update members.** A later update narrows the plan's required members to those every call sent.
- **Numbered unless sent.** A member one create sent and stored as sent, and another left out and the application
numbered one past the others, is a `sequence` with `sent`: serving stores the value a call sends, else the next
number.
- **Cleared by a PUT.** A field a `PUT` left out that the application cleared to an empty value (`""`, `null`, `false`,
`[]` or `{}`) is that member's default (`body.defaults`): serving stores it for an update that leaves the member out.
- **Update clocks.** The state holds an update's clock as the first read of the entity after it spells the same instant
at a coarser precision, as it already does for nested clocks; answers keep the update's own spelling.
- **Skill.** Recordings leave more than a second between two creates, as between a create and its first edit.
Plans and views that need none of this are unchanged.
## Not covered
- Vikunja numbers tasks per project. Creates into projects the recording did not list cannot be numbered, so its task
creates stay withheld.
- Linkding renumbers bundles' `order` after a delete, which no rule models.
- Homebox item creates expand sent tag ids into tag objects and pin `parentId`; they stay withheld.
## Acceptance
- No write tool of the recorded captures goes from served to withheld.
- The writes the diagnosis named are served where their recordings show nothing else against them.
@@ -0,0 +1,11 @@
# Tasks
- [x] Narrow an update plan's required members to those every call sent.
- [x] Serve a member the value a call sends, else the next number, when calls showed both.
- [x] Learn and serve the value a `PUT` stores for a member it leaves out.
- [x] Hold an update's clock as the read after it spells it.
- [x] Ask recordings to space creates.
- [x] Add view, serving and unit tests.
- [x] Document the rules.
- [x] Measure the recorded captures.
- [x] Run static, formatting, documentation and test gates.
@@ -0,0 +1,64 @@
# Verification
Executed on macOS with Node v24.22.1 on this branch, on #7332 at `7d01a5d5` (main `5aa23c3d`), and on Ubuntu 24.04 VMs
with snap Chromium for the recording round.
## Diagnosis
Claude Code (`claude-sonnet-5-5`) built an environment of each of seven applications from the same prompt, once with
REA main `5aa23c3d` (skill 50) and once with #7332 at `7d01a5d5` (skill 53). Each bundle's write tasks ran with
`fireworks_ai/gpt-oss-120b` and `fireworks_ai/glm-5p3-flash`, which scored alike:
| Application | Write tasks | Main | #7332 |
| ----------- | ----------- | ------- | ------- |
| Miniflux | 3 | 3/3 | 3/3 |
| Grafana | 2 | 2/2 | 2/2 |
| Vikunja | 3 + 2 | 0/3 0/2 | 1/3 2/2 |
| Paperless | 3 | 1/3 | 2/3 |
| Linkding | 2 | 0/2 | 2/2 |
| Homebox | 2 | 2/2 | 0/2 |
| EspoCRM | 3 | 1/3 | 0/3 |
| Total | 20 | 9/20 | 12/20 |
Served writes were 14 and 25. Recorded again, Homebox and EspoCRM gave main 2/2 and 1/3 and #7332 2/2 and 3/3: the
first losses were the agents' runs, not REA. Built with both versions, none of the 18 recordings served fewer writes
with #7332 (44 and 53 in all).
Replaying #7332's recordings with failing checks and refusals logged gave the causes this branch takes: EspoCRM's
second contact update left out `addressCity` and was refused for lacking a required member (both recordings); Linkding's
second bundle create left `order` to the application, which the merge took as disagreeing with the first create's
echo; Vikunja's task update stamped `done_at` to the nanosecond where the read after it showed seconds. Over the
earlier captures, 14 withheld updates were `PUT`s whose application cleared a member they left out (Paperless `owner`,
Homebox `assetId`).
## Gates
- `npm run -s check:fast`, `format:check`, `scan:todos`, `docs:check`, `verify:test-discovery`, `mcp-catalog:check` and
`jscpd`: passed. `npx knip` reports `staticFileUrlPath`, `ObjcClassImportBindingCoverage` and
`AndroidManifestComponentInventory`, as main `5aa23c3d` does.
- `npx vitest run --project domain --project services`: 11,694 passed, 8 failed (`appleApplication.test.ts`), which fail
the same way on main `5aa23c3d`.
- `npm run -s test:focused --` the environment build, export and serve CLI boundaries, the analysis-view CLI and MCP
boundaries and the environment serve MCP boundary: 80 passed. `outputSchemaProfile`, `toolSchemaValidity`,
`mcpSchemaValidation`, `outputSchemaSemanticConstraints` and `analysisViewMcp`: 54 passed; the compact catalog stays
under its frame.
- The seven new view and serving tests fail on #7332 at `7d01a5d5` and pass here.
## Write plans over recorded captures
The 130 captures #7332 measured and the round's 18 recordings, built by #7332 at `7d01a5d5` and by this branch: 694
plans, 386 served and 398. No write goes from served to withheld and every view is unchanged. Newly served:
- Paperless correspondent, document type and tag updates in four recordings, whose `PUT` clears `owner`.
- EspoCRM's contact update and Linkding's bundle create in #7332's recordings of the round.
- Grafana's folder update and delete in a fresh recording whose second update sent fewer members.
- Homebox's item update in the published capture, whose `PUT` clears `assetId`.
Vikunja's task update reads back confirmed, but stays withheld: the task creates before it are not modelled (per-project
numbers), so their entities carry the creates' answer spellings into later reads.
## Not verified
- No recording round ran with this branch, so its write tasks are not measured.
- The skill's spacing of creates.
- Vikunja per-project task numbers, Linkding's renumbering after a delete and Homebox item creates, which stay withheld.
+10 -4
View File
@@ -796,7 +796,9 @@ past those deleted before the capture. Two `made-object` rules agree in their
clocks and own members, and a count only the later call shows starts at the
first object's value. Members either call sent are accepted, only those every call sent are
required, and an echoed member a call left out takes the value that call stored
(`body.defaults`). A member a create sent empty (`""`, `null`, `false`, `[]` or
(`body.defaults`). A member one call sent and stored as sent, and another left
out and the application numbered one past the others, takes the value a call
sends, else that number (a `sequence` with `sent`). A member a create sent empty (`""`, `null`, `false`, `[]` or
`{}`), as a form sends a field left as it was, is not required: a call that
leaves it out is applied as sending that value (`body.blank`), unless a call
that left it out showed a default, which it then keeps. A call may send it as any
@@ -859,13 +861,17 @@ is unchanged) must be a clock, a nested clock, an increment, a slug, a fresh `un
value, a `stamp` of the acting user, a value `derived` from the one member it
stores nowhere, a `reference` to the entity a sent member names (kept by a call that names the entity the field already repeats), a `composed` string whose parts read members it stored as sent
or fields it left as they were and spell the value before the call too, or for
an item action a `set` of a scalar to the value it showed, or the plan is
an item action a `set` of a scalar to the value it showed, or, for a `PUT`, a
member it left out that the application cleared to an empty value, which calls
that leave it out then store (`body.defaults`), or the plan is
`unexplained-change`; an increment, slug, stamp, derived
value or set must hold for every call of the tool, and an action is served
only once a call found what it sets already set. A write is served only
with the query its first call sent, which serving applies to a call that
leaves a parameter out and the catalog advertises as required. Every member the first call sent is required, except by `PATCH` and
`POST` updates and members a create sent empty.
leaves a parameter out and the catalog advertises as required. Every member every call sent is required, except by `PATCH` and
`POST` updates and members a create sent empty. The state holds an update's
clock as the first read of the entity after it spells the same instant, at a
coarser precision too.
A read on a line `query` answers is checked against the query evaluated over
the replayed state. It covers the collection only when every parameter sorts or
@@ -0,0 +1,65 @@
import { describe, expect, it } from "vitest";
import {
FEWER_MEMBERS_ROWS,
NUMBERED_UNLESS_SENT_ROWS,
PUT_CLEARS_ROWS,
} from "../../../tests/fixtures/environmentStateVariedCalls.js";
import { servedFrom } from "../../../tests/fixtures/environmentStateServing.js";
describe("served writes whose calls send different members", () => {
it("numbers a created bundle that leaves its order out, and keeps one a call sends", () => {
const bundles = servedFrom(NUMBERED_UNLESS_SENT_ROWS);
expect(
bundles.answer("post_bundles", { body: { name: "Echo" } }).body,
).toMatchObject({ name: "Echo", order: 2 });
expect(
bundles.answer("post_bundles", { body: { name: "Foxtrot", order: 9 } })
.body,
).toMatchObject({
name: "Foxtrot",
order: 9,
});
});
it("clears a member a PUT leaves out, as the application did, and keeps one it sends", () => {
const tags = servedFrom(PUT_CLEARS_ROWS);
expect(
tags.answer("put_tags_by_id", { id: "2", body: { name: "Receipts" } })
.body,
).toMatchObject({
name: "Receipts",
owner: null,
});
expect(tags.answer("get_tags_by_id", { id: "2" }).body).toMatchObject({
owner: null,
});
expect(
tags.answer("put_tags_by_id", {
id: "1",
body: { name: "Mail", owner: 3 },
}).body,
).toMatchObject({
name: "Mail",
owner: 3,
});
});
it("serves an update that sends only the member every recorded update sent", () => {
const contacts = servedFrom(FEWER_MEMBERS_ROWS);
expect(
contacts.answer("put_contacts_by_id", {
id: "1",
body: { email: "ada.f@example.test" },
}).body,
).toMatchObject({ name: "Ada Finch", email: "ada.f@example.test" });
expect(
contacts.refusal("put_contacts_by_id", {
id: "2",
body: { city: "Kumasi" },
}),
).toMatchObject({
message: expect.stringContaining("email"),
});
});
});
@@ -0,0 +1,56 @@
import { describe, expect, it } from "vitest";
import {
timedCaptureEvidence,
type TimedCaptureRow,
} from "../../../tests/fixtures/analysisView.js";
import {
DONE_CLOCK_ROWS,
FEWER_MEMBERS_ROWS,
NUMBERED_UNLESS_SENT_ROWS,
PUT_CLEARS_ROWS,
} from "../../../tests/fixtures/environmentStateVariedCalls.js";
import { environmentStateViewOf } from "../../../tests/fixtures/environmentStateWrites.js";
const planOf = (rows: readonly TimedCaptureRow[], tool: string) => {
const plan = environmentStateViewOf(timedCaptureEvidence(rows)).writes.find(
(candidate) => candidate.tool === tool,
);
if (plan === undefined) throw new Error(`No plan for ${tool}`);
return plan;
};
describe("writes whose calls send different members", () => {
it("serves an update a later call sends fewer members to, requiring only those every call sent", () => {
const plan = planOf(FEWER_MEMBERS_ROWS, "put_contacts_by_id");
expect(plan).toMatchObject({ served: true });
expect(plan.body?.required).toEqual(["email"]);
expect(plan.body?.accepted).toEqual(
expect.arrayContaining(["name", "city", "email"]),
);
});
it("serves a create whose member one call sent and another left for the application to number", () => {
const plan = planOf(NUMBERED_UNLESS_SENT_ROWS, "post_bundles");
expect(plan).toMatchObject({ served: true });
expect(plan.rules).toContainEqual({
field: "order",
rule: "sequence",
scope: "collection",
sent: true,
});
expect(plan.body?.required).toEqual(["name"]);
});
it("confirms an update whose read shows the clock it stamped to the second", () => {
expect(planOf(DONE_CLOCK_ROWS, "patch_tasks_by_id")).toMatchObject({
served: true,
});
});
it("serves a PUT that clears a member it left out, as a default", () => {
const plan = planOf(PUT_CLEARS_ROWS, "put_tags_by_id");
expect(plan).toMatchObject({ served: true });
expect(plan.body?.defaults).toEqual([{ member: "owner", value: null }]);
});
});
@@ -246,6 +246,7 @@ const fieldRuleSchema = z.discriminatedUnion("rule", [
rule: z.literal("sequence"),
scope: z.enum(["collection", "type"]),
start: z.number().int().exactOptional(),
sent: z.literal(true).exactOptional(),
}),
z.strictObject({ ...ruled, rule: z.literal("unique") }),
z.strictObject({
@@ -229,8 +229,11 @@ const ruledValue = (
switch (rule.rule) {
case "reference":
return referencedValue(rule, body, referenced);
case "identity":
case "sequence":
return rule.sent === true && Object.hasOwn(body, rule.field)
? ok(body[rule.field]!)
: generate(rule);
case "identity":
case "unique":
case "renewed":
case "clock":
@@ -489,6 +492,9 @@ export const updatedFields = (
setOwnMember(written, rule.field, value.value);
}
}
// A member the call leaves out takes the default a whole-object update showed.
for (const { member, value } of plan.body.defaults ?? [])
if (!Object.hasOwn(sent, member)) setOwnMember(written, member, value);
const unspelled = derive(
plan.rules.filter(
(rule) => rule.rule !== "slug" || Object.hasOwn(sent, rule.from),
@@ -228,11 +228,21 @@ export const refinedBody = (
const accepted = later.accepted.filter(
(member) => !body.accepted.includes(member),
);
const held = body.defaults ?? [];
const defaults = [
...held,
...(later.defaults ?? []).filter(
(entry) => !held.some((kept) => kept.member === entry.member),
),
];
return {
...body,
...(defaults.length === 0 ? {} : { defaults }),
// A later call may be the first to show the body repeating the path's id.
identity: body.identity ?? later.identity,
accepted: [...body.accepted, ...accepted],
// A member is required only when every call sent it.
required: body.required.filter((member) => later.required.includes(member)),
fixed: [
...body.fixed,
...later.fixed.filter((entry) => !fixed.has(entry.member)),
@@ -322,6 +322,31 @@ const shownAlone = (
return sent ? undefined : { rule: own };
};
/** A member one call sent and stored as sent, and another left out and the application numbered: the value a call sends, else the next number. */
const sentOrNumbered = (
before: FieldRule,
after: FieldRule,
field: string,
calls: FieldCalls,
agree: (held: FieldRule) => FieldRule | undefined,
): FieldRule | undefined => {
const laterSent = Object.hasOwn(calls.later.body, field);
if (before.rule === "echo")
return after.rule === "sequence" && !laterSent
? { ...after, sent: true }
: undefined;
if (before.rule !== "sequence") return undefined;
if (after.rule === "echo")
return before.sent === true || !calls.sentBefore.has(field)
? { ...before, sent: true }
: undefined;
if (before.sent !== true || after.rule !== "sequence" || laterSent)
return undefined;
const { sent: _sent, ...numbered } = before;
const merged = agree(numbered);
return merged?.rule === "sequence" ? { ...merged, sent: true } : undefined;
};
/** One field's rule in the merged plan, and the default it gives a member; undefined when the calls disagree on it. */
const mergedField = (
field: string,
@@ -334,13 +359,18 @@ const mergedField = (
if (!inExemplar || !Object.hasOwn(later.entity, field))
return shownAlone(inExemplar, calls);
if (before !== undefined && after !== undefined) {
const rule = agreeing(before, after, {
const values = {
field,
later: later.entity[field],
earlier: calls.earlier,
exemplar,
laterEntity: later.entity,
});
};
const rule =
agreeing(before, after, values) ??
sentOrNumbered(before, after, field, calls, (held) =>
agreeing(held, after, values),
);
return rule === undefined ? undefined : { rule };
}
// The plan echoes a member this call left out: what it stored there is the member's default.
@@ -522,6 +522,8 @@ export const explainUpdate = (
const rules: FieldRule[] = [];
const echoed = new Set<string>();
const unexplained: string[] = [];
// A whole-object PUT clears a member it leaves out: that value is the member's default.
const defaults: { readonly member: string; readonly value: JsonValue }[] = [];
// A member the application replaces with a value it generates is accepted with any value.
const generated = new Set<string>();
let identity: string | null = null;
@@ -593,8 +595,14 @@ export const explainUpdate = (
body,
knownEntities,
);
if (rule === undefined) unexplained.push(field);
else rules.push(rule);
if (rule !== undefined) rules.push(rule);
else if (
evidence.method === "PUT" &&
isBlankValue(ownMember(evidence.after, field)) &&
!isBlankValue(ownMember(evidence.before, field))
)
defaults.push({ member: field, value: evidence.after[field]! });
else unexplained.push(field);
}
rules.push(...pairedStamps(evidence, rules));
// A member a reference reads names an entity, which any held one may be.
@@ -614,7 +622,11 @@ export const explainUpdate = (
);
return {
rules,
body: { ...shaped, identity },
body: {
...shaped,
identity,
...(defaults.length === 0 ? {} : { defaults }),
},
unexplained,
unstamped: sameSecondClocks(evidence),
};
@@ -53,12 +53,13 @@ export type GeneratedRule =
readonly rule: "identity";
readonly generator: "sequence" | "uuid" | "token";
}
/** One past the largest value its scope holds, and never below `start`, a value the first create took above them. */
/** One past the largest value its scope holds, and never below `start`, a value the first create took above them; with `sent`, the value a call sends instead. */
| {
readonly field: string;
readonly rule: "sequence";
readonly scope: "collection" | "type";
readonly start?: number;
readonly sent?: true;
}
| { readonly field: string; readonly rule: "unique" }
/** The value kept with each UUID at the pointers (`*` for any array index) replaced by a fresh one, as rewritten child rows are. */
@@ -11,6 +11,7 @@ import {
} from "./environmentStateRules.js";
import {
laterObservation,
respelledInstant,
type ObservationSearch,
} from "./environmentStateWriteObserve.js";
import { singleQueryValues } from "../recordedTransportUrl.js";
@@ -649,7 +650,10 @@ export const pinnedEchoes = (
);
};
/** An update's fields as stored: each nested clock as the first later read of the entity shows it restamped, else as the call showed it. */
/**
* An update's fields as stored: each nested clock as the first later read of the entity shows it restamped, and each
* clock as that read spells the same instant at a coarser precision, else as the call showed it.
*/
export const restampedFields = (
search: ObservationSearch,
target: Parameters<typeof laterObservation>[1],
@@ -659,9 +663,21 @@ export const restampedFields = (
const nested = rules.flatMap((rule) =>
rule.rule === "nested-clock" ? [rule] : [],
);
if (nested.length === 0) return shown;
const clocks = rules.flatMap((rule) =>
rule.rule === "clock" ? [rule.field] : [],
);
if (nested.length === 0 && clocks.length === 0) return shown;
const later = laterObservation(search, target);
const fields = { ...shown };
for (const field of clocks) {
const read = ownMember(later?.object, field);
if (
read !== undefined &&
!same(read, ownMember(shown, field)) &&
respelledInstant(ownMember(shown, field), read)
)
setOwnMember(fields, field, read);
}
for (const rule of nested) {
const read = storedRestamp(
ownMember(shown, rule.field),
+1 -1
View File
@@ -6,5 +6,5 @@ export const PACKAGE_METADATA = {
serverSdkVersion: "2.3.1",
clientSdkVersion: "2.3.1",
coreSdkVersion: "2.3.1",
skillVersion: "53",
skillVersion: "54",
} as const;
+120
View File
@@ -0,0 +1,120 @@
import type { TimedCaptureRow } from "./analysisView.js";
import { bookClock } from "./environmentStateWrites.js";
import type { JsonValue } from "../../src/domain/jsonValue.js";
type Entity = { readonly [key: string]: JsonValue };
/** Two contact updates, the second sending only the email the first sent with a name and a city. */
export const FEWER_MEMBERS_ROWS: readonly TimedCaptureRow[] = (() => {
const contact = (
id: number,
name: string,
city: string,
email: string,
): Entity => ({ id, name, city, email });
const [ada, mina] = [
contact(1, "Ada Finch", "Porto", "ada@example.test"),
contact(2, "Mina Okafor", "Accra", "mina@example.test"),
];
const adaMoved = contact(
1,
"Ada Finch-Lowe",
"Lisbon",
"ada.lowe@example.test",
);
const minaMail = { ...mina, email: "mina.o@example.test" };
return [
["GET", "/api/contacts", [ada, mina]],
["GET", "/api/contacts/1", ada],
["GET", "/api/contacts/2", mina],
[
"PUT",
"/api/contacts/1",
adaMoved,
200,
{
name: "Ada Finch-Lowe",
city: "Lisbon",
email: "ada.lowe@example.test",
},
],
["GET", "/api/contacts/1", adaMoved],
["PUT", "/api/contacts/2", minaMail, 200, { email: "mina.o@example.test" }],
["GET", "/api/contacts/2", minaMail],
["GET", "/api/contacts", [adaMoved, minaMail]],
];
})();
/** Bundle creates: the first sends its order, the second leaves it to the application, which numbers it next. */
export const NUMBERED_UNLESS_SENT_ROWS: readonly TimedCaptureRow[] = (() => {
const bundle = (id: number, name: string, order: number): Entity => ({
id,
name,
order,
});
const [alpha, bravo] = [bundle(1, "Alpha", 0), bundle(2, "Bravo", 1)];
const [charlie, delta] = [bundle(3, "Charlie", 5), bundle(4, "Delta", 6)];
return [
["GET", "/api/bundles", [alpha, bravo]],
["GET", "/api/bundles/1", alpha],
["GET", "/api/bundles/2", bravo],
["POST", "/api/bundles", charlie, 201, { name: "Charlie", order: 5 }],
["GET", "/api/bundles/3", charlie],
["GET", "/api/bundles", [alpha, bravo, charlie]],
["POST", "/api/bundles", delta, 201, { name: "Delta" }],
["GET", "/api/bundles/4", delta],
["GET", "/api/bundles", [alpha, bravo, charlie, delta]],
];
})();
/** Tasks marked done: each answer stamps `done_at` to the nanosecond, the read after it to the second. */
export const DONE_CLOCK_ROWS: readonly TimedCaptureRow[] = (() => {
const task = (id: number, title: string): Entity => ({
id,
title,
done: false,
done_at: "0001-01-01T00:00:00Z",
});
const [trim, oil] = [task(1, "Trim the wicks"), task(2, "Order lamp oil")];
const done = (entity: Entity, at: number, fraction: string): Entity => ({
...entity,
done: true,
done_at: bookClock(at).replace("Z", fraction),
});
return [
["GET", "/api/tasks", [trim, oil]],
["GET", "/api/tasks/1", trim],
["GET", "/api/tasks/2", oil],
[
"PATCH",
"/api/tasks/1",
done(trim, 3, ".001234567Z"),
200,
{ done: true },
],
["GET", "/api/tasks/1", done(trim, 3, "Z")],
["PATCH", "/api/tasks/2", done(oil, 5, ".002345678Z"), 200, { done: true }],
["GET", "/api/tasks/2", done(oil, 5, "Z")],
];
})();
/** Tag PUTs: one leaves the owner out and the application clears it, the other sends it and keeps it. */
export const PUT_CLEARS_ROWS: readonly TimedCaptureRow[] = (() => {
const tag = (id: number, name: string, owner: number | null): Entity => ({
id,
name,
owner,
});
const [inbox, bills] = [tag(1, "Inbox", 3), tag(2, "Bills", 3)];
const [triage, paid] = [tag(1, "Triage", null), tag(2, "Paid", 3)];
return [
["GET", "/api/tags", [inbox, bills]],
["GET", "/api/tags/1", inbox],
["GET", "/api/tags/2", bills],
["PUT", "/api/tags/1", triage, 200, { name: "Triage" }],
["GET", "/api/tags/1", triage],
["PUT", "/api/tags/2", paid, 200, { name: "Paid", owner: 3 }],
["GET", "/api/tags/2", paid],
["GET", "/api/tags", [triage, paid]],
];
})();