From 4418dc3e97b6af1b7303fb0a3bee7cdca87a049e Mon Sep 17 00:00:00 2001 From: clawbot <35+clawbot@noreply.example.org> Date: Tue, 10 Nov 2026 22:12:46 +0100 Subject: [PATCH] Print remote info and verify status lines through internal/ui (closes #410) The status and progress lines of remote info and snapshot verify (shallow and deep) were written with stdoutf, so they had no marker and no color, unlike every other command's. They now go through Beginf, Completef and Progressf, as the README's output style section says. The reports stay plain, and --quiet and --json still drop these lines. Model: opus-5-5 --- TODO.md | 6 ++++ internal/vaultik/info.go | 6 ++-- internal/vaultik/remote_info_test.go | 22 ++++++++++++ internal/vaultik/snapshot.go | 2 +- internal/vaultik/verify.go | 20 +++++------ internal/vaultik/verify_quiet_test.go | 48 +++++++++++++++++++++++++++ 6 files changed, 90 insertions(+), 14 deletions(-) diff --git a/TODO.md b/TODO.md index 64f0e47..69d31aa 100644 --- a/TODO.md +++ b/TODO.md @@ -24,6 +24,12 @@ owner's; a `v*` tag runs the release build in # Completed Steps +- 2026-11-10: Printed the status and progress lines of `remote info` + and `snapshot verify` through `internal/ui`, with its marker and + color, as the README describes + ([issue #410](https://git.eeqj.de/sneak/vaultik/issues/410)). Their + reports stay plain. + - 2026-11-10: Made `config init` write the `apps` snapshot, which backs up `/Applications`, only on macOS ([issue #408](https://git.eeqj.de/sneak/vaultik/issues/408)). On diff --git a/internal/vaultik/info.go b/internal/vaultik/info.go index 9cd034f..c4cc18c 100644 --- a/internal/vaultik/info.go +++ b/internal/vaultik/info.go @@ -246,7 +246,7 @@ func (v *Vaultik) RemoteInfo(jsonOutput bool) error { v.stdoutf("Type: %s\n", storageInfo.Type) v.stdoutf("Location: %s\n", storageInfo.Location) v.printlnStdout() - v.stdoutf("Scanning snapshot metadata...\n") + v.UI.Beginf("Scanning snapshot metadata...") } snapshotMetadata, snapshotIDs, skippedManifestCount, err := v.collectSnapshotMetadata() @@ -265,7 +265,7 @@ func (v *Vaultik) RemoteInfo(jsonOutput bool) error { } } - v.stdoutf("Downloading %d manifest(s)...\n", manifestCount) + v.UI.Beginf("Downloading %d manifest(s)...", manifestCount) } referencedBlobs, unreadableManifests := v.collectReferencedBlobsFromManifests( @@ -447,7 +447,7 @@ func (v *Vaultik) scanRemoteBlobStorage( result *RemoteInfoResult, referencedBlobs map[string]int64, showText bool, ) error { if showText { - v.stdoutf("Scanning blobs...\n") + v.UI.Beginf("Scanning blobs...") } // Use prune's listing, so a file under blobs/ that prune would never diff --git a/internal/vaultik/remote_info_test.go b/internal/vaultik/remote_info_test.go index 76bac72..5d68a03 100644 --- a/internal/vaultik/remote_info_test.go +++ b/internal/vaultik/remote_info_test.go @@ -12,6 +12,7 @@ import ( "github.com/stretchr/testify/require" "sneak.berlin/go/vaultik/internal/log" "sneak.berlin/go/vaultik/internal/snapshot" + "sneak.berlin/go/vaultik/internal/ui" ) // testBlobHashB is a blob that the manifest written by addRemote does @@ -259,3 +260,24 @@ func TestRemoteInfo_DirectoryWithoutManifestLeavesOrphansKnown(t *testing.T) { assert.NotContains(t, doc, "unreadable_manifests") assert.NotContains(t, doc, "skipped_manifest_count") } + +// TestRemoteInfo_StatusLineUsesUIMarker checks that the remote info +// status lines go through internal/ui: marker-prefixed, and silenced by +// --quiet. +func TestRemoteInfo_StatusLineUsesUIMarker(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)) + + const statusLine = ui.Marker + " Scanning snapshot metadata...\n" + + require.NoError(t, env.v.RemoteInfo(false)) + assert.Contains(t, env.stdout.String(), statusLine) + + env.stdout.Reset() + env.v.UI.SetQuiet(true) + require.NoError(t, env.v.RemoteInfo(false)) + assert.NotContains(t, env.stdout.String(), "Scanning snapshot metadata") +} diff --git a/internal/vaultik/snapshot.go b/internal/vaultik/snapshot.go index 68b7d00..663656e 100644 --- a/internal/vaultik/snapshot.go +++ b/internal/vaultik/snapshot.go @@ -1019,7 +1019,7 @@ func (v *Vaultik) VerifySnapshotWithOptions( v.printlnStdout() // Check each blob is present with the size the manifest records. - v.stdoutf("Checking blob presence and sizes...\n") + v.UI.Beginf("Checking blob presence and sizes...") } v.checkSnapshotDatabase(remoteKey, result) diff --git a/internal/vaultik/verify.go b/internal/vaultik/verify.go index 6c88d93..ba6c2d3 100644 --- a/internal/vaultik/verify.go +++ b/internal/vaultik/verify.go @@ -190,7 +190,7 @@ func (v *Vaultik) loadVerificationData( log.Info("Downloading manifest", "remote_key", remoteKey) if !opts.JSON && !v.UI.Quiet() { - v.stdoutf("Downloading manifest...\n") + v.UI.Beginf("Downloading manifest...") } manifest, err := v.downloadManifestByKey(remoteKey) @@ -205,9 +205,9 @@ func (v *Vaultik) loadVerificationData( "manifest_total_size", ubytes(manifest.TotalCompressedSize)) if !opts.JSON && !v.UI.Quiet() { - v.stdoutf("Manifest loaded: %d blobs (%s)\n", + v.UI.Completef("Manifest loaded: %d blobs (%s)", manifest.BlobCount, ubytes(manifest.TotalCompressedSize)) - v.stdoutf("Downloading and decrypting database...\n") + v.UI.Beginf("Downloading and decrypting database...") } tdb, err := v.downloadVerifiedSnapshotDB( @@ -235,7 +235,7 @@ func (v *Vaultik) loadVerificationData( "db_total_size", ubytes(dbTotalSize)) if !opts.JSON && !v.UI.Quiet() { - v.stdoutf("Database loaded: %d blobs (%s)\n", + v.UI.Completef("Database loaded: %d blobs (%s)", len(dbBlobs), ubytes(dbTotalSize)) } @@ -295,7 +295,7 @@ func (v *Vaultik) runVerificationSteps( identities []age.Identity, ) error { if !opts.JSON && !v.UI.Quiet() { - v.stdoutf("Verifying manifest against database...\n") + v.UI.Beginf("Verifying manifest against database...") } err := v.verifyManifestAgainstDatabase(manifest, dbBlobs) @@ -304,8 +304,8 @@ func (v *Vaultik) runVerificationSteps( } if !opts.JSON && !v.UI.Quiet() { - v.stdoutf("Manifest verified.\n") - v.stdoutf("Checking blob existence in remote storage...\n") + v.UI.Completef("Manifest verified.") + v.UI.Beginf("Checking blob existence in remote storage...") } err = v.verifyBlobExistenceFromDB(dbBlobs, result) @@ -314,8 +314,8 @@ func (v *Vaultik) runVerificationSteps( } if !opts.JSON && !v.UI.Quiet() { - v.stdoutf("All blobs exist.\n") - v.stdoutf("Downloading and verifying blob contents (%d blobs, %s)...\n", + v.UI.Completef("All blobs exist.") + v.UI.Beginf("Downloading and verifying blob contents (%d blobs, %s)...", len(dbBlobs), ubytes(totalSize)) } @@ -790,7 +790,7 @@ func (v *Vaultik) performDeepVerificationFromDB( ) if !opts.JSON && !v.UI.Quiet() { - v.stdoutf(" Verified %d/%d blobs (%d remaining) - %s/%s - elapsed %s, eta %s\n", + v.UI.Progressf("Verified %d/%d blobs (%d remaining) - %s/%s - elapsed %s, eta %s", i+1, len(blobs), remaining, ubytes(bytesProcessed), ubytes(totalBytesExpected), diff --git a/internal/vaultik/verify_quiet_test.go b/internal/vaultik/verify_quiet_test.go index c8dd630..bc3b26e 100644 --- a/internal/vaultik/verify_quiet_test.go +++ b/internal/vaultik/verify_quiet_test.go @@ -100,3 +100,51 @@ func TestVerify_QuietSuppressesReport(t *testing.T) { require.Empty(t, stdout.String(), "a failed verify must write no report under --quiet") } + +// TestVerify_DeepStatusLineUsesUIMarker checks that the deep verify +// status lines go through internal/ui: marker-prefixed, and silenced by +// --quiet. +func TestVerify_DeepStatusLineUsesUIMarker(t *testing.T) { + log.Initialize(log.Config{}) + t.Parallel() + + fs := afero.NewOsFs() + tempDir := t.TempDir() + + dataDir := filepath.Join(tempDir, "source") + storeDir := t.TempDir() + dbPath := filepath.Join(tempDir, "index.sqlite") + + require.NoError(t, fs.MkdirAll(dataDir, 0o755)) + require.NoError(t, afero.WriteFile(fs, + filepath.Join(dataDir, "data.bin"), + bytesPattern("marker-", 64*1024), 0o644)) + + ctx := context.Background() + + cfg, storer, snapshotID := runFileStorageBackup( + ctx, t, fs, dataDir, storeDir, dbPath, 32*1024, 128*1024) + + var stdout bytes.Buffer + + v := &vaultik.Vaultik{ + Config: cfg, + Storage: storer, + Fs: fs, + Stdout: &stdout, + Stderr: io.Discard, + UI: ui.NewWithColor(&stdout, false), + } + v.SetContext(ctx) + + opts := &vaultik.VerifyOptions{Deep: true} + + require.NoError(t, v.VerifySnapshotWithOptions(snapshotID, opts)) + require.Contains(t, stdout.String(), + "\n"+ui.Marker+" Downloading manifest...\n") + + stdout.Reset() + v.UI.SetQuiet(true) + require.NoError(t, v.VerifySnapshotWithOptions(snapshotID, opts)) + require.NotContains(t, stdout.String(), "Downloading manifest") +}