1 Commits
Author SHA1 Message Date
clawbot a040f9831b Lock the state directory and write vault files atomically (closes #34)
check / check (push) Successful in 46s
Each command that changes the state directory holds one lock: flock(2)
on `lock` in the state directory, dropped by the kernel if the process
dies, or a process-wide mutex on the in-memory test filesystem. It
covers the state directory, not each vault, because `currentvault`,
`vault create` and cross-vault moves span vaults, and a lock file in a
vault would be deleted by `vault remove` under a waiting command.

Files go through `secret.WriteFileAtomic`; versions, new secrets and
cross-vault copies are built in a temporary directory and renamed into
place; removals rename out of the way first. Left for later: replacing
an unlocker (#71) and deleting
what an interrupted command leaves under a `.tmp-` name
(#75).

Model: opus-5-5
2026-10-03 16:19:55 +00:00
2 changed files with 3 additions and 6 deletions
+1 -2
View File
@@ -446,8 +446,6 @@ func TestChangingCommandsWaitForLock(t *testing.T) {
// state directory lock by the time it writes its output. Holding it while
// streaming would stall every other changing command for as long as the
// stream lasts, and forever when the other end of the pipe is one of them.
//
//nolint:paralleltest // t.Setenv forbids t.Parallel
func TestEncryptStreamsUnlocked(t *testing.T) {
t.Setenv(secret.EnvMnemonic, testMnemonic)
@@ -484,6 +482,7 @@ func TestEncryptStreamsUnlocked(t *testing.T) {
// Let encrypt finish, so that it releases the lock, then free it
// again for the tests that follow
_, _ = io.Copy(io.Discard, outputReader)
(<-taken)()
t.Fatal("secret encrypt held the lock while streaming")
}
+2 -4
View File
@@ -36,6 +36,8 @@ const currentFile = "current"
const unlockerMetadataFile = "unlocker-metadata.json"
// unlockerPassphrase protects the passphrase unlockers the tests create.
//
//nolint:gosec // G101: test data, not a real credential
const unlockerPassphrase = "unlocker passphrase"
// hookFs passes every call through to Fs, but first calls before for each
@@ -612,8 +614,6 @@ func TestWriteFileAtomicTempFile(t *testing.T) {
// vault whose long-term key cannot be had: it must fail without writing
// anything, so that it never leaves a partial unlocker, nor breaks the one
// it would replace.
//
//nolint:paralleltest // t.Setenv forbids t.Parallel
func TestPassphraseUnlockerGetsKeyFirst(t *testing.T) {
// No mnemonic, and no current unlocker to get the key from
t.Setenv(secret.EnvMnemonic, "")
@@ -640,8 +640,6 @@ func TestPassphraseUnlockerGetsKeyFirst(t *testing.T) {
// passphrase unlocker writes in its directory is its metadata: an unlocker
// directory without metadata is never used, so one interrupted earlier
// cannot be.
//
//nolint:paralleltest // t.Setenv forbids t.Parallel
func TestPassphraseUnlockerWritesMetadataLast(t *testing.T) {
t.Setenv(secret.EnvMnemonic, testMnemonic)