Files
reverse-engineering-anything/tests/fixtures/environmentStateFormWrites.ts
T
Edgar ArakelyanandClaude Opus 5.5 f1675f82a6 fix(environment): accept every empty spelling recorded creates sent
A create that fills only what a person must give sends `null` where the
full create sent a string, and `null` or `false` for the same flag. The
merged plan now accepts each recorded empty value, and a call that leaves
the member out is applied as the earliest call sent it.

Also adds the spec folder for this change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 09:51:27 -07:00

259 lines
8.3 KiB
TypeScript

import type { JsonValue } from "../../src/domain/jsonValue.js";
import type { TimedCaptureRow } from "./analysisView.js";
import { bookClock } from "./environmentStateWrites.js";
type Entity = Record<string, JsonValue>;
const label = (
id: number,
name: string,
fields: { readonly description?: string; readonly color?: string } = {},
): Entity => ({
id,
name,
description: fields.description ?? "",
color: fields.color ?? "",
icon: "",
});
const KITCHEN = label(1, "Kitchen", { description: "Pots", color: "#aa3300" });
const TOOLS = label(2, "Tools");
/** What a label form sends: every member, those left as they were empty. */
const formBody = (entity: Entity): Entity => {
const { id: _id, ...sent } = entity;
return sent;
};
/** Two label creates through a form that sends every member, the members left empty differing between them, in a collection kept sorted by name. */
export const formLabelRows = (
icons: readonly [JsonValue, JsonValue] = ["", ""],
): TimedCaptureRow[] => {
const garden = label(3, "Garden", { description: "Seeds" });
const attic = label(4, "Attic", { color: "#00aa44" });
return [
["GET", "/api/labels", [KITCHEN, TOOLS]],
["GET", "/api/labels/1", KITCHEN],
["GET", "/api/labels/2", TOOLS],
[
"POST",
"/api/labels",
garden,
201,
{ ...formBody(garden), icon: icons[0] },
],
["GET", "/api/labels/3", garden],
["GET", "/api/labels", [garden, KITCHEN, TOOLS]],
["POST", "/api/labels", attic, 201, { ...formBody(attic), icon: icons[1] }],
["GET", "/api/labels/4", attic],
["GET", "/api/labels", [attic, garden, KITCHEN, TOOLS]],
];
};
/** The form label creates, then a rename that moves a label to the end of the name order, read back. */
export const RENAMED_LABEL_ROWS: readonly TimedCaptureRow[] = (() => {
const rows = formLabelRows();
const zinc = { ...KITCHEN, name: "Zinc" };
const [attic, garden] = [
label(4, "Attic", { color: "#00aa44" }),
label(3, "Garden", { description: "Seeds" }),
];
return [
...rows,
["PATCH", "/api/labels/1", zinc, 200, { name: "Zinc" }],
["GET", "/api/labels/1", zinc],
["GET", "/api/labels", [attic, garden, TOOLS, zinc]],
];
})();
const page = (results: readonly Entity[]): Entity => ({
count: results.length,
next: null,
previous: null,
all: results.map((entity) => entity["id"]!),
results: [...results],
});
/** Tag creates whose list answers carry `all`, the listed ids in list order, as Paperless-ngx's do. */
export const LISTED_ID_ROWS: readonly TimedCaptureRow[] = (() => {
const harbour = label(3, "Harbour");
const quarry = label(4, "Quarry");
return [
["GET", "/api/tags/", page([KITCHEN, TOOLS])],
["GET", "/api/tags/1/", KITCHEN],
["POST", "/api/tags/", harbour, 201, { name: "Harbour" }],
["GET", "/api/tags/3/", harbour],
["GET", "/api/tags/", page([harbour, KITCHEN, TOOLS])],
["POST", "/api/tags/", quarry, 201, { name: "Quarry" }],
["GET", "/api/tags/4/", quarry],
["GET", "/api/tags/", page([harbour, KITCHEN, quarry, TOOLS])],
];
})();
const folder = (id: number, title: string, version = 1): Entity => ({
id,
title,
version,
});
/**
* One seeded folder, two creates that sort after it, and a rename that sorts first, as Grafana lists folders by
* title: no read before the rename shows whether the folders are kept by title or in creation order.
*/
export const TITLE_ORDER_ROWS: readonly TimedCaptureRow[] = (() => {
const rota = folder(1, "Lighthouse Rota");
const alpha = folder(2, "Scratch Alpha");
const beta = folder(3, "Scratch Beta");
const renamed = folder(3, "Beta Renamed", 2);
return [
["GET", "/api/folders", [rota]],
["GET", "/api/folders/1", rota],
["POST", "/api/folders", alpha, 200, { title: "Scratch Alpha" }],
["GET", "/api/folders/2", alpha],
["GET", "/api/folders", [rota, alpha]],
["POST", "/api/folders", beta, 200, { title: "Scratch Beta" }],
["GET", "/api/folders/3", beta],
["GET", "/api/folders", [rota, alpha, beta]],
[
"PUT",
"/api/folders/3",
renamed,
200,
{ title: "Beta Renamed", version: 1 },
],
["GET", "/api/folders/3", renamed],
["GET", "/api/folders", [renamed, rota, alpha]],
];
})();
const OWNER = {
id: 1,
name: "Rea Admin",
created: "2026-10-01T09:00:00Z",
};
/** A create answer before the application loads what it stored: the owner's unset values and an empty list spelled `[]` where reads spell `null`. */
const unloaded = (entity: Entity, at: number): Entity => ({
...entity,
assignees: [],
owner: { id: 1, name: "", created: "0001-01-01T00:00:00Z" },
created: bookClock(at).replace("Z", ".123456789Z"),
});
const task = (id: number, title: string, at: number): Entity => ({
id,
title,
done: false,
assignees: null,
owner: OWNER,
created: bookClock(at),
});
/** Task creates whose answers show the owner and assignees unloaded, then a done flag whose answer spells empty assignees `[]`. */
export const UNLOADED_ANSWER_ROWS: readonly TimedCaptureRow[] = (() => {
const seed = task(1, "Trim the wicks", 0);
const first = task(2, "Order lamp oil", 2);
const second = task(3, "Paint the rail", 5);
const done = { ...first, done: true };
return [
["GET", "/api/tasks", [seed]],
["GET", "/api/tasks/1", seed],
[
"POST",
"/api/tasks",
unloaded(first, 2),
201,
{ title: "Order lamp oil" },
],
["GET", "/api/tasks/2", first],
["GET", "/api/tasks", [seed, first]],
[
"POST",
"/api/tasks",
unloaded(second, 5),
201,
{ title: "Paint the rail" },
],
["GET", "/api/tasks/3", second],
["GET", "/api/tasks", [seed, first, second]],
["PATCH", "/api/tasks/2", { ...done, assignees: [] }, 200, { done: true }],
["GET", "/api/tasks/2", done],
["GET", "/api/tasks", [seed, done, second]],
];
})();
/** A tag as the application stores it: a text color it derives from the color, which no rule rebuilds. */
const painted = (id: number, name: string, color: string, text: string) => ({
id,
name,
color,
text_color: text,
});
/** Two tag creates whose derived text colors differ, then deletes of both, each read back as gone. */
export const DERIVED_TEXT_ROWS: readonly TimedCaptureRow[] = (() => {
const seed = painted(1, "Inbox", "#2a9d8f", "#ffffff");
const light = painted(2, "Light", "#a8dadc", "#000000");
const dark = painted(3, "Dark", "#264653", "#ffffff");
const { id: _light, text_color: _lightText, ...lightSent } = light;
const { id: _dark, text_color: _darkText, ...darkSent } = dark;
return [
["GET", "/api/tags", [seed]],
["GET", "/api/tags/1", seed],
["POST", "/api/tags", light, 201, lightSent],
["GET", "/api/tags/2", light],
["GET", "/api/tags", [seed, light]],
["POST", "/api/tags", dark, 201, darkSent],
["GET", "/api/tags/3", dark],
["GET", "/api/tags", [seed, light, dark]],
["DELETE", "/api/tags/2", "", 204],
["GET", "/api/tags/2", { detail: "Not found." }, 404],
["DELETE", "/api/tags/3", "", 204],
["GET", "/api/tags/3", { detail: "Not found." }, 404],
["GET", "/api/tags", [seed]],
];
})();
const contact = (
id: number,
fields: {
readonly firstName: string | null;
readonly lastName: string;
readonly email: string | null;
},
): Entity => ({
id,
...fields,
emailOptedOut: fields.email === null ? null : false,
});
/** A contact form's full create, then one with only the last name, whose empty members store as sent: `null` where the first stored a string, and an opt-out flag `null` where it stored `false`. */
export const MINIMAL_CONTACT_ROWS: readonly TimedCaptureRow[] = (() => {
const seed = contact(1, {
firstName: "Ada",
lastName: "Finch",
email: "ada@example.test",
});
const full = contact(2, {
firstName: "Tomas",
lastName: "Vey",
email: "tomas@example.test",
});
const minimal = contact(3, {
firstName: null,
lastName: "Okafor",
email: null,
});
const sent = ({ id: _id, ...members }: Entity) => members;
return [
["GET", "/api/contacts", [seed]],
["GET", "/api/contacts/1", seed],
["POST", "/api/contacts", full, 201, sent(full)],
["GET", "/api/contacts/2", full],
["GET", "/api/contacts", [seed, full]],
["POST", "/api/contacts", minimal, 201, sent(minimal)],
["GET", "/api/contacts/3", minimal],
["GET", "/api/contacts", [seed, full, minimal]],
];
})();