package memoriatest import ( "net/http" "net/http/httptest" "strings" "testing" "golang.org/x/net/html" ) // The tests in this file cover the list that stops being stale. // // Since 2026-09-29 the drop zone and the tray live on /add, and the // document list stays on the index. So the page drop.js re-fetches is /add, // and its only refreshable region is the passage count in the footer. The // browser runs recorded below were made on the index, when the drop zone // was there; they are kept as the record of how the swap itself behaves. // // A drop posts each file in the background and never navigates, so // everything the server rendered before the drop was still on the screen // afterwards: the document list, the search results, the passage count in // the footer. The tray said so and offered a link. It now asks the server // for the same page again — the page the reader is ON, query string and // all — and puts the freshly rendered regions in place of the old ones. // // The server stays the only thing that renders a document row. Nothing in // the browser composes markup; the regions come out of the answer as nodes // and go in as nodes. Two renderers for one row is the drift this codebase // has already been bitten by twice. // // Go cannot run the script, so what these tests hold is the half a Go test // can hold, and it is the half the design rests on: WHERE the regions are. // A swap replaces its element whole, so a region reaching one tag too far // would destroy the paste box someone is typing in, or the drop zone, or // the tray itself with rows still uploading in it. // // The other half was checked in a real browser, because a Go test cannot // see a list change. drop_test.go's header carries the CDP recipe; // Chromium 151, 2026-09-05, against tools/drive, with the viewport at // 780x493 (drop coordinates have to be INSIDE it — a drop dispatched at // y=700 in that window does nothing at all, silently, which cost the // first run of this): // // - Three files dropped on the front page, one of them binary, with // "half-typed, and it must survive" in the paste box first. The tray // showed three rows; the summary read "Stored 2 documents. 1 // refused: scan.png is not text. The document list below is up to // date."; location.href never changed; the table went from four rows // to six and its heading from "4 documents" to "6 documents"; the // footer went from 9 to 11 passages; the stale notice stayed hidden; // the paste box still held its text. // - The same drop while looking at "/?q=zebrafinch", with one file // matching that search and one not. The results went from ["two"] to // ["two","four"] and the heading from "1 result" to "2 results" — // the non-matching file did NOT appear there, and both files did // appear in the document list below, which the search does not // filter. The address stayed "/?q=zebrafinch". // - With the keyboard focus on a Delete button INSIDE the document // list, a drop stored its file and did not swap: the notice appeared, // the summary was "Stored 1 document." with no claim about the list, // the table was unchanged, and document.activeElement was still that // button. // - With only the re-fetch blocked (Network.setBlockedURLs on the // page's own address, so the POST still went through), the file // stored, the summary was "Stored 1 document." with no claim about // the list, the notice and the reload link came back, and the link's // href was "/?q=zebrafinch" — the view the reader was on. Unblocking // and following it showed the document really had landed. // - The other way round, and the one the unconfirmed case turns on: // only the UPLOAD blocked, so the answer was lost and the re-fetch // got through. The row said "six.txt got no answer, so whether it // was stored is unknown", the summary added "The document list below // is up to date.", and the list — refreshed, three rows, unchanged — // did not contain six.txt. Which is the truth about that file, // arrived at by looking rather than by claiming. Nothing new is said // about the file itself, because nothing here knows anything new // about it. // - Focus was on the search box throughout the runs above and stayed // there: every region is outside it. // - Zero console messages and zero log entries across all of it. // - And the scriptless path, with Emulation.setScriptExecutionDisabled // on: the zone was unmarked, the tray, hint and note all `hidden`, // and choosing two files (DOM.setFileInputFiles) and pressing Store // it navigated to "/?stored=2" with the list rendered fresh by the // server. No script ran at any point in that. // The regions the tray swaps hold NOTHING the person is in the middle of // using. // // A swap replaces an element whole: everything inside it is destroyed and // rebuilt from the server's answer. So a region reaching one tag too far // would take the half-typed paste box with it, or the drop zone, or the // tray itself — including rows for files still uploading, whose outcome // has not been said yet. // // Each page is checked for the things it protects BEFORE checking they // sit outside every region. A page that had lost its form would otherwise // pass the containment check for the wrong reason, which is what happened // when the form moved to /add and this test kept reading the index. // // Names are checked for uniqueness because the script pairs each region // on screen with the one of that name in the answer. // // MUTATION-TESTED 2026-09-29, each reverted: // // (a) rst-refresh="upload" added to /add's
→ "the // "upload" region on /add contains the upload form", and the same // for the drop zone, the tray and the paste box. Astra's // single-quoted rst-refresh='upload' passed the regex this test // used to be; it reads the parsed page now, and is red on it too. // (b) the rst-dropzone attribute deleted from add.html → "/add has no // drop zone", the presence check, which fatals before any // containment line runs // (c) rst-refresh deleted from the footer count → "/add has no // "passages" region" // (d) Astra's: rst-refresh="title" on the title input, a region INSIDE // the form rather than around it → "the "title" region on /add is // inside the upload form" func TestTheRefreshedRegionsCannotTakeTheFormOrTheTrayWithThem(t *testing.T) { h := newApp(t) submit(t, h, "", "", file{"revenue.txt", "Revenue was 100 across the first region we serve."}) type kept struct { what string match func(*html.Node) bool } for _, pg := range []struct { path string want []string keep []kept }{ {"/add", []string{"passages"}, []kept{ {"upload form", elementWith("form", "action", "/documents")}, {"drop zone", hasAttr("rst-dropzone")}, {"tray", hasAttr("rst-tray")}, {"paste box", elementWith("textarea", "id", "body")}, }}, // search.js writes into the results region, so the search box // must not be inside it. A searched page, so the region has // something in it to over-reach with. {"/?q=revenue", []string{"results", "passages"}, []kept{ {"search form", hasAttr("rst-search")}, {"search box", elementWith("input", "type", "search")}, }}, } { doc := parsePage(t, get(t, h, pg.path).Body.String()) protected := map[string][]*html.Node{} for _, k := range pg.keep { protected[k.what] = findAll(doc, k.match) if len(protected[k.what]) == 0 { t.Fatalf("%s has no %s, so the containment check below would pass for a page "+ "without it", pg.path, k.what) } } regions := findAll(doc, hasAttr("rst-refresh")) seen := map[string]bool{} for _, r := range regions { name, _ := attrOf(r, "rst-refresh") if name == "" { t.Errorf("%s: a region carries an empty rst-refresh; the script pairs regions "+ "by name and an unnamed one can be paired with anything", pg.path) } if seen[name] { t.Errorf("%s: two regions are both named %q, so a swap puts one region's "+ "content into the other's place", pg.path, name) } seen[name] = true } for _, want := range pg.want { if !seen[want] { t.Errorf("%s has no %q region", pg.path, want) } } for _, r := range regions { name, _ := attrOf(r, "rst-refresh") for what, nodes := range protected { for _, n := range nodes { if within(n, r) { t.Errorf("the %q region on %s contains the %s. A swap replaces the "+ "region whole, so it would be destroyed and rebuilt from the server's "+ "answer — a half-typed paste, a drop in progress, or a tray row whose "+ "outcome has not been said yet, gone without a word.", name, pg.path, what) } // The other direction: a region INSIDE the form is a // control the swap replaces with the server's empty one. if within(r, n) && r != n { t.Errorf("the %q region on %s is inside the %s, so a swap replaces "+ "something in it with the server's copy — a typed title, say, with an "+ "empty field.", name, pg.path, what) } } } } } count := textOf(region(t, parsePage(t, get(t, h, "/add").Body.String()), "passages")) if !strings.Contains(count, "indexed") { t.Errorf("the %q region does not hold the passage count: %q", "passages", count) } if strings.Contains(count, "no document ever") { t.Errorf("the %q region has swallowed the egress claim beside it: %q. That sentence is "+ "not derived from anything and has no business being re-fetched.", "passages", count) } } // The re-fetch drop.js makes of /add brings the passage count up to date. // // It is made the way drop.js makes it, with the header the page tells the // script to send. // // MUTATION-TESTED 2026-09-29, reverted: `Chunks: a.chunkCount()` → // `Chunks: 0` in addContent → "the passage count on /add did not change // after 1 document was stored". func TestARefetchOfAddMovesThePassageCount(t *testing.T) { h := newApp(t) add := get(t, h, "/add").Body.String() before := textOf(region(t, parsePage(t, add), "passages")) if !strings.Contains(before, "indexed") { t.Fatalf("/add's passage region holds no count to compare: %q", before) } readReply(t, submitJSON(t, h, file{"hosting.txt", "Hosting costs fell after the migration completed."})) req := httptest.NewRequest(http.MethodGet, "/add", nil) req.Header.Set(refreshHeaderOn(t, add), "1") rec := httptest.NewRecorder() h.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("the re-fetch of /add answered %d, want 200", rec.Code) } if after := textOf(region(t, parsePage(t, rec.Body.String()), "passages")); after == before { t.Errorf("the passage count on /add did not change after 1 document was stored: %q. "+ "The tray's re-fetch is the only thing that brings it up to date.", after) } }