From 3ba2fe3b8278e14969af3d677ea88e126fceb3e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 10 Sep 2026 01:26:24 +0200 Subject: [PATCH] fix(ops): prove the journey's reading is gone, not just accepted Low, review 2. The delete leg asserted HTTP 200 and printed "account left clean". An instance that answers 200 and keeps the row would have printed the same line, which is the failure mode the journey exists to catch. The leg now reads the id back and requires a 404. The self-test carries a mock that answers 200 without deleting; with the read-back removed that case goes green, which is the proof the check can fail. --- scripts/synthetic-journey.mjs | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/scripts/synthetic-journey.mjs b/scripts/synthetic-journey.mjs index dd28e5e65..d5ddfb732 100644 --- a/scripts/synthetic-journey.mjs +++ b/scripts/synthetic-journey.mjs @@ -24,7 +24,9 @@ * 3. write — `POST /api/measurements` with an `Idempotency-Key`. * 4. read — `GET /api/measurements?type=WEIGHT&limit=1` and assert * the row's id, value, unit and source. - * 5. delete — `DELETE /api/measurements/{id}` so the account stays clean. + * 5. delete — `DELETE /api/measurements/{id}`, then a direct `GET` on + * the same id that must 404, so the account stays clean and + * the leg proves it rather than trusting a 200. * * Legs 3 to 5 run inside a `try`/`finally`: from the write on, the run owns * a row on a real account, and a red read leg must not leave it behind. The @@ -340,6 +342,16 @@ async function removeReading(baseUrl, jar, writtenId) { jar, }); expectStatus(result, 200, "delete"); + // A 200 is the instance's claim, not the outcome. An instance that answers + // 200 and keeps the row would have printed "account left clean" — so read + // the id back and require the 404. + const gone = await call(baseUrl, `/api/measurements/${writtenId}`, { jar }); + if (gone.status !== 404) { + throw new LegFailure( + `delete: ${writtenId} still reads back after a 200 delete`, + { status: gone.status, body: gone.text }, + ); + } return { detail: "account left clean" }; } @@ -592,10 +604,26 @@ function startMockInstance(breaks) { if (breaks === "delete") { return send(404, { data: null, error: "Measurement not found" }); } + // 200 and the row still there — the status alone cannot catch this, + // only the read-back the delete leg does afterwards. + if (breaks === "delete: kept") { + return send(200, { data: { success: true }, error: null }); + } rows.delete(url.pathname.split("/").pop()); return send(200, { data: { success: true }, error: null }); } + if ( + url.pathname.startsWith("/api/measurements/") && + request.method === "GET" + ) { + const row = rows.get(url.pathname.split("/").pop()); + if (!row) { + return send(404, { data: null, error: "Measurement not found" }); + } + return send(200, { data: row, error: null }); + } + send(404, { data: null, error: "Not found" }); }); @@ -682,6 +710,7 @@ async function selfTest() { clean: true, }, { breaks: "delete", expect: "fail", leg: "delete" }, + { breaks: "delete: kept", expect: "fail", leg: "delete" }, ]; let failures = selfTestHostAllowlist();