console: pin the chrome to inline SVG and the templates to no off-origin fetch — vendoring is only true while a test says so
Five assertions, all of which fail loudly the day someone reaches for a CDN
link or types a tick character back in:
- the switcher trigger renders a real <svg> in the vendored idiom, and no
text glyph (▾ ✓ ✕ → …) survives anywhere in the header;
- the footer keeps its factor-X robot, so a later "strip the emoji" sweep
cannot take the disclosure with it — emoji are content;
- no template references a subresource. This is internal/status's
banned-substrings idiom with the one carve-out the console needs and the
status page does not: the console legitimately LINKS out (11factor.org,
a canary's host), and a link is navigation the reader chooses. What is
banned is anything the browser fetches on its own — <script src>, <link>,
src=, @import, url(http, protocol-relative hrefs, any cdn./unpkg/jsdelivr
/googleapis host, and lucide.dev itself;
- every {{icon "slug"}} in every template resolves, which is what turns
icon()'s deliberately forgiving empty-string return into a build-time
failure instead of a silently missing caret;
- every vendored icon is self-contained: 24×24, currentColor, aria-hidden,
no absolute URL, no <image>, no hardcoded size.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 file changed,
+175
−0
internal/console/icons_test.go+175 −0
diff --git a/internal/console/icons_test.go b/internal/console/icons_test.go| new file mode 100644 |
| index 000000000..9ccb1e8b8 |
| --- /dev/null |
| +++ b/internal/console/icons_test.go |
| @@ -0,0 +1,175 @@ |
| +package console |
| + |
| +import ( |
| + "io/fs" |
| + "regexp" |
| + "strings" |
| + "testing" |
| +) |
| + |
| +// The switcher's chrome is drawn, not typed: the disclosure caret and the |
| +// menu's marks are inline SVG from icons.go, and the text glyphs they replace |
| +// ("▾", "✓") are gone from the header. A glyph is whatever the reader's font |
| +// decides it is — a caret that renders as a black triangle on one machine and |
| +// a coloured emoji on another is not a control, and neither size nor colour |
| +// obeys the page. An icon does both. |
| +func TestSwitcherChromeIsInlineSVGNotTextGlyphs(t *testing.T) { |
| + fx := newConsoleFixture(t) |
| + c := signInPaul(t, fx) |
| + _, _, body := get(t, c, fx.u("/")) |
| + h := header(t, body) |
| + |
| + summary, _, ok := strings.Cut(h, "</summary>") |
| + if !ok { |
| + t.Fatalf("no <summary> in the header:\n%s", h) |
| + } |
| + // The trigger's caret: a real <svg>, in the vendored idiom, hidden from |
| + // assistive tech because the account name beside it is the label. |
| + if !strings.Contains(summary, "<svg") { |
| + t.Errorf("the switcher trigger has no inline <svg> caret:\n%s", summary) |
| + } |
| + for _, want := range []string{`viewBox="0 0 24 24"`, `stroke="currentColor"`, `aria-hidden="true"`} { |
| + if !strings.Contains(summary, want) { |
| + t.Errorf("the caret svg is missing %s:\n%s", want, summary) |
| + } |
| + } |
| + // The menu marks the current account and offers a new one, both with |
| + // icons rather than a reused tick character. |
| + if n := strings.Count(h, "<svg"); n < 3 { |
| + t.Errorf("header has %d inline icons, want the caret plus a mark per menu row:\n%s", n, h) |
| + } |
| + // No chrome glyph survives anywhere in the header. Text arrows, ticks and |
| + // crosses are exactly what the icon set exists to replace. |
| + for _, glyph := range []string{"▾", "▼", "▽", "⌄", "✓", "✔", "✕", "✖", "×", "→", "←"} { |
| + if strings.Contains(h, glyph) { |
| + t.Errorf("header still uses the text glyph %q as chrome:\n%s", glyph, h) |
| + } |
| + } |
| +} |
| + |
| +// Emoji are content, never chrome (Eleven's docs/icons.md). The footer's |
| +// robot is a factor-X disclosure — a mark this platform puts on LLM-written |
| +// text everywhere, from README.md to site/platform — so it is content and it |
| +// stays. This test pins that distinction so a later sweep of "remove the |
| +// emoji" cannot quietly take the disclosure with it. |
| +func TestFactorXDisclosureKeepsItsEmoji(t *testing.T) { |
| + fx := newConsoleFixture(t) |
| + c := signInPaul(t, fx) |
| + _, _, body := get(t, c, fx.u("/")) |
| + foot, _, ok := strings.Cut(body, "</footer>") |
| + if !ok { |
| + t.Fatalf("no <footer> in:\n%s", body) |
| + } |
| + if !strings.Contains(foot, "🤖") || !strings.Contains(foot, "written by an LLM") { |
| + t.Errorf("the footer lost its factor-X disclosure:\n%s", foot) |
| + } |
| +} |
| + |
| +// Same-origin, the console's version. internal/status asserts this on its |
| +// published page with a flat "no http(s):// anywhere" ban; the console cannot |
| +// use that ban verbatim because it legitimately LINKS out — the footer's |
| +// 11factor.org citation, an app's canary host. A link is navigation the |
| +// reader chooses. What must never appear is a SUBRESOURCE: anything the |
| +// browser fetches on its own while rendering the page — a script, a |
| +// stylesheet, a font, an image, an @import, a CSS url(). The console ships |
| +// one HTML document with its CSS inline and its icons inline, and that is the |
| +// whole of it. Vendoring the icons is what keeps this true. |
| +func TestConsoleTemplatesFetchNothingOffOrigin(t *testing.T) { |
| + banned := []string{ |
| + "<script src", // an external script, the usual way an icon CDN arrives |
| + "<link ", // stylesheets, fonts, preconnects, prefetches |
| + "src=", // no <img>, <iframe> or <source> of any origin |
| + "@import", // a stylesheet importing another |
| + "url(http", // a CSS-referenced asset... |
| + "url(//", // ...and the protocol-relative spelling of one |
| + `href="//`, // protocol-relative, dodging the "http" ban entirely |
| + "cdn.", // any CDN host, spelled anywhere at all |
| + "lucide.dev", // the icons are vendored; the source URL lives in a Go comment |
| + "unpkg", |
| + "jsdelivr", |
| + "googleapis", |
| + } |
| + err := fs.WalkDir(tmplFS, ".", func(path string, d fs.DirEntry, err error) error { |
| + if err != nil || d.IsDir() { |
| + return err |
| + } |
| + b, err := tmplFS.ReadFile(path) |
| + if err != nil { |
| + return err |
| + } |
| + for _, bad := range banned { |
| + if strings.Contains(string(b), bad) { |
| + t.Errorf("%s must fetch nothing off-origin; found %q", path, bad) |
| + } |
| + } |
| + return nil |
| + }) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| +} |
| + |
| +var iconCall = regexp.MustCompile(`{{\s*icon\s+"([^"]*)"\s*}}`) |
| + |
| +// icon returns "" for an unknown slug so a typo cannot 500 a page mid-render. |
| +// This is the test that turns that empty string back into a failure, at build |
| +// time, where a missing caret is cheap to fix. |
| +func TestTemplateIconSlugsAllExist(t *testing.T) { |
| + used := 0 |
| + err := fs.WalkDir(tmplFS, ".", func(path string, d fs.DirEntry, err error) error { |
| + if err != nil || d.IsDir() { |
| + return err |
| + } |
| + b, err := tmplFS.ReadFile(path) |
| + if err != nil { |
| + return err |
| + } |
| + for _, m := range iconCall.FindAllStringSubmatch(string(b), -1) { |
| + used++ |
| + if icon(m[1]) == "" { |
| + t.Errorf("%s calls {{icon %q}}, which is not in icons.go", path, m[1]) |
| + } |
| + } |
| + return nil |
| + }) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if used == 0 { |
| + t.Error("no template uses {{icon}} — the chrome went back to text glyphs") |
| + } |
| +} |
| + |
| +// Every vendored icon is one self-contained, colour-inheriting, screen-reader |
| +// -silent 24×24 SVG. Getting any of these wrong is invisible in review and |
| +// obvious on the page: a hardcoded fill ignores dark mode, a stray absolute |
| +// URL reintroduces the request vendoring exists to avoid. |
| +func TestVendoredIconsAreSelfContained(t *testing.T) { |
| + if len(icons) == 0 { |
| + t.Fatal("the icon set is empty") |
| + } |
| + for slug, svg := range icons { |
| + s := string(svg) |
| + for _, want := range []string{ |
| + "<svg", "</svg>", `viewBox="0 0 24 24"`, |
| + `stroke="currentColor"`, `aria-hidden="true"`, `class="icon"`, |
| + } { |
| + if !strings.Contains(s, want) { |
| + t.Errorf("icon %q is missing %s: %s", slug, want, s) |
| + } |
| + } |
| + for _, bad := range []string{"http://", "https://", "<image", "xlink:href", "url("} { |
| + if strings.Contains(s, bad) { |
| + t.Errorf("icon %q reaches outside the page (%q): %s", slug, bad, s) |
| + } |
| + } |
| + // No width/height of their own: they size from the .icon CSS rule so |
| + // one icon at a 0.9rem menu row and the same icon in body text are the |
| + // same shape at two sizes, not two hand-tuned copies. (The leading |
| + // space is what keeps stroke-width, a different thing entirely, out |
| + // of this check.) |
| + if strings.Contains(s, " width=") || strings.Contains(s, " height=") { |
| + t.Errorf("icon %q hardcodes a size instead of sizing from the text: %s", slug, s) |
| + } |
| + } |
| +} |