Bind the local index only after a backup writes the marker (closes #420) #445

Merged
clawbot merged 1 commits from issue-420-bind-after-first-write into next 2026-11-11 04:49:11 +01:00
Collaborator

Fixes #420: a first backup that fails before writing anything no longer binds the local index to the configured destination.

What changed

  • checkStorageBinding (new, in internal/vaultik/storage_bind.go) refuses a local index bound to a different destination and records nothing.
  • EnsureStorageBinding does the same check and records the binding when there is none.
  • markDestination, which only snapshot create calls, now runs checkStorageBinding, writes vaultik-destination.txt, then calls EnsureStorageBinding. A backup that fails on the marker write leaves the local index unbound. The steps live there rather than in CreateSnapshot, which is at the linter's complexity limit.
  • These commands also bound the local index before their first write. They now call checkStorageBinding only and never record a binding:
    • snapshot purge
    • prune, including its reconciliation of local snapshots
    • snapshot remove, single and --all
    • remote nuke

Why the other commands no longer bind

Only a backup puts data in the local index, and it records the binding before it does. An unbound local index therefore holds nothing from any destination, so there is nothing for these commands to protect by binding it.

For the README in #399

Its sentence "The first command that changes data saves the configured destination in the local index" should say the first backup does, once it has written to the destination.

Model: opus-5-5

Fixes https://git.eeqj.de/sneak/vaultik/issues/420: a first backup that fails before writing anything no longer binds the local index to the configured destination. **What changed** - `checkStorageBinding` (new, in `internal/vaultik/storage_bind.go`) refuses a local index bound to a different destination and records nothing. - `EnsureStorageBinding` does the same check and records the binding when there is none. - `markDestination`, which only `snapshot create` calls, now runs `checkStorageBinding`, writes `vaultik-destination.txt`, then calls `EnsureStorageBinding`. A backup that fails on the marker write leaves the local index unbound. The steps live there rather than in `CreateSnapshot`, which is at the linter's complexity limit. - These commands also bound the local index before their first write. They now call `checkStorageBinding` only and never record a binding: - `snapshot purge` - `prune`, including its reconciliation of local snapshots - `snapshot remove`, single and `--all` - `remote nuke` **Why the other commands no longer bind** Only a backup puts data in the local index, and it records the binding before it does. An unbound local index therefore holds nothing from any destination, so there is nothing for these commands to protect by binding it. **For the README in https://git.eeqj.de/sneak/vaultik/pulls/399** Its sentence "The first command that changes data saves the configured destination in the local index" should say the first backup does, once it has written to the destination. Model: opus-5-5
clawbot added the needs-review label 2026-11-11 01:47:54 +01:00
clawbot self-assigned this 2026-11-11 01:47:54 +01:00
Author
Collaborator

FAIL: needs-rework

  1. No test that a backup refuses a different binding before it writes anything. In internal/vaultik/destination_marker.go, markDestination now relies on its own checkStorageBinding call running before the marker Put. No test covers that order. Without it, a backup pointed at a new destination writes vaultik-destination.txt there before it is refused, and a stray marker on an empty mount point defeats the "not present" check for that path. Acceptable: a test in internal/vaultik/storage_bind_test.go that backs up to one file:// destination, points the config and v.Storage at a second one, and runs a backup. It asserts the "bound to a different backup destination" error and that the second destination has no vaultik-destination.txt.

  2. Only prune has a test that it leaves an unbound local index unbound. The PR changes the same thing in four more entry points, none of them tested:

    • snapshot purge (PurgeSnapshotsWithOptions)
    • snapshot remove (RemoveSnapshot)
    • snapshot remove --all (RemoveAllSnapshots)
    • remote nuke (NukeRemote)

    Acceptable: turn TestPruneLeavesIndexUnbound into a table over all five entry points, each run on a fresh local index. Each case asserts the local index is still unbound afterwards, whether or not the command returns an error.

  3. EnsureStorageBinding in internal/vaultik/storage_bind.go is harder to follow than it needs to be. It calls checkStorageBinding and then reads the binding a second time. It exits through combined guards, if err != nil || v.Repositories == nil || v.Config == nil { return err } and if err != nil || stored != "" { return err }, where a nil err means "nothing to do". In a backup, the mismatch is also checked twice, before and after the marker write. Acceptable: read the binding once with storageBinding and use plain branches: unbound, record it; bound elsewhere, refuse; same, return nil.

  4. The markDestination doc comment is wrong. It says the function "checks the backup destination store with checkStorageBinding and checkDestinationMarker". checkStorageBinding compares the local index's binding with the config and never touches the destination store. Acceptable: say that it refuses a local index bound elsewhere, checks the store for its marker, writes the marker, then records the binding. Give the reason (a failed first backup leaves the local index unbound) once, not in all three doc comments that now repeat it.

Model: opus-5-5

**FAIL: `needs-rework`** 1. **No test that a backup refuses a different binding before it writes anything.** In `internal/vaultik/destination_marker.go`, `markDestination` now relies on its own `checkStorageBinding` call running before the marker `Put`. No test covers that order. Without it, a backup pointed at a new destination writes `vaultik-destination.txt` there before it is refused, and a stray marker on an empty mount point defeats the "not present" check for that path. Acceptable: a test in `internal/vaultik/storage_bind_test.go` that backs up to one `file://` destination, points the config and `v.Storage` at a second one, and runs a backup. It asserts the "bound to a different backup destination" error and that the second destination has no `vaultik-destination.txt`. 2. **Only `prune` has a test that it leaves an unbound local index unbound.** The PR changes the same thing in four more entry points, none of them tested: - `snapshot purge` (`PurgeSnapshotsWithOptions`) - `snapshot remove` (`RemoveSnapshot`) - `snapshot remove --all` (`RemoveAllSnapshots`) - `remote nuke` (`NukeRemote`) Acceptable: turn `TestPruneLeavesIndexUnbound` into a table over all five entry points, each run on a fresh local index. Each case asserts the local index is still unbound afterwards, whether or not the command returns an error. 3. **`EnsureStorageBinding` in `internal/vaultik/storage_bind.go` is harder to follow than it needs to be.** It calls `checkStorageBinding` and then reads the binding a second time. It exits through combined guards, `if err != nil || v.Repositories == nil || v.Config == nil { return err }` and `if err != nil || stored != "" { return err }`, where a nil `err` means "nothing to do". In a backup, the mismatch is also checked twice, before and after the marker write. Acceptable: read the binding once with `storageBinding` and use plain branches: unbound, record it; bound elsewhere, refuse; same, return nil. 4. **The `markDestination` doc comment is wrong.** It says the function "checks the backup destination store with checkStorageBinding and checkDestinationMarker". `checkStorageBinding` compares the local index's binding with the config and never touches the destination store. Acceptable: say that it refuses a local index bound elsewhere, checks the store for its marker, writes the marker, then records the binding. Give the reason (a failed first backup leaves the local index unbound) once, not in all three doc comments that now repeat it. Model: opus-5-5
Author
Collaborator

For the reworker: fix the four findings of #445 (comment) as their "Acceptable" lines say, and change nothing else. Rebase onto current origin/next, run make fmt and make check, push with --force-with-lease, and reply here with one line per finding.

For the next reviewer: check those four findings against the rework, and anything the rework broke.

Model: opus-5-5

For the reworker: fix the four findings of https://git.eeqj.de/sneak/vaultik/pulls/445#issuecomment-176765 as their "Acceptable" lines say, and change nothing else. Rebase onto current `origin/next`, run `make fmt` and `make check`, push with `--force-with-lease`, and reply here with one line per finding. For the next reviewer: check those four findings against the rework, and anything the rework broke. Model: opus-5-5
clawbot added needs-rework and removed needs-review labels 2026-11-11 02:23:02 +01:00
clawbot force-pushed issue-420-bind-after-first-write from 14e4c85365 to 62719f4bd1 2026-11-11 02:30:19 +01:00 Compare
clawbot force-pushed issue-420-bind-after-first-write from 62719f4bd1 to 75eafe04b5 2026-11-11 02:46:57 +01:00 Compare
clawbot added 1 commit 2026-11-11 03:26:37 +01:00
A first backup that failed before writing anything (a mistyped bucket,
a refused credential) bound the local index to that destination, and
the corrected config was then refused.

A backup now checks for a different binding first, writes the
destination marker, and only then records the configured destination.
Purge, prune, snapshot remove and remote nuke still refuse a different
binding but no longer record one: only a backup puts data in the local
index, so an unbound local index holds nothing from any destination.

Model: opus-5-5
clawbot force-pushed issue-420-bind-after-first-write from 75eafe04b5 to 19896120ff 2026-11-11 03:26:37 +01:00 Compare
Author
Collaborator
  1. Added TestBackupToOtherDestinationWritesNoMarker to internal/vaultik/storage_bind_test.go.
  2. TestPruneLeavesIndexUnbound is now the table test TestCommandsOtherThanBackupLeaveIndexUnbound.
  3. EnsureStorageBinding reads the binding once, with the three branches listed. A backup still checks the binding before the marker write too, which finding 1 needs.
  4. Rewrote the markDestination doc comment; the reason is now given there only.

Judgement call: EnsureStorageBinding no longer returns early when Repositories or Config is nil. Its only caller, markDestination in a backup, always has both.

Model: opus-5-5

1. Added `TestBackupToOtherDestinationWritesNoMarker` to `internal/vaultik/storage_bind_test.go`. 2. `TestPruneLeavesIndexUnbound` is now the table test `TestCommandsOtherThanBackupLeaveIndexUnbound`. 3. `EnsureStorageBinding` reads the binding once, with the three branches listed. A backup still checks the binding before the marker write too, which finding 1 needs. 4. Rewrote the `markDestination` doc comment; the reason is now given there only. Judgement call: `EnsureStorageBinding` no longer returns early when `Repositories` or `Config` is nil. Its only caller, `markDestination` in a backup, always has both. Model: opus-5-5
clawbot added needs-review and removed needs-rework labels 2026-11-11 04:10:38 +01:00
Author
Collaborator

PASS: a backup now binds the local index only after it writes vaultik-destination.txt, the other commands only refuse a different binding, and the four findings of the earlier review are fixed.

Model: opus-5-5

**PASS**: a backup now binds the local index only after it writes `vaultik-destination.txt`, the other commands only refuse a different binding, and the four findings of the earlier review are fixed. Model: opus-5-5
clawbot merged commit c07db21b45 into next 2026-11-11 04:49:11 +01:00
clawbot deleted branch issue-420-bind-after-first-write 2026-11-11 04:49:11 +01:00
Sign in to join this conversation.