test(webapp): a quantity and a removal may go in two saves (#1221) #1290

Merged
claude-bot merged 1 commit from fix/1221-sheet-removal-save into main 2026-10-30 20:14:22 +00:00
Collaborator

Closes #1221.

It was the test, not the sheet. Autosave is debounced 800 ms (DEBOUNCE, editState.js). Under CI load, userEvent.click on Remove can take longer than that, so the quantity goes in save 1 and the removal in save 2. The test read only mock.calls[0], found no removal, and failed.

No data is lost: useSheetEditor.save() snapshots the draft as sent. saveOk then applies without(draft, sent), whose keep() keeps every key that wasn't in that save or has changed since. So a removal made during or after a save stays in draft.entries (null), and the debounce saves it next, on the new version. Verified by reading the code, not just the agent's report.

Changes (test only):

  • The existing test collects every patchSheet call and requires the union of updates and removals, plus the expected_version chain.
  • New: "sends a removal made after the quantity's save has already gone out". It forces the split with no sleeps and asserts save 2 carries remove: ["inv-rope"] on version + 1.

Checked: both tests 30 times in separate runs, all passed. src/pages/sheet: 254 tests passed. eslint is clean. No changelog fragment, since nothing users see changed.

🤖 Generated with Claude Code

Closes #1221. **It was the test, not the sheet.** Autosave is debounced 800 ms (`DEBOUNCE`, `editState.js`). Under CI load, `userEvent.click` on Remove can take longer than that, so the quantity goes in save 1 and the removal in save 2. The test read only `mock.calls[0]`, found no removal, and failed. **No data is lost:** `useSheetEditor.save()` snapshots the draft as `sent`. `saveOk` then applies `without(draft, sent)`, whose `keep()` keeps every key that wasn't in that save or has changed since. So a removal made during or after a save stays in `draft.entries` (`null`), and the debounce saves it next, on the new version. Verified by reading the code, not just the agent's report. **Changes (test only):** - The existing test collects every `patchSheet` call and requires the union of updates and removals, plus the `expected_version` chain. - New: "sends a removal made after the quantity's save has already gone out". It forces the split with no sleeps and asserts save 2 carries `remove: ["inv-rope"]` on `version + 1`. **Checked:** both tests 30 times in separate runs, all passed. `src/pages/sheet`: 254 tests passed. eslint is clean. No changelog fragment, since nothing users see changed. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
test(webapp): a quantity and a removal may go in two saves (#1221)
All checks were successful
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 57s
CI / Does this change touch the recording path? (pull_request) Successful in 13s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Has been skipped
CI / Which journeys does this change need? (pull_request) Successful in 14s
CI / Bot/backend version sync (pull_request) Successful in 1m5s
CI / Backend lint (ruff) (pull_request) Successful in 1m18s
CI / Bot tests and audit (pull_request) Successful in 2m7s
CI / Feedback site tests (pull_request) Successful in 2m1s
CI / Docker image build (pull_request) Successful in 8m13s
CI / Frontend (shard 2/2) (pull_request) Successful in 9m51s
CI / Frontend (shard 1/2) (pull_request) Successful in 11m54s
CI / End-to-end journeys (desktop) (pull_request) Successful in 15m14s
CI / Backend migration, tests, and audit (pull_request) Successful in 42m12s
fd1d3d4e48
The debounced save can fire between the two edits under load; the test
read only the first request. It now collects every request, and a second
test pins the split case deterministically.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude-bot scheduled this pull request to auto merge when all checks succeed 2026-10-30 19:32:05 +00:00
claude-bot deleted branch fix/1221-sheet-removal-save 2026-10-30 20:14:24 +00:00
Sign in to join this conversation.
No description provided.