2 Commits
Author SHA1 Message Date
sneak 61be280e48 Count every file of each version in secret info's Total Size (closes #147)
check / check (push) Canceled after 0s
Total Size added up each version's data.age and metadata.age, but a
version keeps its value in value.age, so no secret's value was counted.
It now adds up every file in each version directory: the value, the
metadata and the version's two keys, as the plan on the issue decided.
A version directory that cannot be read adds nothing and gets a warning
naming it.

The new tests add a 10000-byte secret and check that the total grows by
at least that much, and check the warning for a version directory that
cannot be read.

Model: opus-5-5
2026-10-15 00:38:47 +00:00
clawbot acb4b9cb6a version promote --help says the current file is rewritten (closes #153)
check / check (push) Canceled after 0s
The help text for `secret version promote` and a comment in
PromoteVersion said promote updates a "current symlink". `current` is a
plain file holding the version name, and promote rewrites it; the README
already says so. A new test runs `secret version promote --help` and
checks the output does not mention a symlink and does name the current
file.

Model: opus-5-5
2026-10-15 02:31:47 +02:00
5 changed files with 148 additions and 15 deletions
+12 -13
View File
@@ -18,24 +18,23 @@ type vaultStats struct {
// addVersion accumulates size and timestamp info for one version directory
func (s *vaultStats) addVersion(fs afero.Fs, versionPath string) {
// Add size of encrypted data
dataPath := filepath.Join(versionPath, "data.age")
stat, err := fs.Stat(dataPath)
if err == nil {
s.totalSize += stat.Size()
// Add the size of every file in the version directory: its value,
// metadata and keys. A directory that cannot be read adds nothing and
// gets a warning; its timestamp is still tracked below.
files, err := afero.ReadDir(fs, versionPath)
if err != nil {
secret.Warn("Could not read version directory",
"directory", versionPath, "error", err)
}
// Add size of metadata
metaPath := filepath.Join(versionPath, "metadata.age")
stat, err = fs.Stat(metaPath)
if err == nil {
s.totalSize += stat.Size()
for _, file := range files {
if !file.IsDir() {
s.totalSize += file.Size()
}
}
// Track timestamps
stat, err = fs.Stat(versionPath)
stat, err := fs.Stat(versionPath)
if err == nil {
modTime := stat.ModTime()
if s.oldestTime.IsZero() || modTime.Before(s.oldestTime) {
+51
View File
@@ -0,0 +1,51 @@
//nolint:testpackage // white-box test of unexported internals
package cli
import (
"io"
"os"
"path/filepath"
"testing"
"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestAddVersionWarnsWhenVersionDirUnreadable asserts that a version
// directory that cannot be listed, so that its files are left out of the
// total size, gets a warning naming it and the error, and that its
// timestamp is still tracked.
//
//nolint:paralleltest // replaces os.Stderr, where secret.Warn writes
func TestAddVersionWarnsWhenVersionDirUnreadable(t *testing.T) {
versionDir := filepath.Join(testVaultDir(listTestVaultName),
unreadableTestSecretsDirName, unreadableTestSecretName,
"versions", "20260809.001")
base := afero.NewMemMapFs()
require.NoError(t, base.MkdirAll(versionDir, listTestDirPerm))
stderrReader, stderrWriter, err := os.Pipe()
require.NoError(t, err)
defer func() { _ = stderrReader.Close() }()
realStderr := os.Stderr
os.Stderr = stderrWriter
var stats vaultStats
stats.addVersion(&openFailFs{Fs: base, path: versionDir}, versionDir)
os.Stderr = realStderr
require.NoError(t, stderrWriter.Close())
warning, err := io.ReadAll(stderrReader)
require.NoError(t, err)
assert.Contains(t, string(warning), versionDir)
assert.Contains(t, string(warning), errOpenFailed.Error())
assert.False(t, stats.latestTime.IsZero(),
"the timestamp must still be tracked")
}
+64
View File
@@ -0,0 +1,64 @@
package cli_test
import (
"bytes"
"crypto/rand"
"encoding/json"
"testing"
"github.com/awnumar/memguard"
"github.com/spf13/afero"
"github.com/spf13/cobra"
"github.com/stretchr/testify/require"
"sneak.berlin/go/secret/internal/cli"
"sneak.berlin/go/secret/internal/vault"
)
// TestInfoTotalSizeCountsSecretValues is a regression test for
// https://git.eeqj.de/sneak/secret/issues/147, where the total size shown by
// `secret info` left out every secret's value. Adding a secret must make it
// grow by at least the size of the value.
func TestInfoTotalSizeCountsSecretValues(t *testing.T) {
t.Parallel()
const valueSize = 10000
fs := afero.NewMemMapFs()
vlt, err := vault.CreateVault(fs, testStateDir, "default",
testMnemonicBuffer(t), nil)
require.NoError(t, err)
before := infoTotalSize(t, fs)
valueBytes := make([]byte, valueSize)
_, err = rand.Read(valueBytes)
require.NoError(t, err)
value := memguard.NewBufferFromBytes(valueBytes)
defer value.Destroy()
require.NoError(t, vlt.AddSecret("s1", value, false))
after := infoTotalSize(t, fs)
require.GreaterOrEqual(t, after-before, int64(valueSize))
}
// infoTotalSize returns the total size that `secret info --json` reports for
// the state directory on fs.
func infoTotalSize(t *testing.T, fs afero.Fs) int64 {
t.Helper()
var out bytes.Buffer
cmd := &cobra.Command{}
cmd.SetOut(&out)
err := cli.NewCLIInstanceWithStateDir(fs, testStateDir).Info(cmd, true)
require.NoError(t, err)
var info cli.InfoOutput
require.NoError(t, json.Unmarshal(out.Bytes(), &info))
return info.TotalSize
}
+2 -2
View File
@@ -64,7 +64,7 @@ func VersionCommands(cli *Instance) *cobra.Command {
promoteCmd := &cobra.Command{
Use: "promote <secret-name> <version>",
Short: "Promote a specific version to current",
Long: "Updates the current symlink to point to the specified " +
Long: "Rewrites the secret's current file to name the specified " +
"version. The versions' own times are left unchanged; secret " +
"list then gives the time of the promote as the secret's last " +
"update.",
@@ -293,7 +293,7 @@ func (cli *Instance) PromoteVersion(
version, vault.ErrVersionNotFound, secretName)
}
// Update the current symlink using the proper function
// Rewrite the secret's current file to name the version
err = secret.SetCurrentVersion(cli.fs, secretDir, version)
if err != nil {
return fmt.Errorf("failed to update current version: %w", err)
+19
View File
@@ -6,6 +6,8 @@
// - TestListVersionsNonExistentSecret: Tests error handling for missing secrets
// - TestPromoteVersionCommand: Tests `secret version promote` command
// - TestPromoteNonExistentVersion: Tests error handling for invalid promotion
// - TestPromoteHelpSaysCurrentFileIsRewritten: Tests `secret version promote
// --help` text
// - TestGetSecretWithVersion: Tests `secret get --version` flag functionality
// - TestGetSecretWritesBinaryValue: Tests `secret get` output of binary values
// - TestGetSecretReturnsWriteError: Tests `secret get` when stdout fails
@@ -270,6 +272,23 @@ func TestPromoteNonExistentVersion(t *testing.T) {
require.ErrorIs(t, err, vault.ErrVersionNotFound)
}
// TestPromoteHelpSaysCurrentFileIsRewritten is a regression test for
// https://git.eeqj.de/sneak/secret/issues/153: `secret version promote
// --help` said it updates a symlink, but promote rewrites the secret's
// `current` file, which is a plain file.
//
//nolint:paralleltest // executes the CLI in-process and sets the environment
func TestPromoteHelpSaysCurrentFileIsRewritten(t *testing.T) {
env := map[string]string{secret.EnvStateDir: t.TempDir()}
output, err := ExecuteCommandInProcess(
[]string{"version", "promote", "--help"}, "", env)
require.NoError(t, err)
assert.NotContains(t, output, "symlink")
assert.Contains(t, output, "current file")
}
func TestGetSecretWithVersion(t *testing.T) {
t.Parallel()