Make prompt debug creation concurrency safe

This commit is contained in:
2026-08-02 13:11:41 +00:00
parent e0229d9c90
commit 1716702c99
4 changed files with 453 additions and 740 deletions

View File

@@ -269,7 +269,23 @@ func ensureSecureDirectory(path string) error {
info, err := os.Lstat(current)
if os.IsNotExist(err) {
if err := os.Mkdir(current, debugDirectoryMode); err != nil {
return err
if !os.IsExist(err) {
return err
}
info, err = os.Lstat(current)
if err != nil {
return err
}
if info.Mode()&os.ModeSymlink != 0 {
return fmt.Errorf("directory component %q must not be a symlink", current)
}
if !info.IsDir() {
return fmt.Errorf("directory component %q is not a directory", current)
}
if err := os.Chmod(current, debugDirectoryMode); err != nil {
return err
}
continue
}
if err := os.Chmod(current, debugDirectoryMode); err != nil {
return err

View File

@@ -1,10 +1,12 @@
package promptdebug
import (
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
"sync"
"testing"
"time"
@@ -91,6 +93,73 @@ func TestPromptDebugWriterAtomicallyReplacesArtifacts(t *testing.T) {
}
}
func TestPromptDebugWriterCreatesSharedMissingAncestorsConcurrently(t *testing.T) {
root := filepath.Join(t.TempDir(), "debug")
writer, err := NewPromptDebugWriter(root)
if err != nil {
t.Fatalf("NewPromptDebugWriter() error = %v", err)
}
const writerCount = 8
start := make(chan struct{})
type writeResult struct {
directory string
err error
}
results := make(chan writeResult, writerCount)
var writers sync.WaitGroup
for index := 0; index < writerCount; index++ {
writers.Add(1)
go func(index int) {
defer writers.Done()
<-start
directory, err := writer.WritePreparation(PromptDebugRef{
ReportID: report.Daily, ValidDate: "2026-05-29", RunID: fmt.Sprintf("run-%02d", index),
}, promptDebugPreparationFixture(), nil)
results <- writeResult{directory: directory, err: err}
}(index)
}
close(start)
finished := make(chan struct{})
go func() {
writers.Wait()
close(finished)
}()
select {
case <-finished:
case <-time.After(5 * time.Second):
t.Fatal("concurrent prompt debug writes did not finish")
}
close(results)
directories := map[string]struct{}{}
for result := range results {
if result.err != nil {
t.Fatalf("WritePreparation() error = %v", result.err)
}
if _, duplicate := directories[result.directory]; duplicate {
t.Fatalf("duplicate debug directory %q", result.directory)
}
directories[result.directory] = struct{}{}
if _, err := os.Stat(filepath.Join(result.directory, "preparation.json")); err != nil {
t.Fatalf("preparation artifact %q: %v", result.directory, err)
}
}
if len(directories) != writerCount {
t.Fatalf("debug directories = %#v", directories)
}
if runtime.GOOS != "windows" {
assertPromptDebugMode(t, root, debugDirectoryMode)
assertPromptDebugMode(t, filepath.Join(root, "daily"), debugDirectoryMode)
assertPromptDebugMode(t, filepath.Join(root, "daily", "2026-05-29"), debugDirectoryMode)
for directory := range directories {
assertPromptDebugMode(t, directory, debugDirectoryMode)
assertPromptDebugMode(t, filepath.Join(directory, "preparation.json"), debugFileMode)
}
}
}
func TestPromptDebugWriterDisabledDoesNotAccessFilesystem(t *testing.T) {
writer, err := NewPromptDebugWriter("")
if err != nil {