From 248d133804e1a472b4ec0b4728fd8f045d4e4f08 Mon Sep 17 00:00:00 2001 From: xgopilot Date: Sat, 18 Apr 2026 08:18:16 +0000 Subject: [PATCH] fix(memo): merge scoped and legacy topics in ListTopics Generated with [codeagent](https://github.com/qbox/codeagent) Co-authored-by: Yumiue <188874804+Yumiue@users.noreply.github.com> --- internal/memo/store.go | 18 +++++++++++++----- internal/memo/store_test.go | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 5 deletions(-) diff --git a/internal/memo/store.go b/internal/memo/store.go index c8251021..befbb527 100644 --- a/internal/memo/store.go +++ b/internal/memo/store.go @@ -184,6 +184,7 @@ func (s *FileStore) ListTopics(ctx context.Context, scope Scope) ([]string, erro s.mu.RLock() defer s.mu.RUnlock() + seen := make(map[string]struct{}) for _, dir := range s.topicsDirs(scope) { entries, err := os.ReadDir(dir) if err != nil { @@ -192,14 +193,22 @@ func (s *FileStore) ListTopics(ctx context.Context, scope Scope) ([]string, erro } return nil, fmt.Errorf("memo: list topics: %w", err) } - if names := collectTopicNames(entries); len(names) > 0 { - return names, nil + for _, name := range collectTopicNames(entries) { + seen[name] = struct{}{} } } - return nil, nil + if len(seen) == 0 { + return nil, nil + } + names := make([]string, 0, len(seen)) + for name := range seen { + names = append(names, name) + } + sort.Strings(names) + return names, nil } -// collectTopicNames 将目录项过滤为 topic 文件名列表,并按字典序排序保证稳定输出。 +// collectTopicNames 将目录项过滤为 topic 文件名列表。 func collectTopicNames(entries []os.DirEntry) []string { names := make([]string, 0, len(entries)) for _, entry := range entries { @@ -208,7 +217,6 @@ func collectTopicNames(entries []os.DirEntry) []string { } names = append(names, entry.Name()) } - sort.Strings(names) return names } diff --git a/internal/memo/store_test.go b/internal/memo/store_test.go index 38c35be7..1a917dc0 100644 --- a/internal/memo/store_test.go +++ b/internal/memo/store_test.go @@ -268,6 +268,41 @@ func TestFileStoreLoadTopicAndListTopicsFallbackToLegacyProjectPath(t *testing.T } } +func TestFileStoreListTopicsMergesScopedAndLegacyProjectTopics(t *testing.T) { + store, legacyDir := newLegacyProjectStore(t) + scopedTopicsDir := store.topicsDir(ScopeProject) + legacyTopicsDir := filepath.Join(legacyDir, topicsDirName) + if err := os.MkdirAll(scopedTopicsDir, 0o755); err != nil { + t.Fatalf("MkdirAll(scoped topics) error = %v", err) + } + if err := os.MkdirAll(legacyTopicsDir, 0o755); err != nil { + t.Fatalf("MkdirAll(legacy topics) error = %v", err) + } + if err := os.WriteFile(filepath.Join(scopedTopicsDir, "scoped.md"), []byte("scoped"), 0o644); err != nil { + t.Fatalf("WriteFile(scoped topic) error = %v", err) + } + if err := os.WriteFile(filepath.Join(legacyTopicsDir, "legacy.md"), []byte("legacy"), 0o644); err != nil { + t.Fatalf("WriteFile(legacy topic) error = %v", err) + } + if err := os.WriteFile(filepath.Join(legacyTopicsDir, "scoped.md"), []byte("legacy dup"), 0o644); err != nil { + t.Fatalf("WriteFile(legacy duplicate topic) error = %v", err) + } + + topics, err := store.ListTopics(context.Background(), ScopeProject) + if err != nil { + t.Fatalf("ListTopics() error = %v", err) + } + want := []string{"legacy.md", "scoped.md"} + if len(topics) != len(want) { + t.Fatalf("len(topics) = %d, want %d, topics = %#v", len(topics), len(want), topics) + } + for i := range want { + if topics[i] != want[i] { + t.Fatalf("topics[%d] = %q, want %q (topics=%#v)", i, topics[i], want[i], topics) + } + } +} + func TestFileStoreSaveIndexMigratesLegacyProjectData(t *testing.T) { store, legacyDir := newLegacyProjectStore(t) legacyTopicsDir := filepath.Join(legacyDir, topicsDirName)