package gateway import ( "os" "path/filepath" "regexp" "strings" "testing" ) // The Electron shell (cmd/gui) had NO tests at all, so the plugin panel went in // with references to CSS classes that do not exist (.tag, .sm) and to helper // functions that were never defined in that document (esc / escAttr). All of // it rendered as unstyled text and would have thrown a ReferenceError at click // time — and none of that is visible without opening the app. // // These tests are deliberately static. They do not launch Electron: what they // guard is the class of mistake that "looks fine until someone themes it", // which is exactly what a missing CSS class or a missing helper is. func guiFile(t *testing.T, rel string) string { t.Helper() // The tests live in internal/gateway, so walk up to the repo root. p := filepath.Join("..", "..", rel) b, err := os.ReadFile(p) if err != nil { t.Skipf("%s not readable: %v", rel, err) } return string(b) } // classUseRe finds class="..." occurrences in a document. var classUseRe = regexp.MustCompile(`class="([^"]+)"`) // classTokenRe matches ANY ".name" inside the stylesheet. Deliberately loose: // it also matches inside compound selectors (".tb-btn.tb-close:hover" must count // as defining .tb-close, which a "must be at the start of a selector" rule // misses) and inside comments, which only ever makes the check MORE permissive. // A false pass here would be bad, so the strictness lives elsewhere: the // variable below is what actually guards the new code. var classTokenRe = regexp.MustCompile(`\.([A-Za-z_][A-Za-z0-9_-]*)`) // guiKnownUnstyled lists classes the shell markup has always used with no // matching rule. They are pre-existing cosmetic gaps, not regressions, and // failing on them would make this test useless as a guard for NEW work. var guiKnownUnstyled = map[string]bool{ "blob": true, // decorative blur blobs, styled per-instance via .b1/.b2/.b3 "tgl": true, // rail toggle affordance that leaned on .rail-btn "rail": true, // the rail container itself has no rule; .rail-btn children carry the look } // TestGUICSSClassesExist is the guard that would have caught .tag and .sm: every // class used in the shell's markup must be defined in its stylesheet. // // The comparison is on the LAST segment of a selector, because the stylesheet // scopes things (`.form .actions .primary`, `#bgfx .b1`): a rule for // `.pl-acts button` defines no class at all, and `.form .row .toggle` defines // `.toggle`. Requiring a top-level class would be too strict; requiring that // some selector's last identifier matches is the right level. func TestGUICSSClassesExist(t *testing.T) { html := guiFile(t, "cmd/gui/renderer/index.html") css := guiFile(t, "cmd/gui/renderer/style.css") defined := map[string]bool{} // Collect every class token that appears at the START of a selector // position. A full CSS parser is overkill and was the source of two wrong // turns here; what the check needs is simply "does the name .foo appear // anywhere in the stylesheet as a selector component". // // Scoping is respected loosely: `.form .actions .primary` counts as // defining `.primary`, and `.pl-acts button` defines no class — which is // exactly why the plugin panel needed its own rules. for _, m := range classTokenRe.FindAllStringSubmatch(css, -1) { defined[m[1]] = true } if len(defined) == 0 { t.Fatal("no classes parsed from the stylesheet; the check is broken") } // Classes the JS builds as strings must exist too. js := guiFile(t, "cmd/gui/renderer/app.js") var missing []string seen := map[string]bool{} note := func(cls, where string) { // A "${...}" token is a template literal being spliced at runtime, not // a class name; the classes it can expand to are checked at their // definition sites instead. if cls == "" || strings.ContainsAny(cls, "${}") || seen[cls] { return } seen[cls] = true if guiKnownUnstyled[cls] { return } if !defined[cls] { missing = append(missing, cls+" ("+where+")") } } for _, m := range classUseRe.FindAllStringSubmatch(html, -1) { for _, c := range strings.Fields(m[1]) { note(c, "index.html") } } for _, m := range classUseRe.FindAllStringSubmatch(js, -1) { for _, c := range strings.Fields(m[1]) { note(c, "app.js") } } if len(missing) > 0 { t.Errorf("classes used but not defined in style.css (they render unstyled):\n %s", strings.Join(missing, "\n ")) } } // TestGUIHelperFunctionsAreDefined catches the other half: renderer/app.js is a // separate document from the WebUI, so it does NOT have the WebUI's esc/escAttr. // Referencing them gives a ReferenceError only when the line runs. func TestGUIHelperFunctionsAreDefined(t *testing.T) { js := guiFile(t, "cmd/gui/renderer/app.js") for _, fn := range []string{"esc", "escAttr", "toast", "loadPlugins", "togglePlugin", "enableAllPlugins"} { defined := regexp.MustCompile(`function ` + fn + `\b`).MatchString(js) called := regexp.MustCompile(`\b` + fn + `\s*\(`).MatchString(js) if called && !defined { t.Errorf("%s() is called but never defined in app.js", fn) } if !called && !defined { // A defined-but-unused helper is dead code, not an error. continue } } } // TestGUIPluginPanelIsReachable: the panel must be inside the settings overlay // AND the settings overlay must actually open it. A panel wired to a button // that was never bound is invisible-but-present, which passes a grep review. func TestGUIPluginPanelIsReachable(t *testing.T) { html := guiFile(t, "cmd/gui/renderer/index.html") js := guiFile(t, "cmd/gui/renderer/app.js") if !strings.Contains(html, `id="pl-list"`) { t.Error("no #pl-list in the settings overlay") } if !strings.Contains(html, `id="settings-overlay"`) { t.Fatal("the settings overlay is gone") } // inside the overlay: the element index must come after the overlay's if strings.Index(html, `id="settings-overlay"`) > strings.Index(html, `id="pl-list"`) { t.Error("#pl-list appears before the settings overlay, so it renders outside the panel") } // The buttons must be bound. for _, id := range []string{"pl-reload", "pl-toggle-all"} { if !strings.Contains(html, `id="`+id+`"`) { t.Errorf("#%s is missing from the markup", id) } if !strings.Contains(js, `"`+id+`"`) { t.Errorf("#%s exists but app.js never binds it", id) } } // And openSettings must trigger the load, or the panel shows a stale empty // list on every open. if !strings.Contains(js, "loadPlugins()") { t.Error("app.js never calls loadPlugins()") } } // TestGUIIPCPathIsConstrained: plugins:proxy is a raw pass-through, which is // convenient but would be a hole if it accepted arbitrary paths. The main // process must reject anything outside /api/plugins and any traversal. func TestGUIIPCPathIsConstrained(t *testing.T) { main := guiFile(t, "cmd/gui/main.js") for _, needle := range []string{ `path.startsWith("/api/plugins")`, `path.includes("..")`, "plugins:proxy", "unsealViaCore()", } { if !strings.Contains(main, needle) { t.Errorf("cmd/gui/main.js is missing the guard %q", needle) } } // The plugins channel must be a proxy, not a key passthrough: the renderer // sends (method, path) and the main process attaches the key. // // NOTE: preload does expose a pre-existing `core.key` getter — the shell // needs the admin key to load the embedded WebUI without a login. That is // existing, deliberate design and out of scope here; asserting on "key:" in // preload would flag a pre-existing feature as a new hole. pre := guiFile(t, "cmd/gui/preload.js") if !strings.Contains(pre, "request: (method, path, body)") { t.Error("preload does not expose the plugins request proxy") } }