fix(console): fill switcher Seg on /accounts/new too; guard Nav when Accounts is unloaded
Review of the Task 4 commit found handleNewAccountForm building its own
p.Accounts without ever setting Seg on each row — layout.html's switcher
now renders href="/a/{{.Seg}}/", so every non-current row on /accounts/new
was a dead href="/a//" link. Mirror accountPage's fill there too.
Also guard p.Nav when AccountsFor errors and p.Accounts is nil: preferredSeg
over an empty slice can't see collisions, so fall back to p.Base instead of
trusting the raw name (brief's "where the accounts list isn't loaded, set
Nav equal to Base").
Added TestSwitcherLinksUsePreferredSegmentsEverywhere (server_account_test.go),
covering a two-account viewer's switcher hrefs on both an account page and
/accounts/new — the gap that let the dead link through untested. Verified it
fails without the fix (reverted server.go and re-ran) and passes with it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QGdTfzwT492ysLPDaWfpvJ
2 files changed,
+54
−1
internal/console/server.go+8 −1internal/console/server_account_test.go+46 −0
diff --git a/internal/console/server.go b/internal/console/server.go| index e96dea40b..f3fa77f6c 100644 |
| --- a/internal/console/server.go |
| +++ b/internal/console/server.go |
| @@ -646,6 +646,9 @@ func (s *Server) handleNewAccountForm(w http.ResponseWriter, r *http.Request) { |
| } else { |
| p.Accounts = accts |
| } |
| + for i := range p.Accounts { |
| + p.Accounts[i].Seg = preferredSeg(p.Accounts, &p.Accounts[i]) |
| + } |
| // The header chrome wants an account to name in the switcher's trigger. |
| // memberAccount, not currentAccount: looking at this page must never mint. |
| // A user with no memberships at all simply gets the wordmark header. |
| @@ -655,7 +658,11 @@ func (s *Server) handleNewAccountForm(w http.ResponseWriter, r *http.Request) { |
| p.Account = a |
| p.Role = a.Role |
| p.Base = "/a/" + url.PathEscape(a.Sqid) |
| - p.Nav = "/a/" + url.PathEscape(preferredSeg(p.Accounts, a)) |
| + if len(p.Accounts) == 0 { |
| + p.Nav = p.Base |
| + } else { |
| + p.Nav = "/a/" + url.PathEscape(preferredSeg(p.Accounts, a)) |
| + } |
| } |
| s.render(w, http.StatusOK, "newaccount", p) |
| } |
diff --git a/internal/console/server_account_test.go b/internal/console/server_account_test.go| index 7f33e1960..e6d6b2f14 100644 |
| --- a/internal/console/server_account_test.go |
| +++ b/internal/console/server_account_test.go |
| @@ -114,6 +114,52 @@ func TestLandingPrefersNameSegment(t *testing.T) { |
| } |
| } |
| +// The switcher's non-current rows must link somewhere real on every page that |
| +// renders it — not just the pages that happen to load Accounts through |
| +// accountPage. /accounts/new fills its own Accounts (for the switcher's |
| +// trigger, not chrome accountPage builds), and it must fill Seg on each row |
| +// too, or the link degenerates to href="/a//" for any account whose name |
| +// isn't its own preferred segment. |
| +func TestSwitcherLinksUsePreferredSegmentsEverywhere(t *testing.T) { |
| + fx := newConsoleFixture(t) |
| + fx.setUser("duo@keymail.dev") |
| + c := newBrowser(t) |
| + signUp(t, fx, c, "code-duo") // mints a personal account named by address (not slug-shaped) |
| + |
| + accts, err := fx.st.AccountsFor("duo@keymail.dev") |
| + if err != nil || len(accts) != 1 { |
| + t.Fatalf("AccountsFor = %+v %v, want one minted account", accts, err) |
| + } |
| + personalSqid := accts[0].Sqid |
| + |
| + id := testAccountID() |
| + sqid := accountSqid(id) |
| + if err := fx.st.CreateAccount(id, sqid, "amadan", "duo@keymail.dev", time.Now()); err != nil { |
| + t.Fatal(err) |
| + } |
| + |
| + // On the personal account's own page, "amadan" is the non-current row — |
| + // its href must be the name, not a dead "/a//". |
| + if st, _, _ := get(t, c, fx.ts.URL+"/a/"+personalSqid+"/"); st != http.StatusOK { |
| + t.Fatalf("personal account page = %d", st) |
| + } |
| + _, _, body := get(t, c, fx.ts.URL+"/a/"+personalSqid+"/") |
| + if !strings.Contains(body, `href="/a/amadan/"`) { |
| + t.Errorf("account page switcher: non-current row for the named account has no usable href:\n%s", body) |
| + } |
| + |
| + // /accounts/new builds its own Accounts list (not through accountPage). |
| + // The MRU is still the personal account, so "amadan" is again the |
| + // non-current row there too. |
| + _, _, body = get(t, c, fx.ts.URL+"/accounts/new") |
| + if !strings.Contains(body, `href="/a/amadan/"`) { |
| + t.Errorf("/accounts/new switcher: non-current row for the named account has no usable href:\n%s", body) |
| + } |
| + if strings.Contains(body, `href="/a//"`) { |
| + t.Errorf("/accounts/new switcher rendered a dead href:\n%s", body) |
| + } |
| +} |
| + |
| // A user with no memberships at all gets a personal account minted on the |
| // spot — with a bucket manifest, so every box materializes the same identity. |
| // A name the viewer cannot resolve — someone ELSE's account — answers with |