readtracker_test.go
1 package agent 2 3 import ( 4 "context" 5 "os" 6 "path/filepath" 7 "testing" 8 9 "github.com/Kocoro-lab/ShanClaw/internal/cwdctx" 10 ) 11 12 func TestReadTracker_MarkAndHasRead(t *testing.T) { 13 rt := NewReadTracker() 14 15 dir := t.TempDir() 16 path := filepath.Join(dir, "test.txt") 17 os.WriteFile(path, []byte("hello"), 0644) 18 19 if rt.HasRead(path) { 20 t.Error("expected HasRead to return false before MarkRead") 21 } 22 23 rt.MarkRead(path) 24 25 if !rt.HasRead(path) { 26 t.Error("expected HasRead to return true after MarkRead") 27 } 28 } 29 30 func TestReadTracker_RelativePath(t *testing.T) { 31 rt := NewReadTracker() 32 33 // Create a file in a temp dir. The tracker should treat absolute and 34 // relative paths as the same file when SetCWD is called — this mirrors 35 // the explicit-CWD contract the rest of the filesystem tools enforce 36 // after the CWD-hardening work; process cwd is NOT consulted. 37 dir := t.TempDir() 38 path := filepath.Join(dir, "rel.txt") 39 os.WriteFile(path, []byte("data"), 0644) 40 41 rt.SetCWD(dir) 42 43 // Mark with absolute, check with relative 44 rt.MarkRead(path) 45 if !rt.HasRead("rel.txt") { 46 t.Error("expected HasRead to match relative path against absolute after SetCWD") 47 } 48 } 49 50 // TestReadTracker_RelativePathWithoutCWD verifies that a relative path with 51 // no SetCWD is treated as a distinct key from its absolute form — the 52 // tracker must NOT silently resolve against the process cwd. 53 func TestReadTracker_RelativePathWithoutCWD(t *testing.T) { 54 rt := NewReadTracker() 55 56 dir := t.TempDir() 57 abs := filepath.Join(dir, "rel.txt") 58 os.WriteFile(abs, []byte("data"), 0644) 59 60 rt.MarkRead(abs) 61 if rt.HasRead("rel.txt") { 62 t.Error("expected relative path to not match absolute when no CWD is set") 63 } 64 } 65 66 func TestReadTracker_Symlink(t *testing.T) { 67 rt := NewReadTracker() 68 69 dir := t.TempDir() 70 real := filepath.Join(dir, "real.txt") 71 link := filepath.Join(dir, "link.txt") 72 os.WriteFile(real, []byte("data"), 0644) 73 if err := os.Symlink(real, link); err != nil { 74 t.Skip("symlinks not supported") 75 } 76 77 // Mark the symlink as read, check via real path 78 rt.MarkRead(link) 79 if !rt.HasRead(real) { 80 t.Error("expected HasRead to resolve symlinks") 81 } 82 } 83 84 func TestReadTracker_EmptyPath(t *testing.T) { 85 rt := NewReadTracker() 86 rt.MarkRead("") 87 if rt.HasRead("") { 88 t.Error("expected empty path to not be tracked") 89 } 90 } 91 92 func TestReadTracker_NonexistentFile(t *testing.T) { 93 rt := NewReadTracker() 94 95 dir := t.TempDir() 96 path := filepath.Join(dir, "noexist.txt") 97 98 // Should still work (normalization falls back to clean path) 99 rt.MarkRead(path) 100 if !rt.HasRead(path) { 101 t.Error("expected HasRead to work for nonexistent files") 102 } 103 } 104 105 func TestReadTracker_NormalizesWithSessionCWD(t *testing.T) { 106 rt := NewReadTracker() 107 rt.SetCWD("/projects/foo") 108 109 rt.MarkRead("src/main.go") 110 if !rt.HasRead("src/main.go") { 111 t.Error("should find relative path after MarkRead") 112 } 113 if !rt.HasRead("/projects/foo/src/main.go") { 114 t.Error("should find absolute path equivalent") 115 } 116 if rt.HasRead("/other/src/main.go") { 117 t.Error("should not match different absolute path") 118 } 119 } 120 121 func TestIsMemoryFile_UsesSessionCWD(t *testing.T) { 122 memDir := t.TempDir() 123 ctx := context.Background() 124 ctx = WithMemoryDir(ctx, memDir) 125 ctx = cwdctx.WithSessionCWD(ctx, "/projects/foo") 126 127 // Absolute path to memory file 128 if !IsMemoryFile(ctx, filepath.Join(memDir, "MEMORY.md")) { 129 t.Error("absolute path to MEMORY.md should match") 130 } 131 }