From f2b95351d427177cf3f9339ca12cd678a3e450df Mon Sep 17 00:00:00 2001 From: Edgar Arakelyan Date: Tue, 20 Oct 2026 16:35:52 -0700 Subject: [PATCH] fix(environment): answer a missing entity with a 2xx null only for reads A served write of a missing entity also looked at the write tool's own recordings, so a delete that recorded 200 null for a successful deletion answered a missing target with success. A 2xx null is now an absence only when the call is a read, as the gate takes it from item reads; a write still answers with a recorded 400, 404 or 410, or is refused. Co-Authored-By: Claude Opus 5.5 --- .../environment/environmentStateAnswer.ts | 7 +++++-- .../environmentStateWriteAnswer.test.ts | 16 ++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/application/environment/environmentStateAnswer.ts b/src/application/environment/environmentStateAnswer.ts index b99379f58c..4e44dc58e5 100644 --- a/src/application/environment/environmentStateAnswer.ts +++ b/src/application/environment/environmentStateAnswer.ts @@ -264,7 +264,9 @@ export const missingAnswer = ( readonly id: string; }, ): Result => { - // The write gate takes a 2xx JSON null as an absence too, so a delete it confirmed that way can be answered. + // A read also answers with a 2xx JSON null, the absence the write gate accepts; a write never does, as a 2xx would + // report success for an entity that is not there. + const nullAbsence = tool.method === "GET"; const latestAbsence = (ordinals: readonly number[]) => ordinals .map((ordinal) => serving.source.exchanges.get(ordinal)) @@ -272,7 +274,8 @@ export const missingAnswer = ( (exchange): exchange is RecordedExchange => exchange !== undefined && (isAbsence(exchange.status) || - (exchange.status !== null && + (nullAbsence && + exchange.status !== null && exchange.status >= 200 && exchange.status < 300 && recordedJson(exchange) === null)), diff --git a/src/application/environment/environmentStateWriteAnswer.test.ts b/src/application/environment/environmentStateWriteAnswer.test.ts index b74b86402a..678f764060 100644 --- a/src/application/environment/environmentStateWriteAnswer.test.ts +++ b/src/application/environment/environmentStateWriteAnswer.test.ts @@ -472,4 +472,20 @@ describe("missing entities after a delete", () => { state: { view: "missing", id: "1" }, }); }); + + it("never answers a write of a missing entity with success, even when the write recorded a 2xx null", () => { + const books = servedFrom([ + ...BOOK_INIT_ROWS, + ["DELETE", "/api/books/2", null, 200], + ["GET", "/api/books/2", BOOK_NOT_FOUND, 404], + ]); + expect(books.state.writes.get("delete_books_by_id")).toMatchObject({ + served: true, + }); + expect(books.answer("delete_books_by_id", { id: "9" })).toMatchObject({ + status: 404, + body: BOOK_NOT_FOUND, + state: { view: "missing", id: "9" }, + }); + }); });