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") }