//go:build integration package web // 1523/AC4: mesh-mail-web started whole (the harness issue 1285 built, its stand-in seat extended to // mail-reading by runtime_integration_test.go's standInMailReading): a correction posted on the page // reaches the stand-in's Correct, an undo reaches Undo, and nothing of mail is read but through mail-store // (the classical list's own mail-store.accounts; never mail-store.search or .read, which this flow never // needs). Compiled by `go vet -tags integration ./...`; never run here — the lead's try runs it for real. import ( "io" "net/http" "net/url" "sync" "testing" ) // recordedArgs is a mutex-guarded holder: a stand-in's own closure (called from the player's readLoop // goroutine, runtime_integration_test.go's handleBundleRequest) records what it was asked here, and the // test (its own, main goroutine) reads it back safely afterward. The coordinator's own fix for issue 1523: // the merge check's `go test -race` found this exact unsynchronized pattern — a bare variable written by // one goroutine and read by another, with no memory synchronization the race detector can see (the two // communicate only over a stdio pipe and an HTTP round trip, neither of which it tracks as ordinary Go // synchronization). type recordedArgs struct { mu sync.Mutex args map[string]any set bool } func (r *recordedArgs) record(args map[string]any) { r.mu.Lock() r.args, r.set = args, true r.mu.Unlock() } func (r *recordedArgs) clear() { r.mu.Lock() r.args, r.set = nil, false r.mu.Unlock() } func (r *recordedArgs) get() map[string]any { r.mu.Lock() defer r.mu.Unlock() return r.args } func (r *recordedArgs) wasSet() bool { r.mu.Lock() defer r.mu.Unlock() return r.set } func TestAcceptanceMailCorrectionAndUndoReachTheStandIn(t *testing.T) { mw := startMailWeb(t, acceptanceAccounts(), []string{"alice"}) // The classical list below the mail-reading section still asks seat:mail-store.accounts on every page // load (accounts.go's own accountsPage), exactly as milestone 1's own tests set it up (review round 1 // finding 1, issue 1523): left unset, GET "/" fails this test as an unknown ask, never reaching the // mail-reading flow this test is actually about. mw.store.Accounts = func(map[string]any) (any, error) { return map[string]any{"accounts": []any{map[string]any{"account": "a", "kind": "imap", "address": "a@example.org", "state": "current"}}}, nil } mw.mailReading.Status = func(map[string]any) (any, error) { return map[string]any{"healthy": true, "weeks": []map[string]any{}}, nil } mw.mailReading.Entries = func(map[string]any) (any, error) { return map[string]any{"entries": []map[string]any{ {"id": 1, "sender-name": "Alice", "summary": "pay the bill", "mark": "matters"}, }}, nil } mw.mailReading.Groups = func(map[string]any) (any, error) { return map[string]any{"groups": []map[string]any{}}, nil } mw.mailReading.Lessons = func(map[string]any) (any, error) { return map[string]any{"lessons": []map[string]any{ {"id": 9, "sender-name": "Bob", "subject": "fyi", "change": "mark: neither -> matters", "made": "2026-10-20T10:00:00Z"}, }}, nil } var correctArgs, undoArgs recordedArgs mw.mailReading.Correct = func(args map[string]any) (any, error) { correctArgs.record(args) return map[string]any{"id": args["id"]}, nil } mw.mailReading.Undo = func(args map[string]any) (any, error) { undoArgs.record(args) return map[string]any{"undone": args["id"]}, nil } cookie := signIn(t, mw, "alice", "/") page := doWithCookie(t, mw, http.MethodGet, "/all", nil, cookie) pageBody, _ := io.ReadAll(page.Body) _ = page.Body.Close() if page.StatusCode != http.StatusOK { t.Fatalf("the accounts page: status %d: %s", page.StatusCode, pageBody) } token := extractHiddenValue(string(pageBody), FormTokenField) if token == "" { t.Fatalf("the accounts page carries no form token:\n%s", pageBody) } // 1523/AC1: a correction posted with the session's own token reaches the stand-in's Correct. correctResp := doForm(t, mw, "/mail-correct", url.Values{ "id": {"1"}, "mark": {"matters"}, "group": {"bills"}, FormTokenField: {token}, }, cookie) correctBody, _ := io.ReadAll(correctResp.Body) _ = correctResp.Body.Close() if correctResp.StatusCode != http.StatusOK { t.Fatalf("the correction: status %d: %s", correctResp.StatusCode, correctBody) } if got := correctArgs.get(); got["id"] != float64(1) || got["mark"] != "matters" || got["group"] != "bills" { t.Fatalf("mail-reading.correct was asked %v, want id 1, mark matters, group bills", got) } // Without the token, the correction never reaches the stand-in (1523/AC1). correctArgs.clear() noTokenResp := doForm(t, mw, "/mail-correct", url.Values{"id": {"1"}, "mark": {"neither"}}, cookie) _ = noTokenResp.Body.Close() if noTokenResp.StatusCode != http.StatusForbidden { t.Fatalf("a correction with no token: status %d, want 403", noTokenResp.StatusCode) } if correctArgs.wasSet() { t.Fatalf("mail-reading.correct was asked %v with no token", correctArgs.get()) } // 1523/AC2: an undo posted with the session's own token reaches the stand-in's Undo. undoResp := doForm(t, mw, "/mail-undo", url.Values{"id": {"9"}, FormTokenField: {token}}, cookie) undoBody, _ := io.ReadAll(undoResp.Body) _ = undoResp.Body.Close() if undoResp.StatusCode != http.StatusOK { t.Fatalf("the undo: status %d: %s", undoResp.StatusCode, undoBody) } if got := undoArgs.get(); got["id"] != float64(9) { t.Fatalf("mail-reading.undo was asked %v, want id 9", got) } // 1523/AC4: nothing of mail is read but through mail-store — this flow never asks mail-store.search or // mail-store.read at all (only .accounts, for the classical list beneath). for _, k := range mw.store.Keys() { if k == "seat:mail-store.search" || k == "seat:mail-store.read" { t.Fatalf("mail-store was asked %s, which this flow never needs", k) } } }