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 internal/memory/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ func (s Store) archiveLocked(name string) (string, error) {
ref := strings.TrimSpace(name)
parsed := parseMemoryReference(ref)
if active, path, ok := s.findActive(ref); ok && ref == active.ID {
return archiveMemoryInDir(filepath.Dir(path), active.Name)
return s.archiveByID(active.Name, ref)
} else if ok && parsed.qualified {
return archiveMemoryInDir(filepath.Dir(path), active.Name)
} else if ok {
Expand Down
32 changes: 32 additions & 0 deletions internal/memory/store_archive_id.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package memory

import "path/filepath"

// archiveByID archives every active copy of a fact that shares the given ID
// across directories (migration duplicates share the deterministic ID), while
// same-named facts with a different ID stay put. Returns the last path
// archived. The unqualified-name path already archives from every directory.
func (s Store) archiveByID(name, id string) (string, error) {
var lastPath string
for _, dir := range s.dirs() {
if dir == "" || !memoryIDInDir(dir, name, id) {
continue
}
p, err := archiveMemoryInDir(dir, name)
if err != nil {
return "", err
}
if p != "" {
lastPath = p
}
}
return lastPath, nil
}

// memoryIDInDir reports whether dir holds an active fact with the given ID.
// Used to archive every migration duplicate of an ID while leaving same-named
// facts with a different identity untouched.
func memoryIDInDir(dir, name, id string) bool {
m, ok := loadMemory(filepath.Join(dir, name+".md"))
return ok && m.ID == id
}
52 changes: 52 additions & 0 deletions internal/memory/store_archive_id_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package memory

import (
"os"
"path/filepath"
"testing"
)

// TestStoreDeleteByIDRemovesFromAllDirs covers the ID-reference path of
// Archive: an ID may name the same fact in several directories (migration
// duplicates share the deterministic legacy ID), so deletion by ID must
// archive from every directory — the unqualified-name path already does.
func TestStoreDeleteByIDRemovesFromAllDirs(t *testing.T) {
dir := t.TempDir()
s := Store{
Dir: filepath.Join(dir, "project", "memory"),
GlobalDir: filepath.Join(dir, "global"),
}

name := "prefers-tabs"
for _, d := range []string{s.Dir, s.GlobalDir} {
if err := os.MkdirAll(d, 0o755); err != nil {
t.Fatal(err)
}
m := Memory{Name: name, Description: "user pref", Type: TypeUser, Body: "use tabs"}
if err := os.WriteFile(filepath.Join(d, name+".md"), []byte(render(m, name)), 0o644); err != nil {
t.Fatal(err)
}
if err := reindexIn(d, name, m); err != nil {
t.Fatal(err)
}
}

// Both copies share one identity (same name + scope → same legacy ID).
list := s.List()
if len(list) != 1 {
t.Fatalf("want 1 deduplicated memory, got %d", len(list))
}

// Delete by the identity ID, not the name.
if err := s.Delete(list[0].ID); err != nil {
t.Fatal(err)
}
for _, d := range []string{s.Dir, s.GlobalDir} {
if _, err := os.Stat(filepath.Join(d, name+".md")); !os.IsNotExist(err) {
t.Fatalf("copy in %s should be gone after ID delete", d)
}
}
if idx := s.Index(); idx != "" {
t.Fatalf("Index() should be empty after deleting all entries, got:\n%s", idx)
}
}