R-361: the safety dump destroyed the app's own database backup
gates / gates (push) Successful in 11s

writeSafetyDump called DumpOne into the app's OWN unit dir and renamed the result
to pre-restore-* afterwards. DumpOne writes <stack>-<dbtype>.sql - the app's
canonical dump - so every safety dump overwrote the app's real backup and then
moved it away, leaving the app with no database backup until the next nightly
run. A local restore-from-unit in that window tells the customer the app never
had a database.

The comment beside it asserted the rename meant it 'can never overwrite the app's
real dump'. False as written, and believed for four months. Measured live before
the fix: docmost and bookstack each held only pre-restore-* files and no
canonical dump.

DumpOneTo takes the final path and derives its own .tmp from it. DumpOne keeps
its signature and calls it with the canonical name. writeSafetyDump asks for its
own name directly; the rename is gone; the comment now states the invariant and
how it is enforced.

db_dumps no longer lists the undo copies. All three consumers of Manifest.DBDumps
were grepped and named - all inside recovery_unit.go, none reads it for recovery.
The files are neither deleted nor hidden.

Tests 1485 -> 1493. FIVE red-proofs, TWO PASSED first time and both are reported:
the behavioural tests inject the dump seam so a mutation inside DumpOneTo was
invisible, and 1.3 had no test at all. Guards added at the layer each defect
lives in; both mutations then convicted.
This commit is contained in:
2026-08-22 23:39:59 +02:00
parent 2024ed9982
commit 968c968559
11 changed files with 524 additions and 40 deletions
+44
View File
@@ -1,3 +1,47 @@
## v0.221.0 — taking the undo copy destroyed the app's own database backup (2026-08-22, R-361)
**MinAgent: 0.129.0** (unchanged — no new agent coupling)
**A comment asserted an invariant the code did not have, and the comment was believed for four
months.** `writeSafetyDump` called `DumpOne` into the app's OWN unit directory and renamed the result
to `pre-restore-*` afterwards. `DumpOne` writes `<stack>-<dbtype>.sql` — **the app's canonical dump,
the name the replay loop matches exactly** — so every safety dump **overwrote the app's real backup
and then moved it away**. The comment beside it said the rename meant it *"can never overwrite the
app's real dump"*. It was false as written.
**The consequence, not the mechanism:** between a restore and the next nightly run the app had **no
database backup of its own**. A local restore-from-unit in that window finds no `.sql` and tells the
customer the app never had a database.
**Measured live before the fix, on `demo-hp` 2026-08-22:** `docmost` and `bookstack` each held only
`pre-restore-*` files in their unit and **no canonical dump at all**.
**The fix is a destination.** `DumpOneTo` takes the final path and derives its own `.tmp` from it;
`DumpOne` keeps its signature and calls it with the canonical name, so its other callers do not move.
`writeSafetyDump` now asks for `pre-restore-<stamp>-…` **directly** and the rename is gone. The
comment states the invariant and how it is enforced.
**The scratch file matters too:** `.tmp` is derived from the FINAL path, so a nightly dump and a
safety dump running into the same directory cannot share it.
**The manifest no longer lists the undo copies** (`db_dumps`). They are local material for a restore
that went wrong, not part of the app's recovery set. **Every consumer of `Manifest.DBDumps` was
grepped and named: there are three, all inside `recovery_unit.go`** — the declaration, this
enumeration, and the change-detection compare. Nothing reads it for recovery; no hub or agent
consumer exists. Excluding them also makes that compare stable, since the copies come and go with
every restore and prune. **The files are neither deleted nor hidden** — their visibility is a
recorded design decision and it stands.
**Tests:** `internal/backup/r361_canonical_dump_test.go`, additions to
`internal/appbackup/r381_undo_naming_test.go`, `cmd/controller/r361_classifier_control_test.go`.
Test count 1485 → 1493.
**Red-proofs: five planted, and TWO PASSED FIRST TIME — both reported.** The behavioural tests inject
the dump seam, so a mutation *inside* `DumpOneTo` was invisible to them; and Part 1.3 initially had no
test at all. Guards were added at the layer each defect lives in and both mutations then convicted.
**The assertion that convicts R-361 is the canonical dump's bytes, unchanged, across a restore** — a
test asserting merely that the undo copy exists passes just as well when the app's backup was
destroyed.
## v0.220.2 — the operator route to clear a hold now says the restart is required (2026-08-22, R-379)
**MinAgent: 0.129.0** (unchanged)