package bot import ( "os" "regexp" "strings" "testing" ) // TestChipToolsMatchJS pins the Go chip-tool names to agent/actions.mjs // CHIP_TOOLS — same drift-proofing as the command floor. func TestChipToolsMatchJS(t *testing.T) { raw, err := os.ReadFile("../../agent/actions.mjs") if err != nil { t.Fatal(err) } var js []string for _, m := range regexp.MustCompile(`name: "([a-z_]+)"`).FindAllStringSubmatch(string(raw), -1) { js = append(js, m[1]) } tools := chipTools() if len(js) != len(tools) { t.Fatalf("actions.mjs has %d tools, Go has %d", len(js), len(tools)) } for i, tool := range tools { if js[i] != tool.Name { t.Errorf("tool %d: js %q, go %q", i, js[i], tool.Name) } } } func TestActionsFromToolUse(t *testing.T) { content := []anthBlock{ {Type: "text", Text: "Here you go."}, {Type: "tool_use", Name: "set_theme", Input: map[string]any{"theme": "lagoon"}}, {Type: "tool_use", Name: "set_theme", Input: map[string]any{"theme": "neon"}}, // unknown theme → dropped {Type: "tool_use", Name: "set_appearance", Input: map[string]any{"appearance": "dark"}}, {Type: "tool_use", Name: "delete_everything", Input: map[string]any{}}, // unknown tool → dropped {Type: "tool_use", Name: "open_profile"}, } got := actionsFromToolUse(content) if len(got) != 3 { t.Fatalf("got %d actions: %+v", len(got), got) } if got[0]["theme"] != "lagoon" || got[1]["appearance"] != "dark" || got[2]["type"] != "open_profile" { t.Fatalf("bad actions: %+v", got) } } func TestAdaptiveThinking(t *testing.T) { // Sonnet/Opus accept adaptive thinking. for _, m := range []string{"claude-sonnet-5", "claude-opus-4-8"} { if adaptiveThinking(m)["type"] != "adaptive" { t.Errorf("%s should use adaptive thinking", m) } } // Haiku rejects it → nil (omitted from the request). for _, m := range []string{"claude-haiku-4-5", "claude-haiku-4-5-20251001"} { if adaptiveThinking(m) != nil { t.Errorf("%s must send no thinking field, got %v", m, adaptiveThinking(m)) } } } // TestPromisedButtonCatchesFetchPromises pins the self-heal trigger: the // classic button-promises AND the fetch-promise family — the exact live stall // ("Great! Let me pull up the open issues for you.", no tool call, no chip, // nothing happens) must match, ordinary conversation must not. func TestPromisedButtonCatchesFetchPromises(t *testing.T) { matches := []string{ "Tap the button below.", "Here's a chip for that.", "Great! Let me pull up the open issues for you.", "I'll fetch the list now.", "Sure — let me check the domains on your account.", "One moment while I query the repository.", "I will list them for you.", "I'll remind you at 4pm.", "Let me add that to your list.", } for _, s := range matches { if !promisedButton.MatchString(s) { t.Errorf("should match (a promise with no tool call): %q", s) } } misses := []string{ "Want help setting up a reminder for yourself instead?", "Here you go — three domains, all healthy.", "You can connect GitHub from Integrations.", "That's a great idea!", } for _, s := range misses { if promisedButton.MatchString(s) { t.Errorf("should NOT match (ordinary conversation): %q", s) } } } // TestRetryToolsIncludeServiceOp pins the forced retry's palette: it must be // able to propose the op that was promised (service_op — the live stall) and // must never include the builders (a forced retry cannot trigger a build). func TestRetryToolsIncludeServiceOp(t *testing.T) { names := map[string]bool{} for _, tool := range retryTools() { names[tool.Name] = true } for _, want := range []string{"service_op", "service_research", "app_command"} { if !names[want] { t.Fatalf("forced retry cannot propose %s — the promised action stays unfulfillable", want) } } for _, banned := range []string{"create_app", "create_bot", "remove_bot", "read_app_source"} { if names[banned] { t.Fatalf("forced retry must never offer %s", banned) } } } // The app tools (builder + source reader) are offered together, and only when // the instance served the authoring guide — no guide, no app surface at all. func TestReplyToolsAppTools(t *testing.T) { withGuide := &Runtime{appGuide: "the guide"} names := map[string]bool{} for _, tool := range withGuide.replyTools(nil) { names[tool.Name] = true } if !names["create_app"] || !names["read_app_source"] { t.Fatalf("guide present: want create_app + read_app_source, got %v", names) } noGuide := &Runtime{} for _, tool := range noGuide.replyTools(nil) { if tool.Name == "create_app" || tool.Name == "read_app_source" { t.Fatalf("no guide: %s must not be offered", tool.Name) } } } // The app_command mapping: an exact slash command becomes a run_command chip; // anything that isn't one clean app command never becomes a chip at all. func TestActionsFromToolUseAppCommand(t *testing.T) { got := actionsFromToolUse([]anthBlock{ {Type: "tool_use", Name: "app_command", Input: map[string]any{"command": " /remind pick up the kids at 4pm "}}, }) if len(got) != 1 || got[0]["type"] != "run_command" || got[0]["command"] != "/remind pick up the kids at 4pm" { t.Fatalf("chip: %+v", got) } bad := actionsFromToolUse([]anthBlock{ {Type: "tool_use", Name: "app_command", Input: map[string]any{"command": "remind me at 4"}}, // no slash {Type: "tool_use", Name: "app_command", Input: map[string]any{"command": "/todo @here buy milk"}}, // group side effect {Type: "tool_use", Name: "app_command", Input: map[string]any{"command": "/todo a\nb"}}, // multi-line {Type: "tool_use", Name: "app_command", Input: map[string]any{"command": "/" + strings.Repeat("x", 300)}}, // too long }) if len(bad) != 0 { t.Fatalf("bad proposals became chips: %+v", bad) } }