oficina / meet Public

Tests for the shared chrome: titles, bar, errors by Accept, return_to, loop, gates

Each new black-box test fails on origin/main 84ed5dc (13 of 15; the
forged-cookie and token-coverage tests are guards, shown red by
mutation), and the revoked-member loop test fails with the signin
check mutated out.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev pushed by paul@keymail.dev a0d9e13811ca0db5208c6226182eb18da4e354d8 parent aa24edb
4 files changed, +469 −0
  • internal/calls/errors_test.go +30 −0
  • internal/calls/home_router_test.go +18 −0
  • internal/calls/static/calls-shell.css +1 −0
  • internal/callstest/chrome_test.go +420 −0
diff --git a/internal/calls/errors_test.go b/internal/calls/errors_test.go
new file mode 100644
index 0000000..c6034ea
--- /dev/null
+++ b/internal/calls/errors_test.go
@@ -0,0 +1,30 @@
+package calls
+
+import (
+ "net/http"
+ "net/http/httptest"
+ "strings"
+ "testing"
+)
+
+// A panic inside the router is the 500 page with a reference and the
+// viewer's chrome for a browser, and plain text for a fetch (spec §3.3).
+func TestAPanicIsTheErrorPageWithAReference(t *testing.T) {
+ f := newHomeRouterFixture(t)
+ boom := f.app.recoverToErrorPage(http.HandlerFunc(func(http.ResponseWriter, *http.Request) { panic("boom") }))
+ r := httptest.NewRequest("GET", homeTestOrigin+"/m/x", nil)
+ r.Header.Set("Accept", "text/html")
+ w := httptest.NewRecorder()
+ boom.ServeHTTP(w, r)
+ body := w.Body.String()
+ if w.Code != 500 || !strings.Contains(body, "<title>Something went wrong · Calls</title>") || !strings.Contains(body, "Reference:") ||
+ !strings.Contains(body, "<header estilo-bar>") || !strings.Contains(body, `href="/m/x">Try again</a>`) {
+ t.Fatalf("panic page: %d\n%s", w.Code, body)
+ }
+ r = httptest.NewRequest("GET", homeTestOrigin+"/m/x/info", nil)
+ w = httptest.NewRecorder()
+ boom.ServeHTTP(w, r)
+ if w.Code != 500 || !strings.HasPrefix(w.Header().Get("Content-Type"), "text/plain") {
+ t.Fatalf("panic for a fetch: %d %q", w.Code, w.Header().Get("Content-Type"))
+ }
+}
diff --git a/internal/calls/home_router_test.go b/internal/calls/home_router_test.go
index e89ba46..65e45c8 100644
--- a/internal/calls/home_router_test.go
+++ b/internal/calls/home_router_test.go
@@ -367,3 +367,21 @@ func TestHomeSignedOutIsTheOficinaDoor(t *testing.T) {
}
}
}
+
+// The revoked-user loop Astra found in Docs (#65, 1) and Sheets (#39, 2):
+// connect's Signin skips the exchange for any live session cookie and
+// sends it back to the page, which refuses it again. A removed member's
+// GET /signin must end that session and say why, and the next /signin
+// must start a real sign-in.
+func TestARemovedHomeMemberIsNotBouncedBetweenSigninAndThePage(t *testing.T) {
+ f := newHomeRouterFixture(t)
+ f.link(t)
+ f.app.cfg.allow = allowlist{}
+ w := f.do("GET", "/signin?return_to=%2Fsettings", "")
+ if w.Code != 303 || !strings.Contains(w.Header().Get("Location"), "err=removed") {
+ t.Fatalf("a removed member's /signin: %d %q, want the removed notice", w.Code, w.Header().Get("Location"))
+ }
+ if next := homeRedirect(t, f.do("GET", "/signin", "")); !strings.HasPrefix(next, "/auth/start") {
+ t.Fatalf("after the notice, /signin did not start a sign-in: %q", next)
+ }
+}
diff --git a/internal/calls/static/calls-shell.css b/internal/calls/static/calls-shell.css
index c5f4e1e..ce86e71 100644
--- a/internal/calls/static/calls-shell.css
+++ b/internal/calls/static/calls-shell.css
@@ -27,3 +27,4 @@ body { background: var(--rst-bg); }
.calls-holding-more { display: grid; gap: var(--rst-sp-3); justify-items: start; margin-block-start: var(--rst-sp-5); }
.calls-holding-more p { margin: 0; }
.calls-holding-more form { margin: 0; }
+.calls-menu-item-danger { color: var(--rst-tone-negative-fg); }
diff --git a/internal/callstest/chrome_test.go b/internal/callstest/chrome_test.go
new file mode 100644
index 0000000..144976e
--- /dev/null
+++ b/internal/callstest/chrome_test.go
@@ -0,0 +1,420 @@
+package callstest
+
+// The shared chrome (estilo spec/shared-chrome, round 2), black-box:
+// every page wears the 52px estilo bar with one account menu and one
+// Sign out, is titled "<Page> · Calls", answers errors as pages for a
+// browser and as text for a fetch, and sends a signed-out person back to
+// the page they asked for once they have signed in.
+
+import (
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "os"
+ "path/filepath"
+ "regexp"
+ "strings"
+ "testing"
+ "time"
+
+ "amadan.net/oficina/estilo.git/base"
+ "amadan.net/oficina/estilo.git/chrome"
+ estilomenu "amadan.net/oficina/estilo.git/menu"
+)
+
+var titleRe = regexp.MustCompile(`<title>([^<]*)</title>`)
+
+func pageTitle(t *testing.T, body string) string {
+ t.Helper()
+ m := titleRe.FindStringSubmatch(body)
+ if m == nil {
+ t.Fatalf("no <title>:\n%s", body)
+ }
+ return m[1]
+}
+
+// Spec §10: every page's <title> is "<Page> · … · Calls", the page's h1
+// first, and the app root is "Calls" alone.
+func TestEveryPageIsTitledPageDotCalls(t *testing.T) {
+ x, sqid, _ := memberWithMeeting(t)
+ c := x.browser(t)
+ c.signIn("paul@example.com")
+ for path, want := range map[string]string{
+ "/": "Calls",
+ "/settings": "Settings · Calls",
+ "/m/" + sqid + "/join": "Board meeting · Calls",
+ "/m/" + sqid: "Board meeting · Calls",
+ "/meetings/" + sqid + "/settings": "Meeting settings · Board meeting · Calls",
+ "/meetings/" + sqid + "/end": "End meeting for everyone? · Board meeting · Calls",
+ "/help/open-source": "Open source · Calls",
+ "/no/such/page": "Page not found · Calls",
+ "/m/zzzzzzzzzz/join": "Meeting not available · Calls",
+ } {
+ if got := pageTitle(t, c.getNav(path).Body.String()); got != want {
+ t.Errorf("GET %s: title %q, want %q", path, got, want)
+ }
+ }
+ out := x.browser(t)
+ for path, want := range map[string]string{
+ "/signin": "Sign in · Calls",
+ "/signin?sent=1": "Check your email · Calls",
+ "/signed-out": "Signed out · Calls",
+ "/no/such/page": "Page not found · Calls",
+ } {
+ if got := pageTitle(t, out.getNav(path).Body.String()); got != want {
+ t.Errorf("signed out GET %s: title %q, want %q", path, got, want)
+ }
+ }
+}
+
+// Spec §5 and §8: the estilo bar, not rastrillo's topbar; signed in, one
+// account menu holding the only Sign out; signed out, a "Sign in" that
+// comes back to this page; on the sign-in pages themselves, nothing.
+func TestPagesWearTheEstiloBarWithOneAccountMenu(t *testing.T) {
+ x, sqid, _ := memberWithMeeting(t)
+ c := x.browser(t)
+ c.signIn("paul@example.com")
+ for _, path := range []string{"/", "/settings", "/m/" + sqid + "/join", "/meetings/" + sqid + "/settings", "/help/open-source", "/no/such/page"} {
+ body := c.getNav(path).Body.String()
+ if !strings.Contains(body, "<header estilo-bar>") || strings.Contains(body, "rst-shell-bar") {
+ t.Errorf("GET %s does not wear the estilo bar", path)
+ }
+ if n := strings.Count(body, ">Sign out<"); n != 1 {
+ t.Errorf("GET %s has %d Sign out, want exactly 1", path, n)
+ }
+ if !strings.Contains(body, `<summary estilo-bar-trigger aria-label="Account: paul@example.com">`) {
+ t.Errorf("GET %s has no account menu", path)
+ }
+ for _, want := range []string{`<span estilo-menu-identity-label>Signed in as</span>`, `<p estilo-menu-heading id="estilo-appearance">Appearance</p>`, `<a estilo-menu-item href="/help/open-source"><span estilo-menu-label>Open source</span></a>`} {
+ if !strings.Contains(body, want) {
+ t.Errorf("GET %s: the account menu lacks %q", path, want)
+ }
+ }
+ for _, gone := range []string{"Help: open source", "rst-shell-account", ">Help</summary>"} {
+ if strings.Contains(body, gone) {
+ t.Errorf("GET %s still carries %q", path, gone)
+ }
+ }
+ }
+ if body := c.getNav("/settings").Body.String(); !strings.Contains(body, `<a href="/settings" aria-current="page">Settings</a>`) {
+ t.Error("the bar's nav does not mark Settings as the current page")
+ }
+ out := x.browser(t)
+ body := out.getNav("/no/such/page").Body.String()
+ if !strings.Contains(body, `<a rst-btn href="/signin?return_to=%2Fno%2Fsuch%2Fpage" estilo-bar-signin>Sign in</a>`) || strings.Contains(body, "estilo-account") {
+ t.Errorf("a signed-out page's corner is not Sign in, back to it:\n%s", body)
+ }
+ for _, path := range []string{"/signin", "/signin?sent=1", "/signed-out"} {
+ body := out.getNav(path).Body.String()
+ if !strings.Contains(body, "<header estilo-bar>") || strings.Contains(body, "estilo-bar-signin") || strings.Contains(body, "estilo-account") {
+ t.Errorf("GET %s: the sign-in pages' corner must be empty", path)
+ }
+ }
+}
+
+// Spec §8.4: a guest gets the same "Sign in" corner, never an account.
+func TestAGuestGetsTheSignInCorner(t *testing.T) {
+ x, sqid, token := memberWithMeeting(t)
+ g := x.browser(t)
+ invite := g.getNav("/j/" + token).Body.String()
+ if !strings.Contains(invite, `<a rst-btn href="/signin" estilo-bar-signin>Sign in</a>`) {
+ t.Errorf("the invitation page's corner must be a bare Sign in (its path is the capability):\n%s", invite)
+ }
+ if rec := g.post("/j/"+token+"/guest", url.Values{"name": {"Visitor"}}); rec.Code != http.StatusSeeOther {
+ t.Fatalf("guest join: %d", rec.Code)
+ }
+ body := g.getNav("/m/" + sqid + "/join").Body.String()
+ if !strings.Contains(body, "estilo-bar-signin") || strings.Contains(body, "estilo-account") || strings.Contains(body, ">Sign out<") {
+ t.Errorf("a guest's holding page corner is not Sign in:\n%s", body)
+ }
+ if !strings.Contains(body, "You’ll join as Visitor, a guest.") {
+ t.Errorf("the holding page does not say who the guest joins as:\n%s", body)
+ }
+}
+
+// Spec §3: an error is a page for a browser and plain text for anything
+// else, so the room's own fetches keep the answers room.js reads.
+func TestErrorPagesAreSplitByAccept(t *testing.T) {
+ x, _, _ := memberWithMeeting(t)
+ c := x.browser(t)
+ c.signIn("paul@example.com")
+ page := c.getNav("/no/such/page")
+ if page.Code != http.StatusNotFound || !strings.HasPrefix(page.Header().Get("Content-Type"), "text/html") ||
+ !strings.Contains(page.Body.String(), "We can’t find that page") || !strings.Contains(page.Body.String(), `<a rst-btn="primary" href="/">Go to Calls</a>`) {
+ t.Errorf("unknown route for a browser: %d %q\n%s", page.Code, page.Header().Get("Content-Type"), page.Body.String())
+ }
+ plain := c.get("/no/such/page")
+ if plain.Code != http.StatusNotFound || !strings.HasPrefix(plain.Header().Get("Content-Type"), "text/plain") {
+ t.Errorf("unknown route for a fetch: %d %q", plain.Code, plain.Header().Get("Content-Type"))
+ }
+ meeting := c.getNav("/m/zzzzzzzzzz/join")
+ if meeting.Code != http.StatusNotFound || !strings.Contains(meeting.Body.String(), "This meeting isn’t available") {
+ t.Errorf("missing meeting for a browser: %d\n%s", meeting.Code, meeting.Body.String())
+ }
+ if fetch := c.get("/m/zzzzzzzzzz/info"); fetch.Code != http.StatusNotFound || !strings.HasPrefix(fetch.Header().Get("Content-Type"), "text/plain") {
+ t.Errorf("missing meeting for a fetch: %d %q", fetch.Code, fetch.Header().Get("Content-Type"))
+ }
+ // A POST-only path opened from history is no page, not a 405.
+ if rec := c.getNav("/signout"); rec.Code != http.StatusNotFound || !strings.Contains(rec.Body.String(), "We can’t find that page") {
+ t.Errorf("GET /signout: %d", rec.Code)
+ }
+ del := c.do(httptest.NewRequest(http.MethodDelete, "/signin", nil))
+ if del.Code != http.StatusMethodNotAllowed || !strings.Contains(del.Header().Get("Allow"), "GET") {
+ t.Errorf("DELETE /signin: %d Allow %q", del.Code, del.Header().Get("Allow"))
+ }
+}
+
+// Spec §9.4: a signed-out page request carries the page to sign-in, and
+// the emailed link lands back on it. The room's fetches keep today's
+// bare redirect, which room.js reads as "signed out".
+func TestSignInComesBackToTheDeepLink(t *testing.T) {
+ x, sqid, _ := memberWithMeeting(t)
+ c := x.browser(t)
+ rec := c.getNav("/meetings/" + sqid + "/settings")
+ want := "/signin?return_to=" + url.QueryEscape("/meetings/"+sqid+"/settings")
+ if rec.Code != http.StatusSeeOther || rec.Header().Get("Location") != want {
+ t.Fatalf("signed-out deep link: %d %q, want %q", rec.Code, rec.Header().Get("Location"), want)
+ }
+ if fetch := c.getStream("/m/" + sqid + "/stream"); fetch.Code != http.StatusSeeOther || fetch.Header().Get("Location") != "/signin" {
+ t.Errorf("the stream's signed-out answer changed: %d %q", fetch.Code, fetch.Header().Get("Location"))
+ }
+ door := c.getNav(want).Body.String()
+ if !strings.Contains(door, `name="return_to" value="/meetings/`+sqid+`/settings"`) {
+ t.Errorf("the door does not carry return_to:\n%s", door)
+ }
+ if rec := c.post("/signin", url.Values{"address": {"paul@example.com"}}); rec.Code != http.StatusSeeOther {
+ t.Fatalf("POST /signin: %d", rec.Code)
+ }
+ // "Use a different address" keeps the destination (Docs #65, 2).
+ sent := c.getNav("/signin?sent=1").Body.String()
+ if !strings.Contains(sent, `href="/signin?return_to=`+url.QueryEscape("/meetings/"+sqid+"/settings")+`">Use a different address</a>`) {
+ t.Errorf("Use a different address loses the destination:\n%s", sent)
+ }
+ _, link := x.mail.last()
+ if dest := c.follow(link); dest != "/meetings/"+sqid+"/settings" {
+ t.Fatalf("after the link: %q, want the deep link", dest)
+ }
+ // Signed in, /signin skips the form and goes where it was asked to.
+ if rec := c.getNav("/signin?return_to=%2Fsettings"); rec.Code != http.StatusSeeOther || rec.Header().Get("Location") != "/settings" {
+ t.Errorf("signed in, GET /signin: %d %q", rec.Code, rec.Header().Get("Location"))
+ }
+ // And a destination off this origin is never followed.
+ if rec := c.getNav("/signin?return_to=%2F%2Fevil.example"); rec.Header().Get("Location") != "/" {
+ t.Errorf("signed in, GET /signin?return_to=//evil.example: %q", rec.Header().Get("Location"))
+ }
+}
+
+// The remembered page is sealed: a hand-written cookie is ignored.
+func TestAForgedReturnCookieIsIgnored(t *testing.T) {
+ x := newHarness(t, "paul@example.com")
+ c := x.browser(t)
+ c.cookies["calls_return"] = &http.Cookie{Name: "calls_return", Value: "%2F%2Fevil.example"}
+ if dest := c.signIn("paul@example.com"); dest != "/" {
+ t.Fatalf("a forged return cookie was followed: %q", dest)
+ }
+}
+
+// r3 audit C6: a malformed address is the field's own error, not
+// "Check your email" — and nothing is sent.
+func TestAMalformedAddressIsTheFieldsError(t *testing.T) {
+ x := newHarness(t, "paul@example.com")
+ c := x.browser(t)
+ rec := c.post("/signin", url.Values{"address": {"not-an-email@"}})
+ if rec.Code != http.StatusSeeOther || rec.Header().Get("Location") != "/signin?err=address" {
+ t.Fatalf("malformed address: %d %q", rec.Code, rec.Header().Get("Location"))
+ }
+ if x.mail.count() != 0 {
+ t.Fatal("a malformed address was mailed")
+ }
+}
+
+// Paul, 2026-10-01: the door's button says "Sign in"; the sent page
+// says what happened in the suite's words (spec §9.2).
+func TestTheDoorSpeaksTheSuitesWords(t *testing.T) {
+ x := newHarness(t, "paul@example.com")
+ c := x.browser(t)
+ door := c.getNav("/signin").Body.String()
+ for _, want := range []string{`type="submit">Sign in</button>`, "We’ll email you a link that signs you in. It works once and expires in 15 minutes.", `<a href="/help/open-source">Open source</a>`} {
+ if !strings.Contains(door, want) {
+ t.Errorf("the door lacks %q", want)
+ }
+ }
+ if strings.Contains(door, "Email me a link") {
+ t.Error("the door still says Email me a link")
+ }
+ sent := c.getNav("/signin?sent=1").Body.String()
+ if !strings.Contains(sent, "If that address can sign in here, we’ve sent it a link. It works once and expires in 15 minutes.") || strings.Contains(sent, "should be on its way") {
+ t.Errorf("the sent page's copy:\n%s", sent)
+ }
+}
+
+// Sign out lands on a page that says so (spec §9.2), not on a bare form.
+func TestSignOutLandsOnTheSignedOutPage(t *testing.T) {
+ x := newHarness(t, "paul@example.com")
+ c := x.browser(t)
+ c.signIn("paul@example.com")
+ rec := c.post("/signout", nil)
+ if rec.Code != http.StatusSeeOther || rec.Header().Get("Location") != "/signed-out" {
+ t.Fatalf("POST /signout: %d %q", rec.Code, rec.Header().Get("Location"))
+ }
+ page := c.getNav("/signed-out")
+ if page.Code != http.StatusOK || !strings.Contains(page.Body.String(), "You’re signed out") || !strings.Contains(page.Body.String(), `<a rst-btn="primary" href="/signin">Sign in</a>`) {
+ t.Errorf("GET /signed-out: %d\n%s", page.Code, page.Body.String())
+ }
+}
+
+// Spec §8.3: Appearance is a cookie the layout writes into <html>.
+func TestAppearanceIsRememberedPerApp(t *testing.T) {
+ x := newHarness(t, "paul@example.com")
+ c := x.browser(t)
+ c.signIn("paul@example.com")
+ rec := c.post("/appearance", url.Values{"mode": {"dark"}, "return_to": {"/settings"}})
+ if rec.Code != http.StatusSeeOther || rec.Header().Get("Location") != "/settings" {
+ t.Fatalf("POST /appearance: %d %q", rec.Code, rec.Header().Get("Location"))
+ }
+ body := c.getNav("/settings").Body.String()
+ if !strings.Contains(body, `<html lang="en" dir="ltr" data-theme="dark">`) || !strings.Contains(body, `value="dark" aria-pressed="true"`) {
+ t.Errorf("dark was not remembered:\n%s", body[:min(len(body), 600)])
+ }
+}
+
+// Real list rows (spec §2 of the audit): each meeting is an rst-row whose
+// actions are one ⋯ menu in estilo's floating menu, with one label for
+// ending a meeting.
+func TestMeetingsAreListRowsWithOneMenu(t *testing.T) {
+ x, sqid, _ := memberWithMeeting(t)
+ c := x.browser(t)
+ c.signIn("paul@example.com")
+ body := c.getNav("/").Body.String()
+ for _, want := range []string{
+ `<span rst-row-main><a href="/m/` + sqid + `/join">Board meeting</a>`,
+ `<details rst-row-menu name="estilo-menus">`,
+ `<summary aria-label="Actions for Board meeting">`,
+ `<a estilo-menu-item class="calls-menu-item-danger" href="/meetings/` + sqid + `/end"><span estilo-menu-label>End meeting for everyone</span></a>`,
+ } {
+ if !strings.Contains(body, want) {
+ t.Errorf("the meeting list lacks %q", want)
+ }
+ }
+ if strings.Contains(body, "<article>") {
+ t.Error("the meeting list still renders bare articles")
+ }
+}
+
+// r3 audit C4 / spec S10: Start a call names the call in the starter's
+// own time, and without script names the zone it used.
+func TestStartACallTitlesInTheStartersZone(t *testing.T) {
+ x := newHarness(t, "paul@example.com")
+ c := x.browser(t)
+ c.signIn("paul@example.com")
+ tokyo, _ := time.LoadLocation("Asia/Tokyo")
+ rec := c.post("/meetings/start", url.Values{"zone": {"Asia/Tokyo"}})
+ if want := "Call, " + x.now.In(tokyo).Format("2 Jan 15:04"); !strings.Contains(rec.Body.String(), want) {
+ t.Errorf("with the browser's zone the title is not %q:\n%s", want, rec.Body.String())
+ }
+ rec = c.post("/meetings/start", nil)
+ if want := "Call, " + x.now.UTC().Format("2 Jan 15:04") + " UTC"; !strings.Contains(rec.Body.String(), want) {
+ t.Errorf("without one the title is not %q", want)
+ }
+}
+
+// Spec §4's grep gate: one notice vocabulary, rastrillo's callout in a
+// tone. rst-notice is always success green, and carried the relay
+// warning and "open in another tab".
+func TestTemplatesUseCalloutTonesOnly(t *testing.T) {
+ bad := regexp.MustCompile(`rst-notice|rst-callout="|rst-tone="warn"`)
+ n := 0
+ err := filepath.WalkDir(filepath.Join("..", "calls", "templates"), func(path string, d os.DirEntry, err error) error {
+ if err != nil || d.IsDir() || !strings.HasSuffix(path, ".html") {
+ return err
+ }
+ n++
+ b, err := os.ReadFile(path)
+ if err != nil {
+ return err
+ }
+ if m := bad.Find(b); m != nil {
+ t.Errorf("%s uses %q", path, m)
+ }
+ return nil
+ })
+ if err != nil || n < 10 {
+ t.Fatalf("walked %d templates: %v", n, err)
+ }
+}
+
+// Spec §3.3's second gate: every remaining plain-text http.Error is
+// listed here with why it is not a page. A new one fails until it is
+// either a page (errorPage) or classified.
+func TestEveryPlainTextErrorIsClassified(t *testing.T) {
+ classified := map[string]int{
+ // The native app's JSON endpoints (/native/*): a Swift client
+ // reads the status, and a page is the wrong answer to it.
+ "native_auth.go": 12,
+ // POST /native/devices: JSON from the native app.
+ "knockpush.go": 7,
+ // methodNotAllowed's 405 (no person navigates with DELETE) and
+ // plainError, the answer to a request that did not ask for a page.
+ "errors.go": 2,
+ // render's last resort when a template itself fails.
+ "render.go": 1,
+ }
+ files, err := filepath.Glob(filepath.Join("..", "calls", "*.go"))
+ if err != nil || len(files) < 20 {
+ t.Fatalf("glob: %d files, %v", len(files), err)
+ }
+ got := map[string]int{}
+ for _, f := range files {
+ if strings.HasSuffix(f, "_test.go") {
+ continue
+ }
+ b, err := os.ReadFile(f)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if n := strings.Count(string(b), "http.Error("); n > 0 {
+ got[filepath.Base(f)] = n
+ }
+ }
+ for f, n := range got {
+ if classified[f] != n {
+ t.Errorf("%s has %d http.Error calls, %d classified: make it a page, or classify it here with the reason", f, n, classified[f])
+ }
+ }
+ for f, n := range classified {
+ if got[f] != n {
+ t.Errorf("%s: %d classified, %d found", f, n, got[f])
+ }
+ }
+}
+
+// The two-pin check (estilo AGENTS.md; spec §12.1 item 1): Calls pins
+// rastrillo v0.26, estilo's own token tests run against v0.23, so every
+// var(--rst-*) estilo's base, menu and chrome sheets use must be defined
+// in the tokens.css and theme.css Calls actually serves.
+func TestEstilosChromeUsesTokensCallsDefines(t *testing.T) {
+ var defined strings.Builder
+ for _, f := range []string{"tokens.css", "theme.css"} {
+ b, err := os.ReadFile(filepath.Join("..", "calls", "static", f))
+ if err != nil {
+ t.Fatal(err)
+ }
+ defined.Write(b)
+ }
+ defs := map[string]bool{}
+ for _, m := range regexp.MustCompile(`(--rst-[a-z0-9-]+)\s*:`).FindAllStringSubmatch(defined.String(), -1) {
+ defs[m[1]] = true
+ }
+ use := regexp.MustCompile(`var\((--rst-[a-z0-9-]+)`)
+ for name, css := range map[string][]byte{"base.css": base.CSS(), "menu.css": estilomenu.CSS(), "chrome.css": chrome.CSS()} {
+ uses := use.FindAllSubmatch(css, -1)
+ if len(uses) == 0 && name != "base.css" {
+ t.Errorf("%s uses no tokens: this check proves nothing", name)
+ }
+ for _, m := range uses {
+ if !defs[string(m[1])] {
+ t.Errorf("estilo %s uses %s, which Calls' tokens.css and theme.css do not define", name, m[1])
+ }
+ }
+ }
+}