eleven / ocho Public

Clone
git clone https://amadan.net/eleven/ocho

Plain git — no account needed to clone.

Download

Download this file

1package bot
2
3import (
4 "os"
5 "regexp"
6 "strings"
7 "testing"
8)
9
10// TestChipToolsMatchJS pins the Go chip-tool names to agent/actions.mjs
11// CHIP_TOOLS — same drift-proofing as the command floor.
12func TestChipToolsMatchJS(t *testing.T) {
13 raw, err := os.ReadFile("../../agent/actions.mjs")
14 if err != nil {
15 t.Fatal(err)
16 }
17 var js []string
18 for _, m := range regexp.MustCompile(`name: "([a-z_]+)"`).FindAllStringSubmatch(string(raw), -1) {
19 js = append(js, m[1])
20 }
21 tools := chipTools()
22 if len(js) != len(tools) {
23 t.Fatalf("actions.mjs has %d tools, Go has %d", len(js), len(tools))
24 }
25 for i, tool := range tools {
26 if js[i] != tool.Name {
27 t.Errorf("tool %d: js %q, go %q", i, js[i], tool.Name)
28 }
29 }
30}
31
32func TestActionsFromToolUse(t *testing.T) {
33 content := []anthBlock{
34 {Type: "text", Text: "Here you go."},
35 {Type: "tool_use", Name: "set_theme", Input: map[string]any{"theme": "lagoon"}},
36 {Type: "tool_use", Name: "set_theme", Input: map[string]any{"theme": "neon"}}, // unknown theme → dropped
37 {Type: "tool_use", Name: "set_appearance", Input: map[string]any{"appearance": "dark"}},
38 {Type: "tool_use", Name: "delete_everything", Input: map[string]any{}}, // unknown tool → dropped
39 {Type: "tool_use", Name: "open_profile"},
40 }
41 got := actionsFromToolUse(content)
42 if len(got) != 3 {
43 t.Fatalf("got %d actions: %+v", len(got), got)
44 }
45 if got[0]["theme"] != "lagoon" || got[1]["appearance"] != "dark" || got[2]["type"] != "open_profile" {
46 t.Fatalf("bad actions: %+v", got)
47 }
48}
49
50func TestAdaptiveThinking(t *testing.T) {
51 // Sonnet/Opus accept adaptive thinking.
52 for _, m := range []string{"claude-sonnet-5", "claude-opus-4-8"} {
53 if adaptiveThinking(m)["type"] != "adaptive" {
54 t.Errorf("%s should use adaptive thinking", m)
55 }
56 }
57 // Haiku rejects it → nil (omitted from the request).
58 for _, m := range []string{"claude-haiku-4-5", "claude-haiku-4-5-20251001"} {
59 if adaptiveThinking(m) != nil {
60 t.Errorf("%s must send no thinking field, got %v", m, adaptiveThinking(m))
61 }
62 }
63}
64
65// TestPromisedButtonCatchesFetchPromises pins the self-heal trigger: the
66// classic button-promises AND the fetch-promise family — the exact live stall
67// ("Great! Let me pull up the open issues for you.", no tool call, no chip,
68// nothing happens) must match, ordinary conversation must not.
69func TestPromisedButtonCatchesFetchPromises(t *testing.T) {
70 matches := []string{
71 "Tap the button below.",
72 "Here's a chip for that.",
73 "Great! Let me pull up the open issues for you.",
74 "I'll fetch the list now.",
75 "Sure — let me check the domains on your account.",
76 "One moment while I query the repository.",
77 "I will list them for you.",
78 "I'll remind you at 4pm.",
79 "Let me add that to your list.",
80 }
81 for _, s := range matches {
82 if !promisedButton.MatchString(s) {
83 t.Errorf("should match (a promise with no tool call): %q", s)
84 }
85 }
86 misses := []string{
87 "Want help setting up a reminder for yourself instead?",
88 "Here you go — three domains, all healthy.",
89 "You can connect GitHub from Integrations.",
90 "That's a great idea!",
91 }
92 for _, s := range misses {
93 if promisedButton.MatchString(s) {
94 t.Errorf("should NOT match (ordinary conversation): %q", s)
95 }
96 }
97}
98
99// TestRetryToolsIncludeServiceOp pins the forced retry's palette: it must be
100// able to propose the op that was promised (service_op — the live stall) and
101// must never include the builders (a forced retry cannot trigger a build).
102func TestRetryToolsIncludeServiceOp(t *testing.T) {
103 names := map[string]bool{}
104 for _, tool := range retryTools() {
105 names[tool.Name] = true
106 }
107 for _, want := range []string{"service_op", "service_research", "app_command"} {
108 if !names[want] {
109 t.Fatalf("forced retry cannot propose %s — the promised action stays unfulfillable", want)
110 }
111 }
112 for _, banned := range []string{"create_app", "create_bot", "remove_bot", "read_app_source"} {
113 if names[banned] {
114 t.Fatalf("forced retry must never offer %s", banned)
115 }
116 }
117}
118
119// The app tools (builder + source reader) are offered together, and only when
120// the instance served the authoring guide — no guide, no app surface at all.
121func TestReplyToolsAppTools(t *testing.T) {
122 withGuide := &Runtime{appGuide: "the guide"}
123 names := map[string]bool{}
124 for _, tool := range withGuide.replyTools(nil) {
125 names[tool.Name] = true
126 }
127 if !names["create_app"] || !names["read_app_source"] {
128 t.Fatalf("guide present: want create_app + read_app_source, got %v", names)
129 }
130 noGuide := &Runtime{}
131 for _, tool := range noGuide.replyTools(nil) {
132 if tool.Name == "create_app" || tool.Name == "read_app_source" {
133 t.Fatalf("no guide: %s must not be offered", tool.Name)
134 }
135 }
136}
137
138// The app_command mapping: an exact slash command becomes a run_command chip;
139// anything that isn't one clean app command never becomes a chip at all.
140func TestActionsFromToolUseAppCommand(t *testing.T) {
141 got := actionsFromToolUse([]anthBlock{
142 {Type: "tool_use", Name: "app_command", Input: map[string]any{"command": " /remind pick up the kids at 4pm "}},
143 })
144 if len(got) != 1 || got[0]["type"] != "run_command" || got[0]["command"] != "/remind pick up the kids at 4pm" {
145 t.Fatalf("chip: %+v", got)
146 }
147 bad := actionsFromToolUse([]anthBlock{
148 {Type: "tool_use", Name: "app_command", Input: map[string]any{"command": "remind me at 4"}}, // no slash
149 {Type: "tool_use", Name: "app_command", Input: map[string]any{"command": "/todo @here buy milk"}}, // group side effect
150 {Type: "tool_use", Name: "app_command", Input: map[string]any{"command": "/todo a\nb"}}, // multi-line
151 {Type: "tool_use", Name: "app_command", Input: map[string]any{"command": "/" + strings.Repeat("x", 300)}}, // too long
152 })
153 if len(bad) != 0 {
154 t.Fatalf("bad proposals became chips: %+v", bad)
155 }
156}
157