package memoriatest import ( "context" "os" "path/filepath" "strings" "testing" memoria "amadan.net/oficina/memoria/internal/memoria" ) // A page with ancestry, as a connector would supply it. func notionish(title, body string, path ...string) memoria.IngestSpec { return memoria.IngestSpec{ Source: "notion", Key: "page-" + title, Title: title, Body: body, Path: path, AllowTitleOnly: true, } } // TestTitleOnlyIsRefusedForPeopleAndAcceptedForConnectors pins the // asymmetry AllowTitleOnly exists for. // // An empty paste is a person's mistake and the useful answer is to refuse // it. An empty Notion page is a fact about the source — real workspaces // are full of placeholders — and both alternatives to storing it are // wrong: failing the run lets one placeholder block an entire import, and // skipping makes the page an absence with nothing to explain it. // // MUTATION-TESTED 2026-09-05: deleting the AllowTitleOnly branch in // Store.Ingest makes the connector half fail with "the document is // empty"; defaulting AllowTitleOnly to true makes the person half fail. // Reverted. func TestTitleOnlyIsRefusedForPeopleAndAcceptedForConnectors(t *testing.T) { _, store, _ := newAppWithStore(t) ctx := context.Background() if _, err := store.Ingest(ctx, memoria.IngestSpec{ Source: "paste", Key: "k", Title: "A title", Body: " ", }); err == nil { t.Error("an empty paste was accepted; a person's empty submission should be refused") } id, err := store.Ingest(ctx, notionish("Quarterly planning", "", "Handbook")) if err != nil { t.Fatalf("a title-only Notion page was refused: %v", err) } if id == 0 { t.Fatal("no document id") } // It must be FINDABLE by the only words it has. Storing it with an // empty body would index nothing and hide it. hits, err := searchAsReader(store, ctx, "quarterly planning", 10) if err != nil { t.Fatal(err) } if len(hits) == 0 { t.Error("a title-only page was stored but is not searchable by its title") } } // TestRenameReachesMemoriaWithoutBumpingRevision. // // Store.Ingest short-circuits when the body fingerprint is unchanged. It // used to return there unconditionally, so a page renamed upstream kept // its old title in memoria forever — the body never changes on a rename, // so nothing else would ever notice. Found by a design review. // // The revision must NOT move: a citation points at passages, and those // have not changed. A revision that moves without the content moving // makes every cached authorization stale for no reason. // // MUTATION-TESTED 2026-09-05: restoring the unconditional `unchanged = // true; return nil` makes the title assertion fail. Bumping the revision // in the title-only branch makes the revision assertion fail. Reverted. func TestRenameReachesMemoriaWithoutBumpingRevision(t *testing.T) { _, store, _ := newAppWithStore(t) ctx := context.Background() const body = "Expenses are reimbursed within thirty days of submission." spec := memoria.IngestSpec{Source: "notion", Key: "page-1", Title: "Old name", Body: body, AllowTitleOnly: true} id, err := store.Ingest(ctx, spec) if err != nil { t.Fatal(err) } _, rev0, _, err := store.Meta(ctx, id) if err != nil { t.Fatal(err) } spec.Title = "New name" if _, err := store.Ingest(ctx, spec); err != nil { t.Fatal(err) } title, _, err := store.Body(ctx, id) if err != nil { t.Fatal(err) } if title != "New name" { t.Errorf("title is %q after a rename, want %q — the rename never reached memoria", title, "New name") } _, rev1, _, err := store.Meta(ctx, id) if err != nil { t.Fatal(err) } if rev1 != rev0 { t.Errorf("revision moved %d→%d on a rename; the passages did not change", rev0, rev1) } } // TestMoveReachesMemoriaAndReindexes. // // A page moved to a different parent — or one whose ANCESTOR was renamed // — has an unchanged body and an unchanged title. Only the path moves, // and the path is what the index scores in its own column, so the index // must be rebuilt for that document or the old ancestry stays searchable // and the new one never becomes searchable. // // MUTATION-TESTED 2026-09-05: dropping `sealed_path` from the title/path // Updates map leaves the old ancestry findable and fails this. Reverted. func TestMoveReachesMemoriaAndReindexes(t *testing.T) { _, store, _ := newAppWithStore(t) ctx := context.Background() const body = "Laptops are ordered through the equipment request form." spec := memoria.IngestSpec{ Source: "notion", Key: "page-2", Title: "Equipment", Body: body, Path: []string{"Handbook", "Onboarding"}, AllowTitleOnly: true, } if _, err := store.Ingest(ctx, spec); err != nil { t.Fatal(err) } if hits, _ := searchAsReader(store, ctx, "onboarding", 10); len(hits) == 0 { t.Fatal("the original ancestry is not searchable, so this test proves nothing") } // The page moves under Engineering. spec.Path = []string{"Engineering", "Platform"} if _, err := store.Ingest(ctx, spec); err != nil { t.Fatal(err) } if hits, _ := searchAsReader(store, ctx, "platform", 10); len(hits) == 0 { t.Error("the new ancestry is not searchable: the move did not reach the index") } if hits, _ := searchAsReader(store, ctx, "onboarding", 10); len(hits) != 0 { t.Error("the OLD ancestry is still searchable after a move: a stale coordinate survives") } } // TestAncestryIsSealedInTheDatabase. A path is content — "Acquisitions › // Project Bluebird" names a thing whether or not anyone opens the page — // so it is sealed like a title, under its own purpose. // // The existing plaintext gate cannot cover this: it drives the HTTP form, // and no form carries ancestry. // // MUTATION-TESTED 2026-09-05: storing pathStr with a plain []byte cast // instead of Seal makes this fail. Reverted. func TestAncestryIsSealedInTheDatabase(t *testing.T) { _, store, path := newAppWithStore(t) ctx := context.Background() const secret = "PROJECTBLUEBIRD7742" if _, err := store.Ingest(ctx, notionish("Deal terms", "The terms were agreed in March and revisited in June.", "Acquisitions", secret)); err != nil { t.Fatal(err) } // Vacuity guard: the ancestry must actually be indexed, or the scan // below is a scan of nothing relevant. if hits, _ := searchAsReader(store, ctx, secret, 10); len(hits) == 0 { t.Fatal("the ancestry is not searchable, so it may not have been stored at all") } entries, err := os.ReadDir(filepath.Dir(path)) if err != nil { t.Fatal(err) } checked := 0 for _, e := range entries { if e.IsDir() { continue } raw, err := os.ReadFile(filepath.Join(filepath.Dir(path), e.Name())) if err != nil { t.Fatal(err) } checked++ if strings.Contains(strings.ToUpper(string(raw)), secret) { t.Errorf("%s holds the ancestry in plaintext: a path is content", e.Name()) } } if checked == 0 { t.Fatal("no files to check; the gate proved nothing") } }