144 lines
5.0 KiB
Go
144 lines
5.0 KiB
Go
package dream
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/smallnest/pigo/internal/memory"
|
|
)
|
|
|
|
func TestUpdateScopeIndexesDropsDanglingLinks(t *testing.T) {
|
|
root := t.TempDir()
|
|
// A global MEMORY.md linking to two entries; b.md will be deleted.
|
|
writeMemFile(t, root, "global/user/a.md", "keep me")
|
|
b := writeMemFile(t, root, "global/user/b.md", "remove me")
|
|
idx := writeMemFile(t, root, "global/MEMORY.md",
|
|
"# Index\n- [a](user/a.md)\n- [b](user/b.md)\n- freeform note\n")
|
|
|
|
deleted := map[string]struct{}{filepath.Clean(b): {}}
|
|
if err := updateScopeIndexes(root, "", deleted); err != nil {
|
|
t.Fatalf("updateScopeIndexes: %v", err)
|
|
}
|
|
raw, err := os.ReadFile(idx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got := string(raw)
|
|
if strings.Contains(got, "user/b.md") || strings.Contains(got, "b.md") {
|
|
t.Fatalf("dangling link to b.md not removed:\n%s", got)
|
|
}
|
|
if !strings.Contains(got, "user/a.md") {
|
|
t.Fatalf("live link to a.md wrongly removed:\n%s", got)
|
|
}
|
|
if !strings.Contains(got, "freeform note") {
|
|
t.Fatalf("unrelated line wrongly removed:\n%s", got)
|
|
}
|
|
}
|
|
|
|
// TestUpdateScopeIndexesBoundaryMatch: deleting b.md must not drop an index line
|
|
// referencing a different entry whose name merely contains "b.md" as a
|
|
// substring (e.g. club.md).
|
|
func TestUpdateScopeIndexesBoundaryMatch(t *testing.T) {
|
|
root := t.TempDir()
|
|
writeMemFile(t, root, "global/user/club.md", "keep me")
|
|
b := writeMemFile(t, root, "global/user/b.md", "remove me")
|
|
idx := writeMemFile(t, root, "global/MEMORY.md",
|
|
"- [club](user/club.md)\n- [b](user/b.md)\n")
|
|
|
|
deleted := map[string]struct{}{filepath.Clean(b): {}}
|
|
if err := updateScopeIndexes(root, "", deleted); err != nil {
|
|
t.Fatalf("updateScopeIndexes: %v", err)
|
|
}
|
|
got, _ := os.ReadFile(idx)
|
|
if !strings.Contains(string(got), "user/club.md") {
|
|
t.Fatalf("club.md link wrongly removed by substring match:\n%s", got)
|
|
}
|
|
if strings.Contains(string(got), "user/b.md") {
|
|
t.Fatalf("b.md link not removed:\n%s", got)
|
|
}
|
|
}
|
|
|
|
func TestUpdateScopeIndexesNoIndexIsNoOp(t *testing.T) {
|
|
root := t.TempDir()
|
|
writeMemFile(t, root, "global/user/a.md", "x")
|
|
deleted := map[string]struct{}{filepath.Join(root, "global", "user", "a.md"): {}}
|
|
if err := updateScopeIndexes(root, "", deleted); err != nil {
|
|
t.Fatalf("updateScopeIndexes with no MEMORY.md should be a no-op, got %v", err)
|
|
}
|
|
}
|
|
|
|
// TestRunEndToEndWithMerge exercises the full non-dry-run write path with a stub
|
|
// Consolidator that merges b.md into a.md and prunes c.md: files converge on
|
|
// disk, the MEMORY.md index loses its dangling links, Reconcile runs, the Report
|
|
// counters are correct, and a full-text search no longer hits the merged-away
|
|
// fragment (US-003 / US-006 / US-009).
|
|
func TestRunEndToEndWithMerge(t *testing.T) {
|
|
root := t.TempDir()
|
|
a := writeMemFile(t, root, "global/user/a.md", "shared topic original a")
|
|
b := writeMemFile(t, root, "global/user/b.md", "shared topic zebrafragment only in b")
|
|
c := writeMemFile(t, root, "global/user/c.md", "outdated standalone note")
|
|
idx := writeMemFile(t, root, "global/MEMORY.md",
|
|
"# Index\n- [a](user/a.md)\n- [b](user/b.md)\n- [c](user/c.md)\n")
|
|
|
|
stub := &stubConsolidator{result: ConsolidateResult{
|
|
MergedBodies: map[string]string{a: "shared topic merged and current"},
|
|
Deletions: []string{b, c},
|
|
Merged: 1,
|
|
Pruned: 1,
|
|
Notes: []string{"pruned c: outdated"},
|
|
}}
|
|
r := &Runner{MemoryRoot: root, Consolidator: stub}
|
|
rep, err := r.Run(context.Background(), RunOptions{})
|
|
if err != nil {
|
|
t.Fatalf("Run: %v", err)
|
|
}
|
|
if !stub.called {
|
|
t.Fatal("Consolidator not called")
|
|
}
|
|
if rep.Merged != 1 || rep.Pruned != 1 {
|
|
t.Fatalf("counters wrong: %+v", rep)
|
|
}
|
|
// a.md rewritten, b.md + c.md gone.
|
|
if got, _ := os.ReadFile(a); string(got) != "shared topic merged and current" {
|
|
t.Fatalf("a.md not rewritten: %q", got)
|
|
}
|
|
if _, err := os.Stat(b); !os.IsNotExist(err) {
|
|
t.Fatal("b.md should be deleted")
|
|
}
|
|
if _, err := os.Stat(c); !os.IsNotExist(err) {
|
|
t.Fatal("c.md should be deleted")
|
|
}
|
|
// MEMORY.md no longer links to the removed entries.
|
|
rawIdx, _ := os.ReadFile(idx)
|
|
if strings.Contains(string(rawIdx), "b.md") || strings.Contains(string(rawIdx), "c.md") {
|
|
t.Fatalf("MEMORY.md retains dangling links:\n%s", rawIdx)
|
|
}
|
|
if !strings.Contains(string(rawIdx), "a.md") {
|
|
t.Fatalf("MEMORY.md lost the live a.md link:\n%s", rawIdx)
|
|
}
|
|
// Reconcile ran and indexed the surviving files.
|
|
if rep.Reconciled.Indexed == 0 {
|
|
t.Fatalf("Reconcile did not index: %+v", rep.Reconciled)
|
|
}
|
|
if rep.FilesAfter != 2 { // a.md + MEMORY.md
|
|
t.Fatalf("FilesAfter = %d, want 2", rep.FilesAfter)
|
|
}
|
|
|
|
// The merged-away fragment must no longer be searchable.
|
|
store, err := memory.Open(filepath.Join(root, "index.db"), root, "")
|
|
if err != nil {
|
|
t.Fatalf("open store: %v", err)
|
|
}
|
|
defer store.Close()
|
|
hits, err := store.Search("zebrafragment", memory.SearchOptions{ReconcileFirst: true})
|
|
if err != nil {
|
|
t.Fatalf("search: %v", err)
|
|
}
|
|
if len(hits) != 0 {
|
|
t.Fatalf("merged-away fragment still searchable: %+v", hits)
|
|
}
|
|
}
|