Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ jobs:
run: |
total=$(go tool cover -func=coverage.out | awk '/^total:/ {gsub("%","",$3); print $3}')
# Current enforced coverage floor. Codex PRs raise this incrementally toward 90%.
min=45.0
min=50.0
awk -v t="$total" -v m="$min" 'BEGIN {
if (t+0 < m+0) {
Comment on lines 52 to 55
printf "Coverage %.1f%% is below floor %.1f%%\n", t, m
Expand Down
7 changes: 7 additions & 0 deletions watch/events_debounce_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,10 @@ func TestEventDebouncerPrunesStaleEntries(t *testing.T) {
t.Fatal("expected recent path entry to be retained")
}
}

func TestNewEventDebouncerHasMinimumPruneWindow(t *testing.T) {
debouncer := newEventDebouncer(10 * time.Millisecond)
if debouncer.pruneAfter != time.Second {
t.Fatalf("pruneAfter = %v, want %v", debouncer.pruneAfter, time.Second)
}
}
167 changes: 167 additions & 0 deletions watch/events_handle_test.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package watch

import (
"os"
"path/filepath"
"reflect"
"sort"
"strings"
"testing"
"time"

Expand Down Expand Up @@ -157,3 +159,168 @@ func TestFindRelatedHot(t *testing.T) {
})
}
}

func TestHandleEventWriteUpdatesTrackedState(t *testing.T) {
root := t.TempDir()
if err := os.MkdirAll(filepath.Join(root, ".codemap"), 0o755); err != nil {
t.Fatal(err)
}

rel := "pkg/main.go"
abs := filepath.Join(root, rel)
if err := os.MkdirAll(filepath.Dir(abs), 0o755); err != nil {
t.Fatal(err)
}
content := "package main\n\nfunc run() {}\n"
if err := os.WriteFile(abs, []byte(content), 0o644); err != nil {
t.Fatal(err)
}
info, err := os.Stat(abs)
if err != nil {
t.Fatal(err)
}

d := &Daemon{
root: root,
eventLog: filepath.Join(root, ".codemap", "events.log"),
graph: &Graph{
Files: map[string]*scanner.FileInfo{
rel: {Path: rel, Size: 10, Ext: ".go"},
},
State: map[string]*FileState{
rel: {Lines: 1, Size: 10},
},
},
}

d.handleEvent(fsnotify.Event{Name: abs, Op: fsnotify.Write})

d.graph.mu.RLock()
defer d.graph.mu.RUnlock()

state, ok := d.graph.State[rel]
if !ok {
t.Fatalf("expected %q to remain tracked in graph state", rel)
}
if state.Lines != 3 {
t.Fatalf("expected updated line count 3, got %d", state.Lines)
}
if state.Size != info.Size() {
t.Fatalf("expected updated size %d, got %d", info.Size(), state.Size)
}

file, ok := d.graph.Files[rel]
if !ok {
t.Fatalf("expected %q to remain tracked in graph files", rel)
}
if file.Ext != ".go" {
t.Fatalf("expected .go extension, got %q", file.Ext)
}

if len(d.graph.Events) != 1 {
t.Fatalf("expected one event, got %d", len(d.graph.Events))
}
event := d.graph.Events[0]
if event.Op != "WRITE" || event.Path != rel {
t.Fatalf("unexpected event: %+v", event)
}
if event.Delta != 2 {
t.Fatalf("expected line delta +2, got %d", event.Delta)
}
if event.SizeDelta != info.Size()-10 {
t.Fatalf("expected size delta %d, got %d", info.Size()-10, event.SizeDelta)
}

logData, err := os.ReadFile(filepath.Join(root, ".codemap", "events.log"))
if err != nil {
t.Fatalf("expected log file to be written: %v", err)
}
if !strings.Contains(string(logData), "WRITE") || !strings.Contains(string(logData), rel) {
t.Fatalf("expected event log to contain write event for %q, got:\n%s", rel, string(logData))
}
}

