Files
kandev/apps/backend/internal/agent/settings/controller/controller_test.go
T
2026-02-16 16:26:26 +00:00

412 lines
11 KiB
Go

package controller
import (
"context"
"strings"
"testing"
"github.com/kandev/kandev/internal/agent/agents"
"github.com/kandev/kandev/internal/agent/registry"
"github.com/kandev/kandev/internal/agent/settings/dto"
"github.com/kandev/kandev/internal/agent/settings/modelfetcher"
"github.com/kandev/kandev/internal/agentctl/server/adapter"
"github.com/kandev/kandev/internal/common/logger"
)
// testAgent is a minimal implementation of agents.Agent for testing purposes.
// Embeds StandardPassthrough to optionally satisfy agents.PassthroughAgent.
type testAgent struct {
agents.StandardPassthrough
id string
name string
displayName string
description string
enabled bool
runtime *agents.RuntimeConfig
permissionSettings map[string]agents.PermissionSetting
logoData []byte
}
func (a *testAgent) ID() string { return a.id }
func (a *testAgent) Name() string { return a.name }
func (a *testAgent) DisplayName() string { return a.displayName }
func (a *testAgent) Description() string { return a.description }
func (a *testAgent) Enabled() bool { return a.enabled }
func (a *testAgent) DisplayOrder() int { return 0 }
func (a *testAgent) Logo(v agents.LogoVariant) []byte { return a.logoData }
func (a *testAgent) IsInstalled(ctx context.Context) (*agents.DiscoveryResult, error) {
return &agents.DiscoveryResult{Available: false}, nil
}
func (a *testAgent) DefaultModel() string { return "" }
func (a *testAgent) ListModels(ctx context.Context) (*agents.ModelList, error) {
return &agents.ModelList{}, nil
}
func (a *testAgent) CreateAdapter(cfg *adapter.Config, log *logger.Logger) (adapter.AgentAdapter, error) {
return nil, agents.ErrNotSupported
}
// BuildCommand builds a command using runtime config, model flag, and permission flags.
func (a *testAgent) BuildCommand(opts agents.CommandOptions) agents.Command {
rt := a.Runtime()
if rt == nil {
return agents.Command{}
}
cmd := make([]string, len(rt.Cmd.Args()))
copy(cmd, rt.Cmd.Args())
if opts.Model != "" && !rt.ModelFlag.IsEmpty() {
for _, arg := range rt.ModelFlag.Args() {
cmd = append(cmd, strings.ReplaceAll(arg, "{model}", opts.Model))
}
}
cmd = applyTestPermissionFlags(cmd, a.permissionSettings, opts.PermissionValues)
return agents.NewCommand(cmd...)
}
func applyTestPermissionFlags(cmd []string, permSettings map[string]agents.PermissionSetting, values map[string]bool) []string {
if permSettings == nil || values == nil {
return cmd
}
for name, setting := range permSettings {
if !setting.Supported || setting.ApplyMethod != "cli_flag" || setting.CLIFlag == "" {
continue
}
v, ok := values[name]
if !ok || !v {
continue
}
if setting.CLIFlagValue != "" {
cmd = append(cmd, setting.CLIFlag, setting.CLIFlagValue)
} else {
parts := strings.Fields(setting.CLIFlag)
cmd = append(cmd, parts...)
}
}
return cmd
}
func (a *testAgent) PermissionSettings() map[string]agents.PermissionSetting {
return a.permissionSettings
}
func (a *testAgent) Runtime() *agents.RuntimeConfig {
return a.runtime
}
func newTestController(agentList map[string]agents.Agent) *Controller {
log, _ := logger.NewLogger(logger.LoggingConfig{
Level: "error",
Format: "json",
})
reg := registry.NewRegistry(log)
for _, ag := range agentList {
_ = reg.Register(ag)
}
return &Controller{
agentRegistry: reg,
modelCache: modelfetcher.NewCache(),
logger: log,
}
}
func TestController_PreviewAgentCommand_StandardCommand(t *testing.T) {
agentList := map[string]agents.Agent{
"test-agent": &testAgent{
id: "test-agent",
name: "test-agent",
enabled: true,
runtime: &agents.RuntimeConfig{
Cmd: agents.NewCommand("test-cli", "--verbose"),
ModelFlag: agents.NewParam("--model", "{model}"),
},
permissionSettings: map[string]agents.PermissionSetting{
"auto_approve": {
Supported: true,
ApplyMethod: "cli_flag",
CLIFlag: "--yes",
},
},
},
}
controller := newTestController(agentList)
req := CommandPreviewRequest{
Model: "gpt-4",
PermissionSettings: map[string]bool{"auto_approve": true},
CLIPassthrough: false,
}
result, err := controller.PreviewAgentCommand(context.Background(), "test-agent", req)
if err != nil {
t.Fatalf("PreviewAgentCommand() error = %v", err)
}
if !result.Supported {
t.Error("PreviewAgentCommand() Supported = false, want true")
}
expectedParts := []string{"test-cli", "--verbose", "--model", "gpt-4", "--yes"}
for _, part := range expectedParts {
found := false
for _, cmdPart := range result.Command {
if cmdPart == part {
found = true
break
}
}
if !found {
t.Errorf("PreviewAgentCommand() command missing %q, got %v", part, result.Command)
}
}
}
func TestController_PreviewAgentCommand_PassthroughCommand(t *testing.T) {
agentList := map[string]agents.Agent{
"claude-code": &testAgent{
id: "claude-code",
name: "claude-code",
enabled: true,
runtime: &agents.RuntimeConfig{
Cmd: agents.NewCommand("claude"),
ModelFlag: agents.NewParam("--model", "{model}"),
},
StandardPassthrough: agents.StandardPassthrough{
Cfg: agents.PassthroughConfig{
Supported: true,
PassthroughCmd: agents.NewCommand("npx", "-y", "@anthropic-ai/claude-code"),
ModelFlag: agents.NewParam("--model", "{model}"),
PromptFlag: agents.NewParam("--prompt", "{prompt}"),
},
PermSettings: map[string]agents.PermissionSetting{
"dangerously_skip_permissions": {
Supported: true,
ApplyMethod: "cli_flag",
CLIFlag: "--dangerously-skip-permissions",
},
},
},
permissionSettings: map[string]agents.PermissionSetting{
"dangerously_skip_permissions": {
Supported: true,
ApplyMethod: "cli_flag",
CLIFlag: "--dangerously-skip-permissions",
},
},
},
}
controller := newTestController(agentList)
req := CommandPreviewRequest{
Model: "claude-sonnet-4-20250514",
PermissionSettings: map[string]bool{"dangerously_skip_permissions": true},
CLIPassthrough: true,
}
result, err := controller.PreviewAgentCommand(context.Background(), "claude-code", req)
if err != nil {
t.Fatalf("PreviewAgentCommand() error = %v", err)
}
// Verify it uses passthrough command
if len(result.Command) < 3 || result.Command[0] != "npx" {
t.Errorf("PreviewAgentCommand() should use passthrough command, got %v", result.Command)
}
// Verify model flag is present
hasModel := false
for i, part := range result.Command {
if part == "--model" && i+1 < len(result.Command) && result.Command[i+1] == "claude-sonnet-4-20250514" {
hasModel = true
break
}
}
if !hasModel {
t.Errorf("PreviewAgentCommand() missing model flag, got %v", result.Command)
}
// Verify permission flag is present
hasPermFlag := false
for _, part := range result.Command {
if part == "--dangerously-skip-permissions" {
hasPermFlag = true
break
}
}
if !hasPermFlag {
t.Errorf("PreviewAgentCommand() missing permission flag, got %v", result.Command)
}
// Verify prompt placeholder is present
hasPrompt := false
for _, part := range result.Command {
if part == "--prompt" || part == "{prompt}" {
hasPrompt = true
break
}
}
if !hasPrompt {
t.Errorf("PreviewAgentCommand() missing prompt placeholder, got %v", result.Command)
}
}
func TestController_PreviewAgentCommand_AgentNotFound(t *testing.T) {
controller := newTestController(map[string]agents.Agent{})
_, err := controller.PreviewAgentCommand(context.Background(), "nonexistent", CommandPreviewRequest{})
if err == nil {
t.Error("PreviewAgentCommand() should return error for unknown agent")
}
}
func TestController_PreviewAgentCommand_PassthroughDisabled(t *testing.T) {
agentList := map[string]agents.Agent{
"test-agent": &testAgent{
id: "test-agent",
name: "test-agent",
enabled: true,
runtime: &agents.RuntimeConfig{
Cmd: agents.NewCommand("test-cli"),
},
StandardPassthrough: agents.StandardPassthrough{
Cfg: agents.PassthroughConfig{
Supported: true,
PassthroughCmd: agents.NewCommand("passthrough-cli"),
},
},
},
}
controller := newTestController(agentList)
// CLIPassthrough is false, so should use standard command
req := CommandPreviewRequest{
CLIPassthrough: false,
}
result, err := controller.PreviewAgentCommand(context.Background(), "test-agent", req)
if err != nil {
t.Fatalf("PreviewAgentCommand() error = %v", err)
}
if result.Command[0] != "test-cli" {
t.Errorf("PreviewAgentCommand() should use standard command when passthrough disabled, got %v", result.Command)
}
}
func TestBuildCommandString(t *testing.T) {
tests := []struct {
name string
cmd []string
expected string
}{
{
name: "simple command",
cmd: []string{"echo", "hello"},
expected: "echo hello",
},
{
name: "command with spaces",
cmd: []string{"echo", "hello world"},
expected: `echo "hello world"`,
},
{
name: "command with quotes",
cmd: []string{"echo", `say "hi"`},
expected: `echo "say \"hi\""`,
},
{
name: "command with special chars",
cmd: []string{"bash", "-c", "echo $HOME"},
expected: `bash -c "echo $HOME"`,
},
{
name: "empty command",
cmd: []string{},
expected: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := buildCommandString(tt.cmd)
if result != tt.expected {
t.Errorf("buildCommandString(%v) = %q, want %q", tt.cmd, result, tt.expected)
}
})
}
}
func TestCommandPreviewResponse_DTO(t *testing.T) {
resp := dto.CommandPreviewResponse{
Supported: true,
Command: []string{"npx", "claude-code", "--model", "gpt-4"},
CommandString: `npx claude-code --model gpt-4`,
}
if !resp.Supported {
t.Error("CommandPreviewResponse.Supported should be true")
}
if len(resp.Command) != 4 {
t.Errorf("CommandPreviewResponse.Command length = %d, want 4", len(resp.Command))
}
if resp.CommandString == "" {
t.Error("CommandPreviewResponse.CommandString should not be empty")
}
}
func TestController_GetAgentLogo_Success(t *testing.T) {
logoBytes := []byte("<svg>test</svg>")
agentList := map[string]agents.Agent{
"test-agent": &testAgent{
id: "test-agent",
name: "test-agent",
enabled: true,
logoData: logoBytes,
},
}
ctrl := newTestController(agentList)
data, err := ctrl.GetAgentLogo(context.Background(), "test-agent", agents.LogoLight)
if err != nil {
t.Fatalf("GetAgentLogo() error = %v", err)
}
if string(data) != string(logoBytes) {
t.Errorf("GetAgentLogo() = %q, want %q", data, logoBytes)
}
}
func TestController_GetAgentLogo_AgentNotFound(t *testing.T) {
ctrl := newTestController(map[string]agents.Agent{})
_, err := ctrl.GetAgentLogo(context.Background(), "nonexistent", agents.LogoLight)
if err != ErrAgentNotFound {
t.Errorf("GetAgentLogo() error = %v, want ErrAgentNotFound", err)
}
}
func TestController_GetAgentLogo_NoLogoData(t *testing.T) {
agentList := map[string]agents.Agent{
"test-agent": &testAgent{
id: "test-agent",
name: "test-agent",
enabled: true,
// logoData is nil
},
}
ctrl := newTestController(agentList)
_, err := ctrl.GetAgentLogo(context.Background(), "test-agent", agents.LogoLight)
if err != ErrLogoNotAvailable {
t.Errorf("GetAgentLogo() error = %v, want ErrLogoNotAvailable", err)
}
}