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 "net/http/httptest"
6 "strings"
7 "testing"
8
9 "golang.org/x/net/html"
10)
11
12// The tests in this file cover the list that stops being stale.
13//
14// Since 2026-09-29 the drop zone and the tray live on /add, and the
15// document list stays on the index. So the page drop.js re-fetches is /add,
16// and its only refreshable region is the passage count in the footer. The
17// browser runs recorded below were made on the index, when the drop zone
18// was there; they are kept as the record of how the swap itself behaves.
19//
20// A drop posts each file in the background and never navigates, so
21// everything the server rendered before the drop was still on the screen
22// afterwards: the document list, the search results, the passage count in
23// the footer. The tray said so and offered a link. It now asks the server
24// for the same page again — the page the reader is ON, query string and
25// all — and puts the freshly rendered regions in place of the old ones.
26//
27// The server stays the only thing that renders a document row. Nothing in
28// the browser composes markup; the regions come out of the answer as nodes
29// and go in as nodes. Two renderers for one row is the drift this codebase
30// has already been bitten by twice.
31//
32// Go cannot run the script, so what these tests hold is the half a Go test
33// can hold, and it is the half the design rests on: WHERE the regions are.
34// A swap replaces its element whole, so a region reaching one tag too far
35// would destroy the paste box someone is typing in, or the drop zone, or
36// the tray itself with rows still uploading in it.
37//
38// The other half was checked in a real browser, because a Go test cannot
39// see a list change. drop_test.go's header carries the CDP recipe;
40// Chromium 151, 2026-09-05, against tools/drive, with the viewport at
41// 780x493 (drop coordinates have to be INSIDE it — a drop dispatched at
42// y=700 in that window does nothing at all, silently, which cost the
43// first run of this):
44//
45// - Three files dropped on the front page, one of them binary, with
46// "half-typed, and it must survive" in the paste box first. The tray
47// showed three rows; the summary read "Stored 2 documents. 1
48// refused: scan.png is not text. The document list below is up to
49// date."; location.href never changed; the table went from four rows
50// to six and its heading from "4 documents" to "6 documents"; the
51// footer went from 9 to 11 passages; the stale notice stayed hidden;
52// the paste box still held its text.
53// - The same drop while looking at "/?q=zebrafinch", with one file
54// matching that search and one not. The results went from ["two"] to
55// ["two","four"] and the heading from "1 result" to "2 results" —
56// the non-matching file did NOT appear there, and both files did
57// appear in the document list below, which the search does not
58// filter. The address stayed "/?q=zebrafinch".
59// - With the keyboard focus on a Delete button INSIDE the document
60// list, a drop stored its file and did not swap: the notice appeared,
61// the summary was "Stored 1 document." with no claim about the list,
62// the table was unchanged, and document.activeElement was still that
63// button.
64// - With only the re-fetch blocked (Network.setBlockedURLs on the
65// page's own address, so the POST still went through), the file
66// stored, the summary was "Stored 1 document." with no claim about
67// the list, the notice and the reload link came back, and the link's
68// href was "/?q=zebrafinch" — the view the reader was on. Unblocking
69// and following it showed the document really had landed.
70// - The other way round, and the one the unconfirmed case turns on:
71// only the UPLOAD blocked, so the answer was lost and the re-fetch
72// got through. The row said "six.txt got no answer, so whether it
73// was stored is unknown", the summary added "The document list below
74// is up to date.", and the list — refreshed, three rows, unchanged —
75// did not contain six.txt. Which is the truth about that file,
76// arrived at by looking rather than by claiming. Nothing new is said
77// about the file itself, because nothing here knows anything new
78// about it.
79// - Focus was on the search box throughout the runs above and stayed
80// there: every region is outside it.
81// - Zero console messages and zero log entries across all of it.
82// - And the scriptless path, with Emulation.setScriptExecutionDisabled
83// on: the zone was unmarked, the tray, hint and note all `hidden`,
84// and choosing two files (DOM.setFileInputFiles) and pressing Store
85// it navigated to "/?stored=2" with the list rendered fresh by the
86// server. No script ran at any point in that.
87
88// The regions the tray swaps hold NOTHING the person is in the middle of
89// using.
90//
91// A swap replaces an element whole: everything inside it is destroyed and
92// rebuilt from the server's answer. So a region reaching one tag too far
93// would take the half-typed paste box with it, or the drop zone, or the
94// tray itself — including rows for files still uploading, whose outcome
95// has not been said yet.
96//
97// Each page is checked for the things it protects BEFORE checking they
98// sit outside every region. A page that had lost its form would otherwise
99// pass the containment check for the wrong reason, which is what happened
100// when the form moved to /add and this test kept reading the index.
101//
102// Names are checked for uniqueness because the script pairs each region
103// on screen with the one of that name in the answer.
104//
105// MUTATION-TESTED 2026-09-29, each reverted:
106//
107// (a) rst-refresh="upload" added to /add's <section rst-section> → "the
108// "upload" region on /add contains the upload form", and the same
109// for the drop zone, the tray and the paste box. Astra's
110// single-quoted rst-refresh='upload' passed the regex this test
111// used to be; it reads the parsed page now, and is red on it too.
112// (b) the rst-dropzone attribute deleted from add.html → "/add has no
113// drop zone", the presence check, which fatals before any
114// containment line runs
115// (c) rst-refresh deleted from the footer count → "/add has no
116// "passages" region"
117// (d) Astra's: rst-refresh="title" on the title input, a region INSIDE
118// the form rather than around it → "the "title" region on /add is
119// inside the upload form"
120func TestTheRefreshedRegionsCannotTakeTheFormOrTheTrayWithThem(t *testing.T) {
121 h := newApp(t)
122 submit(t, h, "", "", file{"revenue.txt", "Revenue was 100 across the first region we serve."})
123
124 type kept struct {
125 what string
126 match func(*html.Node) bool
127 }
128 for _, pg := range []struct {
129 path string
130 want []string
131 keep []kept
132 }{
133 {"/add", []string{"passages"}, []kept{
134 {"upload form", elementWith("form", "action", "/documents")},
135 {"drop zone", hasAttr("rst-dropzone")},
136 {"tray", hasAttr("rst-tray")},
137 {"paste box", elementWith("textarea", "id", "body")},
138 }},
139 // search.js writes into the results region, so the search box
140 // must not be inside it. A searched page, so the region has
141 // something in it to over-reach with.
142 {"/?q=revenue", []string{"results", "passages"}, []kept{
143 {"search form", hasAttr("rst-search")},
144 {"search box", elementWith("input", "type", "search")},
145 }},
146 } {
147 doc := parsePage(t, get(t, h, pg.path).Body.String())
148 protected := map[string][]*html.Node{}
149 for _, k := range pg.keep {
150 protected[k.what] = findAll(doc, k.match)
151 if len(protected[k.what]) == 0 {
152 t.Fatalf("%s has no %s, so the containment check below would pass for a page "+
153 "without it", pg.path, k.what)
154 }
155 }
156
157 regions := findAll(doc, hasAttr("rst-refresh"))
158 seen := map[string]bool{}
159 for _, r := range regions {
160 name, _ := attrOf(r, "rst-refresh")
161 if name == "" {
162 t.Errorf("%s: a region carries an empty rst-refresh; the script pairs regions "+
163 "by name and an unnamed one can be paired with anything", pg.path)
164 }
165 if seen[name] {
166 t.Errorf("%s: two regions are both named %q, so a swap puts one region's "+
167 "content into the other's place", pg.path, name)
168 }
169 seen[name] = true
170 }
171 for _, want := range pg.want {
172 if !seen[want] {
173 t.Errorf("%s has no %q region", pg.path, want)
174 }
175 }
176
177 for _, r := range regions {
178 name, _ := attrOf(r, "rst-refresh")
179 for what, nodes := range protected {
180 for _, n := range nodes {
181 if within(n, r) {
182 t.Errorf("the %q region on %s contains the %s. A swap replaces the "+
183 "region whole, so it would be destroyed and rebuilt from the server's "+
184 "answer — a half-typed paste, a drop in progress, or a tray row whose "+
185 "outcome has not been said yet, gone without a word.", name, pg.path, what)
186 }
187 // The other direction: a region INSIDE the form is a
188 // control the swap replaces with the server's empty one.
189 if within(r, n) && r != n {
190 t.Errorf("the %q region on %s is inside the %s, so a swap replaces "+
191 "something in it with the server's copy — a typed title, say, with an "+
192 "empty field.", name, pg.path, what)
193 }
194 }
195 }
196 }
197 }
198
199 count := textOf(region(t, parsePage(t, get(t, h, "/add").Body.String()), "passages"))
200 if !strings.Contains(count, "indexed") {
201 t.Errorf("the %q region does not hold the passage count: %q", "passages", count)
202 }
203 if strings.Contains(count, "no document ever") {
204 t.Errorf("the %q region has swallowed the egress claim beside it: %q. That sentence is "+
205 "not derived from anything and has no business being re-fetched.", "passages", count)
206 }
207}
208
209// The re-fetch drop.js makes of /add brings the passage count up to date.
210//
211// It is made the way drop.js makes it, with the header the page tells the
212// script to send.
213//
214// MUTATION-TESTED 2026-09-29, reverted: `Chunks: a.chunkCount()` →
215// `Chunks: 0` in addContent → "the passage count on /add did not change
216// after 1 document was stored".
217func TestARefetchOfAddMovesThePassageCount(t *testing.T) {
218 h := newApp(t)
219 add := get(t, h, "/add").Body.String()
220 before := textOf(region(t, parsePage(t, add), "passages"))
221 if !strings.Contains(before, "indexed") {
222 t.Fatalf("/add's passage region holds no count to compare: %q", before)
223 }
224
225 readReply(t, submitJSON(t, h, file{"hosting.txt", "Hosting costs fell after the migration completed."}))
226
227 req := httptest.NewRequest(http.MethodGet, "/add", nil)
228 req.Header.Set(refreshHeaderOn(t, add), "1")
229 rec := httptest.NewRecorder()
230 h.ServeHTTP(rec, req)
231 if rec.Code != http.StatusOK {
232 t.Fatalf("the re-fetch of /add answered %d, want 200", rec.Code)
233 }
234 if after := textOf(region(t, parsePage(t, rec.Body.String()), "passages")); after == before {
235 t.Errorf("the passage count on /add did not change after 1 document was stored: %q. "+
236 "The tray's re-fetch is the only thing that brings it up to date.", after)
237 }
238}
239