fix(environment): answer bulk updates without fields only later reads show

A bulk update's answer renders each entity with the same allowance a single
update's answer has: a field only reads after the type's first write showed,
which no initial entity holds, is left out for an entity without it, so a
bulk update of a seeded task is no longer refused for lacking it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Edgar ArakelyanandClaude Opus 5.5 committed 2026-10-26 14:48:38 -07:00
1 parent 4515a000a6
commit d1bb6477f6
6 files changed
+66 -5

No files matched your search

@@ -24,8 +24,8 @@ each write back:
membership and for a list query's `limit` role.
- **Fields only later reads show.** An item view also shows the members that reads after its type's first write show
of the entities they name, when no initial entity holds them. They are never required. An update or delete that
answers with the entity leaves such a field out when the entity does not hold it, and the field no longer makes the
write `incomplete-entities`. A field some initial entity holds keeps the existing rule: an answer showing it is
answers with the entity, a bulk update's answer included, leaves such a field out when the entity does not hold it,
and the field no longer makes the write `incomplete-entities`. A field some initial entity holds keeps the existing rule: an answer showing it is
refused for an entity without it.
- **Skill.** Updates change the fields a person would change (a name, an email address, a status, a due date) rather
than only a description, since a served update accepts only the members a recorded update stored, and a deleted
@@ -21,8 +21,8 @@ Ubuntu 24.04 VMs for the task runs.
the list; only a `true` `deleted`, `isDeleted` or `is_deleted` marks an object deleted; a delete is confirmed by a
list read paged with `maxSize`; a field only created tasks show joins the task item view without being required; a
task update whose answers show it is served.
- Serving: a deleted contact leaves the served list; an update of a seeded task answers without the field only created
tasks show.
- Serving: a deleted contact leaves the served list; an update of a seeded task, single or bulk, answers without the
field only created tasks show (the bulk case fails on `4515a000`, before the review fix).
- The six behaviour tests fail on `053264bd`.
- The existing serving test that refuses to give chapter rows to a book the state holds without them still passes. A
first version of the answer rule left out every field the item view does not require, which served that update; the
@@ -4,6 +4,8 @@ import {
} from "../../domain/analysisView/environmentStateWriteAddress.js";
import {
bodyProblem,
laterItemFields,
optionalFields,
updatedEntity,
updatedFields,
writtenBulkResponse,
@@ -180,6 +182,12 @@ export const answerBulk = (
...entity,
sent: Object.keys(targets.value[index]!.body ?? {}),
})),
optionalFields(plan),
laterItemFields(
serving.store.state.views.values(),
serving.store.state.entities,
plan.target,
),
),
{
source: "state",
@@ -1,6 +1,7 @@
import { describe, expect, it } from "vitest";
import {
LATER_BULK_ROWS,
LATER_FIELD_ROWS,
SOFT_DELETED_ROWS,
} from "../../../tests/fixtures/environmentStateLaterReads.js";
@@ -37,4 +38,12 @@ describe("served writes modelled on later reads", () => {
}).body;
expect(body).toEqual({ id: 1, title: "Trim the wicks", done: true });
});
it("answers a bulk update of an initial entity without the field only created entities show", () => {
const tasks = servedFrom(LATER_BULK_ROWS);
const body = tasks.answer("patch_tasks", {
body: [{ id: 1, done: true }],
}).body;
expect(body).toEqual([{ id: 1, title: "Trim the wicks", done: true }]);
});
});
@@ -616,7 +616,7 @@ export const writtenResponse = (
);
};
/** The shared bulk renderer: fixed acknowledgements, or one first-template element per written entity. */
/** The shared bulk renderer: fixed acknowledgements, or one first-template element per written entity, each with the single answer's optional and later fields. */
export const writtenBulkResponse = (
response: WriteResponse,
recorded: JsonValue | undefined,
@@ -625,6 +625,8 @@ export const writtenBulkResponse = (
readonly id: string;
readonly sent?: readonly string[];
}[],
optional: readonly string[] = [],
later: ReadonlySet<string> = new Set(),
): Applied<JsonValue | undefined> => {
if (response.shape === "empty") return ok(undefined);
if (response.shape === "recorded") return ok(recorded);
@@ -637,6 +639,8 @@ export const writtenBulkResponse = (
{ ...response, pointer: "" },
template,
entity,
optional,
later,
);
if (!element.ok) return element;
elements.push(element.value ?? null);
+40
View File
@@ -86,3 +86,43 @@ export const LATER_FIELD_ROWS: readonly TimedCaptureRow[] = (() => {
],
];
})();
/** The same seeded and created tasks, then one bulk update marking both created tasks done, whose answer shows their subscriptions. */
export const LATER_BULK_ROWS: readonly TimedCaptureRow[] = (() => {
const seed = task(1, "Trim the wicks");
const [first, second] = [
task(2, "Order lamp oil"),
task(3, "Paint the rail"),
];
const created = (entity: Entity) => ({
...entity,
...subscribed(Number(entity["id"])),
});
const done = (entity: Entity) => ({ ...created(entity), done: true });
return [
["GET", "/api/tasks", [seed]],
["GET", "/api/tasks/1", seed],
["POST", "/api/tasks", first, 201, { title: "Order lamp oil" }],
["GET", "/api/tasks/2", created(first)],
["POST", "/api/tasks", second, 201, { title: "Paint the rail" }],
["GET", "/api/tasks/3", created(second)],
["GET", "/api/tasks", [seed, first, second]],
[
"PATCH",
"/api/tasks",
[done(first), done(second)],
200,
[
{ id: 2, done: true },
{ id: 3, done: true },
],
],
["GET", "/api/tasks/2", done(first)],
["GET", "/api/tasks/3", done(second)],
[
"GET",
"/api/tasks",
[seed, { ...first, done: true }, { ...second, done: true }],
],
];
})();