Write only the document to stdout from snapshot remove --json (closes #251)
check / check (push) Waiting to run

When the destination store cannot be reached, `snapshot remove` still
removes the snapshot from the local index and warns. The warning went
through the UI, which writes to stdout, so under `--json` it landed
ahead of the document and `| jq` failed on a command that exited 0.
Under `--json` the UI warning is now skipped. The logger's warning,
which goes to stderr in every mode, now also says to run `vaultik
prune` once the destination store is reachable.

The new test runs the command through `Entry` against a missing
destination directory, decodes stdout as exactly one JSON document, and
finds the warning and the `vaultik prune` follow-up on stderr.

Model: opus-5-5
This commit is contained in:
2026-10-07 00:27:59 +00:00
parent 85d4ef118d
commit 1e9644edbd
3 changed files with 56 additions and 12 deletions
+39 -7
View File
@@ -87,12 +87,31 @@ func TestEntryJSONFailureIsReportedOnStderr(t *testing.T) {
}
}
// writeUnusableDestinationConfig builds a config whose destination
// directory does not exist, which fails `remote info`, and whose local
// index is bound to another destination, which fails `prune` and
// `snapshot remove` (a missing destination alone only makes `snapshot
// remove` warn). Returns the config path.
func writeUnusableDestinationConfig(t *testing.T) string {
// TestEntrySnapshotRemoveJSONWarningIsOnStderr runs `snapshot remove
// --json` against a destination directory that does not exist. The
// command still exits 0 after removing the snapshot from the local
// index. Its stdout must hold the document alone, with the warning
// about the destination store, and the `vaultik prune` follow-up, on
// stderr.
//
//nolint:paralleltest // replaces os.Args, os.Stdout, os.Stderr and the xdg globals
func TestEntrySnapshotRemoveJSONWarningIsOnStderr(t *testing.T) {
configPath, _ := writeMissingDestinationConfig(t)
code, stdout, stderr := runEntry(t, flagConfig, configPath,
cmdSnapshot, cmdRemove, someSnapshotID, flagJSON)
require.Equal(t, 0, code)
requireExactlyOneJSONDocument(t, stdout)
assert.Contains(t, stderr,
"Could not remove snapshot metadata from remote storage")
assert.Contains(t, stderr, "run 'vaultik prune'")
}
// writeMissingDestinationConfig builds a config whose destination
// directory does not exist. Returns the config path and the path of
// its local index, which is not created here.
func writeMissingDestinationConfig(t *testing.T) (string, string) {
t.Helper()
dir := t.TempDir()
@@ -114,6 +133,19 @@ func writeUnusableDestinationConfig(t *testing.T) string {
xdg.Reload()
t.Cleanup(xdg.Reload)
return configPath, indexPath
}
// writeUnusableDestinationConfig builds a config whose destination
// directory does not exist, which fails `remote info`, and whose local
// index is bound to another destination, which fails `prune` and
// `snapshot remove` (a missing destination alone only makes `snapshot
// remove` warn). Returns the config path.
func writeUnusableDestinationConfig(t *testing.T) string {
t.Helper()
configPath, indexPath := writeMissingDestinationConfig(t)
ctx := context.Background()
db, err := database.New(ctx, indexPath)
@@ -122,7 +154,7 @@ func writeUnusableDestinationConfig(t *testing.T) string {
defer func() { require.NoError(t, db.Close()) }()
require.NoError(t, database.NewRepositories(db).LocalMeta.Set(ctx,
database.LocalMetaKeyStorageURL, "file://"+filepath.Join(dir, "other")))
database.LocalMetaKeyStorageURL, "file://"+t.TempDir()))
return configPath
}
+8 -5
View File
@@ -1167,7 +1167,7 @@ func (v *Vaultik) RemoveSnapshot(
}
if !opts.LocalOnly {
result.RemoteRemoved = v.removeSnapshotRemote(snapshotID)
result.RemoteRemoved = v.removeSnapshotRemote(snapshotID, opts)
}
if v.SnapshotManager != nil {
@@ -1252,7 +1252,7 @@ func (v *Vaultik) confirmRemoveSnapshot(snapshotID string, opts *RemoveOptions)
// removal has already happened, so the user is told the remote half
// didn't finish and can retry with `vaultik prune` once the destination
// store is reachable. Returns true when the remote removal succeeded.
func (v *Vaultik) removeSnapshotRemote(snapshotID string) bool {
func (v *Vaultik) removeSnapshotRemote(snapshotID string, opts *RemoveOptions) bool {
log.Info("Removing snapshot metadata from remote storage",
"snapshot_id", snapshotID)
@@ -1260,10 +1260,13 @@ func (v *Vaultik) removeSnapshotRemote(snapshotID string) bool {
err := v.deleteRemoteSnapshotByKey(remoteKey)
if err != nil {
log.Warn("Could not remove snapshot metadata from remote storage",
"error", err)
log.Warn("Could not remove snapshot metadata from remote storage; "+
"run '"+pruneCommandHint+"' once the remote is reachable "+
"to finish cleanup", "error", err)
if v.UI != nil {
// The UI writes to stdout, which under --json holds only the
// document; the log record above is the warning on stderr.
if v.UI != nil && !opts.JSON {
v.UI.Warningf("Could not remove snapshot metadata from remote: "+
"%v. Run '%s' once the remote is reachable to finish cleanup.",
err, pruneCommandHint)