Add content: close the third review's guard gaps (GPT-6 Astra)
Astra's third pass found no runtime defect and three mutations that passed make ci. Containment protected the form but not the controls inside it: rst-refresh on the title input let a swap replace a typed title with an empty one. A region inside a protected element is now refused as well as one around it, and the index's search form is protected too. The script checks proved spelling and counts. `refreshHeader += "-Count"` after the read, and moving the pointer's hiding from hideTray() into put(), both kept every substring. Go cannot run the script, so the checks stay structural, but they are now the shape drop_test.go already uses for `endpoint`: refreshHeader is assigned exactly once (compound assignment included) and sent only from refresh(); `listed` is mentioned exactly four times, is hidden only inside hideTray(), and put() never touches it. Astra watched the real behaviour in Chromium on each pass. Mutations, each built and seen red: Astra's three, the header line deleted from refresh(), an uppercase unquoted region, and the fourteen from before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 files changed,
+90
−17
internal/memoriatest/notice_test.go+79 −17internal/memoriatest/refresh_test.go+11 −0
CI passed — run details
CI log
=== vet === go vet ./... go: downloading golang.org/x/net v0.57.0 === fmt === === test === go test ./... ? amadan.net/oficina/memoria/cmd/memoria [no test files] ok amadan.net/oficina/memoria/internal/memoria 3.288s ok amadan.net/oficina/memoria/internal/memoria/chunk (cached) ok amadan.net/oficina/memoria/internal/memoria/fact (cached) ? amadan.net/oficina/memoria/internal/memoria/icons [no test files] ok amadan.net/oficina/memoria/internal/memoria/index (cached) ok amadan.net/oficina/memoria/internal/memoria/notion (cached) ok amadan.net/oficina/memoria/internal/memoria/provider (cached) ok amadan.net/oficina/memoria/internal/memoria/query (cached) ok amadan.net/oficina/memoria/internal/memoria/seal (cached) ok amadan.net/oficina/memoria/internal/memoria/sibling (cached) ok amadan.net/oficina/memoria/internal/memoriatest 1.609s ? amadan.net/oficina/memoria/scripts/writeestilo [no test files] ? amadan.net/oficina/memoria/tools/bench [no test files] ? amadan.net/oficina/memoria/tools/drive [no test files] ? amadan.net/oficina/memoria/tools/seed [no test files] ? amadan.net/oficina/memoria/tools/vendorassets [no test files] === migration-check === go run github.com/carlosframework/rastrillo/cmd/rastrillo migration check rastrillo migration check: models and migrations agree
diff --git a/internal/memoriatest/notice_test.go b/internal/memoriatest/notice_test.go| index 77367cf..10d73da 100644 |
| --- a/internal/memoriatest/notice_test.go |
| +++ b/internal/memoriatest/notice_test.go |
| @@ -5,6 +5,7 @@ import ( |
| "net/http/cookiejar" |
| "net/http/httptest" |
| "net/url" |
| + "regexp" |
| "strings" |
| "testing" |
| @@ -123,21 +124,62 @@ func refreshHeaderOn(t *testing.T, addPage string) string { |
| return name |
| } |
| +// jsFunction returns the body of the named function declaration in src, |
| +// braces matched. drop.js writes no braces inside strings in the functions |
| +// this is used on; a body that does not close is a fatal, not a pass. |
| +func jsFunction(t *testing.T, src, name string) string { |
| + t.Helper() |
| + at := strings.Index(src, "function "+name+"(") |
| + if at < 0 { |
| + t.Fatalf("drop.js has no function %s", name) |
| + } |
| + open := strings.Index(src[at:], "{") |
| + if open < 0 { |
| + t.Fatalf("function %s has no body", name) |
| + } |
| + depth := 0 |
| + for i := at + open; i < len(src); i++ { |
| + switch src[i] { |
| + case '{': |
| + depth++ |
| + case '}': |
| + depth-- |
| + if depth == 0 { |
| + return src[at+open : i+1] |
| + } |
| + } |
| + } |
| + t.Fatalf("function %s never closes", name) |
| + return "" |
| +} |
| + |
| +// assignments counts every assignment to name, compound ones included — |
| +// the same rule drop_test.go applies to `endpoint`. |
| +func assignments(src, name string) int { |
| + return len(regexp.MustCompile(`\b`+name+`\s*(?:>>>|\*\*|<<|>>|\|\||&&|\?\?|[-+*/%&|^])?=[^=]`).FindAllString(src, -1)) |
| +} |
| + |
| // drop.js sends the header the page names, and no other. |
| // |
| -// Go cannot run the script, so this reads it. There is no header name in |
| -// drop.js to drift: it reads data-refresh-header off the drop zone and |
| -// passes that to its one setRequestHeader for the re-fetch. What this |
| -// pins is that shape. TestAnUploadNoticeWaitsForTheAddPage pins that the |
| -// server honours the name the page publishes. |
| +// Go cannot run the script, so this reads it, and what it can hold is |
| +// structure: there is no header name in drop.js to drift, the one |
| +// variable holding the page's name is assigned once — at the read — and |
| +// the re-fetch in refresh() is the one place it is sent. The behaviour |
| +// (the header a real re-fetch carries, and the notice surviving it) was |
| +// watched in Chromium by Astra on 2026-09-29; TestAnUploadNoticeWaitsForTheAddPage |
| +// pins that the server honours the name the page publishes. |
| // |
| // MUTATION-TESTED 2026-09-29, each reverted: |
| // |
| // (a) setRequestHeader(refreshHeader, "1") → setRequestHeader( |
| -// "X-Memoria-Refresh-Count", "1"), Astra's divergence → "drop.js does |
| -// not send the header it read off the page" |
| +// "X-Memoria-Refresh-Count", "1"), Astra's divergence → "drop.js |
| +// does not send the header it read off the page" |
| // (b) the attribute read changed to data-refresh → "drop.js does not |
| // read its refresh header off the drop zone" |
| +// (c) Astra's: `refreshHeader += "-Count";` after the read → "drop.js |
| +// assigns refreshHeader 2 times" |
| +// (d) the setRequestHeader line deleted from refresh() → "drop.js sends |
| +// refreshHeader in 0 places, want 1" |
| func TestTheDropScriptSendsTheHeaderThePageNames(t *testing.T) { |
| h := newApp(t) |
| rec := get(t, h, "/static/drop.js") |
| @@ -148,9 +190,19 @@ func TestTheDropScriptSendsTheHeaderThePageNames(t *testing.T) { |
| if !strings.Contains(js, `var refreshHeader = zone.getAttribute("data-refresh-header");`) { |
| t.Error("drop.js does not read its refresh header off the drop zone") |
| } |
| + if n := assignments(js, "refreshHeader"); n != 1 { |
| + t.Errorf("drop.js assigns refreshHeader %d times; once, at the read, or what it sends "+ |
| + "is not what the page named", n) |
| + } |
| + if n := strings.Count(js, "setRequestHeader(refreshHeader"); n != 1 { |
| + t.Errorf("drop.js sends refreshHeader in %d places, want 1", n) |
| + } |
| if !strings.Contains(js, `req.setRequestHeader(refreshHeader, "1");`) { |
| t.Error("drop.js does not send the header it read off the page") |
| } |
| + if !strings.Contains(jsFunction(t, js, "refresh"), `req.setRequestHeader(refreshHeader, "1");`) { |
| + t.Error("refresh() does not send the header; the re-fetch is the request that needs it") |
| + } |
| if strings.Contains(strings.ToLower(js), "x-memoria") { |
| t.Error("drop.js spells a header name of its own; the page is where it reads one") |
| } |
| @@ -160,15 +212,19 @@ func TestTheDropScriptSendsTheHeaderThePageNames(t *testing.T) { |
| // pointing there. |
| // |
| // Astra, 2026-09-29: the link went back to /add, which has no list, and |
| -// changing it back passed make ci. Visibility after the refresh is the |
| -// script's, so the script is read: the pointer is shown in one place and |
| -// hidden in one place, when the tray is dismissed. |
| +// changing it back passed make ci. Staying visible after the refresh is |
| +// the script's, and Go cannot run it, so the script is read: `listed` is |
| +// touched in exactly four places — found, checked, shown, hidden — and the |
| +// hiding is in hideTray(), which is dismissal. Watched in Chromium by |
| +// Astra on 2026-09-29: the link stayed after a real refresh. |
| // |
| // MUTATION-TESTED 2026-09-29, each reverted: |
| // |
| // (a) href="/" → href="/add" → "the tray's link goes to "/add"" |
| -// (b) `listed.hidden = true;` added back to put() → "drop.js hides the |
| -// tray's pointer in 2 places" |
| +// (b) `listed.hidden = true;` added to put() → "drop.js mentions |
| +// `listed` 5 times" |
| +// (c) Astra's: that line MOVED from hideTray() into put() → "hideTray() |
| +// does not hide the pointer" and "put() touches the pointer" |
| func TestTheTrayPointsAtTheDocumentList(t *testing.T) { |
| h := newApp(t) |
| doc := parsePage(t, get(t, h, "/add").Body.String()) |
| @@ -185,11 +241,17 @@ func TestTheTrayPointsAtTheDocumentList(t *testing.T) { |
| } |
| js := get(t, h, "/static/drop.js").Body.String() |
| - if n := strings.Count(js, "listed.hidden = false;"); n != 1 { |
| - t.Errorf("drop.js shows the tray's pointer in %d places, want 1", n) |
| + if n := len(regexp.MustCompile(`(^|[^-\w])listed\b`).FindAllString(js, -1)); n != 4 { |
| + t.Errorf("drop.js mentions `listed` %d times, want 4: found, checked, shown on "+ |
| + "announce, hidden on dismiss. Anything else can hide it after a refresh.", n) |
| + } |
| + if !strings.Contains(jsFunction(t, js, "hideTray"), "listed.hidden = true;") { |
| + t.Error("hideTray() does not hide the pointer") |
| + } |
| + if strings.Contains(jsFunction(t, js, "put"), "listed") { |
| + t.Error("put() touches the pointer; it is true whether or not the refresh arrives") |
| } |
| - if n := strings.Count(js, "listed.hidden = true;"); n != 1 { |
| - t.Errorf("drop.js hides the tray's pointer in %d places, want 1 (dismissing the tray). "+ |
| - "It is true whether or not the refresh arrives.", n) |
| + if strings.Count(js, "listed.hidden = false;") != 1 { |
| + t.Error("drop.js does not show the pointer in exactly one place") |
| } |
| } |
diff --git a/internal/memoriatest/refresh_test.go b/internal/memoriatest/refresh_test.go| index 5d280e6..98c3cab 100644 |
| --- a/internal/memoriatest/refresh_test.go |
| +++ b/internal/memoriatest/refresh_test.go |
| @@ -114,6 +114,9 @@ import ( |
| // 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."}) |
| @@ -137,6 +140,7 @@ func TestTheRefreshedRegionsCannotTakeTheFormOrTheTrayWithThem(t *testing.T) { |
| // 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")}, |
| }}, |
| } { |
| @@ -180,6 +184,13 @@ func TestTheRefreshedRegionsCannotTakeTheFormOrTheTrayWithThem(t *testing.T) { |
| "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) |
| + } |
| } |
| } |
| } |