func TestHandleEventRemoveDeletesTrackedState(t *testing.T) {
root := t.TempDir()
rel := "old.go"

d := &Daemon{
root: root,
graph: &Graph{
Files: map[string]*scanner.FileInfo{
rel: {Path: rel, Size: 40, Ext: ".go"},
},
State: map[string]*FileState{
rel: {Lines: 4, Size: 40},
},
},
}

d.handleEvent(fsnotify.Event{Name: filepath.Join(root, rel), Op: fsnotify.Remove})

d.graph.mu.RLock()
defer d.graph.mu.RUnlock()

if _, ok := d.graph.Files[rel]; ok {
t.Fatalf("expected %q to be removed from graph files", rel)
}
if _, ok := d.graph.State[rel]; ok {
t.Fatalf("expected %q to be removed from graph state", rel)
}

if len(d.graph.Events) != 1 {
t.Fatalf("expected one event, got %d", len(d.graph.Events))
}
event := d.graph.Events[0]
if event.Op != "REMOVE" || event.Path != rel {
t.Fatalf("unexpected event: %+v", event)
}
if event.Delta != -4 {
t.Fatalf("expected line delta -4, got %d", event.Delta)
}
if event.SizeDelta != -40 {
t.Fatalf("expected size delta -40, got %d", event.SizeDelta)
}
}

func TestHandleEventSkipsGitignoredPath(t *testing.T) {
root := t.TempDir()
if err := os.MkdirAll(filepath.Join(root, ".codemap"), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(root, ".gitignore"), []byte("ignored.go\n"), 0o644); err != nil {
t.Fatal(err)
}

rel := "ignored.go"
abs := filepath.Join(root, rel)
if err := os.WriteFile(abs, []byte("package ignored\n"), 0o644); err != nil {
t.Fatal(err)
}

gitCache := scanner.NewGitIgnoreCache(root)
gitCache.EnsureDir(root)

d := &Daemon{
root: root,
gitCache: gitCache,
eventLog: filepath.Join(root, ".codemap", "events.log"),
graph: &Graph{
Files: map[string]*scanner.FileInfo{},
State: map[string]*FileState{},
Events: []Event{},
},
}

d.handleEvent(fsnotify.Event{Name: abs, Op: fsnotify.Write})

d.graph.mu.RLock()
defer d.graph.mu.RUnlock()

if len(d.graph.Events) != 0 {
t.Fatalf("expected gitignored event to be skipped, got %+v", d.graph.Events)
}
if len(d.graph.Files) != 0 || len(d.graph.State) != 0 {
t.Fatalf("expected gitignored event to avoid graph updates, files=%d state=%d", len(d.graph.Files), len(d.graph.State))
}
}
34 changes: 34 additions & 0 deletions watch/events_limits_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,37 @@ func TestTrimEventLogToBytes(t *testing.T) {
t.Fatalf("expected newest entry to be retained after trim")
}
}

func TestTrimEventLogToBytesIgnoresInvalidLimits(t *testing.T) {
tmpDir := t.TempDir()
logPath := filepath.Join(tmpDir, "events.log")
original := "line1\nline2\n"
if err := os.WriteFile(logPath, []byte(original), 0o644); err != nil {
t.Fatal(err)
}

tests := []struct {
name string
maxBytes int64
keepBytes int64
}{
{name: "max bytes non-positive", maxBytes: 0, keepBytes: 10},
{name: "keep bytes non-positive", maxBytes: 10, keepBytes: 0},
{name: "keep bytes larger than max", maxBytes: 10, keepBytes: 11},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := trimEventLogToBytes(logPath, tt.maxBytes, tt.keepBytes); err != nil {
t.Fatalf("trimEventLogToBytes() error = %v", err)
}
data, err := os.ReadFile(logPath)
if err != nil {
t.Fatal(err)
}
if string(data) != original {
t.Fatalf("expected log content to remain unchanged, got %q", string(data))
}
})
}
}
Loading