check / check (push) Successful in 2m16s
Closes #110. CleanupLocalSnapshots wrote three prose lines to stdout with no --json awareness, covering every branch, so `vaultik prune --json | jq` failed on any input. -q never helped either: printlnStdout and stdoutf write straight to v.Stdout and never consult v.UI, which is what SetQuiet affects. It now takes *PruneOptions, symmetric with its sibling phase PruneBlobs, and gates all three writes. Threading opts.JSON was chosen over moving the lines to log.Info, because internal/log/log.go defaults the level to Warn: log.Info would not have relocated them to stderr, it would have deleted them from a plain `vaultik prune`, and "Removing stale local record" narrates the deletion of local index rows. The stale-record count is deliberately not added to PruneBlobsResult - every field there is blob-scoped and produced by the phase that runs after this reconciliation, so adding it would change a published --json schema as a side effect of a stream fix. Note for anyone reading the --json contract: under --json the stale-record removal now produces no signal in either stream. stdout is correctly gated, stderr is level-pinned to Warn because --json sets Quiet, and the count is not in the document. That is inherited behaviour - PruneBlobs' own log.Info calls are equally invisible under --json - not something this change introduced, and it is tracked separately. make build exited 0 and produced nothing: .PHONY listed build with no build: rule, and a phony target with no prerequisites and no recipe is considered already satisfied, which turns what would be a hard error into a silent success. In a repo where `make build` is the documented way to build, a caller checking the exit code concluded the build worked. Now `build: vaultik`, verified in both directions - a clean build produces the binary, a deliberately broken one exits non-zero and produces none. All 19 .PHONY names were audited; build was the only one lacking a rule. TestPhonyTargetsAllHaveRules keeps that true for names added later, so the class is closed rather than the instance.
135 lines
4.4 KiB
Go
135 lines
4.4 KiB
Go
package vaultik_test
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"sneak.berlin/go/vaultik/internal/log"
|
|
"sneak.berlin/go/vaultik/internal/vaultik"
|
|
)
|
|
|
|
// cleanupStaleID is a local snapshot record with no remote manifest —
|
|
// the record CleanupLocalSnapshots exists to remove.
|
|
const cleanupStaleID = "testhost_home_2026-04-01T09:00:00Z"
|
|
|
|
// remainingSnapshotLimit bounds the post-cleanup listing. ListRecent
|
|
// takes a SQL LIMIT, so it must be positive; the fixtures never exceed
|
|
// a handful of rows.
|
|
const remainingSnapshotLimit = 100
|
|
|
|
// cleanupStart is the fixture snapshot's start time. Its exact value is
|
|
// irrelevant; only presence in the index matters here.
|
|
//
|
|
//nolint:gochecknoglobals // read-only fixture shared by the tests below
|
|
var cleanupStart = time.Date(2026, 4, 1, 9, 0, 0, 0, time.UTC)
|
|
|
|
// TestCleanupLocalSnapshots_JSONWritesNothingToStdout is the regression
|
|
// guard for issue #108: `vaultik prune --json | jq` failed because this
|
|
// function wrote prose to stdout on every branch, ahead of the
|
|
// PruneBlobsResult document, with no --json awareness at all.
|
|
//
|
|
// Both branches are covered because the three writes between them left
|
|
// no input that avoided the contamination: with stale records there was
|
|
// a line per record plus a summary, and with none there was still the
|
|
// "No stale local snapshots found." line.
|
|
func TestCleanupLocalSnapshots_JSONWritesNothingToStdout(t *testing.T) {
|
|
log.Initialize(log.Config{})
|
|
t.Parallel()
|
|
|
|
for name, seed := range map[string]func(*listEnv){
|
|
"no stale records": func(env *listEnv) {
|
|
// A snapshot present both locally and remotely: nothing to
|
|
// remove, which used to print the "No stale" line.
|
|
env.addLocal(t, listLocalID, cleanupStart)
|
|
env.addRemote(t, listLocalID, cleanupStart)
|
|
},
|
|
"stale records present": func(env *listEnv) {
|
|
env.addLocal(t, cleanupStaleID, cleanupStart)
|
|
},
|
|
"nothing at all": func(_ *listEnv) {},
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
env := newListEnv(t)
|
|
seed(env)
|
|
|
|
err := env.v.CleanupLocalSnapshots(&vaultik.PruneOptions{JSON: true})
|
|
require.NoError(t, err)
|
|
|
|
assert.Empty(t, env.stdout.String(),
|
|
"stdout carries the --json document and nothing else")
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestCleanupLocalSnapshots_HumanOutputRetained pins the other half of
|
|
// the contract. Without it the test above would be satisfied by
|
|
// deleting the three lines outright, and a `vaultik prune` with no
|
|
// flags must still say that it removed records from the local index —
|
|
// that is the deletion of local state, not decoration.
|
|
func TestCleanupLocalSnapshots_HumanOutputRetained(t *testing.T) {
|
|
log.Initialize(log.Config{})
|
|
t.Parallel()
|
|
|
|
t.Run("stale records present", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
env := newListEnv(t)
|
|
env.addLocal(t, cleanupStaleID, cleanupStart)
|
|
|
|
err := env.v.CleanupLocalSnapshots(&vaultik.PruneOptions{})
|
|
require.NoError(t, err)
|
|
|
|
out := env.stdout.String()
|
|
|
|
assert.Contains(t, out, "Removing stale local record: "+cleanupStaleID)
|
|
assert.Contains(t, out, "Removed 1 stale local snapshot record(s).")
|
|
})
|
|
|
|
t.Run("no stale records", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
env := newListEnv(t)
|
|
env.addLocal(t, listLocalID, cleanupStart)
|
|
env.addRemote(t, listLocalID, cleanupStart)
|
|
|
|
err := env.v.CleanupLocalSnapshots(&vaultik.PruneOptions{})
|
|
require.NoError(t, err)
|
|
|
|
assert.Contains(t, env.stdout.String(),
|
|
"No stale local snapshots found.")
|
|
})
|
|
}
|
|
|
|
// TestCleanupLocalSnapshots_RemovesOnlyStaleRecords checks that the
|
|
// --json gate did not change what the function does, only what it
|
|
// says: the stale record is gone from the index and the one with a
|
|
// remote manifest is untouched.
|
|
func TestCleanupLocalSnapshots_RemovesOnlyStaleRecords(t *testing.T) {
|
|
log.Initialize(log.Config{})
|
|
t.Parallel()
|
|
|
|
env := newListEnv(t)
|
|
env.addLocal(t, listLocalID, cleanupStart)
|
|
env.addRemote(t, listLocalID, cleanupStart)
|
|
env.addLocal(t, cleanupStaleID, cleanupStart)
|
|
|
|
err := env.v.CleanupLocalSnapshots(&vaultik.PruneOptions{JSON: true})
|
|
require.NoError(t, err)
|
|
|
|
remaining, err := env.v.Repositories.Snapshots.ListRecent(
|
|
env.v.Context(), remainingSnapshotLimit)
|
|
require.NoError(t, err)
|
|
|
|
ids := make([]string, 0, len(remaining))
|
|
for _, snap := range remaining {
|
|
ids = append(ids, snap.ID.String())
|
|
}
|
|
|
|
assert.Equal(t, []string{listLocalID}, ids,
|
|
"only the record with no remote manifest may be removed")
|
|
}
|