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 "html"
5 "net/http"
6 "net/url"
7 "regexp"
8 "strings"
9 "testing"
10)
11
12// The fact box, end to end: through the real handler, the real template
13// and the real escaping.
14//
15// **Through HTTP and not against the struct, deliberately, and gate 34 is
16// the opposite deliberately.** Gate 34 is about what the box may CONTAIN,
17// where two dispositions render identical pages and only the struct can
18// tell them apart. These are about what reaches the reader, where a
19// template that escapes a span, drops a label or renders the box outside
20// the swapped region is exactly the defect — and a struct assertion
21// cannot see any of it. AGENTS.md: a template test proves markup exists,
22// not that it renders.
23
24var factValueRe = regexp.MustCompile(`<span rst-fact-value>([^<]*)</span>`)
25var factLabelRe = regexp.MustCompile(`<span rst-fact-label>([^<]*)</span>`)
26
27func factValues(body string) []string { return capture(factValueRe, body) }
28func factLabels(body string) []string { return capture(factLabelRe, body) }
29func hasFactBox(body string) bool { return strings.Contains(body, "<div rst-fact-box>") }
30
31// capture pulls the rendered text out of the box and UNESCAPES it.
32//
33// html/template escapes "+" to "&#43;" in text content, which is correct
34// and which the reader never sees. Gate 36 is about the bytes that reach
35// the READER — "byte-identical to the document" is a claim about what is
36// displayed, not about the wire — so comparing raw markup would fail the
37// gate on the escaping working properly, and "fixing" that would mean
38// unescaping the span into the page, which is an injection.
39func capture(re *regexp.Regexp, s string) []string {
40 var out []string
41 for _, m := range re.FindAllStringSubmatch(s, -1) {
42 out = append(out, html.UnescapeString(m[1]))
43 }
44 return out
45}
46
47func search(t *testing.T, h http.Handler, q string) string {
48 t.Helper()
49 return get(t, h, "/?q="+url.QueryEscape(q)).Body.String()
50}
51
52// GATE 35 — candidates are canonical values, ambiguity is enumerated
53// rather than collapsed, and the cap is three (§8.8, §10).
54//
55// One test rather than five, because the rules share a definition and a
56// gate that splits them cannot see the interactions: the same number in
57// five documents is one candidate AND therefore does not pass the cap.
58//
59// Mutations run, each verified red and reverted, 2026-09-14:
60// - returning found[k][:1] when there is more than one — "two distinct"
61// goes red.
62// - keying dedupe on Span instead of Canonical — "two spellings" red.
63// - keying dedupe on the passage rather than the value — "five
64// documents" red.
65// - cap written len(cs) >= MaxFacts — "three still renders" red, and
66// nothing else, which is the point of having it.
67// - cap and collision counted across kinds rather than per kind — the
68// two-kind case red.
69func TestGate35CandidatesAmbiguityAndTheCap(t *testing.T) {
70 t.Run("presence: one number, one box, one value", func(t *testing.T) {
71 h := newApp(t)
72 store(t, h, "Tito office", "## Contact\n\nOffice phone: +353 (0)1 547 7854")
73 body := search(t, h, "phone number")
74 if got := factValues(body); len(got) != 1 || got[0] != "+353 (0)1 547 7854" {
75 t.Fatalf("values = %q, want the one number", got)
76 }
77 })
78
79 // Two right answers to an underspecified question. §8.7 refuses in
80 // this position; §8.8 enumerates, because two phone numbers are not
81 // a wrong average — and the labels are what let the reader choose.
82 t.Run("two distinct numbers are both named, neither picked", func(t *testing.T) {
83 h := newApp(t)
84 store(t, h, "Tito office", "## Contact\n\nOffice phone: +353 (0)1 547 7854")
85 store(t, h, "Support", "## Contact\n\nSupport phone: +353 (0)1 598 1234")
86 got := factValues(search(t, h, "phone number"))
87 if len(got) != 2 {
88 t.Fatalf("values = %q, want both", got)
89 }
90 })
91
92 // A dedupe keyed on the span passes any fixture that spells the
93 // number the same way twice, which is why this one does not.
94 t.Run("two spellings of one number are one candidate", func(t *testing.T) {
95 h := newApp(t)
96 store(t, h, "Tito office", "## Contact\n\nOffice phone: +353 (0)1 547 7854")
97 store(t, h, "Handbook", "## Contact\n\nOffice phone: 01 547 7854")
98 if got := factValues(search(t, h, "phone number")); len(got) != 1 {
99 t.Fatalf("values = %q, want one: the same number written two ways", got)
100 }
101 })
102
103 // The commonest real shape — a signature repeated through forwarded
104 // mail. An implementation counting passages rather than values shows
105 // five rows here, or passes the cap and shows none.
106 t.Run("the same number in five documents is one candidate", func(t *testing.T) {
107 h := newApp(t)
108 for _, title := range []string{"One", "Two", "Three", "Four", "Five"} {
109 store(t, h, title, "## Contact\n\nOffice phone: +353 (0)1 547 7854")
110 }
111 if got := factValues(search(t, h, "phone number")); len(got) != 1 {
112 t.Fatalf("values = %q, want one", got)
113 }
114 })
115
116 // index/rank_test.go's own paragraph, which came off the live
117 // instance. An implementation treating the CHUNK as the unit hides
118 // the second number — the one the document calls unused.
119 t.Run("two numbers in one passage are two candidates", func(t *testing.T) {
120 h := newApp(t)
121 store(t, h, "Office address", "## Office Address\n\nTurn the knob. "+
122 "The office phone number is: +353 (0)1 547 7854. "+
123 "We also have the following unused phone numbers: +353 (0)1 598 1234.")
124 body := search(t, h, "phone number")
125 if got := factValues(body); len(got) != 2 {
126 t.Fatalf("values = %q, want both", got)
127 }
128 // And the labels are what make the enumeration honest: the
129 // document said which is unused, so the page does.
130 labels := factLabels(body)
131 if len(labels) != 2 || !strings.Contains(labels[1], "unused") {
132 t.Fatalf("labels = %q; the document's own word for the second number is missing", labels)
133 }
134 })
135
136 // **Three AND four.** A cap written `>= MaxFacts` passes a suite that
137 // tests only one, two and four.
138 t.Run("three renders, four renders nothing", func(t *testing.T) {
139 nums := []string{"+353 (0)1 547 7854", "+353 (0)1 598 1234", "+353 (0)1 555 0101", "+353 (0)1 555 0202"}
140 for _, n := range []int{3, 4} {
141 h := newApp(t)
142 for i := 0; i < n; i++ {
143 store(t, h, "Office "+string(rune('A'+i)), "## Contact\n\nOffice phone: "+nums[i])
144 }
145 body := search(t, h, "phone number")
146 switch {
147 case n == 3 && len(factValues(body)) != 3:
148 t.Errorf("three candidates rendered %q, want three", factValues(body))
149 case n == 4 && hasFactBox(body):
150 t.Errorf("four candidates rendered a box: %q", factValues(body))
151 }
152 }
153 })
154
155 // Every rule is per kind. Counted across kinds, two of each passes
156 // the cap and both boxes vanish.
157 t.Run("a two-kind query renders both kinds", func(t *testing.T) {
158 h := newApp(t)
159 store(t, h, "Tito office", "## Contact\n\nOffice phone: +353 (0)1 547 7854\n\nEircode: D02 X285")
160 store(t, h, "Support", "## Contact\n\nSupport phone: +353 (0)1 598 1234\n\nEircode: A65 F4E2")
161 got := factValues(search(t, h, "phone number and eircode"))
162 if len(got) != 4 {
163 t.Fatalf("values = %q, want two of each kind", got)
164 }
165 })
166
167 // The whole citation line collides, so the page would print one
168 // identical row twice against two different values.
169 t.Run("colliding rows render no box", func(t *testing.T) {
170 h := newApp(t)
171 store(t, h, "Suppliers", "## Phone\n\n- +353 (0)1 555 0101\n- +353 (0)1 555 0202")
172 if body := search(t, h, "phone number"); hasFactBox(body) {
173 t.Errorf("rendered a box with two identical rows: %q / %q", factLabels(body), factValues(body))
174 }
175 })
176
177 // §10 says this gate asserts first-seen order, and for a while it
178 // asserted only the count. A box sorted by canonical value is
179 // deterministic and passes every length check, and the two-values-in-
180 // one-passage fixture happens to sort the same way it was seen — so
181 // nothing here could see it. Found by review, 2026-09-14.
182 t.Run("rows are in first-seen order", func(t *testing.T) {
183 h := newApp(t)
184 // Both orderings a wrong implementation would pick disagree with
185 // document order here: sorted by VALUE the 547 number comes
186 // first, and sorted by LABEL "Alternate" sorts before "Work".
187 // Only first-seen gives 598, so this one fixture holds against
188 // both — and the version it replaces held against neither,
189 // because its two labels happened to sort as they were seen.
190 store(t, h, "Tito office", "## Contact\n\n"+
191 "Work phone is: +353 (0)1 598 1234. "+
192 "Alternate phone is: +353 (0)1 547 7854.")
193 got := factValues(search(t, h, "phone number"))
194 if len(got) != 2 {
195 t.Fatalf("values = %q, want two", got)
196 }
197 if got[0] != "+353 (0)1 598 1234" {
198 t.Errorf("first row = %q; the box is not in the order the document states them", got[0])
199 }
200 })
201
202 // Every collision fixture above is a PAIR, so a check comparing only
203 // neighbours passes all of them. Three rows whose first and third are
204 // identical is the shape that sees it.
205 t.Run("a collision between non-adjacent rows still suppresses", func(t *testing.T) {
206 h := newApp(t)
207 store(t, h, "Tito office", "## Contact\n\n"+
208 "Ring the office phone on 01 547 7854 any weekday. "+
209 "Support phone: 01 598 1234. "+
210 "Or dial the phone on 01 555 0101 instead.")
211 if body := search(t, h, "phone number"); hasFactBox(body) {
212 t.Errorf("rendered a box whose first and third rows are identical: %q / %q",
213 factLabels(body), factValues(body))
214 }
215 })
216
217 // The mirror of the title case below, for the trail. Same title, same
218 // label, different headings — the trail is what tells them apart, and
219 // a collision check ignoring it loses a real answer.
220 t.Run("the same label under different headings still renders", func(t *testing.T) {
221 h := newApp(t)
222 store(t, h, "Tito office", "## Office\n\nPhone: +353 (0)1 547 7854\n\n## Support\n\nPhone: +353 (0)1 598 1234")
223 if got := factValues(search(t, h, "phone number")); len(got) != 2 {
224 t.Fatalf("values = %q, want both: the headings tell them apart", got)
225 }
226 })
227
228 // A different title disambiguates perfectly, so this one MUST box.
229 // Keying collision on the label alone loses a real answer here.
230 t.Run("the same label under different titles still renders", func(t *testing.T) {
231 h := newApp(t)
232 store(t, h, "Tito office", "## Contact\n\nOffice phone: +353 (0)1 547 7854")
233 store(t, h, "Acme supplier", "## Contact\n\nOffice phone: +353 (0)1 598 1234")
234 if got := factValues(search(t, h, "phone number")); len(got) != 2 {
235 t.Fatalf("values = %q, want both: the titles tell them apart", got)
236 }
237 })
238}
239
240// GATE 36 — the displayed value is byte-identical to the document's
241// (§8.8, §10). The canonical form exists for matching and must never
242// reach the page.
243//
244// Mutation: canonicalise on the display path — one line's difference from
245// what the code already does for identity — and every case goes red.
246func TestGate36TheDisplayedValueIsTheDocumentsBytes(t *testing.T) {
247 for _, c := range []struct{ name, body, want string }{
248 {"as written", "## Contact\n\nOffice phone: +353 (0)1 547 7854", "+353 (0)1 547 7854"},
249 {"with an extension", "## Contact\n\nOffice phone: +353 (0)1 547 7854 ext. 12", "+353 (0)1 547 7854 ext. 12"},
250 // Pasted from Word or Notion. Go's \s does not match U+00A0, so
251 // a recogniser written with it finds nothing here and this case
252 // fails on PRESENCE rather than on the bytes — which is the
253 // design demand and not an accident.
254 {"pasted with non-breaking spaces", "## Contact\n\nOffice phone: +353 (0)1 547 7854", "+353 (0)1 547 7854"},
255 } {
256 t.Run(c.name, func(t *testing.T) {
257 h := newApp(t)
258 store(t, h, "Tito office", c.body)
259 got := factValues(search(t, h, "phone number"))
260 if len(got) != 1 {
261 t.Fatalf("values = %q, want one", got)
262 }
263 if got[0] != c.want {
264 t.Errorf("value = %q, want %q", got[0], c.want)
265 }
266 })
267 }
268}
269
270// GATE 37 — the label is the document's, and it reaches the page.
271//
272// The exhaustive matrix — bullets, adjacent values, abbreviations, the
273// cap boundary, the no-heading fallback — is unit-tested in
274// internal/memoria/fact, where each rule can be driven directly. What
275// this gate adds is the half a unit test cannot see: that the label
276// survives the store, the template and the escaping, and that a value the
277// document labelled too long to show renders NO BOX rather than a vaguer
278// substitute.
279func TestGate37TheLabelReachesThePage(t *testing.T) {
280 t.Run("the document's own label is what is shown", func(t *testing.T) {
281 h := newApp(t)
282 store(t, h, "Tito office", "## Contact\n\nOffice phone: +353 (0)1 547 7854")
283 if got := factLabels(search(t, h, "phone number")); len(got) != 1 || got[0] != "Office phone" {
284 t.Fatalf("labels = %q, want [Office phone]", got)
285 }
286 })
287
288 t.Run("no label in the line falls to the heading trail", func(t *testing.T) {
289 h := newApp(t)
290 store(t, h, "Tito office", "## Contact\n\nRing the office phone on +353 (0)1 547 7854 any weekday.")
291 if got := factLabels(search(t, h, "phone number")); len(got) != 1 || got[0] != "Contact" {
292 t.Fatalf("labels = %q, want [Contact]", got)
293 }
294 })
295
296 // Substituting the trail would replace the document's own warning
297 // with a vaguer word, beside a number the document did not warn
298 // about. Dropping just this candidate would turn a two-candidate
299 // ambiguity into a confident single answer. Both are worse than
300 // silence.
301 t.Run("a label too long to show renders no box at all", func(t *testing.T) {
302 h := newApp(t)
303 store(t, h, "Tito office", "## Contact\n\nReception phone: +353 (0)1 547 7854\n\n"+
304 "The number you must never give to any customer because it rings the chief executive directly is: +353 (0)1 598 1234")
305 if body := search(t, h, "phone number"); hasFactBox(body) {
306 t.Errorf("rendered a box: %q / %q", factLabels(body), factValues(body))
307 }
308 })
309}
310
311// The box lives INSIDE the results define, so the fragment search.js
312// swaps carries it. Outside, query N-1's answer would sit over query N's
313// results on every keystroke — §8.4 inverted, several times a second.
314//
315// This is the page-and-fragment agreement that TestALookupPrimesTheFirstResult
316// makes for priming, and it is needed for the same reason: two renderings
317// of one region that can disagree are two regions.
318func TestTheFactBoxIsInsideTheSwappedRegion(t *testing.T) {
319 h := newApp(t)
320 store(t, h, "Tito office", "## Contact\n\nOffice phone: +353 (0)1 547 7854")
321 page := search(t, h, "phone number")
322 fragment := get(t, h, "/search?q="+url.QueryEscape("phone number")).Body.String()
323 if !hasFactBox(page) {
324 t.Fatal("no box on the page")
325 }
326 if !hasFactBox(fragment) {
327 t.Error("no box in the fragment: it would not survive a keystroke")
328 }
329 if a, b := factValues(page), factValues(fragment); len(a) != len(b) || a[0] != b[0] {
330 t.Errorf("page %q and fragment %q disagree", a, b)
331 }
332}
333
334// Enter goes to the first RESULT, never to the box's citation. §8.8:
335// changing what Enter means depending on whether a regular expression
336// matched is the unreliable-Enter failure PrimesFirstResult exists to
337// avoid. search.js targets rst-result-title, so the guard is that the
338// box's link never wears it.
339func TestTheFactBoxCitationIsNotAnEnterTarget(t *testing.T) {
340 h := newApp(t)
341 store(t, h, "Tito office", "## Contact\n\nOffice phone: +353 (0)1 547 7854")
342 body := search(t, h, "phone number")
343 if !hasFactBox(body) {
344 t.Fatal("no box to check: this assertion needs one to be wrong about")
345 }
346 box := body[strings.Index(body, "<div rst-fact-box>"):]
347 box = box[:strings.Index(box, "</div>")]
348 if strings.Contains(box, "rst-result-title") {
349 t.Errorf("the box carries search.js's Enter target:\n%s", box)
350 }
351}
352
353// **The reachability limit, pinned rather than discovered.**
354//
355// §8.8 states it: the box only sees what retrieval returned, so a fact in
356// a document the query does not reach is a fact the box does not have.
357// This is not a small edge — it is the ordinary case for a contact page
358// that writes "Office: +353 …" and never uses the word "phone" — and
359// writing the gates is what made it concrete, because every fixture above
360// had to be given a word the query could find.
361//
362// Asserted in BOTH directions, because a limit tested only in the
363// direction that fails is indistinguishable from the feature not working
364// at all:
365//
366// - a document sharing no word with the query renders no box;
367// - the SAME document, reached by a query that shares one, does.
368//
369// §12 carries the fix and names what it must not be. Until then this test
370// is the honest statement of what the feature does, and it is here so
371// that the day somebody builds the lift, this goes red and gets deleted
372// on purpose.
373func TestTheBoxOnlySeesWhatRetrievalReturned(t *testing.T) {
374 const body = "## Contact\n\nOffice: +353 (0)1 547 7854"
375
376 h := newApp(t)
377 store(t, h, "Tito office", body)
378
379 // "phone number" appears nowhere in that document, so nothing ranks
380 // and there is nothing to extract from.
381 if got := search(t, h, "phone number"); hasFactBox(got) {
382 t.Errorf("a box appeared for a query the document does not match: %q", factValues(got))
383 }
384
385 // One shared word — "office", in the title and in the line — and the
386 // same fact is found. This is the half that proves the absence above
387 // is the limit rather than a broken recogniser.
388 if got := factValues(search(t, h, "office phone number")); len(got) != 1 {
389 t.Fatalf("values = %q, want the number: the query shares a word with the document", got)
390 }
391}
392
393var factNoteRe = regexp.MustCompile(`<p rst-fact-note>([^<]*)</p>`)
394
395// The page that motivated §8.8's narrowing (Paul, 2026-09-29), through
396// the real handler and template, on the page and in the fragment
397// search.js swaps. Gate 40 holds the rule at store level; this holds that
398// the row and its line reach the reader, and that the bullet is gone.
399func TestTheNarrowedBoxReachesThePageWithItsLine(t *testing.T) {
400 h := newApp(t)
401 store(t, h, "Office", "## Office Address\n\n"+
402 "- Turn the knob\n"+
403 "- The office phone number is: +353 (0)1 547 7854\n"+
404 "- We also have the following unused phone numbers: +353 (0)1 598 1234\n"+
405 " - +353 (0)1 598 1235\n"+
406 " - +353 (0)1 598 1236\n")
407 q := "what is the office phone number?"
408 for name, body := range map[string]string{
409 "page": search(t, h, q),
410 "fragment": get(t, h, "/search?q="+url.QueryEscape(q)).Body.String(),
411 } {
412 if got := factValues(body); len(got) != 1 || got[0] != "+353 (0)1 547 7854" {
413 t.Fatalf("%s: values = %q, want the office number alone", name, got)
414 }
415 if got := factLabels(body); len(got) != 1 || got[0] != "The office phone number is" {
416 t.Errorf("%s: labels = %q, want the document's own without its bullet", name, got)
417 }
418 want := "Matched “office”. 3 other phone numbers are in these results."
419 if got := capture(factNoteRe, body); len(got) != 1 || got[0] != want {
420 t.Errorf("%s: note = %q, want %q", name, got, want)
421 }
422 // The citation survives narrowing: title, trail and a link to the
423 // passage, not just the value.
424 if !regexp.MustCompile(`<a href="/documents/\d+#p\d+" rst-fact-cite>Office › Office Address</a>`).MatchString(body) {
425 t.Errorf("%s: the narrowed row's citation is missing or wrong:\n%s", name, body)
426 }
427 }
428}
429
430// The note sits under the row it describes, not under whichever kind
431// renders last. Phone renders before VAT, so a note placed after every
432// row would sit under the VAT number and describe the wrong thing.
433//
434// MUTATION-TESTED 2026-09-29, each reverted: the note rendered after all
435// rows (Astra's finding) → "order is phone 2432, note 2818, vat 2668";
436// the note rendered above its row → "phone 2525, note 2362".
437func TestTheNarrowingLineSitsUnderItsOwnRow(t *testing.T) {
438 h := newApp(t)
439 store(t, h, "Office", "## Office\n\nOffice phone: +353 (0)1 547 0001\n\nReception: +353 (0)1 547 0002\n\n"+
440 "Sales: +353 (0)1 547 0003\n\nFax: +353 (0)1 547 0004\n\nOffice VAT: IE3384527RH")
441 body := search(t, h, "office phone and vat")
442 if got := factValues(body); len(got) != 2 {
443 t.Fatalf("values = %q, want the narrowed phone and the VAT number", got)
444 }
445 phone := strings.Index(body, "<span rst-fact-value>&#43;353 (0)1 547 0001</span>")
446 note := strings.Index(body, "<p rst-fact-note>")
447 vat := strings.Index(body, "<span rst-fact-value>IE3384527RH</span>")
448 if phone < 0 || note < 0 || vat < 0 {
449 t.Fatalf("phone %d, note %d, vat %d: something is missing:\n%s", phone, note, vat, body)
450 }
451 if !(phone < note && note < vat) {
452 t.Errorf("order is phone %d, note %d, vat %d; the note must sit between its row and the next", phone, note, vat)
453 }
454}
455