Skip to content
Merged
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: 2 additions & 0 deletions cmd/remindb/serve_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ func TestNewServeLogger_JsonFileOutput(t *testing.T) {
}
if file == nil {
t.Fatal("output_path set, file handle should be returned for cleanup")
return
}
defer func() { _ = file.Close() }()

Expand Down Expand Up @@ -204,6 +205,7 @@ func TestNewServeLogger_ConfiguredBufferCaptures(t *testing.T) {
}
if buf == nil {
t.Fatal("buffer should be returned for the logs resource")
return
}

lg.Info("a")
Expand Down
2 changes: 2 additions & 0 deletions internal/pathmatch/ignore_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ func TestLoadIgnore_EmptyFile(t *testing.T) {
}
if m == nil {
t.Fatal("expected non-nil matcher for empty file")
return
}
if m.Match("anything.md", false) {
t.Error("empty matcher should not match")
Expand All @@ -57,6 +58,7 @@ func TestLoadIgnore_CommentsAndBlanks(t *testing.T) {
}
if m == nil {
t.Fatal("expected non-nil matcher")
return
}
if len(m.patterns) != 0 {
t.Errorf("expected 0 patterns, got %d", len(m.patterns))
Expand Down
9 changes: 9 additions & 0 deletions mcp_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -944,6 +944,7 @@ func TestMcp_OverviewResource(t *testing.T) {
}
if overview == nil {
t.Fatalf("resources/list missing remindb://overview, got %d resources", len(listed.Resources))
return
}
if overview.MIMEType != "application/json" {
t.Errorf("overview MIME type = %q, want application/json", overview.MIMEType)
Expand Down Expand Up @@ -1032,6 +1033,7 @@ func TestMcp_FilesResource(t *testing.T) {
}
if files == nil {
t.Fatalf("resources/list missing remindb://files, got %d resources", len(listed.Resources))
return
}
if files.MIMEType != "application/json" {
t.Errorf("files MIME type = %q, want application/json", files.MIMEType)
Expand Down Expand Up @@ -1140,6 +1142,7 @@ func TestMcp_TreeResource(t *testing.T) {
}
if tree == nil {
t.Fatalf("resources/list missing remindb://tree, got %d resources", len(listed.Resources))
return
}
if tree.MIMEType != "application/json" {
t.Errorf("tree MIME type = %q, want application/json", tree.MIMEType)
Expand Down Expand Up @@ -1189,6 +1192,7 @@ func TestMcp_TreeResource(t *testing.T) {
}
if pivot == nil {
t.Fatalf("no node with a grandchild found; cannot assert depth bounding")
return
}

// Shape: every node carries the full field set.
Expand Down Expand Up @@ -1288,6 +1292,7 @@ func TestMcp_SnapshotsResource(t *testing.T) {
}
if snapshots == nil {
t.Fatalf("resources/list missing remindb://snapshots, got %d resources", len(listed.Resources))
return
}
if snapshots.MIMEType != "application/json" {
t.Errorf("snapshots MIME type = %q, want application/json", snapshots.MIMEType)
Expand Down Expand Up @@ -1488,6 +1493,7 @@ func TestMcp_TemperatureResource(t *testing.T) {
}
if heat == nil {
t.Fatalf("resources/list missing remindb://temperature, got %d resources", len(listed.Resources))
return
}
if heat.MIMEType != "application/json" {
t.Errorf("temperature MIME type = %q, want application/json", heat.MIMEType)
Expand Down Expand Up @@ -1576,6 +1582,7 @@ func TestMcp_DoctorResource(t *testing.T) {
}
if doctor == nil {
t.Fatalf("resources/list missing remindb://doctor, got %d resources", len(listed.Resources))
return
}
if doctor.MIMEType != "application/json" {
t.Errorf("doctor MIME type = %q, want application/json", doctor.MIMEType)
Expand Down Expand Up @@ -1646,6 +1653,7 @@ func TestMcp_LogsResource(t *testing.T) {
}
if logs == nil {
t.Fatalf("resources/list missing remindb://logs, got %d resources", len(listed.Resources))
return
}
if logs.MIMEType != "application/json" {
t.Errorf("logs MIME type = %q, want application/json", logs.MIMEType)
Expand Down Expand Up @@ -2100,6 +2108,7 @@ func TestMcp_RescanResource(t *testing.T) {
}
if rescan == nil {
t.Fatalf("resources/list missing remindb://rescan, got %d resources", len(listed.Resources))
return
}
if rescan.MIMEType != "application/json" {
t.Errorf("rescan MIME type = %q, want application/json", rescan.MIMEType)
Expand Down
29 changes: 18 additions & 11 deletions pkg/mcp/rescan/rescan.go
Original file line number Diff line number Diff line change
Expand Up @@ -285,27 +285,34 @@ func (r *Loop) scan(ctx context.Context) {
return
}

var deleted []string
var deletedAbs []string
var deletedRel []string
for path := range r.modTimes {
if seen[path] {
continue
}
delete(r.modTimes, path)

rel, err := filepath.Rel(r.dir, path)
if err != nil {
rel = path
}
deleted = append(deleted, rel)
deletedAbs = append(deletedAbs, path)
deletedRel = append(deletedRel, rel)
}

r.store.OpMu.Lock()
defer r.store.OpMu.Unlock()

snap.PurgedFiles = r.reconcileDeleted(ctx, deleted)
purged, ok := r.reconcileDeleted(ctx, deletedRel)
snap.PurgedFiles = purged
if ok {
for _, abs := range deletedAbs {
delete(r.modTimes, abs)
}
}

if len(changed) == 0 {
if len(deleted) > 0 {
if ok && len(deletedRel) > 0 {
r.notifyChange()
}

Expand Down Expand Up @@ -344,18 +351,18 @@ func (r *Loop) scan(ctx context.Context) {
r.notifyChange()
}

func (r *Loop) reconcileDeleted(ctx context.Context, deleted []string) []rescanstat.PurgedFile {
func (r *Loop) reconcileDeleted(ctx context.Context, deleted []string) ([]rescanstat.PurgedFile, bool) {
if len(deleted) == 0 {
return nil
return nil, true
}

nodes, err := r.store.GetNodesByFiles(ctx, deleted)
if err != nil {
r.logger.Error("rescan: load deleted nodes failed", "err", err)
return nil
return nil, false
}
if len(nodes) == 0 {
return nil
return nil, true
}

deltas := make([]diff.Delta, 0, len(nodes))
Expand All @@ -380,7 +387,7 @@ func (r *Loop) reconcileDeleted(ctx context.Context, deleted []string) []rescans
emitter.WithMessage(msg),
); err != nil {
r.logger.Error("rescan: purge emit failed", "err", err)
return nil
return nil, false
}

r.logger.Info("rescan: purged deleted files",
Expand All @@ -395,5 +402,5 @@ func (r *Loop) reconcileDeleted(ctx context.Context, deleted []string) []rescans
}

sort.Slice(purged, func(i, j int) bool { return purged[i].Path < purged[j].Path })
return purged
return purged, true
}
71 changes: 71 additions & 0 deletions pkg/mcp/rescan/rescan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,77 @@ func TestRescanLoop_SkipsPurgeOnWalkError(t *testing.T) {
}
}

func TestRescanLoop_RetainsModTimesOnPurgeEmitFailure(t *testing.T) {
dir := t.TempDir()
writeFile(t, dir, "keep.md", "# Keep\n")
writeFile(t, dir, "gone.md", "# Gone\n\nBody.\n")

st := testutil.OpenTestDB(t)
r := mustRescan(t, st, dir, time.Minute, nil)
r.now = func() time.Time { return time.Now().Add(time.Hour) }

ctx := context.Background()
r.scan(ctx)

before, err := st.GetNodesByFile(ctx, "gone.md")
if err != nil {
t.Fatalf("GetNodesByFile: %v", err)
}
if len(before) == 0 {
t.Fatal("expected gone.md nodes after initial scan")
}

if err := os.Remove(filepath.Join(dir, "gone.md")); err != nil {
t.Fatal(err)
}

failCtx, cancel := context.WithCancel(ctx)
originalWalk := r.walkFn
r.walkFn = func(root string, fn fs.WalkDirFunc) error {
if err := originalWalk(root, fn); err != nil {
return err
}
cancel()
return nil
}

snapsBefore, _ := st.ListSnapshots(ctx, 10)
r.scan(failCtx)

if _, ok := r.modTimes[filepath.Join(dir, "gone.md")]; !ok {
t.Error("modTimes entry for gone.md dropped despite failed purge emit")
}

stillThere, err := st.GetNodesByFile(ctx, "gone.md")
if err != nil {
t.Fatalf("GetNodesByFile (after failed emit): %v", err)
}
if len(stillThere) == 0 {
t.Error("gone.md nodes purged from DB even though emit failed")
}

snapsAfter, _ := st.ListSnapshots(ctx, 10)
if len(snapsAfter) != len(snapsBefore) {
t.Errorf("snapshots changed: before=%d after=%d (emit failed, must not commit purge snapshot)",
len(snapsBefore), len(snapsAfter))
}

r.walkFn = originalWalk
r.scan(ctx)

if _, ok := r.modTimes[filepath.Join(dir, "gone.md")]; ok {
t.Error("modTimes entry for gone.md should be cleared after successful retry")
}

after, err := st.GetNodesByFile(ctx, "gone.md")
if err != nil {
t.Fatalf("GetNodesByFile (retry): %v", err)
}
if len(after) != 0 {
t.Errorf("orphan nodes after successful retry = %d, want 0", len(after))
}
}

func TestRescanLoop_NewFile(t *testing.T) {
dir := t.TempDir()

Expand Down
Loading