package memoriatest
import (
"html"
"net/http"
"net/url"
"regexp"
"strings"
"testing"
)
// The fact box, end to end: through the real handler, the real template
// and the real escaping.
//
// **Through HTTP and not against the struct, deliberately, and gate 34 is
// the opposite deliberately.** Gate 34 is about what the box may CONTAIN,
// where two dispositions render identical pages and only the struct can
// tell them apart. These are about what reaches the reader, where a
// template that escapes a span, drops a label or renders the box outside
// the swapped region is exactly the defect — and a struct assertion
// cannot see any of it. AGENTS.md: a template test proves markup exists,
// not that it renders.
var factValueRe = regexp.MustCompile(`([^<]*)`)
var factLabelRe = regexp.MustCompile(`([^<]*)`)
func factValues(body string) []string { return capture(factValueRe, body) }
func factLabels(body string) []string { return capture(factLabelRe, body) }
func hasFactBox(body string) bool { return strings.Contains(body, "
") }
// capture pulls the rendered text out of the box and UNESCAPES it.
//
// html/template escapes "+" to "+" in text content, which is correct
// and which the reader never sees. Gate 36 is about the bytes that reach
// the READER — "byte-identical to the document" is a claim about what is
// displayed, not about the wire — so comparing raw markup would fail the
// gate on the escaping working properly, and "fixing" that would mean
// unescaping the span into the page, which is an injection.
func capture(re *regexp.Regexp, s string) []string {
var out []string
for _, m := range re.FindAllStringSubmatch(s, -1) {
out = append(out, html.UnescapeString(m[1]))
}
return out
}
func search(t *testing.T, h http.Handler, q string) string {
t.Helper()
return get(t, h, "/?q="+url.QueryEscape(q)).Body.String()
}
// GATE 35 — candidates are canonical values, ambiguity is enumerated
// rather than collapsed, and the cap is three (§8.8, §10).
//
// One test rather than five, because the rules share a definition and a
// gate that splits them cannot see the interactions: the same number in
// five documents is one candidate AND therefore does not pass the cap.
//
// Mutations run, each verified red and reverted, 2026-09-14:
// - returning found[k][:1] when there is more than one — "two distinct"
// goes red.
// - keying dedupe on Span instead of Canonical — "two spellings" red.
// - keying dedupe on the passage rather than the value — "five
// documents" red.
// - cap written len(cs) >= MaxFacts — "three still renders" red, and
// nothing else, which is the point of having it.
// - cap and collision counted across kinds rather than per kind — the
// two-kind case red.
func TestGate35CandidatesAmbiguityAndTheCap(t *testing.T) {
t.Run("presence: one number, one box, one value", func(t *testing.T) {
h := newApp(t)
store(t, h, "Tito office", "## Contact\n\nOffice phone: +353 (0)1 547 7854")
body := search(t, h, "phone number")
if got := factValues(body); len(got) != 1 || got[0] != "+353 (0)1 547 7854" {
t.Fatalf("values = %q, want the one number", got)
}
})
// Two right answers to an underspecified question. §8.7 refuses in
// this position; §8.8 enumerates, because two phone numbers are not
// a wrong average — and the labels are what let the reader choose.
t.Run("two distinct numbers are both named, neither picked", func(t *testing.T) {
h := newApp(t)
store(t, h, "Tito office", "## Contact\n\nOffice phone: +353 (0)1 547 7854")
store(t, h, "Support", "## Contact\n\nSupport phone: +353 (0)1 598 1234")
got := factValues(search(t, h, "phone number"))
if len(got) != 2 {
t.Fatalf("values = %q, want both", got)
}
})
// A dedupe keyed on the span passes any fixture that spells the
// number the same way twice, which is why this one does not.
t.Run("two spellings of one number are one candidate", func(t *testing.T) {
h := newApp(t)
store(t, h, "Tito office", "## Contact\n\nOffice phone: +353 (0)1 547 7854")
store(t, h, "Handbook", "## Contact\n\nOffice phone: 01 547 7854")
if got := factValues(search(t, h, "phone number")); len(got) != 1 {
t.Fatalf("values = %q, want one: the same number written two ways", got)
}
})
// The commonest real shape — a signature repeated through forwarded
// mail. An implementation counting passages rather than values shows
// five rows here, or passes the cap and shows none.
t.Run("the same number in five documents is one candidate", func(t *testing.T) {
h := newApp(t)
for _, title := range []string{"One", "Two", "Three", "Four", "Five"} {
store(t, h, title, "## Contact\n\nOffice phone: +353 (0)1 547 7854")
}
if got := factValues(search(t, h, "phone number")); len(got) != 1 {
t.Fatalf("values = %q, want one", got)
}
})
// index/rank_test.go's own paragraph, which came off the live
// instance. An implementation treating the CHUNK as the unit hides
// the second number — the one the document calls unused.
t.Run("two numbers in one passage are two candidates", func(t *testing.T) {
h := newApp(t)
store(t, h, "Office address", "## Office Address\n\nTurn the knob. "+
"The office phone number is: +353 (0)1 547 7854. "+
"We also have the following unused phone numbers: +353 (0)1 598 1234.")
body := search(t, h, "phone number")
if got := factValues(body); len(got) != 2 {
t.Fatalf("values = %q, want both", got)
}
// And the labels are what make the enumeration honest: the
// document said which is unused, so the page does.
labels := factLabels(body)
if len(labels) != 2 || !strings.Contains(labels[1], "unused") {
t.Fatalf("labels = %q; the document's own word for the second number is missing", labels)
}
})
// **Three AND four.** A cap written `>= MaxFacts` passes a suite that
// tests only one, two and four.
t.Run("three renders, four renders nothing", func(t *testing.T) {
nums := []string{"+353 (0)1 547 7854", "+353 (0)1 598 1234", "+353 (0)1 555 0101", "+353 (0)1 555 0202"}
for _, n := range []int{3, 4} {
h := newApp(t)
for i := 0; i < n; i++ {
store(t, h, "Office "+string(rune('A'+i)), "## Contact\n\nOffice phone: "+nums[i])
}
body := search(t, h, "phone number")
switch {
case n == 3 && len(factValues(body)) != 3:
t.Errorf("three candidates rendered %q, want three", factValues(body))
case n == 4 && hasFactBox(body):
t.Errorf("four candidates rendered a box: %q", factValues(body))
}
}
})
// Every rule is per kind. Counted across kinds, two of each passes
// the cap and both boxes vanish.
t.Run("a two-kind query renders both kinds", func(t *testing.T) {
h := newApp(t)
store(t, h, "Tito office", "## Contact\n\nOffice phone: +353 (0)1 547 7854\n\nEircode: D02 X285")
store(t, h, "Support", "## Contact\n\nSupport phone: +353 (0)1 598 1234\n\nEircode: A65 F4E2")
got := factValues(search(t, h, "phone number and eircode"))
if len(got) != 4 {
t.Fatalf("values = %q, want two of each kind", got)
}
})
// The whole citation line collides, so the page would print one
// identical row twice against two different values.
t.Run("colliding rows render no box", func(t *testing.T) {
h := newApp(t)
store(t, h, "Suppliers", "## Phone\n\n- +353 (0)1 555 0101\n- +353 (0)1 555 0202")
if body := search(t, h, "phone number"); hasFactBox(body) {
t.Errorf("rendered a box with two identical rows: %q / %q", factLabels(body), factValues(body))
}
})
// §10 says this gate asserts first-seen order, and for a while it
// asserted only the count. A box sorted by canonical value is
// deterministic and passes every length check, and the two-values-in-
// one-passage fixture happens to sort the same way it was seen — so
// nothing here could see it. Found by review, 2026-09-14.
t.Run("rows are in first-seen order", func(t *testing.T) {
h := newApp(t)
// Both orderings a wrong implementation would pick disagree with
// document order here: sorted by VALUE the 547 number comes
// first, and sorted by LABEL "Alternate" sorts before "Work".
// Only first-seen gives 598, so this one fixture holds against
// both — and the version it replaces held against neither,
// because its two labels happened to sort as they were seen.
store(t, h, "Tito office", "## Contact\n\n"+
"Work phone is: +353 (0)1 598 1234. "+
"Alternate phone is: +353 (0)1 547 7854.")
got := factValues(search(t, h, "phone number"))
if len(got) != 2 {
t.Fatalf("values = %q, want two", got)
}
if got[0] != "+353 (0)1 598 1234" {
t.Errorf("first row = %q; the box is not in the order the document states them", got[0])
}
})
// Every collision fixture above is a PAIR, so a check comparing only
// neighbours passes all of them. Three rows whose first and third are
// identical is the shape that sees it.
t.Run("a collision between non-adjacent rows still suppresses", func(t *testing.T) {
h := newApp(t)
store(t, h, "Tito office", "## Contact\n\n"+
"Ring the office phone on 01 547 7854 any weekday. "+
"Support phone: 01 598 1234. "+
"Or dial the phone on 01 555 0101 instead.")
if body := search(t, h, "phone number"); hasFactBox(body) {
t.Errorf("rendered a box whose first and third rows are identical: %q / %q",
factLabels(body), factValues(body))
}
})
// The mirror of the title case below, for the trail. Same title, same
// label, different headings — the trail is what tells them apart, and
// a collision check ignoring it loses a real answer.
t.Run("the same label under different headings still renders", func(t *testing.T) {
h := newApp(t)
store(t, h, "Tito office", "## Office\n\nPhone: +353 (0)1 547 7854\n\n## Support\n\nPhone: +353 (0)1 598 1234")
if got := factValues(search(t, h, "phone number")); len(got) != 2 {
t.Fatalf("values = %q, want both: the headings tell them apart", got)
}
})
// A different title disambiguates perfectly, so this one MUST box.
// Keying collision on the label alone loses a real answer here.
t.Run("the same label under different titles still renders", func(t *testing.T) {
h := newApp(t)
store(t, h, "Tito office", "## Contact\n\nOffice phone: +353 (0)1 547 7854")
store(t, h, "Acme supplier", "## Contact\n\nOffice phone: +353 (0)1 598 1234")
if got := factValues(search(t, h, "phone number")); len(got) != 2 {
t.Fatalf("values = %q, want both: the titles tell them apart", got)
}
})
}
// GATE 36 — the displayed value is byte-identical to the document's
// (§8.8, §10). The canonical form exists for matching and must never
// reach the page.
//
// Mutation: canonicalise on the display path — one line's difference from
// what the code already does for identity — and every case goes red.
func TestGate36TheDisplayedValueIsTheDocumentsBytes(t *testing.T) {
for _, c := range []struct{ name, body, want string }{
{"as written", "## Contact\n\nOffice phone: +353 (0)1 547 7854", "+353 (0)1 547 7854"},
{"with an extension", "## Contact\n\nOffice phone: +353 (0)1 547 7854 ext. 12", "+353 (0)1 547 7854 ext. 12"},
// Pasted from Word or Notion. Go's \s does not match U+00A0, so
// a recogniser written with it finds nothing here and this case
// fails on PRESENCE rather than on the bytes — which is the
// design demand and not an accident.
{"pasted with non-breaking spaces", "## Contact\n\nOffice phone: +353 (0)1 547 7854", "+353 (0)1 547 7854"},
} {
t.Run(c.name, func(t *testing.T) {
h := newApp(t)
store(t, h, "Tito office", c.body)
got := factValues(search(t, h, "phone number"))
if len(got) != 1 {
t.Fatalf("values = %q, want one", got)
}
if got[0] != c.want {
t.Errorf("value = %q, want %q", got[0], c.want)
}
})
}
}
// GATE 37 — the label is the document's, and it reaches the page.
//
// The exhaustive matrix — bullets, adjacent values, abbreviations, the
// cap boundary, the no-heading fallback — is unit-tested in
// internal/memoria/fact, where each rule can be driven directly. What
// this gate adds is the half a unit test cannot see: that the label
// survives the store, the template and the escaping, and that a value the
// document labelled too long to show renders NO BOX rather than a vaguer
// substitute.
func TestGate37TheLabelReachesThePage(t *testing.T) {
t.Run("the document's own label is what is shown", func(t *testing.T) {
h := newApp(t)
store(t, h, "Tito office", "## Contact\n\nOffice phone: +353 (0)1 547 7854")
if got := factLabels(search(t, h, "phone number")); len(got) != 1 || got[0] != "Office phone" {
t.Fatalf("labels = %q, want [Office phone]", got)
}
})
t.Run("no label in the line falls to the heading trail", func(t *testing.T) {
h := newApp(t)
store(t, h, "Tito office", "## Contact\n\nRing the office phone on +353 (0)1 547 7854 any weekday.")
if got := factLabels(search(t, h, "phone number")); len(got) != 1 || got[0] != "Contact" {
t.Fatalf("labels = %q, want [Contact]", got)
}
})
// Substituting the trail would replace the document's own warning
// with a vaguer word, beside a number the document did not warn
// about. Dropping just this candidate would turn a two-candidate
// ambiguity into a confident single answer. Both are worse than
// silence.
t.Run("a label too long to show renders no box at all", func(t *testing.T) {
h := newApp(t)
store(t, h, "Tito office", "## Contact\n\nReception phone: +353 (0)1 547 7854\n\n"+
"The number you must never give to any customer because it rings the chief executive directly is: +353 (0)1 598 1234")
if body := search(t, h, "phone number"); hasFactBox(body) {
t.Errorf("rendered a box: %q / %q", factLabels(body), factValues(body))
}
})
}
// The box lives INSIDE the results define, so the fragment search.js
// swaps carries it. Outside, query N-1's answer would sit over query N's
// results on every keystroke — §8.4 inverted, several times a second.
//
// This is the page-and-fragment agreement that TestALookupPrimesTheFirstResult
// makes for priming, and it is needed for the same reason: two renderings
// of one region that can disagree are two regions.
func TestTheFactBoxIsInsideTheSwappedRegion(t *testing.T) {
h := newApp(t)
store(t, h, "Tito office", "## Contact\n\nOffice phone: +353 (0)1 547 7854")
page := search(t, h, "phone number")
fragment := get(t, h, "/search?q="+url.QueryEscape("phone number")).Body.String()
if !hasFactBox(page) {
t.Fatal("no box on the page")
}
if !hasFactBox(fragment) {
t.Error("no box in the fragment: it would not survive a keystroke")
}
if a, b := factValues(page), factValues(fragment); len(a) != len(b) || a[0] != b[0] {
t.Errorf("page %q and fragment %q disagree", a, b)
}
}
// Enter goes to the first RESULT, never to the box's citation. §8.8:
// changing what Enter means depending on whether a regular expression
// matched is the unreliable-Enter failure PrimesFirstResult exists to
// avoid. search.js targets rst-result-title, so the guard is that the
// box's link never wears it.
func TestTheFactBoxCitationIsNotAnEnterTarget(t *testing.T) {
h := newApp(t)
store(t, h, "Tito office", "## Contact\n\nOffice phone: +353 (0)1 547 7854")
body := search(t, h, "phone number")
if !hasFactBox(body) {
t.Fatal("no box to check: this assertion needs one to be wrong about")
}
box := body[strings.Index(body, "
"):]
box = box[:strings.Index(box, "
")]
if strings.Contains(box, "rst-result-title") {
t.Errorf("the box carries search.js's Enter target:\n%s", box)
}
}
// **The reachability limit, pinned rather than discovered.**
//
// §8.8 states it: the box only sees what retrieval returned, so a fact in
// a document the query does not reach is a fact the box does not have.
// This is not a small edge — it is the ordinary case for a contact page
// that writes "Office: +353 …" and never uses the word "phone" — and
// writing the gates is what made it concrete, because every fixture above
// had to be given a word the query could find.
//
// Asserted in BOTH directions, because a limit tested only in the
// direction that fails is indistinguishable from the feature not working
// at all:
//
// - a document sharing no word with the query renders no box;
// - the SAME document, reached by a query that shares one, does.
//
// §12 carries the fix and names what it must not be. Until then this test
// is the honest statement of what the feature does, and it is here so
// that the day somebody builds the lift, this goes red and gets deleted
// on purpose.
func TestTheBoxOnlySeesWhatRetrievalReturned(t *testing.T) {
const body = "## Contact\n\nOffice: +353 (0)1 547 7854"
h := newApp(t)
store(t, h, "Tito office", body)
// "phone number" appears nowhere in that document, so nothing ranks
// and there is nothing to extract from.
if got := search(t, h, "phone number"); hasFactBox(got) {
t.Errorf("a box appeared for a query the document does not match: %q", factValues(got))
}
// One shared word — "office", in the title and in the line — and the
// same fact is found. This is the half that proves the absence above
// is the limit rather than a broken recogniser.
if got := factValues(search(t, h, "office phone number")); len(got) != 1 {
t.Fatalf("values = %q, want the number: the query shares a word with the document", got)
}
}
var factNoteRe = regexp.MustCompile(`
([^<]*)
`)
// The page that motivated §8.8's narrowing (Paul, 2026-09-29), through
// the real handler and template, on the page and in the fragment
// search.js swaps. Gate 40 holds the rule at store level; this holds that
// the row and its line reach the reader, and that the bullet is gone.
func TestTheNarrowedBoxReachesThePageWithItsLine(t *testing.T) {
h := newApp(t)
store(t, h, "Office", "## Office Address\n\n"+
"- Turn the knob\n"+
"- The office phone number is: +353 (0)1 547 7854\n"+
"- We also have the following unused phone numbers: +353 (0)1 598 1234\n"+
" - +353 (0)1 598 1235\n"+
" - +353 (0)1 598 1236\n")
q := "what is the office phone number?"
for name, body := range map[string]string{
"page": search(t, h, q),
"fragment": get(t, h, "/search?q="+url.QueryEscape(q)).Body.String(),
} {
if got := factValues(body); len(got) != 1 || got[0] != "+353 (0)1 547 7854" {
t.Fatalf("%s: values = %q, want the office number alone", name, got)
}
if got := factLabels(body); len(got) != 1 || got[0] != "The office phone number is" {
t.Errorf("%s: labels = %q, want the document's own without its bullet", name, got)
}
want := "Matched “office”. 3 other phone numbers are in these results."
if got := capture(factNoteRe, body); len(got) != 1 || got[0] != want {
t.Errorf("%s: note = %q, want %q", name, got, want)
}
// The citation survives narrowing: title, trail and a link to the
// passage, not just the value.
if !regexp.MustCompile(`
Office › Office Address`).MatchString(body) {
t.Errorf("%s: the narrowed row's citation is missing or wrong:\n%s", name, body)
}
}
}
// The note sits under the row it describes, not under whichever kind
// renders last. Phone renders before VAT, so a note placed after every
// row would sit under the VAT number and describe the wrong thing.
//
// MUTATION-TESTED 2026-09-29, each reverted: the note rendered after all
// rows (Astra's finding) → "order is phone 2432, note 2818, vat 2668";
// the note rendered above its row → "phone 2525, note 2362".
func TestTheNarrowingLineSitsUnderItsOwnRow(t *testing.T) {
h := newApp(t)
store(t, h, "Office", "## Office\n\nOffice phone: +353 (0)1 547 0001\n\nReception: +353 (0)1 547 0002\n\n"+
"Sales: +353 (0)1 547 0003\n\nFax: +353 (0)1 547 0004\n\nOffice VAT: IE3384527RH")
body := search(t, h, "office phone and vat")
if got := factValues(body); len(got) != 2 {
t.Fatalf("values = %q, want the narrowed phone and the VAT number", got)
}
phone := strings.Index(body, "
+353 (0)1 547 0001")
note := strings.Index(body, "
")
vat := strings.Index(body, "IE3384527RH")
if phone < 0 || note < 0 || vat < 0 {
t.Fatalf("phone %d, note %d, vat %d: something is missing:\n%s", phone, note, vat, body)
}
if !(phone < note && note < vat) {
t.Errorf("order is phone %d, note %d, vat %d; the note must sit between its row and the next", phone, note, vat)
}
}