/ internal / agent / readtracker_test.go
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  }