package bot import ( "os" "regexp" "strings" "testing" ) // TestCommandsMatchJS pins the Go command floor to the JS sources of truth: // web/static/bot-commands.js (names + help, shared with the browser typeahead // and the Node sidecar) and agent/actions.mjs (the theme list). If either // changes, this fails and the Go mirror gets updated in the same PR — the same // drift-proofing pattern as the e2ee test vectors. func TestCommandsMatchJS(t *testing.T) { raw, err := os.ReadFile("../../web/static/bot-commands.js") if err != nil { t.Fatal(err) } // Entries look like: "/theme": "switch colour theme, e.g. /theme lagoon", re := regexp.MustCompile(`"(/[a-z]+)":\s*"([^"]*)"`) var js [][2]string for _, m := range re.FindAllStringSubmatch(string(raw), -1) { js = append(js, [2]string{m[1], m[2]}) } if len(js) != len(botCommands) { t.Fatalf("bot-commands.js has %d commands, Go floor has %d", len(js), len(botCommands)) } for i, c := range botCommands { if js[i][0] != c.Name || js[i][1] != c.Help { t.Errorf("command %d: js %q/%q, go %q/%q", i, js[i][0], js[i][1], c.Name, c.Help) } } raw, err = os.ReadFile("../../agent/actions.mjs") if err != nil { t.Fatal(err) } m := regexp.MustCompile(`export const THEMES = \[([^\]]*)\]`).FindStringSubmatch(string(raw)) if m == nil { t.Fatal("THEMES not found in actions.mjs") } var jsThemes []string for _, s := range strings.Split(m[1], ",") { if s = strings.Trim(strings.TrimSpace(s), `"`); s != "" { jsThemes = append(jsThemes, s) } } if strings.Join(jsThemes, ",") != strings.Join(themes, ",") { t.Errorf("themes drift: js %v, go %v", jsThemes, themes) } for _, th := range themes { if themeLabels[th] == "" { t.Errorf("theme %q has no label", th) } } } func TestRunCommand(t *testing.T) { if rep, ok := RunCommand("/theme lagoon", false); !ok || len(rep.Actions) != 1 || rep.Actions[0]["theme"] != "lagoon" { t.Fatalf("theme lagoon: ok=%v rep=%+v", ok, rep) } if rep, ok := RunCommand("/theme neon", false); !ok || rep.Actions[0]["type"] != "open_theme_picker" { t.Fatalf("unknown theme should offer the picker: ok=%v rep=%+v", ok, rep) } if _, ok := RunCommand("/unknown thing", false); ok { t.Fatal("unregistered command should fall through") } if _, ok := RunCommand("hello", false); ok { t.Fatal("plain text should fall through") } rep, ok := RunCommand("/help", false) if !ok || !strings.Contains(rep.Text, "/theme") || !strings.Contains(rep.Text, "aren't enabled") { t.Fatalf("help (no AI): %+v", rep) } if rep, _ := RunCommand("/help", true); !strings.Contains(rep.Text, "in your own words") { t.Fatalf("help (AI): %+v", rep) } } func TestCommandFloor(t *testing.T) { h := CommandFloor("Eleven", false) // Group command with the mention prefix. rep, ok := h(nil, Incoming{Kind: "group", Text: "@Eleven /dark", Triggered: true}) if !ok || rep.Actions[0]["appearance"] != "dark" { t.Fatalf("mention + command: ok=%v rep=%+v", ok, rep) } // Bare command in a DM. if rep, _ := h(nil, Incoming{Kind: "dm", Text: "/light", Triggered: true}); rep.Actions[0]["appearance"] != "light" { t.Fatalf("dm command: %+v", rep) } // Not a command → the deterministic hint. if rep, _ := h(nil, Incoming{Kind: "dm", Text: "hi there", Triggered: true}); rep.Text != degradeText { t.Fatalf("degrade: %+v", rep) } // Unaddressed messages never reach the floor's mouth. if _, ok := h(nil, Incoming{Kind: "group", Text: "/help"}); ok { t.Fatal("unaddressed message must stay silent") } }