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 "net/http"
5 "strings"
6 "testing"
7)
8
9// The corpus every test in this file searches.
10//
11// Two passages match "june revenue", and that is the point rather than
12// an incidental. The first version of this file used a corpus where
13// every query returned exactly ONE result, which made "the primed one is
14// the first one" unfalsifiable: with a single list item, priming the
15// first, priming all and priming the last are the same markup. Fable's
16// review caught it by mutating the template to `{{if $.Primed}}` and
17// watching the suite stay green. Any test below that asserts a POSITION
18// must therefore check it has more than one result to be wrong about.
19func withCorpus(t *testing.T) http.Handler {
20 t.Helper()
21 h := newApp(t)
22 if rec := store(t, h, "Q3 board pack",
23 "## Revenue\n\nJune revenue was 1,350,000 across all regions.\n\n"+
24 "## Costs\n\nHosting fell after the migration.\n\n"+
25 "## Risks\n\nOne customer is 22% of June revenue, and that has not improved.",
26 ); rec.Code != http.StatusSeeOther {
27 t.Fatalf("store: status %d, want 303", rec.Code)
28 }
29 return h
30}
31
32// results counts the rendered result items, so a positional assertion
33// can refuse to run against a list too short to hold a wrong answer.
34func results(body string) int {
35 return strings.Count(body, "<li rst-result")
36}
37
38// A lookup primes the first result, which is what makes Enter go
39// somewhere. The attribute is the whole contract between the server's
40// heuristic and search.js: the script never re-derives the rule.
41//
42// Broken by hard-coding p.Primed to false — red, then reverted.
43// 2026-09-05.
44func TestALookupPrimesTheFirstResult(t *testing.T) {
45 h := withCorpus(t)
46 body := get(t, h, "/?q=june+revenue").Body.String()
47 // Without at least two results this test cannot tell prime-first
48 // from prime-anything, so it refuses to pretend it did.
49 if n := results(body); n < 2 {
50 t.Fatalf("corpus returned %d results; a positional assertion needs at least 2:\n%s", n, body)
51 }
52 if !strings.Contains(body, "rst-result-primed") {
53 t.Errorf("a lookup did not prime a result, so Enter goes nowhere:\n%s", body)
54 }
55 if n := strings.Count(body, "rst-result-primed"); n != 1 {
56 t.Errorf("primed %d results, want exactly 1", n)
57 }
58 // The prime must be on the FIRST result, not merely somewhere.
59 // Split on the <li> opener rather than comparing indexes, because
60 // "rst-result" is a prefix of "rst-result-primed" and an index
61 // comparison would be measuring the same attribute twice.
62 items := strings.Split(body, "<li rst-result")
63 if !strings.Contains(items[1], "rst-result-primed") {
64 t.Errorf("the primed result is not the first one:\n%s", body)
65 }
66 for i := 2; i < len(items); i++ {
67 if strings.Contains(items[i], "rst-result-primed") {
68 t.Errorf("result %d is primed as well as the first", i)
69 }
70 }
71}
72
73// Broken by priming on `len(results) > 0` alone, dropping the question
74// term — red, then reverted. 2026-09-05.
75func TestAQuestionPrimesNothing(t *testing.T) {
76 h := withCorpus(t)
77 body := get(t, h, "/?q=what+was+june+revenue").Body.String()
78 if !strings.Contains(body, "Q3 board pack") {
79 t.Fatalf("the question found nothing, so this test proves nothing:\n%s", body)
80 }
81 if strings.Contains(body, "rst-result-primed") {
82 t.Error("a question primed a result; Enter would navigate away from " +
83 "a page the reader was still reading")
84 }
85}
86
87// There is deliberately no test here for "a page with no results primes
88// nothing". It was written, and it could not fail: with no results the
89// template renders no list items, so priming nothing and priming a
90// result that does not exist are the same bytes. The invariant is real
91// and is guarded where it is visible — query.TestPrimesFirstResult.
92
93// The live-search endpoint returns the results region and nothing else.
94// A fragment that shipped a whole page would nest <html> inside the list
95// on every keystroke.
96//
97// Broken by pointing searchResults at render instead of renderFragment —
98// red, then reverted. 2026-09-05.
99func TestSearchFragmentIsResultsOnly(t *testing.T) {
100 h := withCorpus(t)
101 rec := get(t, h, "/search?q=june+revenue")
102 if rec.Code != http.StatusOK {
103 t.Fatalf("status %d, want 200", rec.Code)
104 }
105 body := rec.Body.String()
106 if !strings.Contains(body, "Q3 board pack") {
107 t.Errorf("the fragment carries no result:\n%s", body)
108 }
109 if !strings.Contains(body, "rst-result-primed") {
110 t.Error("the fragment does not prime, so live search and a form " +
111 "submit would disagree about where Enter goes")
112 }
113 // rst-refresh is on the list because a fragment that ever grew the
114 // wrapper would nest a refreshable region inside one, and drop.js
115 // would then find two regions with the same name.
116 for _, chrome := range []string{"<html", "<body", "rst-page-header", "Add something", "rst-refresh"} {
117 if strings.Contains(body, chrome) {
118 t.Errorf("the fragment carries page chrome (%q); it would nest a "+
119 "whole page inside the results list", chrome)
120 }
121 }
122}
123
124// The empty query is the state the box starts in and returns to when it
125// is cleared. It must render an empty region, not an error and not the
126// whole corpus.
127func TestSearchFragmentOfAnEmptyQueryIsEmpty(t *testing.T) {
128 rec := get(t, withCorpus(t), "/search?q=")
129 if rec.Code != http.StatusOK {
130 t.Fatalf("status %d, want 200", rec.Code)
131 }
132 if body := strings.TrimSpace(rec.Body.String()); body != "" {
133 t.Errorf("an empty query rendered %q, want nothing", body)
134 }
135}
136
137// The fragment and the full page must agree, because Enter can land on
138// either: live search fills the region, and a scriptless submit renders
139// the page. If they disagreed, priming would depend on whether JS had
140// loaded yet.
141func TestFragmentAndPageAgreeOnPriming(t *testing.T) {
142 h := withCorpus(t)
143 // Two queries, not three: the no-match case was here and could not
144 // disagree, because with nothing to render both sides emit the same
145 // empty region whatever they decided. A case that cannot fail on a
146 // list of cases that can reads as coverage it is not.
147 for _, q := range []string{"june+revenue", "what+was+june+revenue"} {
148 page := strings.Contains(get(t, h, "/?q="+q).Body.String(), "rst-result-primed")
149 frag := strings.Contains(get(t, h, "/search?q="+q).Body.String(), "rst-result-primed")
150 if page != frag {
151 t.Errorf("q=%q: page primed=%v but fragment primed=%v", q, page, frag)
152 }
153 }
154}
155
156// When search.js's own fetch fails there is no server response to render
157// a callout from, so the page must arrive already carrying the sentence.
158// Without it the script has nothing to say and nothing to show, and the
159// older results stand on screen as though they were current — which is
160// this codebase's worst failure shape, reached by deleting one attribute.
161//
162// The sentence must be the SAME one the server's callout uses. Two
163// wordings for "the search did not run" is a page arguing with itself.
164//
165// Broken by deleting rst-search-stale-note from index.html — red, then
166// reverted. Broken again by changing its wording so the two disagree —
167// red, then reverted. 2026-09-05.
168func TestThePageCarriesTheFailureSentenceItMayNeed(t *testing.T) {
169 h, _, d := newAppWithLog(t)
170 store(t, h, "Q3 board pack", "June revenue was 1,350,000 across all regions.")
171
172 // The sentence the page hands to search.js, for a fetch that never
173 // answers and so has no callout of its own to render.
174 carried := attr(t, get(t, h, "/").Body.String(), "rst-search-stale-note")
175 if carried == "" {
176 t.Fatal("the page carries no failure sentence, so a failed live search has nothing to show")
177 }
178
179 // The sentence the SERVER renders when the search itself fails.
180 // Taking the database away is how that path is reached; nothing a
181 // well-formed request can carry makes store.Search fail.
182 d.Close()
183 broken := get(t, h, "/?q=june+revenue").Body.String()
184 // **The ATTRIBUTE tone is `rst-tone`**, and this assertion has now
185 // been wrong twice. tokens.css pairs every kind: the class spelling
186 // takes `data-tone`, the attribute spelling takes `rst-tone`, and the
187 // header says so — "rst-tone carries the four tones that data-tone
188 // carries today". `[rst-callout]` is the attribute spelling, so it
189 // wants `rst-tone`.
190 //
191 // Attempt one asserted `rst-callout="danger"`, a value on the kind
192 // attribute that nothing selects. Attempt two asserted `data-tone`,
193 // which is a real string in the stylesheet — inside the CLASS
194 // selector next to the one needed here — so it confirmed a spelling
195 // the markup could not use. Both times the callout rendered untoned
196 // and both times a test said it was fine.
197 if !strings.Contains(broken, `rst-tone="negative"`) {
198 t.Fatalf("a failed search did not render its callout, so there is nothing to compare:\n%s", broken)
199 }
200 // Compare the callout's OWN words to the carried ones. Searching the
201 // whole page for the carried sentence is what this gate did first,
202 // and it could not fail: that page also carries the attribute, so
203 // the sentence always found itself and a drifted callout went
204 // unnoticed. Caught by mutating the attribute's wording.
205 spoken := calloutBody(broken)
206 if spoken == "" {
207 t.Fatalf("could not read the failure callout's own words:\n%s", broken)
208 }
209 if spoken != carried {
210 t.Errorf("the sentence search.js would show and the one the server shows disagree:\n"+
211 " server: %q\n carried: %q", spoken, carried)
212 }
213 if strings.Contains(broken, "rst-result-primed") {
214 t.Error("a search that broke primed a result")
215 }
216}
217
218// calloutBody is the text of the first danger callout's body, trimmed.
219func calloutBody(body string) string {
220 i := strings.Index(body, `rst-tone="negative"`)
221 if i < 0 {
222 return ""
223 }
224 rest := body[i:]
225 open := strings.Index(rest, "<div rst-callout-body><p>")
226 if open < 0 {
227 return ""
228 }
229 rest = rest[open+len("<div rst-callout-body><p>"):]
230 shut := strings.Index(rest, "</p>")
231 if shut < 0 {
232 return ""
233 }
234 return strings.TrimSpace(rest[:shut])
235}
236
237// attr pulls one attribute's value out of rendered markup. Small enough
238// to be obvious, and obvious beats a parser for one attribute.
239func attr(t *testing.T, body, name string) string {
240 t.Helper()
241 i := strings.Index(body, name+`="`)
242 if i < 0 {
243 return ""
244 }
245 rest := body[i+len(name)+2:]
246 j := strings.Index(rest, `"`)
247 if j < 0 {
248 return ""
249 }
250 return rest[:j]
251}
252
253// The stamp is what search.js reads to decide whether the results on
254// screen still describe what is in the box. Reading the input instead
255// was the bug Fable found: after a Back, a browser restores the field on
256// a schedule of its own, and a script that trusted it would treat a
257// stale screen as current and send Enter to the wrong document.
258//
259// Broken by deleting rst-search-results-for from index.html — red, then
260// reverted. 2026-09-05.
261func TestThePageStampsTheQueryItRendered(t *testing.T) {
262 h := withCorpus(t)
263 for _, c := range []struct{ url, want string }{
264 {"/?q=june+revenue", `rst-search-results-for="june revenue"`},
265 {"/", `rst-search-results-for=""`},
266 } {
267 if body := get(t, h, c.url).Body.String(); !strings.Contains(body, c.want) {
268 t.Errorf("GET %s does not stamp the query it rendered (want %s):\n%s", c.url, c.want, body)
269 }
270 }
271}
272
273// search.js is an enhancement. The page it enhances must work with it
274// deleted, which means the form is a real GET form to a real route and
275// the script is not what makes results appear.
276func TestSearchWorksWithoutTheScript(t *testing.T) {
277 h := withCorpus(t)
278 body := get(t, h, "/").Body.String()
279 if !strings.Contains(body, `action="/"`) || !strings.Contains(body, `method="get"`) {
280 t.Errorf("the search form is not a plain GET form:\n%s", body)
281 }
282 if !strings.Contains(get(t, h, "/?q=june+revenue").Body.String(), "Q3 board pack") {
283 t.Error("a plain form submit does not produce results")
284 }
285}
286