oficina / memoria Public

Clone
git clone https://amadan.net/oficina/memoria

Plain git — no account needed to clone.

Download

Download this file

1package memoriatest
2
3import (
4 "context"
5 "os"
6 "path/filepath"
7 "strings"
8 "testing"
9
10 memoria "amadan.net/oficina/memoria/internal/memoria"
11)
12
13// A page with ancestry, as a connector would supply it.
14func notionish(title, body string, path ...string) memoria.IngestSpec {
15 return memoria.IngestSpec{
16 Source: "notion", Key: "page-" + title, Title: title, Body: body,
17 Path: path, AllowTitleOnly: true,
18 }
19}
20
21// TestTitleOnlyIsRefusedForPeopleAndAcceptedForConnectors pins the
22// asymmetry AllowTitleOnly exists for.
23//
24// An empty paste is a person's mistake and the useful answer is to refuse
25// it. An empty Notion page is a fact about the source — real workspaces
26// are full of placeholders — and both alternatives to storing it are
27// wrong: failing the run lets one placeholder block an entire import, and
28// skipping makes the page an absence with nothing to explain it.
29//
30// MUTATION-TESTED 2026-09-05: deleting the AllowTitleOnly branch in
31// Store.Ingest makes the connector half fail with "the document is
32// empty"; defaulting AllowTitleOnly to true makes the person half fail.
33// Reverted.
34func TestTitleOnlyIsRefusedForPeopleAndAcceptedForConnectors(t *testing.T) {
35 _, store, _ := newAppWithStore(t)
36 ctx := context.Background()
37
38 if _, err := store.Ingest(ctx, memoria.IngestSpec{
39 Source: "paste", Key: "k", Title: "A title", Body: " ",
40 }); err == nil {
41 t.Error("an empty paste was accepted; a person's empty submission should be refused")
42 }
43
44 id, err := store.Ingest(ctx, notionish("Quarterly planning", "", "Handbook"))
45 if err != nil {
46 t.Fatalf("a title-only Notion page was refused: %v", err)
47 }
48 if id == 0 {
49 t.Fatal("no document id")
50 }
51 // It must be FINDABLE by the only words it has. Storing it with an
52 // empty body would index nothing and hide it.
53 hits, err := searchAsReader(store, ctx, "quarterly planning", 10)
54 if err != nil {
55 t.Fatal(err)
56 }
57 if len(hits) == 0 {
58 t.Error("a title-only page was stored but is not searchable by its title")
59 }
60}
61
62// TestRenameReachesMemoriaWithoutBumpingRevision.
63//
64// Store.Ingest short-circuits when the body fingerprint is unchanged. It
65// used to return there unconditionally, so a page renamed upstream kept
66// its old title in memoria forever — the body never changes on a rename,
67// so nothing else would ever notice. Found by a design review.
68//
69// The revision must NOT move: a citation points at passages, and those
70// have not changed. A revision that moves without the content moving
71// makes every cached authorization stale for no reason.
72//
73// MUTATION-TESTED 2026-09-05: restoring the unconditional `unchanged =
74// true; return nil` makes the title assertion fail. Bumping the revision
75// in the title-only branch makes the revision assertion fail. Reverted.
76func TestRenameReachesMemoriaWithoutBumpingRevision(t *testing.T) {
77 _, store, _ := newAppWithStore(t)
78 ctx := context.Background()
79 const body = "Expenses are reimbursed within thirty days of submission."
80
81 spec := memoria.IngestSpec{Source: "notion", Key: "page-1", Title: "Old name", Body: body, AllowTitleOnly: true}
82 id, err := store.Ingest(ctx, spec)
83 if err != nil {
84 t.Fatal(err)
85 }
86 _, rev0, _, err := store.Meta(ctx, id)
87 if err != nil {
88 t.Fatal(err)
89 }
90
91 spec.Title = "New name"
92 if _, err := store.Ingest(ctx, spec); err != nil {
93 t.Fatal(err)
94 }
95 title, _, err := store.Body(ctx, id)
96 if err != nil {
97 t.Fatal(err)
98 }
99 if title != "New name" {
100 t.Errorf("title is %q after a rename, want %q — the rename never reached memoria", title, "New name")
101 }
102 _, rev1, _, err := store.Meta(ctx, id)
103 if err != nil {
104 t.Fatal(err)
105 }
106 if rev1 != rev0 {
107 t.Errorf("revision moved %d→%d on a rename; the passages did not change", rev0, rev1)
108 }
109}
110
111// TestMoveReachesMemoriaAndReindexes.
112//
113// A page moved to a different parent — or one whose ANCESTOR was renamed
114// — has an unchanged body and an unchanged title. Only the path moves,
115// and the path is what the index scores in its own column, so the index
116// must be rebuilt for that document or the old ancestry stays searchable
117// and the new one never becomes searchable.
118//
119// MUTATION-TESTED 2026-09-05: dropping `sealed_path` from the title/path
120// Updates map leaves the old ancestry findable and fails this. Reverted.
121func TestMoveReachesMemoriaAndReindexes(t *testing.T) {
122 _, store, _ := newAppWithStore(t)
123 ctx := context.Background()
124 const body = "Laptops are ordered through the equipment request form."
125
126 spec := memoria.IngestSpec{
127 Source: "notion", Key: "page-2", Title: "Equipment", Body: body,
128 Path: []string{"Handbook", "Onboarding"}, AllowTitleOnly: true,
129 }
130 if _, err := store.Ingest(ctx, spec); err != nil {
131 t.Fatal(err)
132 }
133 if hits, _ := searchAsReader(store, ctx, "onboarding", 10); len(hits) == 0 {
134 t.Fatal("the original ancestry is not searchable, so this test proves nothing")
135 }
136
137 // The page moves under Engineering.
138 spec.Path = []string{"Engineering", "Platform"}
139 if _, err := store.Ingest(ctx, spec); err != nil {
140 t.Fatal(err)
141 }
142 if hits, _ := searchAsReader(store, ctx, "platform", 10); len(hits) == 0 {
143 t.Error("the new ancestry is not searchable: the move did not reach the index")
144 }
145 if hits, _ := searchAsReader(store, ctx, "onboarding", 10); len(hits) != 0 {
146 t.Error("the OLD ancestry is still searchable after a move: a stale coordinate survives")
147 }
148}
149
150// TestAncestryIsSealedInTheDatabase. A path is content — "Acquisitions ›
151// Project Bluebird" names a thing whether or not anyone opens the page —
152// so it is sealed like a title, under its own purpose.
153//
154// The existing plaintext gate cannot cover this: it drives the HTTP form,
155// and no form carries ancestry.
156//
157// MUTATION-TESTED 2026-09-05: storing pathStr with a plain []byte cast
158// instead of Seal makes this fail. Reverted.
159func TestAncestryIsSealedInTheDatabase(t *testing.T) {
160 _, store, path := newAppWithStore(t)
161 ctx := context.Background()
162 const secret = "PROJECTBLUEBIRD7742"
163
164 if _, err := store.Ingest(ctx, notionish("Deal terms",
165 "The terms were agreed in March and revisited in June.", "Acquisitions", secret)); err != nil {
166 t.Fatal(err)
167 }
168 // Vacuity guard: the ancestry must actually be indexed, or the scan
169 // below is a scan of nothing relevant.
170 if hits, _ := searchAsReader(store, ctx, secret, 10); len(hits) == 0 {
171 t.Fatal("the ancestry is not searchable, so it may not have been stored at all")
172 }
173
174 entries, err := os.ReadDir(filepath.Dir(path))
175 if err != nil {
176 t.Fatal(err)
177 }
178 checked := 0
179 for _, e := range entries {
180 if e.IsDir() {
181 continue
182 }
183 raw, err := os.ReadFile(filepath.Join(filepath.Dir(path), e.Name()))
184 if err != nil {
185 t.Fatal(err)
186 }
187 checked++
188 if strings.Contains(strings.ToUpper(string(raw)), secret) {
189 t.Errorf("%s holds the ancestry in plaintext: a path is content", e.Name())
190 }
191 }
192 if checked == 0 {
193 t.Fatal("no files to check; the gate proved nothing")
194 }
195}
196