fix(environment): address review of blank members and sorted writes

- Bulk updates move their entities in turn against the others' earlier
  values and install the final order once, as replay does.
- A member's recorded empty values stay accepted once a later call teaches
  its default; a left-out member with a default keeps the default.
- Write answers spell empty lists and maps as their recorded answers did.
- A rename can teach a sort when the creates chose one by a field the
  update did not write, such as ids that ascend with titles.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Edgar ArakelyanandClaude Opus 5.5 committed 2026-10-26 10:56:46 -07:00
1 parent 0ceff6fa69
commit 053264bd77
13 files changed
+317 -145

No files matched your search

@@ -23,18 +23,20 @@ natural-language tasks ask for, or withheld them:
- **Blank members.** A create member a recorded call sent empty is not required. A call that leaves it out is applied
as sending that value (`body.blank`), so the entity, the answer and every rule reading the member come out as for
the recorded call. A member a later call left out takes the default it showed instead. A call may send a member as
any recorded call sent it empty, such as `null` where another stored a string, and one leaving it out is applied as
the earliest did. Updates are unchanged.
any recorded call sent it empty, such as `null` where another stored a string, even once a default is learned, and
one leaving it out without a default is applied as the earliest did. Updates are unchanged.
- **Listed ids.** A list view names top-level envelope members that list two or more listed entities' identity values
in list order (`listed_ids`). A rendered list sets them to its own entities' values, unless it is a page of more
matches, which keeps the recorded value as a partial page's total does.
- **Sorted collections.** An update that writes the field its type's creates are sorted by moves its entity to where
the new value sorts, in replay and serving, and its plan carries that `insert`. When a read listing the whole
collection fails after an update and no create of the type was placed by a sort, replay tries the collection sorted
by a field the update writes; on a match, the type's creates and updates take that order.
the new value sorts, in replay and serving (a bulk update moving each entity in turn), and its plan carries that
`insert`. When a read listing the whole collection fails after an update and the type's creates are not sorted by a
field the update writes, replay tries the collection sorted by such a field, unless a create was placed by a sort
that excludes it; on a match, the type's creates and updates take that order.
- **Unloaded answers.** A create's later item read is taken as its observation when it differs from the answer only in
values the answer had not loaded: empty, zero, `false` or Go's zero time, or objects whose members are each the same
or loaded so. An unsent update field that changes only between `null`, `[]` and `{}` is unchanged.
or loaded so. An unsent update field that changes only between `null`, `[]` and `{}` is unchanged. Write answers
spell empty lists and maps as their recorded answers did.
- **Unruled fields.** A later create that stored a field no rule writes otherwise than the plan keeps it is
`unexplained-change` at the create, so its recorded entity is held and later reads check other writes as before.
- **Skill.** The two scratch creates differ in what they send, the second only what a person must give, however they
@@ -24,8 +24,13 @@ the agent recordings.
for `null` takes its item read; an update answer spelling an empty list `[]` is unchanged; a later create storing a
derived field otherwise is withheld at the create and later deletes stay confirmed.
- Serving: a label created from its name alone stores the empty members as sent and is inserted by name, and one
without the required name is refused; renamed labels and folders move to where their new names sort; a created tag's
id appears in the list envelope's `all` in list order.
without the required name is refused; renamed labels and folders move to where their new names sort, and a bulk
rename of two labels moves each; a created tag's id appears in the list envelope's `all` in list order; a note sent
`null` as one recording did is accepted after a later call taught `""` as its default, which a note left out takes;
a created and an updated task answer `assignees: []` as their recordings did while reads show `null`.
- Review cases: a member's recorded empty value stays accepted once a default is learned; a rename teaches title order
where the creates fit both id and title order; moving written entities in turn sorts `[a, b, c, d]` with `b→h` and
`c→i` as `[a, d, h, i]`. The plan and serving tests for these fail on the branch's first head `0ceff6fa`.
## Write plans over recorded captures
+15 -10
View File
@@ -757,8 +757,10 @@ no longer confirms the create; one that disagrees or omits an answer field its
item view renders still checks it. Clock agreement uses the coarser recorded
precision, including microseconds. A list read
only adds fields the answer lacks and still checks it. Serving answers a create
in its answer's shape and clock precision and stores the fields of both with
the item read-back's clock precision. A `reference` is an object holding fields of the
in its answer's shape and clock precision, with empty lists and maps spelled
(`null`, `[]` or `{}`) as its answer spelled them, and stores the fields of both with
the item read-back's clock precision. Updates answer empty lists and maps as
their answers spelled them too. A `reference` is an object holding fields of the
entity a sent member names, as the state holds it (a parent beside `parentId`):
the member is accepted with any value, serving looks the entity up and refuses a
call naming one the environment does not hold or holds in more than one
@@ -774,9 +776,10 @@ 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
`{}`), 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. A call may send it as any recorded call sent
it empty, and one leaving it out is applied as the earliest did, as when one
create sent `false` and another `null`. A clock rule widens to `trimmed` only when no held entity or
that left it out showed a default, which it then keeps. A call may send it as any
recorded call sent it empty, even after a default is learned, and one leaving it
out is applied as the earliest did, as when one create sent `false` and another
`null`. A clock rule widens to `trimmed` only when no held entity or
earlier call showed that clock at a fixed width. A later create that disagrees
leaves the plan as it was. A create whose observed reference differs from the
entity the plan rebuilds, whether or not it sent the reference member, is
@@ -846,11 +849,13 @@ contradicted it; otherwise its `reason` is the first of its build reason
entities (sorted by a field the collection's recorded order shows, else last),
and `write_clock` is the end of the last timed record, to the next second. An
update that writes the field its type's creates are sorted by moves its entity
to where the new value sorts (its own `insert`). When a read listing the whole
collection fails after an update and no create of the type was placed by a
sort, the view tries the collection sorted by a field that update writes; on a
match, the type's creates and updates take that order, as Grafana keeps
folders by title though no earlier read could tell that from creation order.
to where the new value sorts (its own `insert`); a bulk update moves each entity
it writes in turn, as replay applies them. When a read listing the whole
collection fails after an update and the type's creates are not sorted by a field
that update writes, the view tries the collection sorted by such a field, unless
a create was placed by a sort that excludes it; on a match, the type's creates
and updates take that order, as Grafana keeps folders by title though no earlier
read could tell that from creation order, or from id order where both ascend.
Values in the state are recorded data, so the view refuses the same parents as
the manifest, including those inspected with `sensitive_values`, and a
@@ -32,7 +32,7 @@ import {
generator,
observedEntity,
pathValues,
resortedIndex,
sortedOrder,
unloadable,
heldEntity,
updateTarget,
@@ -184,19 +184,18 @@ export const answerBulk = (
);
if (!answer.ok) return answer;
if (update) {
const updated = serving.store.updateAll(written.value);
const updated = serving.store.updateAll(
written.value,
sortedOrder(
serving,
plan,
written.value.map((after, index) => ({
before: targets.value[index]!.entity,
after,
})),
),
);
if (!updated.ok) return updated;
// In a sorted collection each entity whose sorted field changed moves, one after another.
for (const [index, entity] of written.value.entries()) {
const at = resortedIndex(serving, plan, {
entity: targets.value[index]!.entity,
fields: entity.fields,
});
if (at !== undefined) {
const moved = serving.store.update(entity, at);
if (!moved.ok) return moved;
}
}
} else serving.store.removeAll(written.value);
serving.store.commitWrite(values.taken);
return answer;
@@ -1,10 +1,13 @@
import { describe, expect, it } from "vitest";
import {
BULK_RENAMED_LABEL_ROWS,
formLabelRows,
LISTED_ID_ROWS,
NOTE_DEFAULT_ROWS,
RENAMED_LABEL_ROWS,
TITLE_ORDER_ROWS,
UNLOADED_ANSWER_ROWS,
} from "../../../tests/fixtures/environmentStateFormWrites.js";
import { servedFrom } from "../../../tests/fixtures/environmentStateServing.js";
import type { JsonValue } from "../../domain/jsonValue.js";
@@ -66,6 +69,60 @@ describe("served creates and updates modelled on form recordings", () => {
]);
});
it("moves every entity a bulk rename writes to where its new name sorts", () => {
const labels = servedFrom(BULK_RENAMED_LABEL_ROWS);
const created = (name: string) => {
const body = labels.answer("post_labels", { body: { name } }).body;
if (typeof body !== "object" || body === null || Array.isArray(body))
throw new Error("Expected an object body");
return body["id"]!;
};
created("Attic");
const garden = created("Garden");
expect(field(labels.answer("get_labels").body, "name")).toEqual([
"Attic",
"Garden",
"Kitchen",
"Tools",
]);
labels.answer("patch_labels", {
body: [
{ id: garden, name: "Vane" },
{ id: 1, name: "Wharf" },
],
});
expect(field(labels.answer("get_labels").body, "name")).toEqual([
"Attic",
"Tools",
"Vane",
"Wharf",
]);
});
it("accepts a member sent empty as one recording did, and stores the default for one left out", () => {
const notes = servedFrom(NOTE_DEFAULT_ROWS);
expect(
notes.answer("post_notes", { body: { name: "Blank", note: null } }).body,
).toMatchObject({ name: "Blank", note: null });
expect(
notes.answer("post_notes", { body: { name: "Left out" } }).body,
).toMatchObject({ name: "Left out", note: "" });
});
it("answers writes with empty lists spelled as their recorded answers spelled them", () => {
const tasks = servedFrom(UNLOADED_ANSWER_ROWS);
expect(
tasks.answer("post_tasks", { body: { title: "Coil the ropes" } }).body,
).toMatchObject({ title: "Coil the ropes", assignees: [] });
expect(
tasks.answer("patch_tasks_by_id", { id: "1", body: { done: true } }).body,
).toMatchObject({ done: true, assignees: [] });
expect(tasks.answer("get_tasks_by_id", { id: "1" }).body).toMatchObject({
done: true,
assignees: null,
});
});
it("lists a created tag's id in the list envelope's ids, in list order", () => {
const tags = servedFrom(LISTED_ID_ROWS);
tags.answer("post_tags", { body: { name: "Lamp" } });
@@ -306,48 +306,18 @@ export class EnvironmentStateStore {
return ok(undefined);
}
/** Replace a held entity's values, in place or at `index` of its collection's other entities, unless the state would outgrow what `data/add` loads. */
update(
entity: StateEntity,
index?: number,
): Result<void, AnalysisResourceConstraintError> {
const held = this.#entities.get(
compositeKey([entity.type, entity.collection, entity.id]),
);
if (index === undefined || held === undefined)
return this.updateAll([entity]);
const entities = this.#content.entities.filter(
(candidate) => candidate !== held,
);
const members = entities.filter(
(candidate) =>
candidate.type === entity.type &&
candidate.collection === entity.collection,
);
if (members.length === 0) return this.updateAll([entity]);
const bytes = this.#grown(jsonByteLength(entity) - jsonByteLength(held));
if (!bytes.ok) return bytes;
const anchor =
index < members.length
? entities.indexOf(members[index]!)
: entities.indexOf(members.at(-1)!) + 1;
this.#install(
{
...this.#content,
entities: [
...entities.slice(0, anchor),
entity,
...entities.slice(anchor),
],
},
bytes.value,
);
return ok(undefined);
/** Replace a held entity's values in place, unless the state would outgrow what `data/add` loads. */
update(entity: StateEntity): Result<void, AnalysisResourceConstraintError> {
return this.updateAll([entity]);
}
/** Replace held entities in one step, unless the state would outgrow what `data/add` loads; a refusal changes nothing. */
/**
* Replace held entities in one step, unless the state would outgrow what `data/add` loads; a refusal changes nothing.
* `order` puts held entities, the replacements among them, in that order in the places they hold.
*/
updateAll(
entities: readonly StateEntity[],
order: readonly StateEntity[] = [],
): Result<void, AnalysisResourceConstraintError> {
const replaced = new Map<StateEntity, StateEntity>();
for (const entity of entities) {
@@ -363,11 +333,17 @@ export class EnvironmentStateStore {
delta += jsonByteLength(entity) - jsonByteLength(held);
const bytes = this.#grown(delta);
if (!bytes.ok) return bytes;
const key = (entity: StateEntity) =>
compositeKey([entity.type, entity.collection, entity.id]);
const placed = new Set(order.map(key));
let next = 0;
this.#install(
{
...this.#content,
entities: this.#content.entities.map(
(candidate) => replaced.get(candidate) ?? candidate,
entities: this.#content.entities.map((candidate) =>
placed.has(key(candidate))
? order[next++]!
: (replaced.get(candidate) ?? candidate),
),
},
bytes.value,
@@ -12,6 +12,7 @@ import {
} from "../../domain/analysisView/environmentStateWriteApply.js";
import {
insertIndex,
resortedMembers,
nextInteger,
} from "../../domain/analysisView/environmentStateWriteExplain.js";
import { callRenewal } from "../../domain/analysisView/environmentStateWriteGenerated.js";
@@ -42,7 +43,6 @@ import {
collectionMatch,
decodedSegment,
isObject,
same,
scalarText,
type JsonObject,
type StateEntity,
@@ -602,38 +602,31 @@ const answerUpdate = (
},
);
if (!answer.ok) return answer;
const updated = serving.store.update(
updatedEntity(entity, written.value),
resortedIndex(serving, plan, { entity, fields: written.value }),
const after = updatedEntity(entity, written.value);
const updated = serving.store.updateAll(
[after],
sortedOrder(serving, plan, [{ before: entity, after }]),
);
if (!updated.ok) return updated;
serving.store.commitWrite(values.taken);
return answer;
};
/** Where an update moves its entity among the others of a collection sorted by a field it changed; undefined to keep it in place. */
export const resortedIndex = (
/** The collection's members in the order a sorted collection holds them after these writes; empty when the plan keeps them in place. */
export const sortedOrder = (
serving: StateServing,
plan: StateWritePlan,
written: { readonly entity: StateEntity; readonly fields: JsonObject },
): number | undefined => {
const { insert } = plan;
const { entity, fields } = written;
if (
insert?.rule !== "sorted" ||
same(
ownMember(entity.fields, insert.field),
ownMember(fields, insert.field),
)
)
return undefined;
return insertIndex(
insert,
serving.store
.inCollection(entity.type, entity.collection)
.filter((member) => member.id !== entity.id)
.map((member) => member.fields),
fields,
writes: readonly {
readonly before: StateEntity;
readonly after: StateEntity;
}[],
): StateEntity[] => {
const [first] = writes;
if (plan.insert?.rule !== "sorted" || first === undefined) return [];
return resortedMembers(
plan.insert,
serving.store.inCollection(first.before.type, first.before.collection),
writes,
);
};
@@ -789,15 +789,15 @@ export class EnvironmentStateWriteSimulation {
/**
* The successful check with the collection sorted by a field an update the read exercised writes, when the read
* lists the whole collection in that order and no create of the type was placed otherwise; the type's creates then
* insert by it, and its updates move their entity by it.
* lists the whole collection in that order and no create of the type was placed by a sort that excludes it; the
* type's creates then insert by it, and its updates move their entity by it. A sort the creates chose by a field the
* update did not write does not stop this, since both may have fit every earlier read.
*/
#retrySort(
view: Extract<StateView, { shape: "list" }>,
record: StateRecordFacts,
exercised: ReadonlySet<string>,
): CheckOutcome | undefined {
if (this.#sortOf(view.type) !== undefined) return undefined;
const line = this.#line(view, record);
const idField = this.#types.get(view.type)?.id_field;
if (line === undefined || idField === undefined) return undefined;
@@ -814,6 +814,10 @@ export class EnvironmentStateWriteSimulation {
: [];
}),
);
// A sort by a field the update wrote has moved its entity already; one by another field may be a guess both fit.
const current = this.#sortOf(view.type);
if (current?.rule === "sorted" && written.has(current.field))
return undefined;
const creates = [...this.#drafts.values()].filter(
(draft) =>
draft.plan.effect === "create" &&
@@ -841,7 +845,9 @@ export class EnvironmentStateWriteSimulation {
),
);
});
for (const rule of rules) {
for (const rule of rules.filter(
(candidate) => current === undefined || !same(candidate, current),
)) {
// A create placed by another sort shows the collection is not kept in this order.
if (
creates.some(
@@ -7,14 +7,17 @@ import {
import {
DERIVED_TEXT_ROWS,
formLabelRows,
ID_OR_TITLE_ROWS,
LISTED_ID_ROWS,
MINIMAL_CONTACT_ROWS,
NOTE_DEFAULT_ROWS,
RENAMED_LABEL_ROWS,
TITLE_ORDER_ROWS,
UNLOADED_ANSWER_ROWS,
} from "../../../tests/fixtures/environmentStateFormWrites.js";
import { environmentStateViewOf } from "../../../tests/fixtures/environmentStateWrites.js";
import { mergedCreatePlan } from "./environmentStateWriteCreateCalls.js";
import { resortedMembers } from "./environmentStateWriteExplain.js";
import type { WriteBody } from "./environmentStateWriteRules.js";
const planOf = (rows: readonly TimedCaptureRow[], tool: string) => {
@@ -77,6 +80,19 @@ describe("create members a form sent empty", () => {
});
});
describe("empty values and defaults", () => {
it("keeps accepting a recorded empty value after a later call that left the member out teaches its default", () => {
expect(planOf(NOTE_DEFAULT_ROWS, "post_notes")).toMatchObject({
served: true,
body: {
required: ["name"],
defaults: [{ member: "note", value: "" }],
blank: [{ member: "note", sent: null }],
},
});
});
});
describe("a create with only what a person must give", () => {
it("serves a create whose minimal call left string members `null` and an empty flag spelled another way", () => {
expect(planOf(MINIMAL_CONTACT_ROWS, "post_contacts")).toMatchObject({
@@ -114,6 +130,38 @@ describe("collections kept sorted", () => {
});
});
it("learns the title order from a rename when the creates fit both id and title order", () => {
const byTitle = { rule: "sorted", field: "title", direction: "ascending" };
expect(planOf(ID_OR_TITLE_ROWS, "put_folders_by_id")).toMatchObject({
served: true,
insert: byTitle,
});
expect(planOf(ID_OR_TITLE_ROWS, "post_folders")).toMatchObject({
insert: byTitle,
});
});
it("moves each written entity in turn, against the others' values before their own writes", () => {
const entity = (id: string, name: string) => ({ id, fields: { name } });
const [a, b, c, d] = ["a", "b", "c", "d"].map((name, index) =>
entity(String(index + 1), name),
) as [ReturnType<typeof entity>, ...ReturnType<typeof entity>[]];
const order = resortedMembers(
{ rule: "sorted", field: "name", direction: "ascending" },
[a, b!, c!, d!],
[
{ before: b!, after: entity("2", "h") },
{ before: c!, after: entity("3", "i") },
],
);
expect(order.map(({ fields }) => fields["name"])).toEqual([
"a",
"d",
"h",
"i",
]);
});
it("renders a list envelope's ids of the listed entities from the state", () => {
const view = environmentStateViewOf(
timedCaptureEvidence(LISTED_ID_ROWS),
@@ -13,14 +13,15 @@ import {
type JsonObject,
type StateEntity,
} from "./environmentStateRules.js";
import type {
FieldRule,
GeneratedRule,
JsonKind,
StateWritePlan,
WriteBody,
WriteCall,
WriteResponse,
import {
emptySpellings,
type FieldRule,
type GeneratedRule,
type JsonKind,
type StateWritePlan,
type WriteBody,
type WriteCall,
type WriteResponse,
} from "./environmentStateWriteRules.js";
import {
stampProblem,
@@ -327,12 +328,13 @@ const copiedShapes = (
}),
);
/** A create's call with the members it left out that recorded calls sent empty, as the earliest sent them. */
/** A create's call with the members it left out that recorded calls sent empty, as the earliest sent them; one a call that left it out showed a default for keeps that default. */
const withBlanks = (body: WriteBody, call: WriteCall): WriteCall => {
const given = call.body ?? {};
const left = (body.blank ?? []).filter(
({ member }, index, blank) =>
!Object.hasOwn(given, member) &&
!(body.defaults ?? []).some((entry) => entry.member === member) &&
blank.findIndex((entry) => entry.member === member) === index,
);
return left.length === 0
@@ -566,9 +568,13 @@ export const writtenResponse = (
Object.fromEntries(
fields.map((field) => {
const value = ownMember(written.fields!, field)!;
const answered = ownMember(shown, field);
// An answer spells an empty list or map as its recording did, whichever spelling the state holds.
if (answered !== undefined && emptySpellings(answered, value))
return [field, answered];
const clock =
written.clocks?.includes(field) === true
? readClock(ownMember(shown, field))
? readClock(answered)
: undefined;
return [
field,
@@ -192,11 +192,6 @@ export const mergedCreatePlan = (
? undefined
: joinedDefaults(plan.body.defaults ?? [], defaults);
if (body === undefined || joined === undefined) return undefined;
// A member a call left out stores its default, which a value sent empty elsewhere does not override.
const { blank: sentBlank = [], ...rest } = body;
const blank = sentBlank.filter(
({ member }) => !joined.some((entry) => entry.member === member),
);
// The plan's rules keep their order; rules the later call adds follow.
return {
rules: [
@@ -206,38 +201,22 @@ export const mergedCreatePlan = (
}),
...[...merged.values()].filter((rule) => !held.has(rule.field)),
],
body: {
...rest,
...(joined.length === 0 ? {} : { defaults: joined }),
...(blank.length === 0 ? {} : { blank }),
},
body: joined.length === 0 ? body : { ...body, defaults: joined },
};
};
/**
* Members both calls sent and either sent empty, with each empty value sent, the earliest first; a member only one call
* sent takes its default from the call that left it out instead.
*/
/** Every empty value recorded calls sent a member, the earliest first. */
const joinedBlanks = (
held: readonly Blank[],
added: readonly Blank[],
calls: {
readonly sentBefore: ReadonlySet<string>;
readonly later: JsonObject;
},
): Blank[] => {
const blank: Blank[] = [];
const members = new Set(held.map(({ member }) => member));
for (const member of members) {
if (!Object.hasOwn(calls.later, member)) continue;
const entries = held.filter((entry) => entry.member === member);
const sent = calls.later[member]!;
if (isBlankValue(sent) && !entries.some((entry) => same(entry.sent, sent)))
entries.push({ member, sent });
blank.push(...entries);
}
const blank = [...held];
for (const entry of added)
if (calls.sentBefore.has(entry.member) && !members.has(entry.member))
if (
!blank.some(
({ member, sent }) => member === entry.member && same(sent, entry.sent),
)
)
blank.push(entry);
return blank;
};
@@ -267,13 +246,9 @@ const mergedBody = (
const added = contract.accepted.filter(
(member) => !body.accepted.includes(member),
);
const blank = joinedBlanks(body.blank ?? [], contract.blank ?? [], {
sentBefore: new Set([...body.accepted, ...fixed.keys()]),
later: later.body,
});
const { blank: _blank, ...rest } = body;
const blank = joinedBlanks(body.blank ?? [], contract.blank ?? []);
return {
...rest,
...body,
accepted: [...body.accepted, ...added],
fixed: [
...body.fixed,
@@ -680,6 +680,41 @@ export const insertIndex = (
return index < 0 ? members.length : index;
};
/**
* A sorted collection's members after writes move each entity whose sorted field changed to where its new value sorts,
* one after another, as replay applies them; other written entities keep their places.
*/
export const resortedMembers = <
Entity extends { readonly id: string; readonly fields: JsonObject },
>(
rule: Extract<InsertRule, { rule: "sorted" }>,
members: readonly Entity[],
writes: readonly { readonly before: Entity; readonly after: Entity }[],
): Entity[] => {
const order = [...members];
for (const { before, after } of writes) {
const at = order.findIndex((member) => member.id === before.id);
if (at < 0) continue;
order.splice(at, 1);
const moved = !same(
ownMember(before.fields, rule.field),
ownMember(after.fields, rule.field),
);
order.splice(
moved
? insertIndex(
rule,
order.map((member) => member.fields),
after.fields,
)
: at,
0,
after,
);
}
return order;
};
/** The object in a document value, at the root or in one member, that holds most of a body's members. */
export const documentPointerOf = (
value: JsonValue,
+65
View File
@@ -256,3 +256,68 @@ export const MINIMAL_CONTACT_ROWS: readonly TimedCaptureRow[] = (() => {
["GET", "/api/contacts", [seed, full, minimal]],
];
})();
/** The form label creates, then a bulk rename naming each label and its new name in an array, read back. */
export const BULK_RENAMED_LABEL_ROWS: readonly TimedCaptureRow[] = (() => {
const rows = formLabelRows();
const [attic, garden] = [
label(4, "Attic", { color: "#00aa44" }),
label(3, "Garden", { description: "Seeds" }),
];
const zinc = { ...KITCHEN, name: "Zinc" };
return [
...rows,
["PATCH", "/api/labels", [zinc], 200, [{ id: 1, name: "Zinc" }]],
["GET", "/api/labels/1", zinc],
["GET", "/api/labels", [attic, garden, TOOLS, zinc]],
];
})();
/** Notes created with text, then sent `null`, then left out and stored `""`, each read back. */
export const NOTE_DEFAULT_ROWS: readonly TimedCaptureRow[] = (() => {
const note = (id: number, name: string, text: string | null) => ({
id,
name,
note: text,
});
const seed = note(1, "Seed", "kept");
const first = note(2, "First", "text");
const second = note(3, "Second", null);
const third = note(4, "Third", "");
return [
["GET", "/api/notes", [seed]],
["GET", "/api/notes/1", seed],
["POST", "/api/notes", first, 201, { name: "First", note: "text" }],
["GET", "/api/notes/2", first],
["GET", "/api/notes", [seed, first]],
["POST", "/api/notes", second, 201, { name: "Second", note: null }],
["GET", "/api/notes/3", second],
["GET", "/api/notes", [seed, first, second]],
["POST", "/api/notes", third, 201, { name: "Third" }],
["GET", "/api/notes/4", third],
["GET", "/api/notes", [seed, first, second, third]],
];
})();
/**
* Two seeded folders and two creates whose ids and titles both ascend, so either could be the order kept, then a
* rename that sorts first by title.
*/
export const ID_OR_TITLE_ROWS: readonly TimedCaptureRow[] = (() => {
const [alpha, bravo] = [folder(1, "Alpha"), folder(2, "Bravo")];
const [charlie, delta] = [folder(3, "Charlie"), folder(4, "Delta")];
const renamed = folder(4, "Aardvark", 2);
return [
["GET", "/api/folders", [alpha, bravo]],
["GET", "/api/folders/1", alpha],
["POST", "/api/folders", charlie, 200, { title: "Charlie" }],
["GET", "/api/folders/3", charlie],
["GET", "/api/folders", [alpha, bravo, charlie]],
["POST", "/api/folders", delta, 200, { title: "Delta" }],
["GET", "/api/folders/4", delta],
["GET", "/api/folders", [alpha, bravo, charlie, delta]],
["PUT", "/api/folders/4", renamed, 200, { title: "Aardvark", version: 1 }],
["GET", "/api/folders/4", renamed],
["GET", "/api/folders", [renamed, alpha, bravo, charlie]],
];
})();