mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-05-25 16:00:35 +00:00
409 lines
11 KiB
Go
409 lines
11 KiB
Go
package commands
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func findDefinitionByName(t *testing.T, defs []Definition, name string) Definition {
|
|
t.Helper()
|
|
for _, def := range defs {
|
|
if def.Name == name {
|
|
return def
|
|
}
|
|
}
|
|
t.Fatalf("missing /%s definition", name)
|
|
return Definition{}
|
|
}
|
|
|
|
func TestBuiltinHelpHandler_ReturnsFormattedMessage(t *testing.T) {
|
|
defs := BuiltinDefinitions()
|
|
helpDef := findDefinitionByName(t, defs, "help")
|
|
if helpDef.Handler == nil {
|
|
t.Fatalf("/help handler should not be nil")
|
|
}
|
|
|
|
var reply string
|
|
err := helpDef.Handler(context.Background(), Request{
|
|
Text: "/help",
|
|
Reply: func(text string) error {
|
|
reply = text
|
|
return nil
|
|
},
|
|
}, nil)
|
|
if err != nil {
|
|
t.Fatalf("/help handler error: %v", err)
|
|
}
|
|
// Now uses auto-generated EffectiveUsage which includes agents
|
|
if !strings.Contains(reply, "/show [model|channel|agents|mcp <server>]") {
|
|
t.Fatalf("/help reply missing /show usage, got %q", reply)
|
|
}
|
|
if !strings.Contains(reply, "/list [models|channels|agents|skills|mcp]") {
|
|
t.Fatalf("/help reply missing /list usage, got %q", reply)
|
|
}
|
|
if !strings.Contains(reply, "/stop") {
|
|
t.Fatalf("/help reply missing /stop usage, got %q", reply)
|
|
}
|
|
if !strings.Contains(reply, "/use <skill> <message>") {
|
|
if !strings.Contains(reply, "/use <skill> [message]") {
|
|
t.Fatalf("/help reply missing /use usage, got %q", reply)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBuiltinStop_UsesRuntimeStopper(t *testing.T) {
|
|
rt := &Runtime{
|
|
StopActiveTurn: func() (StopResult, error) {
|
|
return StopResult{
|
|
Stopped: true,
|
|
TaskName: "sync the long running job",
|
|
}, nil
|
|
},
|
|
}
|
|
defs := BuiltinDefinitions()
|
|
ex := NewExecutor(NewRegistry(defs), rt)
|
|
|
|
var reply string
|
|
res := ex.Execute(context.Background(), Request{
|
|
Text: "/stop",
|
|
Reply: func(text string) error {
|
|
reply = text
|
|
return nil
|
|
},
|
|
})
|
|
if res.Outcome != OutcomeHandled {
|
|
t.Fatalf("/stop: outcome=%v, want=%v", res.Outcome, OutcomeHandled)
|
|
}
|
|
if reply != "Task stopped. \"sync the long running job\" was canceled." {
|
|
t.Fatalf("/stop reply=%q", reply)
|
|
}
|
|
}
|
|
|
|
func TestBuiltinStop_NoActiveTask(t *testing.T) {
|
|
rt := &Runtime{
|
|
StopActiveTurn: func() (StopResult, error) {
|
|
return StopResult{}, nil
|
|
},
|
|
}
|
|
defs := BuiltinDefinitions()
|
|
ex := NewExecutor(NewRegistry(defs), rt)
|
|
|
|
var reply string
|
|
res := ex.Execute(context.Background(), Request{
|
|
Text: "/stop",
|
|
Reply: func(text string) error {
|
|
reply = text
|
|
return nil
|
|
},
|
|
})
|
|
if res.Outcome != OutcomeHandled {
|
|
t.Fatalf("/stop: outcome=%v, want=%v", res.Outcome, OutcomeHandled)
|
|
}
|
|
if reply != "No active task to stop." {
|
|
t.Fatalf("/stop reply=%q, want no-active message", reply)
|
|
}
|
|
}
|
|
|
|
func TestBuiltinShowChannel_PreservesUserVisibleBehavior(t *testing.T) {
|
|
defs := BuiltinDefinitions()
|
|
ex := NewExecutor(NewRegistry(defs), nil)
|
|
|
|
cases := []string{"telegram", "whatsapp"}
|
|
for _, channel := range cases {
|
|
var reply string
|
|
res := ex.Execute(context.Background(), Request{
|
|
Channel: channel,
|
|
Text: "/show channel",
|
|
Reply: func(text string) error {
|
|
reply = text
|
|
return nil
|
|
},
|
|
})
|
|
if res.Outcome != OutcomeHandled {
|
|
t.Fatalf("/show channel on %s: outcome=%v, want=%v", channel, res.Outcome, OutcomeHandled)
|
|
}
|
|
want := "Current Channel: " + channel
|
|
if reply != want {
|
|
t.Fatalf("/show channel reply=%q, want=%q", reply, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBuiltinListChannels_UsesGetEnabledChannels(t *testing.T) {
|
|
rt := &Runtime{
|
|
GetEnabledChannels: func() []string {
|
|
return []string{"telegram", "slack"}
|
|
},
|
|
}
|
|
defs := BuiltinDefinitions()
|
|
ex := NewExecutor(NewRegistry(defs), rt)
|
|
|
|
var reply string
|
|
res := ex.Execute(context.Background(), Request{
|
|
Text: "/list channels",
|
|
Reply: func(text string) error {
|
|
reply = text
|
|
return nil
|
|
},
|
|
})
|
|
if res.Outcome != OutcomeHandled {
|
|
t.Fatalf("/list channels: outcome=%v, want=%v", res.Outcome, OutcomeHandled)
|
|
}
|
|
if !strings.Contains(reply, "telegram") || !strings.Contains(reply, "slack") {
|
|
t.Fatalf("/list channels reply=%q, want telegram and slack", reply)
|
|
}
|
|
}
|
|
|
|
func TestBuiltinShowAgents_RestoresOldBehavior(t *testing.T) {
|
|
rt := &Runtime{
|
|
ListAgentIDs: func() []string {
|
|
return []string{"default", "coder"}
|
|
},
|
|
}
|
|
defs := BuiltinDefinitions()
|
|
ex := NewExecutor(NewRegistry(defs), rt)
|
|
|
|
var reply string
|
|
res := ex.Execute(context.Background(), Request{
|
|
Text: "/show agents",
|
|
Reply: func(text string) error {
|
|
reply = text
|
|
return nil
|
|
},
|
|
})
|
|
if res.Outcome != OutcomeHandled {
|
|
t.Fatalf("/show agents: outcome=%v, want=%v", res.Outcome, OutcomeHandled)
|
|
}
|
|
if !strings.Contains(reply, "default") || !strings.Contains(reply, "coder") {
|
|
t.Fatalf("/show agents reply=%q, want agent IDs", reply)
|
|
}
|
|
}
|
|
|
|
func TestBuiltinListAgents_RestoresOldBehavior(t *testing.T) {
|
|
rt := &Runtime{
|
|
ListAgentIDs: func() []string {
|
|
return []string{"default", "coder"}
|
|
},
|
|
}
|
|
defs := BuiltinDefinitions()
|
|
ex := NewExecutor(NewRegistry(defs), rt)
|
|
|
|
var reply string
|
|
res := ex.Execute(context.Background(), Request{
|
|
Text: "/list agents",
|
|
Reply: func(text string) error {
|
|
reply = text
|
|
return nil
|
|
},
|
|
})
|
|
if res.Outcome != OutcomeHandled {
|
|
t.Fatalf("/list agents: outcome=%v, want=%v", res.Outcome, OutcomeHandled)
|
|
}
|
|
if !strings.Contains(reply, "default") || !strings.Contains(reply, "coder") {
|
|
t.Fatalf("/list agents reply=%q, want agent IDs", reply)
|
|
}
|
|
}
|
|
|
|
func TestBuiltinListSkills_UsesRuntimeSkillNames(t *testing.T) {
|
|
rt := &Runtime{
|
|
ListSkillNames: func() []string {
|
|
return []string{"shell", "git"}
|
|
},
|
|
}
|
|
defs := BuiltinDefinitions()
|
|
ex := NewExecutor(NewRegistry(defs), rt)
|
|
|
|
var reply string
|
|
res := ex.Execute(context.Background(), Request{
|
|
Text: "/list skills",
|
|
Reply: func(text string) error {
|
|
reply = text
|
|
return nil
|
|
},
|
|
})
|
|
if res.Outcome != OutcomeHandled {
|
|
t.Fatalf("/list skills: outcome=%v, want=%v", res.Outcome, OutcomeHandled)
|
|
}
|
|
if !strings.Contains(reply, "shell") || !strings.Contains(reply, "git") {
|
|
t.Fatalf("/list skills reply=%q, want installed skill names", reply)
|
|
}
|
|
}
|
|
|
|
func TestBuiltinListMCP_UsesRuntimeServerStatus(t *testing.T) {
|
|
rt := &Runtime{
|
|
ListMCPServers: func(context.Context) []MCPServerInfo {
|
|
return []MCPServerInfo{
|
|
{Name: "filesystem", Enabled: true, Deferred: true, Connected: false},
|
|
{Name: "github", Enabled: true, Deferred: false, Connected: true, ToolCount: 3},
|
|
}
|
|
},
|
|
}
|
|
defs := BuiltinDefinitions()
|
|
ex := NewExecutor(NewRegistry(defs), rt)
|
|
|
|
var reply string
|
|
res := ex.Execute(context.Background(), Request{
|
|
Text: "/list mcp",
|
|
Reply: func(text string) error {
|
|
reply = text
|
|
return nil
|
|
},
|
|
})
|
|
if res.Outcome != OutcomeHandled {
|
|
t.Fatalf("/list mcp: outcome=%v, want=%v", res.Outcome, OutcomeHandled)
|
|
}
|
|
if !strings.Contains(reply, "- `filesystem`\n Enabled: yes\n Deferred: yes\n "+
|
|
"Connected: no\n Active tools: unavailable") {
|
|
t.Fatalf("/list mcp reply=%q, want formatted filesystem block", reply)
|
|
}
|
|
if !strings.Contains(reply, "- `github`\n Enabled: yes\n Deferred: no\n "+
|
|
"Connected: yes\n Active tools: 3") {
|
|
t.Fatalf("/list mcp reply=%q, want formatted github block", reply)
|
|
}
|
|
}
|
|
|
|
func TestBuiltinShowMCP_UsesRuntimeToolNames(t *testing.T) {
|
|
rt := &Runtime{
|
|
ListMCPTools: func(_ context.Context, serverName string) ([]MCPToolInfo, error) {
|
|
if serverName != "github" {
|
|
t.Fatalf("serverName=%q, want github", serverName)
|
|
}
|
|
return []MCPToolInfo{
|
|
{
|
|
Name: "create_issue",
|
|
Description: "Create a GitHub issue",
|
|
Parameters: []MCPToolParameterInfo{
|
|
{Name: "body", Type: "string", Description: "Issue body"},
|
|
{Name: "title", Type: "string", Description: "Issue title", Required: true},
|
|
},
|
|
},
|
|
{
|
|
Name: "list_prs",
|
|
Description: "List open pull requests",
|
|
},
|
|
}, nil
|
|
},
|
|
}
|
|
defs := BuiltinDefinitions()
|
|
ex := NewExecutor(NewRegistry(defs), rt)
|
|
|
|
var reply string
|
|
res := ex.Execute(context.Background(), Request{
|
|
Text: "/show mcp github",
|
|
Reply: func(text string) error {
|
|
reply = text
|
|
return nil
|
|
},
|
|
})
|
|
if res.Outcome != OutcomeHandled {
|
|
t.Fatalf("/show mcp: outcome=%v, want=%v", res.Outcome, OutcomeHandled)
|
|
}
|
|
if !strings.Contains(reply, "Active MCP tools for `github`:\n- `create_issue`") {
|
|
t.Fatalf("/show mcp reply=%q, want tool header", reply)
|
|
}
|
|
if !strings.Contains(reply, "Description: Create a GitHub issue") {
|
|
t.Fatalf("/show mcp reply=%q, want description", reply)
|
|
}
|
|
if !strings.Contains(reply, " - `title` (string, required): Issue title") {
|
|
t.Fatalf("/show mcp reply=%q, want required parameter", reply)
|
|
}
|
|
if !strings.Contains(reply, " - `body` (string): Issue body") {
|
|
t.Fatalf("/show mcp reply=%q, want optional parameter", reply)
|
|
}
|
|
if !strings.Contains(reply, "- `list_prs`\n Description: List open pull requests\n Parameters: none") {
|
|
t.Fatalf("/show mcp reply=%q, want empty parameter block", reply)
|
|
}
|
|
}
|
|
|
|
func TestBuiltinUseCommand_PassthroughsToAgentLogic(t *testing.T) {
|
|
defs := BuiltinDefinitions()
|
|
ex := NewExecutor(NewRegistry(defs), nil)
|
|
|
|
res := ex.Execute(context.Background(), Request{
|
|
Text: "/use shell run ls",
|
|
})
|
|
if res.Outcome != OutcomePassthrough {
|
|
t.Fatalf("/use outcome=%v, want=%v", res.Outcome, OutcomePassthrough)
|
|
}
|
|
if res.Command != "use" {
|
|
t.Fatalf("/use command=%q, want=%q", res.Command, "use")
|
|
}
|
|
}
|
|
|
|
func TestBuiltinBtwCommand_UsesSideQuestionRuntime(t *testing.T) {
|
|
rt := &Runtime{
|
|
AskSideQuestion: func(ctx context.Context, question string) (string, error) {
|
|
if question != "what is 2+2?" {
|
|
t.Fatalf("question=%q, want %q", question, "what is 2+2?")
|
|
}
|
|
return "4", nil
|
|
},
|
|
}
|
|
defs := BuiltinDefinitions()
|
|
ex := NewExecutor(NewRegistry(defs), rt)
|
|
|
|
var reply string
|
|
res := ex.Execute(context.Background(), Request{
|
|
Text: "/btw what is 2+2?",
|
|
Reply: func(text string) error {
|
|
reply = text
|
|
return nil
|
|
},
|
|
})
|
|
if res.Outcome != OutcomeHandled {
|
|
t.Fatalf("/btw outcome=%v, want=%v", res.Outcome, OutcomeHandled)
|
|
}
|
|
if reply != "4" {
|
|
t.Fatalf("/btw reply=%q, want=%q", reply, "4")
|
|
}
|
|
}
|
|
|
|
func TestBuiltinBtwCommand_MissingQuestion(t *testing.T) {
|
|
defs := BuiltinDefinitions()
|
|
ex := NewExecutor(NewRegistry(defs), &Runtime{
|
|
AskSideQuestion: func(context.Context, string) (string, error) {
|
|
return "", nil
|
|
},
|
|
})
|
|
|
|
var reply string
|
|
res := ex.Execute(context.Background(), Request{
|
|
Text: "/btw",
|
|
Reply: func(text string) error {
|
|
reply = text
|
|
return nil
|
|
},
|
|
})
|
|
if res.Outcome != OutcomeHandled {
|
|
t.Fatalf("/btw outcome=%v, want=%v", res.Outcome, OutcomeHandled)
|
|
}
|
|
if reply != "Usage: /btw <question>" {
|
|
t.Fatalf("/btw reply=%q, want usage message", reply)
|
|
}
|
|
}
|
|
|
|
func TestBuiltinBtwCommand_PreservesQuestionWhitespace(t *testing.T) {
|
|
const want = "explain:\n fmt.Println(\"hi\")"
|
|
rt := &Runtime{
|
|
AskSideQuestion: func(ctx context.Context, question string) (string, error) {
|
|
if question != want {
|
|
t.Fatalf("question=%q, want %q", question, want)
|
|
}
|
|
return "ok", nil
|
|
},
|
|
}
|
|
defs := BuiltinDefinitions()
|
|
ex := NewExecutor(NewRegistry(defs), rt)
|
|
|
|
res := ex.Execute(context.Background(), Request{
|
|
Text: "/btw " + want,
|
|
Reply: func(text string) error {
|
|
return nil
|
|
},
|
|
})
|
|
if res.Outcome != OutcomeHandled {
|
|
t.Fatalf("/btw outcome=%v, want=%v", res.Outcome, OutcomeHandled)
|
|
}
|
|
}
|