From 29146b7d318333087d4ccc48e32bab3cb561ec8b Mon Sep 17 00:00:00 2001 From: sneak Date: Wed, 14 Oct 2026 22:57:29 +0000 Subject: [PATCH] secret version list no longer shows a version without NOT_AFTER as expired (closes #150) STATUS was `expired` for every version other than the current one, so after a promote the newest version, which has no NOT_AFTER, read `expired`. STATUS is now `current` for the current version, `expired` for another version whose NOT_AFTER has passed, and `inactive` for any other version. README describes the three. Judgement call: the issue left the third word to the owner; `inactive` is the word the plan comment on the issue chose. Model: opus-5-5 --- README.md | 6 ++++ internal/cli/version.go | 8 +++-- internal/cli/version_test.go | 62 ++++++++++++++++++++++++++++++++++++ 3 files changed, 74 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index dceb58b..b0a199e 100644 --- a/README.md +++ b/README.md @@ -185,6 +185,12 @@ Moves or renames a secret within the current vault. Lists all versions of a secret showing creation time, status, and validity period. +STATUS is `current` for the secret's current version, `expired` for another +version whose NOT_AFTER has passed, and `inactive` for any other version. Adding +a version sets NOT_AFTER on the version that was current; +`secret version promote` sets no times, so after promoting an older version the +newest version shows `inactive`. + #### `secret version promote ` Promotes a specific version to current by rewriting the secret's `current` file diff --git a/internal/cli/version.go b/internal/cli/version.go index ca862a4..a84df64 100644 --- a/internal/cli/version.go +++ b/internal/cli/version.go @@ -229,9 +229,13 @@ func printVersionRow( } // Determine status - status := "expired" - if version == currentVersion { + status := "inactive" + + switch { + case version == currentVersion: status = "current" + case sv.Metadata.NotAfter != nil && sv.Metadata.NotAfter.Before(time.Now()): + status = "expired" } // Format timestamps diff --git a/internal/cli/version_test.go b/internal/cli/version_test.go index 018b5d3..5c98622 100644 --- a/internal/cli/version_test.go +++ b/internal/cli/version_test.go @@ -5,6 +5,8 @@ // - TestListVersionsCommand: Tests `secret version list` command output // - TestListVersionsNonExistentSecret: Tests error handling for missing secrets // - TestPromoteVersionCommand: Tests `secret version promote` command +// - TestListVersionsStatusAfterPromote: Tests each version's status after +// promoting an older version // - TestPromoteNonExistentVersion: Tests error handling for invalid promotion // - TestGetSecretWithVersion: Tests `secret get --version` flag functionality // - TestGetSecretWritesBinaryValue: Tests `secret get` output of binary values @@ -240,6 +242,66 @@ func TestPromoteVersionCommand(t *testing.T) { assert.Equal(t, []byte("version-1"), promoted.Bytes()) } +func TestListVersionsStatusAfterPromote(t *testing.T) { + t.Parallel() + + fs := afero.NewMemMapFs() + cli := NewCLIInstanceWithStateDir(fs, testStateDir) + cli.Mnemonic = testMnemonicBuffer(t) + + setupTestVault(t, fs) + + vlt, err := vault.GetCurrentVault(fs, testStateDir) + require.NoError(t, err) + + vlt.Mnemonic = cli.Mnemonic + + addTestSecret(t, vlt, []byte("version-1"), false) + addTestSecret(t, vlt, []byte("version-2"), true) + addTestSecret(t, vlt, []byte("version-3"), true) + + vaultDir, err := vlt.GetDirectory() + require.NoError(t, err) + + // Newest first + versions, err := secret.ListVersions(fs, + filepath.Join(vaultDir, "secrets.d", "test%secret")) + require.NoError(t, err) + require.Len(t, versions, 3) + + newest, middle, oldest := versions[0], versions[1], versions[2] + + cmd := newRootCmd() + + var buf bytes.Buffer + cmd.SetOut(&buf) + cmd.SetErr(&buf) + + err = cli.PromoteVersion(cmd, "test/secret", oldest) + require.NoError(t, err) + + buf.Reset() + + err = cli.ListVersions(cmd, "test/secret") + require.NoError(t, err) + + // STATUS is the fourth field of a row, since CREATED is a date and a time + statuses := map[string]string{} + + for line := range strings.SplitSeq(buf.String(), "\n") { + fields := strings.Fields(line) + if len(fields) > 3 { + statuses[fields[0]] = fields[3] + } + } + + assert.Equal(t, "current", statuses[oldest]) + // Its NOT_AFTER was set when the newest version was added + assert.Equal(t, "expired", statuses[middle]) + // It has no NOT_AFTER + assert.Equal(t, "inactive", statuses[newest]) +} + func TestPromoteNonExistentVersion(t *testing.T) { t.Parallel()