oficina / memoria Public

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

Plain git — no account needed to clone.

Download

Download this file

1package memoria
2
3import (
4 "amadan.net/oficina/suite"
5 "bytes"
6 "crypto/sha256"
7 "encoding/base64"
8 "encoding/json"
9 "net/http"
10 "net/http/httptest"
11 "os"
12 "os/exec"
13 "path/filepath"
14 "strings"
15 "testing"
16 "time"
17)
18
19// Runtime gate cases selected independently by native_review, 2026-09-12.
20// Mutated fixed Alice subject, note key by title and removed session middleware:
21// each failed its runtime assertion on 2026-09-12; all reverted.
22func TestNativeSearchUsesCurrentPermission(t *testing.T) {
23 f := newHomeRouterFixture(t)
24 custody := &fixedCustody{apps: docsRoster(), give: suite.VISIBLE}
25 f.app.store.custody = custody
26 for _, spec := range []IngestSpec{{Source: "Upload", Key: "own", Title: "Own nebula", Body: "nebula local content"}, {Source: "Docs", Key: "sibling", Title: "Shared nebula", Body: "nebula sibling sentinel", ItemID: "doc-one", SiblingRevision: 3}} {
27 if _, e := f.app.store.Ingest(t.Context(), spec); e != nil {
28 t.Fatal(e)
29 }
30 }
31 search := func() *httptest.ResponseRecorder {
32 r := httptest.NewRequest("POST", homeTestOrigin+"/api/native/search", strings.NewReader(`{"query":"nebula"}`))
33 r.Header.Set("Content-Type", "application/json")
34 r.Header.Set("Origin", homeTestOrigin)
35 for _, c := range f.cookies {
36 r.AddCookie(c)
37 }
38 w := httptest.NewRecorder()
39 f.handler.ServeHTTP(w, r)
40 return w
41 }
42 if w := search(); w.Code == 200 {
43 t.Fatal("anonymous search succeeded")
44 }
45 f.local(t)
46 f.cookies = nativeClientCookies(t, f.handler, f.cookies)
47 if w := search(); w.Code != 200 || !strings.Contains(w.Body.String(), "Shared nebula") || !strings.Contains(w.Body.String(), "Own nebula") {
48 t.Fatal("visible control failed", w.Code)
49 }
50 if custody.asker.Value != "alice@example.test" || custody.asker.Kind != suite.SubjectAddress {
51 t.Fatal("wrong requesting subject")
52 }
53 if binary := os.Getenv("NATIVE_REVIEW_BINARY"); binary != "" || os.Getenv("NATIVE_REVIEW_SIMULATOR") != "" {
54 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
55 // The fixture's canonical test origin differs from its ephemeral listener.
56 // This adapter is exclusively in tests; real origin enforcement is covered above.
57 r.Header.Set("Origin", homeTestOrigin)
58 r.Host = strings.TrimPrefix(homeTestOrigin, "https://")
59 r.URL.Scheme = "https"
60 r.URL.Host = r.Host
61 f.handler.ServeHTTP(w, r)
62 }))
63 defer server.Close()
64 var credential map[string]string
65 for _, c := range f.cookies {
66 if strings.Contains(c.Name, "session") {
67 credential = map[string]string{"name": c.Name, "value": c.Value}
68 }
69 }
70 if credential == nil {
71 t.Fatal("fixture session missing")
72 }
73 input, _ := json.Marshal(map[string]any{"origin": server.URL, "credential": credential, "output": os.Getenv("NATIVE_REVIEW_OUTPUT")})
74 if simulator := os.Getenv("NATIVE_REVIEW_SIMULATOR"); simulator != "" {
75 bundle := "net.amadan.oficina.memoria.review"
76 run := func(args ...string) []byte {
77 t.Helper()
78 out, err := exec.CommandContext(t.Context(), "xcrun", append([]string{"simctl"}, args...)...).CombinedOutput()
79 if err != nil {
80 t.Fatalf("simulator: %v: %s", err, out)
81 }
82 return out
83 }
84 container := strings.TrimSpace(string(run("get_app_container", simulator, bundle, "data")))
85 path := filepath.Join(container, "Documents", "review.json")
86 if err := os.WriteFile(path, input, 0600); err != nil {
87 t.Fatal(err)
88 }
89 defer os.Remove(path)
90
91 if os.Getenv("NATIVE_REVIEW_UI_TESTS") != "" {
92 command := exec.CommandContext(t.Context(), "xcodebuild", "-project", "native/ios/MemoriaPhone.xcodeproj", "-scheme", "MemoriaReviewPhone", "-destination", "platform=iOS Simulator,id="+simulator, "-parallel-testing-enabled", "NO", "-derivedDataPath", "native/build/ios", "CODE_SIGNING_ALLOWED=NO", "test")
93 command.Dir = "../.."
94 output, err := command.CombinedOutput()
95 _ = os.WriteFile(filepath.Join(os.TempDir(), "memoria-native-ui-tests.log"), output, 0600)
96 if err != nil {
97 t.Fatal("Native UI tests failed; see temporary xcodebuild log", err)
98 }
99 }
100 run("launch", simulator, bundle)
101 time.Sleep(3 * time.Second)
102 run("io", simulator, "screenshot", os.Getenv("NATIVE_REVIEW_OUTPUT"))
103 run("terminate", simulator, bundle)
104 } else {
105 cmd := exec.CommandContext(t.Context(), binary)
106 cmd.Stdin = bytes.NewReader(input)
107 if output, err := cmd.CombinedOutput(); err != nil {
108 t.Fatalf("native review: %v: %s", err, output)
109 }
110 }
111
112 }
113
114 for _, verdict := range []suite.Authorization{suite.NOT_VISIBLE, suite.UNKNOWN} {
115 custody.give = verdict
116 w := search()
117 var out struct{ Results []struct{ Title string } }
118 if w.Code != 200 || json.Unmarshal(w.Body.Bytes(), &out) != nil {
119 t.Fatal("search failed", w.Code)
120 }
121 if len(out.Results) != 1 || out.Results[0].Title != "Own nebula" || strings.Contains(w.Body.String(), "sentinel") {
122 t.Fatal("denied sibling returned")
123 }
124 }
125 f.app.SetOwners([]string{"bob@example.test"})
126 if w := search(); w.Code == http.StatusOK {
127 t.Fatal("removed member could search")
128 }
129 f.cookies = map[string]*http.Cookie{}
130 f.localAs(t, "bob@example.test")
131 custody.give = suite.VISIBLE
132 if w := search(); w.Code != 200 || !strings.Contains(w.Body.String(), "Shared nebula") {
133 t.Fatal("second member control failed")
134 }
135 if custody.asker.Value != "bob@example.test" {
136 t.Fatal("second member searched as another identity")
137 }
138
139}
140
141func nativeClientCookies(t *testing.T, handler http.Handler, browser map[string]*http.Cookie) map[string]*http.Cookie {
142 t.Helper()
143 verifier := base64.RawURLEncoding.EncodeToString(bytes.Repeat([]byte{42}, 32))
144 hash := sha256.Sum256([]byte(verifier))
145 call := func(path, body string, signed bool) *httptest.ResponseRecorder {
146 r := httptest.NewRequest("POST", homeTestOrigin+path, strings.NewReader(body))
147 r.Header.Set("Origin", homeTestOrigin)
148 r.Header.Set("Content-Type", "application/json")
149 if signed {
150 for _, c := range browser {
151 r.AddCookie(c)
152 }
153 }
154 w := httptest.NewRecorder()
155 handler.ServeHTTP(w, r)
156 return w
157 }
158 w := call("/native/request", `{"challenge":"`+base64.RawURLEncoding.EncodeToString(hash[:])+`"}`, false)
159 var pending struct{ ID string }
160 if w.Code != 200 || json.Unmarshal(w.Body.Bytes(), &pending) != nil || pending.ID == "" {
161 t.Fatal("native begin failed", w.Code)
162 }
163 if w = call("/native/authorize?id="+pending.ID, "", true); w.Code != 200 {
164 t.Fatal("native approval failed", w.Code)
165 }
166 w = call("/native/exchange", `{"id":"`+pending.ID+`","verifier":"`+verifier+`"}`, false)
167 var session struct{ Name, Value string }
168 if w.Code != 200 || json.Unmarshal(w.Body.Bytes(), &session) != nil || session.Value == "" {
169 t.Fatal("native exchange failed", w.Code)
170 }
171 return map[string]*http.Cookie{session.Name: {Name: session.Name, Value: session.Value}}
172}
173
174func TestNativeNotesSameTitleCreatesDistinctDocuments(t *testing.T) {
175 f := newHomeRouterFixture(t)
176 create := func(body string) *httptest.ResponseRecorder {
177 r := httptest.NewRequest("POST", homeTestOrigin+"/api/native/notes", strings.NewReader(`{"title":"Meeting notes","body":"`+body+`"}`))
178 r.Header.Set("Origin", homeTestOrigin)
179 r.Header.Set("Content-Type", "application/json")
180 for _, c := range f.cookies {
181 r.AddCookie(c)
182 }
183 w := httptest.NewRecorder()
184 f.handler.ServeHTTP(w, r)
185 return w
186 }
187 if create("anonymous sentinel").Code == 200 {
188 t.Fatal("anonymous note created")
189 }
190 f.local(t)
191 ids := map[int64]bool{}
192 for _, body := range []string{"first nebula content", "second nebula content"} {
193 w := create(body)
194 var got struct {
195 ID int64
196 Indexed bool
197 }
198 if w.Code != 200 || json.Unmarshal(w.Body.Bytes(), &got) != nil || got.ID == 0 || !got.Indexed {
199 t.Fatal("note not confirmed", w.Code)
200 }
201 if ids[got.ID] {
202 t.Fatal("same title replaced a prior note")
203 }
204 ids[got.ID] = true
205 }
206 found, err := f.app.store.Search(t.Context(), suite.Subject{Kind: suite.SubjectAddress, Value: "alice@example.test"}, "nebula", 25)
207 if err != nil || len(found.Results) != 2 {
208 t.Fatal("distinct note content was lost")
209 }
210}
211
212// B10: a save whose response was lost is retried with the same draft's
213// client_id, and the retry must be a no-op — same id back, one document
214// stored. Mutations verified red 2026-09-23: keying on a server-minted
215// uuid again (the old behaviour) -> replay returns a second id; keying on
216// the raw client_id -> the uppercase replay makes a second note; dropping
217// the 36-character check -> braced and urn: forms are accepted; the
218// title folded into the key (Fable's M1) -> the edited replay makes a
219// second note; the uuid.Nil refusal removed -> the zero UUID is accepted.
220func TestNativeNoteReplayWithSameClientIDIsANoOp(t *testing.T) {
221 f := newHomeRouterFixture(t)
222 f.local(t)
223 post := func(payload map[string]string) (int, int64) {
224 t.Helper()
225 raw, _ := json.Marshal(payload)
226 r := httptest.NewRequest("POST", homeTestOrigin+"/api/native/notes", strings.NewReader(string(raw)))
227 r.Header.Set("Origin", homeTestOrigin)
228 r.Header.Set("Content-Type", "application/json")
229 for _, c := range f.cookies {
230 r.AddCookie(c)
231 }
232 w := httptest.NewRecorder()
233 f.handler.ServeHTTP(w, r)
234 var got struct{ ID int64 }
235 _ = json.Unmarshal(w.Body.Bytes(), &got)
236 return w.Code, got.ID
237 }
238 documents := func() int64 {
239 t.Helper()
240 var n int64
241 if err := f.app.db.G.Model(&Document{}).Count(&n).Error; err != nil {
242 t.Fatal(err)
243 }
244 return n
245 }
246
247 const draft = "6f1c2a0e-9a55-4c7e-8d3b-2f4e5a6b7c8d"
248 note := map[string]string{"title": "Standup", "body": "replayed nebula content", "client_id": draft}
249 code, first := post(note)
250 if code != 200 || first == 0 {
251 t.Fatalf("first save: %d", code)
252 }
253 before := documents()
254
255 code, again := post(note)
256 if code != 200 || again != first {
257 t.Fatalf("replay: status %d id %d, want 200 and the first id %d", code, again, first)
258 }
259 // Swift's UUID().uuidString is uppercase; the key is canonical.
260 upper := map[string]string{"title": note["title"], "body": note["body"], "client_id": strings.ToUpper(draft)}
261 if code, id := post(upper); code != 200 || id != first {
262 t.Fatalf("uppercase replay: status %d id %d, want the first id %d", code, id, first)
263 }
264 if after := documents(); after != before {
265 t.Fatalf("replay stored %d more document(s)", after-before)
266 }
267
268 // The presence half: a different draft with the same words is a new note,
269 // and so is a note from an old client that sends no client_id.
270 other := map[string]string{"title": note["title"], "body": note["body"], "client_id": "0d9e8f7a-6b5c-4d3e-9f1a-2b3c4d5e6f70"}
271 if code, id := post(other); code != 200 || id == first || id == 0 {
272 t.Fatalf("a different client_id: status %d id %d, want a new note", code, id)
273 }
274 legacy := map[string]string{"title": note["title"], "body": note["body"]}
275 _, l1 := post(legacy)
276 _, l2 := post(legacy)
277 if l1 == 0 || l2 == 0 || l1 == l2 {
278 t.Fatalf("no client_id: ids %d and %d, want two distinct notes", l1, l2)
279 }
280
281 // The other half of B10: the phone lost the response AND the person
282 // then fixed a typo before pressing Save again. Still one draft, so
283 // still one note — revised, not duplicated.
284 t.Run("an edited replay revises the same note", func(t *testing.T) {
285 revision := func(id int64) int {
286 t.Helper()
287 var d Document
288 if err := f.app.db.G.First(&d, id).Error; err != nil {
289 t.Fatal(err)
290 }
291 return d.Revision
292 }
293 was, count := revision(first), documents()
294 edited := map[string]string{"title": "Standup (fixed)", "body": "replayed nebula content, typo fixed", "client_id": draft}
295 if code, id := post(edited); code != 200 || id != first {
296 t.Fatalf("edited replay: status %d id %d, want the first id %d", code, id, first)
297 }
298 if n := documents(); n != count {
299 t.Fatalf("edited replay stored %d more document(s)", n-count)
300 }
301 if now := revision(first); now != was+1 {
302 t.Fatalf("revision %d -> %d, want a bump of one", was, now)
303 }
304 })
305
306 for _, bad := range []string{"00000000-0000-0000-0000-000000000000", "not-a-uuid", "{" + draft + "}", "urn:uuid:" + draft, strings.ReplaceAll(draft, "-", "")} {
307 if code, _ := post(map[string]string{"title": "x", "body": "y", "client_id": bad}); code != 400 {
308 t.Errorf("client_id %q: status %d, want 400", bad, code)
309 }
310 }
311}
312
313// Native search must not call the model. Its response has no field for
314// the fact box, so a model call there is egress for an answer the native
315// client never receives. Through the real native route, with AI on.
316//
317// The presence half is the SAME store and query through the web search,
318// which does call the model — without it, "zero calls" would be true of
319// any setup in which the model could not be reached at all.
320func TestNativeSearchDoesNotCallTheModel(t *testing.T) {
321 f := newHomeRouterFixture(t)
322 if _, err := f.app.store.Ingest(t.Context(), IngestSpec{Source: "Upload", Key: "office", Title: "Tito office", Body: "## Contact\n\nOffice address: 12 Main Street, Dublin 2"}); err != nil {
323 t.Fatal(err)
324 }
325 rec := &recorder{reply: point(0, "12 Main Street, Dublin 2")}
326 f.app.store.extractor = rec
327 f.local(t)
328 f.cookies = nativeClientCookies(t, f.handler, f.cookies)
329
330 r := httptest.NewRequest("POST", homeTestOrigin+"/api/native/search", strings.NewReader(`{"query":"office address"}`))
331 r.Header.Set("Content-Type", "application/json")
332 r.Header.Set("Origin", homeTestOrigin)
333 for _, c := range f.cookies {
334 r.AddCookie(c)
335 }
336 w := httptest.NewRecorder()
337 f.handler.ServeHTTP(w, r)
338 if w.Code != 200 || !strings.Contains(w.Body.String(), "Tito office") {
339 t.Fatalf("native search did not find the document (%d); the zero below would prove nothing", w.Code)
340 }
341 if len(rec.calls) != 0 {
342 t.Errorf("native search sent %d requests to the model for an answer it cannot show", len(rec.calls))
343 }
344
345 if _, err := f.app.store.Search(t.Context(), suite.Subject{Kind: suite.SubjectAddress, Value: "alice@example.test"}, "office address", 25); err != nil {
346 t.Fatal(err)
347 }
348 if len(rec.calls) != 1 {
349 t.Errorf("the web search made %d calls; the native zero is meaningful only if this is 1", len(rec.calls))
350 }
351}
352