| 1 | package sessioncatalog |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "os" |
| 6 | "path/filepath" |
| 7 | "strings" |
| 8 | "testing" |
| 9 | ) |
| 10 | |
| 11 | func TestMetadataRefreshDoesNotRewriteDiscoveredTopics(t *testing.T) { |
| 12 | c, err := Open(t.Context(), Options{InMemory: true, MetadataOnly: true, StartPaused: true}) |
| 13 | if err != nil { |
| 14 | t.Fatal(err) |
| 15 | } |
| 16 | defer c.Close(context.Background()) |
| 17 | _, err = c.db.Exec(`WITH RECURSIVE n(i) AS (VALUES(1) UNION ALL SELECT i+1 FROM n WHERE i<1000) |
| 18 | INSERT INTO catalog_sessions(path,path_key,directory,directory_key,scope,topic_id,topic_title,ordinary_visible) |
| 19 | SELECT printf('/old/%d.jsonl',i),printf('/old/%d.jsonl',i),'/old','/old','global',printf('source-%d',i),'Old',1 FROM n; |
| 20 | INSERT INTO catalog_topics(scope,workspace_root,workspace_root_key,topic_id,title) |
| 21 | SELECT scope,workspace_root,workspace_root_key,topic_id,topic_title FROM catalog_sessions; |
| 22 | CREATE TABLE metadata_writes(topic_id TEXT); |
| 23 | CREATE TRIGGER record_metadata_write AFTER UPDATE ON catalog_topics BEGIN INSERT INTO metadata_writes VALUES(NEW.topic_id); END;`) |
| 24 | if err != nil { |
| 25 | t.Fatal(err) |
| 26 | } |
| 27 | for range 2 { |
| 28 | if err := c.SyncMetadata(t.Context(), nil, []TopicMetadata{{Scope: "global", TopicID: "registry", Title: "Named"}}); err != nil { |
| 29 | t.Fatal(err) |
| 30 | } |
| 31 | } |
| 32 | var writes, sources int |
| 33 | if err := c.db.QueryRow(`SELECT count(*) FROM metadata_writes WHERE topic_id LIKE 'source-%'`).Scan(&writes); err != nil || writes != 0 { |
| 34 | t.Fatalf("metadata refresh rewrote unrelated history: %d %v", writes, err) |
| 35 | } |
| 36 | if err := c.db.QueryRow(`SELECT count(*) FROM catalog_topics WHERE topic_id LIKE 'source-%'`).Scan(&sources); err != nil || sources != 1000 { |
| 37 | t.Fatalf("source projection changed: %d %v", sources, err) |
| 38 | } |
| 39 | rows, err := c.db.Query(`EXPLAIN QUERY PLAN ` + registeredMetadataTopics) |
| 40 | if err != nil { |
| 41 | t.Fatal(err) |
| 42 | } |
| 43 | defer rows.Close() |
| 44 | usedIndex := false |
| 45 | for rows.Next() { |
| 46 | var id, parent, unused int |
| 47 | var detail string |
| 48 | if err := rows.Scan(&id, &parent, &unused, &detail); err != nil { |
| 49 | t.Fatal(err) |
| 50 | } |
| 51 | usedIndex = usedIndex || strings.Contains(detail, "idx_catalog_topics_registered_metadata") |
| 52 | } |
| 53 | if err := rows.Err(); err != nil || !usedIndex { |
| 54 | t.Fatalf("metadata membership scanned all history: %v", err) |
| 55 | } |
| 56 | } |
| 57 | |
| 58 | func TestMetadataRefreshLeavesSourceRetirementToMutationOwner(t *testing.T) { |
| 59 | c, err := Open(t.Context(), Options{InMemory: true, MetadataOnly: true, StartPaused: true}) |
| 60 | if err != nil { |
| 61 | t.Fatal(err) |
| 62 | } |
| 63 | defer c.Close(context.Background()) |
| 64 | dir := t.TempDir() |
| 65 | path := filepath.Join(dir, "source.jsonl") |
| 66 | if err := os.WriteFile(path, []byte("body is not metadata"), 0600); err != nil { |
| 67 | t.Fatal(err) |
| 68 | } |
| 69 | if err := c.ReconcileDirectory(t.Context(), DirectoryTarget{Path: dir, Scope: "global"}); err != nil { |
| 70 | t.Fatal(err) |
| 71 | } |
| 72 | record, exists, err := c.GetSession(t.Context(), path) |
| 73 | if err != nil || !exists { |
| 74 | t.Fatalf("source missing: %v %v", exists, err) |
| 75 | } |
| 76 | if err := c.SyncMetadata(t.Context(), nil, []TopicMetadata{{Scope: "global", TopicID: record.TopicID, Title: "Registered"}}); err != nil { |
| 77 | t.Fatal(err) |
| 78 | } |
| 79 | if err := c.SyncMetadata(t.Context(), nil, nil); err != nil { |
| 80 | t.Fatal(err) |
| 81 | } |
| 82 | if _, exists, err := c.GetTopic(t.Context(), TopicKey{Scope: "global", TopicID: record.TopicID}); err != nil || !exists { |
| 83 | t.Fatalf("retiring registry membership lost a live source: %v %v", exists, err) |
| 84 | } |
| 85 | if err := c.RemoveSession(t.Context(), path, "fixture"); err != nil { |
| 86 | t.Fatal(err) |
| 87 | } |
| 88 | var count int |
| 89 | if err := c.db.QueryRow(`SELECT count(*) FROM catalog_topics WHERE topic_id=?`, record.TopicID).Scan(&count); err != nil || count != 0 { |
| 90 | t.Fatalf("source removal left an orphan: %d %v", count, err) |
| 91 | } |
| 92 | } |
| 93 | |
| 94 | func TestMetadataRefreshUsesOldWritersFlagsAndRollsBackFailedSlice(t *testing.T) { |
| 95 | c, err := Open(t.Context(), Options{InMemory: true, MetadataOnly: true, StartPaused: true}) |
| 96 | if err != nil { |
| 97 | t.Fatal(err) |
| 98 | } |
| 99 | defer c.Close(context.Background()) |
| 100 | if err := c.SyncMetadata(t.Context(), nil, []TopicMetadata{{Scope: "global", TopicID: "keep", Title: "Saved"}}); err != nil { |
| 101 | t.Fatal(err) |
| 102 | } |
| 103 | _, err = c.db.Exec(`INSERT INTO catalog_topics(scope,workspace_root,workspace_root_key,topic_id,title,metadata_present) |
| 104 | VALUES('global','','','older-writer','Old writer',1); |
| 105 | CREATE TRIGGER reject_metadata BEFORE INSERT ON catalog_topics WHEN NEW.topic_id='reject' BEGIN SELECT RAISE(ABORT,'fixture rejection'); END;`) |
| 106 | if err != nil { |
| 107 | t.Fatal(err) |
| 108 | } |
| 109 | if err := c.SyncMetadata(t.Context(), nil, []TopicMetadata{{Scope: "global", TopicID: "reject"}}); err == nil { |
| 110 | t.Fatal("failed refresh committed") |
| 111 | } |
| 112 | var registered int |
| 113 | if err := c.db.QueryRow(`SELECT count(*) FROM catalog_topics WHERE metadata_present=1`).Scan(®istered); err != nil || registered != 2 { |
| 114 | t.Fatalf("failed refresh lost prior membership: %d %v", registered, err) |
| 115 | } |
| 116 | if err := c.SyncMetadata(t.Context(), nil, []TopicMetadata{{Scope: "global", TopicID: "keep", Title: "Updated"}}); err != nil { |
| 117 | t.Fatal(err) |
| 118 | } |
| 119 | var retired int |
| 120 | if err := c.db.QueryRow(`SELECT count(*) FROM catalog_topics WHERE topic_id='older-writer'`).Scan(&retired); err != nil || retired != 0 { |
| 121 | t.Fatalf("older writer row was not retired: %d %v", retired, err) |
| 122 | } |
| 123 | // GetTopic deliberately suppresses shells without source rows. Verify the |
| 124 | // stored registry projection itself, not that unrelated visibility rule. |
| 125 | var title string |
| 126 | if err := c.db.QueryRow(`SELECT title FROM catalog_topics WHERE topic_id='keep' AND metadata_present=1`).Scan(&title); err != nil || title != "Updated" { |
| 127 | t.Fatalf("current metadata missing: %q %v", title, err) |
| 128 | } |
| 129 | } |
| 130 |