diff --git a/docs/specs/agent-environments/write-later-reads/spec.md b/docs/specs/agent-environments/write-later-reads/spec.md index a59fa26987..a64ef9e82e 100644 --- a/docs/specs/agent-environments/write-later-reads/spec.md +++ b/docs/specs/agent-environments/write-later-reads/spec.md @@ -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 diff --git a/docs/specs/agent-environments/write-later-reads/verification.md b/docs/specs/agent-environments/write-later-reads/verification.md index ccd1b21bc7..9872e0ee1d 100644 --- a/docs/specs/agent-environments/write-later-reads/verification.md +++ b/docs/specs/agent-environments/write-later-reads/verification.md @@ -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 diff --git a/src/application/environment/environmentStateBulkAnswer.ts b/src/application/environment/environmentStateBulkAnswer.ts index 09288283d2..1c1d4bc7b0 100644 --- a/src/application/environment/environmentStateBulkAnswer.ts +++ b/src/application/environment/environmentStateBulkAnswer.ts @@ -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", diff --git a/src/application/environment/environmentStateLaterReadsAnswer.test.ts b/src/application/environment/environmentStateLaterReadsAnswer.test.ts index 9721251889..b6c22ad8af 100644 --- a/src/application/environment/environmentStateLaterReadsAnswer.test.ts +++ b/src/application/environment/environmentStateLaterReadsAnswer.test.ts @@ -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 }]); + }); }); diff --git a/src/domain/analysisView/environmentStateWriteApply.ts b/src/domain/analysisView/environmentStateWriteApply.ts index 0a579f62d1..15dbe77856 100644 --- a/src/domain/analysisView/environmentStateWriteApply.ts +++ b/src/domain/analysisView/environmentStateWriteApply.ts @@ -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 = new Set(), ): Applied => { 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); diff --git a/tests/fixtures/environmentStateLaterReads.ts b/tests/fixtures/environmentStateLaterReads.ts index b7342a5af7..00e545a4e3 100644 --- a/tests/fixtures/environmentStateLaterReads.ts +++ b/tests/fixtures/environmentStateLaterReads.ts @@ -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 }], + ], + ]; +})();