mirror of
https://github.com/Twigpine/zero.git
synced 2026-11-11 23:40:28 -10:00
* fix: warn about untracked scratch files left behind after a run Fixes #551. Headless exec runs could leave behind debug/scratch files (e.g. _fix_test.py, _debug.py) created by write_file during iteration, which would then be silently swept up by a later git add -A. - FileTracker now records brand-new files (RecordCreated/CreatedFiles), distinct from the existing version-baseline tracking used for conflict detection. - write_file records a path as created only on first write, not on overwrite of an existing file. - After a headless run completes, exec.go checks git status for any created file still present and untracked, and surfaces a warning via the existing writer.warning() (works across text/-o json/stream-json). No files are deleted automatically — a wrong guess at 'scratch' could destroy real work, so this only surfaces what git already knows. Co-authored-by: Copilot App <[email protected]> * fix: address scratch warning review comments * test: normalize created file tracker paths * fix: track apply_patch-created scratch files * fix: address scratch warning review feedback * fix: tighten scratch warning lifecycle --------- Co-authored-by: Copilot App <[email protected]>
156 lines
5.0 KiB
Go
156 lines
5.0 KiB
Go
package tools
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
func TestFileTrackerRecordsAndReadsBackVersion(t *testing.T) {
|
|
tracker := NewFileTracker()
|
|
path := filepath.Join(t.TempDir(), "a.txt")
|
|
content := []byte("hello world")
|
|
if err := os.WriteFile(path, content, 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
info, err := os.Stat(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
tracker.Record(path, content, info)
|
|
version, ok := tracker.Version(path)
|
|
if !ok {
|
|
t.Fatal("expected a recorded version")
|
|
}
|
|
if version.Hash != HashContent(content) {
|
|
t.Fatalf("hash = %q, want %q", version.Hash, HashContent(content))
|
|
}
|
|
if version.Size != int64(len(content)) {
|
|
t.Fatalf("size = %d, want %d", version.Size, len(content))
|
|
}
|
|
if version.MTime.IsZero() {
|
|
t.Fatal("expected mtime to be recorded from stat info")
|
|
}
|
|
}
|
|
|
|
func TestFileTrackerRecordHashMatchesRecord(t *testing.T) {
|
|
tracker := NewFileTracker()
|
|
content := []byte("streamed content")
|
|
tracker.RecordHash("/repo/x.txt", HashContent(content), nil)
|
|
if err := tracker.CheckConflict("/repo/x.txt", content); err != nil {
|
|
t.Fatalf("recorded hash should match content, got %v", err)
|
|
}
|
|
if err := tracker.CheckConflict("/repo/x.txt", []byte("changed")); err != ErrFileChangedOnDisk {
|
|
t.Fatalf("changed content should conflict, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestCheckConflictAllowsUntrackedPath(t *testing.T) {
|
|
tracker := NewFileTracker()
|
|
// No Record call: a first-touch write has no baseline to conflict against.
|
|
if err := tracker.CheckConflict("/nowhere/x.txt", []byte("anything")); err != nil {
|
|
t.Fatalf("untracked path should not conflict, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestCheckConflictAllowsMatchingContent(t *testing.T) {
|
|
tracker := NewFileTracker()
|
|
content := []byte("stable content")
|
|
tracker.Record("/repo/x.txt", content, nil)
|
|
if err := tracker.CheckConflict("/repo/x.txt", content); err != nil {
|
|
t.Fatalf("unchanged content should not conflict, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestCheckConflictBlocksDriftedContent(t *testing.T) {
|
|
tracker := NewFileTracker()
|
|
tracker.Record("/repo/x.txt", []byte("version one"), nil)
|
|
if err := tracker.CheckConflict("/repo/x.txt", []byte("version two — changed underneath us")); err != ErrFileChangedOnDisk {
|
|
t.Fatalf("drifted content should report ErrFileChangedOnDisk, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestForgetClearsBaselineSoNextWriteIsAllowed(t *testing.T) {
|
|
tracker := NewFileTracker()
|
|
tracker.Record("/repo/x.txt", []byte("version one"), nil)
|
|
tracker.Forget("/repo/x.txt")
|
|
if _, ok := tracker.Version("/repo/x.txt"); ok {
|
|
t.Fatal("Forget should drop the recorded version")
|
|
}
|
|
if err := tracker.CheckConflict("/repo/x.txt", []byte("version two")); err != nil {
|
|
t.Fatalf("forgotten path should behave as untracked, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRecordOverwritesBaselineWithNewContent(t *testing.T) {
|
|
tracker := NewFileTracker()
|
|
tracker.Record("/repo/x.txt", []byte("old"), nil)
|
|
tracker.Record("/repo/x.txt", []byte("new"), nil)
|
|
// After re-recording, the new content is the baseline and matches.
|
|
if err := tracker.CheckConflict("/repo/x.txt", []byte("new")); err != nil {
|
|
t.Fatalf("re-recorded content should be the new baseline, got %v", err)
|
|
}
|
|
if err := tracker.CheckConflict("/repo/x.txt", []byte("old")); err != ErrFileChangedOnDisk {
|
|
t.Fatal("the superseded content should now conflict")
|
|
}
|
|
}
|
|
|
|
func TestNilFileTrackerIsANoop(t *testing.T) {
|
|
var tracker *FileTracker
|
|
tracker.Record("/repo/x.txt", []byte("x"), nil) // must not panic
|
|
tracker.Forget("/repo/x.txt")
|
|
if _, ok := tracker.Version("/repo/x.txt"); ok {
|
|
t.Fatal("nil tracker should report no version")
|
|
}
|
|
if err := tracker.CheckConflict("/repo/x.txt", []byte("x")); err != nil {
|
|
t.Fatalf("nil tracker should never conflict, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestHashContentIsStableAndDistinguishing(t *testing.T) {
|
|
// Store results of separate calls before comparing so the stability check is
|
|
// not a same-expression comparison (staticcheck SA4000).
|
|
first := HashContent([]byte("a"))
|
|
second := HashContent([]byte("a"))
|
|
if first != second {
|
|
t.Fatal("hash must be stable for identical content")
|
|
}
|
|
if first == HashContent([]byte("b")) {
|
|
t.Fatal("hash must differ for different content")
|
|
}
|
|
}
|
|
|
|
func TestRecordCreatedTracksNewFilesInOrder(t *testing.T) {
|
|
tracker := NewFileTracker()
|
|
tracker.RecordCreated("/repo/b.txt")
|
|
tracker.RecordCreated("/repo/a.txt")
|
|
got := tracker.CreatedFiles()
|
|
want := []string{"/repo/b.txt", "/repo/a.txt"}
|
|
if len(got) != len(want) {
|
|
t.Fatalf("CreatedFiles() = %v, want %v", got, want)
|
|
}
|
|
for i := range want {
|
|
if got[i] != want[i] {
|
|
t.Fatalf("CreatedFiles() = %v, want %v", got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRecordCreatedDeduplicates(t *testing.T) {
|
|
tracker := NewFileTracker()
|
|
tracker.RecordCreated("/repo/a.txt")
|
|
tracker.RecordCreated("/repo/a.txt")
|
|
if got := tracker.CreatedFiles(); len(got) != 1 {
|
|
t.Fatalf("CreatedFiles() = %v, want a single entry", got)
|
|
}
|
|
}
|
|
|
|
func TestNilFileTrackerCreatedFilesIsANoop(t *testing.T) {
|
|
var tracker *FileTracker
|
|
tracker.RecordCreated("/repo/a.txt") // must not panic
|
|
if got := tracker.CreatedFiles(); got != nil {
|
|
t.Fatalf("CreatedFiles() on nil tracker = %v, want nil", got)
|
|
}
|
|
}
|