Public Access
Add a host on a laptop with no network, change your mind, delete it: the outbox holds a tombstone for a row the server has never heard of, the push answers Invalid, the change is parked, and the user is left looking at a rejected change for an item they already deleted and a pending count that will never reach zero. It applies to all four item types, because they all go through the one generic repository — the known-host path is only the likeliest way to meet it, since trust is pinned by connecting and withdrawn from the host editor. DeleteAsync now drops the queued create instead, when the server cannot be holding the item. A null expected version means the row is a create — including a create that has since been edited, because coalescing keeps the original expected version — so there is no server row and no mirror row, and dropping the queued change makes the item genuinely gone. The attempt count is what makes that safe rather than merely convenient. Nothing sent cannot have landed. A parked row cannot have landed either, because parking is what the pusher does when the server has refused, so the refusal is the evidence — and a parked create that the user then deletes could not be got rid of at all before this: the tombstone replacing it was parked in its turn. What is left is a create that went out and whose answer was never seen. That one still gets a tombstone, because the server may be holding the item and a local drop would strand it there for ever. A refused tombstone is recoverable; an orphan nobody can see and nobody can delete is not. Eight tests, and the interesting half is the other direction. A repository that quietly dropped tombstones would pass a suite written only around the bug and would lose data on every machine but the one that pressed the button. Which is not hypothetical, because the mutation pass found exactly that hole in the first draft of these tests. Removing the expected-version guard left every test passing: after a sync there is no queued row at all, so deleting a synced item never reaches the shortcut and proves nothing about it. The way to hold an unpushed Upsert over an item the server holds is to edit it offline, and EditingASyncedItemOfflineAndThenDeletingIt_StillQueuesATombstone is the test that was missing. Without the guard it deletes the item here, leaves it on the server, and the next pull brings it back. Three mutations, all caught now: removing the shortcut (5 tests), removing the expected-version guard (1), removing the attempt-count guard (1). The Upsert check itself is conservative rather than load-bearing — a queued Delete with no expected version is not reachable from the interface, and completing one locally would discard a tombstone that might be needed, so it stays and is not independently covered. 106 tests green in Client.Sync, 8 of them new. Zero warnings, format clean.