Files
codebase-memory-mcp/tests
Martin Vogel c577c79dee fix(pipeline): quarantine the real destination and report why a publish stopped
main and this branch each grew their own atomic publication while the branch was
outstanding, and neither knew about the other:

  main    cbm_pipeline_run() copies the live database into an mkstemp staging
          file, points the whole run at that copy, and renames it over the
          destination at the end. That is what stops an INCREMENTAL run from
          mutating the live database in place.
  branch  cbm_pipeline_publish_generation() builds a generation in its own stage
          file, validates it (integrity check, FTS rebuild, seal), quarantines a
          corrupt destination to a fresh .corrupt name, then renames.

The rebase kept both, nested, so the branch's layer ran against main's staging
file rather than the real database. Two consequences, both silent:

1. The quarantine became a no-op. Its "existing destination" was a staging file
   we had just created, so a genuinely corrupt destination reached
   cbm_rename_replace and was overwritten -- the one copy of the bytes that
   would explain the corruption, destroyed by the recovery path.

2. Every publish failure collapsed to CBM_NOT_FOUND, because the outer wrapper
   returned a bare -1 for its own errors, a cancellation, and a failed persist
   alike. A caller could not tell "aborted, your data is intact" from "the
   persist failed", which is the only distinction that matters at that moment.

Rather than pick one implementation, put each concern at the layer that owns it.
The outer wrapper owns the real destination's lifecycle, so it now owns the
quarantine: prepare_publish_destination() calls the branch's existing
prepare_existing_generation_for_replace() when the destination could not be
copied, which moves it aside only when it is verifiably not a readable SQLite
database. A destination that is valid (the backup failed for some other reason)
is sealed and replaced as before, never renamed away, so a good database is
never mislabelled .corrupt. main's guard that refuses to drop sidecars holding
uncommitted pages is kept ahead of it. A failed rename rolls the quarantine
back, so a caller is never left with no database at all.

The wrapper also stops flattening. Everything it does happens before the
publishing rename, so an abort there is genuinely non-destructive and can say
so: cancellation reports CBM_PIPELINE_ABORT_PRESERVE_DB, a failed seal reports
CBM_PIPELINE_PERSIST_FAILED, and a status from the inner publish propagates
unchanged. Both MCP call sites test only `rc == 0` and are unaffected; the codes
stay in pipeline_internal.h beside the stages that raise them, and pipeline.h no
longer claims a -1 it does not return.

Quarantining is now also correctly refused at the inner layer, which was the
same confusion in the other direction. publish_generation's destination is the
staging file the wrapper created moments earlier, so when that file was not a
readable database it was being parked as
`<db>.stage.<random>.corrupt` -- debris named after a temp file, which nothing
collects and no one can interpret. It only surfaced once the status codes stopped
collapsing, because the assertions that count leftover stage artifacts sat behind
the return-code assertions that failed first. The inner caller now discards an
unreadable staging file instead, and only the wrapper, which owns the user's real
database, ever quarantines.

Two of main's own tests asserted the bare -1. Both are named
cancelled_*_reindex_preserves_committed_db and now assert
CBM_PIPELINE_ABORT_PRESERVE_DB -- the value whose name is their subject. That
tightens the assertion rather than relaxing it: -2 answers their question and
-1 did not.

Closes the last 10 pipeline failures from the rebase.
pipeline, incremental, store_nodes, mcp: 662 passed, 2 skipped.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-01 11:53:05 +02:00
..
2026-06-28 13:05:05 +02:00