check / check (push) Waiting to run
When a manifest could not be read, remote info skipped it, counted that snapshot's blobs as orphaned and advised running prune. The orphan figures are now unknown in that case, with no prune advice; --json gives them as null and lists the unreadable remote keys in unreadable_manifests. Only a listed manifest.json.zst is read, so a directory without one, as an interrupted backup leaves, keeps the figures known. Names under metadata/ were used unchecked and printed raw. A name that is not a remote key is now skipped with a warning. A manifest under it is not read either, so it also leaves the figures unknown; --json counts such manifests in skipped_manifest_count. Model: opus-5-5
160 lines
5.6 KiB
Go
160 lines
5.6 KiB
Go
package vaultik_test
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"sneak.berlin/go/vaultik/internal/log"
|
|
"sneak.berlin/go/vaultik/internal/snapshot"
|
|
)
|
|
|
|
// testBlobHashB is a blob that the manifest written by addRemote does
|
|
// not reference.
|
|
const testBlobHashB = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" +
|
|
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
|
|
|
|
// TestRemoteInfo_UnreadableManifestLeavesOrphansUnknown checks that a
|
|
// manifest remote info cannot read makes the orphan figures unknown. A
|
|
// blob referenced only by that snapshot would otherwise be counted as
|
|
// orphaned, and the report would advise running prune.
|
|
func TestRemoteInfo_UnreadableManifestLeavesOrphansUnknown(t *testing.T) {
|
|
log.Initialize(log.Config{})
|
|
t.Parallel()
|
|
|
|
env := newListEnv(t)
|
|
|
|
// The readable manifest references blob A only.
|
|
env.addRemote(t, listRemoteID, time.Date(2026, 3, 2, 0, 0, 0, 0, time.UTC))
|
|
addBlob(t, env.store.testStorer, testBlobHashA)
|
|
addBlob(t, env.store.testStorer, testBlobHashB)
|
|
|
|
// With every manifest readable, blob B is orphaned.
|
|
require.NoError(t, env.v.RemoteInfo(true))
|
|
|
|
var doc map[string]any
|
|
|
|
require.NoError(t, json.Unmarshal(env.stdout.Bytes(), &doc))
|
|
assert.InDelta(t, 1, doc["orphaned_blob_count"], 0)
|
|
|
|
// A second snapshot whose manifest cannot be decoded. Blob B may be
|
|
// one of its blobs.
|
|
unreadableKey := snapshot.RemoteSnapshotKey(listLocalID)
|
|
require.NoError(t, env.store.Put(context.Background(),
|
|
"metadata/"+unreadableKey+"/manifest.json.zst",
|
|
bytes.NewReader([]byte("not a valid manifest"))))
|
|
|
|
env.stdout.Reset()
|
|
require.NoError(t, env.v.RemoteInfo(false))
|
|
|
|
text := env.stdout.String()
|
|
assert.Contains(t, text, "Orphaned (unreferenced): unknown "+
|
|
"(1 manifest(s) could not be read, "+
|
|
"0 manifest(s) under a non-conforming name skipped)")
|
|
assert.NotContains(t, text, "vaultik prune")
|
|
|
|
env.stdout.Reset()
|
|
require.NoError(t, env.v.RemoteInfo(true))
|
|
|
|
doc = nil
|
|
require.NoError(t, json.Unmarshal(env.stdout.Bytes(), &doc))
|
|
assert.Contains(t, doc, "orphaned_blob_count")
|
|
assert.Nil(t, doc["orphaned_blob_count"])
|
|
assert.Contains(t, doc, "orphaned_blob_size")
|
|
assert.Nil(t, doc["orphaned_blob_size"])
|
|
assert.Equal(t, []any{unreadableKey}, doc["unreadable_manifests"])
|
|
}
|
|
|
|
// TestRemoteInfo_SkipsNonConformingMetadataName checks that a directory
|
|
// under metadata/ whose name is not a remote key is left out of the
|
|
// report, and that the orphan figures are unknown when it holds a
|
|
// manifest. The name comes from the destination store; printed raw, its
|
|
// control characters would reach the terminal. Its manifest is not
|
|
// read, so a blob only it references would otherwise be counted as
|
|
// orphaned.
|
|
func TestRemoteInfo_SkipsNonConformingMetadataName(t *testing.T) {
|
|
log.Initialize(log.Config{})
|
|
t.Parallel()
|
|
|
|
env := newListEnv(t)
|
|
env.addRemote(t, listRemoteID, time.Date(2026, 3, 2, 0, 0, 0, 0, time.UTC))
|
|
addBlob(t, env.store.testStorer, testBlobHashA)
|
|
addBlob(t, env.store.testStorer, testBlobHashB)
|
|
require.NoError(t, env.store.Put(context.Background(),
|
|
"metadata/\x1b[31mred/manifest.json.zst",
|
|
bytes.NewReader([]byte("not a valid manifest"))))
|
|
|
|
require.NoError(t, env.v.RemoteInfo(false))
|
|
|
|
text := env.stdout.String()
|
|
assert.NotContains(t, text, "\x1b")
|
|
assert.NotContains(t, text, "31mred")
|
|
assert.Contains(t, text, "Total (1 snapshots)")
|
|
assert.Contains(t, text, "Orphaned (unreferenced): unknown "+
|
|
"(0 manifest(s) could not be read, "+
|
|
"1 manifest(s) under a non-conforming name skipped)")
|
|
assert.NotContains(t, text, "vaultik prune")
|
|
|
|
env.stdout.Reset()
|
|
require.NoError(t, env.v.RemoteInfo(true))
|
|
|
|
out := env.stdout.String()
|
|
assert.NotContains(t, out, "31mred")
|
|
|
|
var doc map[string]any
|
|
|
|
require.NoError(t, json.Unmarshal([]byte(out), &doc))
|
|
assert.Contains(t, doc, "orphaned_blob_count")
|
|
assert.Nil(t, doc["orphaned_blob_count"])
|
|
assert.Contains(t, doc, "orphaned_blob_size")
|
|
assert.Nil(t, doc["orphaned_blob_size"])
|
|
assert.InDelta(t, 1, doc["skipped_manifest_count"], 0)
|
|
assert.NotContains(t, doc, "unreadable_manifests")
|
|
}
|
|
|
|
// TestRemoteInfo_DirectoryWithoutManifestLeavesOrphansKnown checks that
|
|
// a directory under metadata/ holding no manifest.json.zst, such as one
|
|
// left by a backup interrupted before its manifest upload, leaves the
|
|
// orphan figures known. prune does not treat such a directory as a
|
|
// snapshot and deletes the blobs the report lists as orphaned.
|
|
func TestRemoteInfo_DirectoryWithoutManifestLeavesOrphansKnown(t *testing.T) {
|
|
log.Initialize(log.Config{})
|
|
t.Parallel()
|
|
|
|
env := newListEnv(t)
|
|
env.addRemote(t, listRemoteID, time.Date(2026, 3, 2, 0, 0, 0, 0, time.UTC))
|
|
addBlob(t, env.store.testStorer, testBlobHashA)
|
|
addBlob(t, env.store.testStorer, testBlobHashB)
|
|
|
|
// One directory under a remote key and one under a non-conforming
|
|
// name, each holding only a database.
|
|
names := []string{snapshot.RemoteSnapshotKey(listLocalID), "\x1b[31mred"}
|
|
for _, name := range names {
|
|
require.NoError(t, env.store.Put(context.Background(),
|
|
"metadata/"+name+"/db.zst.age",
|
|
bytes.NewReader([]byte("not a valid database"))))
|
|
}
|
|
|
|
require.NoError(t, env.v.RemoteInfo(false))
|
|
|
|
text := env.stdout.String()
|
|
assert.NotContains(t, text, "\x1b")
|
|
assert.Contains(t, text, "Downloading 1 manifest(s)...")
|
|
assert.Contains(t, text, "Orphaned (unreferenced): 1 (")
|
|
assert.Contains(t, text, "Run 'vaultik prune' to remove orphaned blobs.")
|
|
|
|
env.stdout.Reset()
|
|
require.NoError(t, env.v.RemoteInfo(true))
|
|
|
|
var doc map[string]any
|
|
|
|
require.NoError(t, json.Unmarshal(env.stdout.Bytes(), &doc))
|
|
assert.InDelta(t, 1, doc["orphaned_blob_count"], 0)
|
|
assert.NotContains(t, doc, "unreadable_manifests")
|
|
assert.NotContains(t, doc, "skipped_manifest_count")
|
|
}
|