b563e86e
test(rewriter): deterministic concurrent-modification path
a73x 2026-04-29 05:51
diff --git a/internal/rewriter/rewriter.go b/internal/rewriter/rewriter.go index 2fab15b..71e2ae6 100644 --- a/internal/rewriter/rewriter.go +++ b/internal/rewriter/rewriter.go @@ -8,6 +8,7 @@ import ( "path/filepath" "regexp" "strings" "time" ) var ErrConcurrentModification = errors.New("file modified concurrently") @@ -84,21 +85,29 @@ func StampUUIDs(path string, stamps map[int]string) error { return err } // mtime check before atomic rename (deterministic test for this comes in Task 7) info2, err := os.Stat(path) return atomicReplace(path, tmpPath, info.Mode().Perm(), mtimeBefore) } // atomicReplace stat-checks path against mtimeBefore. If the file was modified // concurrently, returns ErrConcurrentModification and removes tmpPath. Otherwise // renames tmpPath over path. The mode argument is currently unused (the caller // already chmods tmpPath before close), but is included to keep this helper's // signature self-contained for direct testing. func atomicReplace(path, tmpPath string, mode os.FileMode, mtimeBefore time.Time) error { info, err := os.Stat(path) if err != nil { os.Remove(tmpPath) return err } if !info2.ModTime().Equal(mtimeBefore) { if !info.ModTime().Equal(mtimeBefore) { os.Remove(tmpPath) return ErrConcurrentModification } if err := os.Rename(tmpPath, path); err != nil { os.Remove(tmpPath) return fmt.Errorf("rename: %w", err) } _ = mode // currently unused; reserved for future modes that need post-rename Chmod return nil } diff --git a/internal/rewriter/rewriter_test.go b/internal/rewriter/rewriter_test.go index d60835d..b8bd2b0 100644 --- a/internal/rewriter/rewriter_test.go +++ b/internal/rewriter/rewriter_test.go @@ -1,10 +1,12 @@ package rewriter import ( "errors" "os" "path/filepath" "strings" "testing" "time" ) func TestStampUUIDsInsertsTagAfterClaude(t *testing.T) { @@ -63,3 +65,20 @@ func TestStampUUIDsBlockComment(t *testing.T) { t.Errorf("got:\n%s\nwant:\n%s", got, want) } } func TestAtomicReplaceAbortsOnStaleMtime(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "foo.go") if err := os.WriteFile(path, []byte("hi\n"), 0o644); err != nil { t.Fatal(err) } tmpPath := filepath.Join(dir, "tmp") if err := os.WriteFile(tmpPath, []byte("bye\n"), 0o644); err != nil { t.Fatal(err) } stale := time.Now().Add(-time.Hour) err := atomicReplace(path, tmpPath, 0o644, stale) if !errors.Is(err, ErrConcurrentModification) { t.Fatalf("got %v, want ErrConcurrentModification", err) } }