Files
kandev/apps/backend/internal/agent/settings/controller/controller_test.go
T
Jelloeater's Agent 65f27ecbff fix(agents): launch opencode-acp binary-first (native binary on PATH), don't break on stale npm cache (#3836)
Co-authored-by: goose <goose@aaif.dev>
Co-authored-by: Benjamin <benjaminchamwb@gmail.com>
Co-authored-by: Carlos Florencio <carlosmflorencio@gmail.com>
Co-authored-by: João Salavisa <joao.salavisa@gmail.com>
2026-09-21 23:20:43 +01:00

688 lines
21 KiB
Go

package controller
import (
"context"
"errors"
"os"
"path/filepath"
"slices"
"strings"
"testing"
"github.com/kandev/kandev/internal/agent/agents"
"github.com/kandev/kandev/internal/agent/discovery"
"github.com/kandev/kandev/internal/agent/managedruntime"
"github.com/kandev/kandev/internal/agent/registry"
"github.com/kandev/kandev/internal/agent/settings/dto"
"github.com/kandev/kandev/internal/agent/usage"
"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
// supportsMCP feeds the DiscoveryResult returned by IsInstalled so the
// E2E-mock discovery bypass can verify capability propagation. Most
// tests don't care and the zero value matches the legacy behaviour.
supportsMCP bool
mcpConfigPaths []string
}
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,
SupportsMCP: a.supportsMCP,
MCPConfigPaths: a.mcpConfigPaths,
}, nil
}
// BuildCommand builds a command using runtime config and permission flags.
// Model/mode are applied through ACP session configuration at session start, not via CLI.
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())
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 (a *testAgent) BillingType() usage.BillingType { return usage.BillingTypeAPIKey }
func (a *testAgent) RemoteAuth() *agents.RemoteAuth { return nil }
func (a *testAgent) InstallScript() string { return "" }
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,
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")
}
// --model is no longer emitted: model is applied through ACP session configuration.
expectedParts := []string{"test-cli", "--verbose", "--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)
}
}
for _, cmdPart := range result.Command {
if cmdPart == "--model" {
t.Errorf("PreviewAgentCommand() should not emit --model, got %v", result.Command)
}
}
}
func TestController_PreviewAgentCommandUsesActiveManagedRuntimeVersion(t *testing.T) {
t.Setenv("PATH", t.TempDir())
agent := agents.NewOpenCodeACP()
controller := newTestController(map[string]agents.Agent{agent.ID(): agent})
selectionStore := newRecoverySelectionStore()
selectionStore.values[agent.ID()+"\x00opencode-ai"] = managedruntime.Selection{
Package: "opencode-ai",
Version: "1.18.5",
}
controller.SetManagedRuntimeSelectionStore(selectionStore)
result, err := controller.PreviewAgentCommand(context.Background(), agent.ID(), CommandPreviewRequest{})
if err != nil {
t.Fatalf("PreviewAgentCommand() error = %v", err)
}
if !slices.Contains(result.Command, "opencode-ai@1.18.5") {
t.Fatalf("preview command = %v, want exact active version", result.Command)
}
}
func TestController_PreviewAgentCommand_RejectsInvalidCommandPrefix(t *testing.T) {
controller := newTestController(map[string]agents.Agent{
"test-agent": &testAgent{
id: "test-agent",
name: "test-agent",
enabled: true,
runtime: &agents.RuntimeConfig{Cmd: agents.NewCommand("test-cli")},
},
})
for _, prefix := range []string{`greywall "unterminated`, `'' --arg`, `--not-a-launcher`} {
_, err := controller.PreviewAgentCommand(context.Background(), "test-agent", CommandPreviewRequest{
CommandPrefix: prefix,
})
if !errors.Is(err, ErrInvalidCommandPrefix) {
t.Errorf("prefix %q: expected ErrInvalidCommandPrefix, got %v", prefix, err)
}
}
}
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 NOT present in preview (prompt is injected at runtime, not in preview)
for _, part := range result.Command {
if part == "--prompt" || part == "{prompt}" {
t.Errorf("PreviewAgentCommand() should not include prompt placeholder in passthrough preview, got %v", result.Command)
break
}
}
}
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)
}
}
// TestController_PreviewAgentCommand_PassthroughDoesNotDuplicateCLIFlag keeps
// legacy permission CLI flags from being emitted twice when the same token is
// also present in the profile CLI-flags list. Preview and launch use the shared
// passthrough builder, so both paths must emit one token.
func TestController_PreviewAgentCommand_PassthroughDoesNotDuplicateCLIFlag(t *testing.T) {
permSettings := map[string]agents.PermissionSetting{
"allow_indexing": {
Supported: true,
ApplyMethod: "cli_flag",
CLIFlag: "--allow-indexing",
},
}
agentList := map[string]agents.Agent{
"auggie": &testAgent{
id: "auggie",
name: "auggie",
enabled: true,
runtime: &agents.RuntimeConfig{
Cmd: agents.NewCommand("auggie"),
},
StandardPassthrough: agents.StandardPassthrough{
Cfg: agents.PassthroughConfig{
Supported: true,
PassthroughCmd: agents.NewCommand("npx", "-y", "@augmentcode/auggie"),
},
PermSettings: permSettings,
},
permissionSettings: permSettings,
},
}
controller := newTestController(agentList)
// Simulate the post-backfill state: allow_indexing toggle ON and the legacy
// backfill has seeded --allow-indexing into CLIFlags. Both sources would
// previously emit the flag.
req := CommandPreviewRequest{
PermissionSettings: map[string]bool{"allow_indexing": true},
CLIPassthrough: true,
CLIFlags: []dto.CLIFlagDTO{
{Flag: "--allow-indexing", Enabled: true},
},
}
result, err := controller.PreviewAgentCommand(context.Background(), "auggie", req)
if err != nil {
t.Fatalf("PreviewAgentCommand() error = %v", err)
}
count := 0
for _, part := range result.Command {
if part == "--allow-indexing" {
count++
}
}
if count != 1 {
t.Errorf("PreviewAgentCommand() emitted --allow-indexing %d times, want 1; got %v", count, result.Command)
}
}
// TestController_PreviewAgentCommand_ACPAppendsCLIFlagsOnce locks in that the
// non-passthrough (ACP) branch still appends user-configured CLI flags exactly
// once. Auggie's real BuildCommand intentionally does not emit --allow-indexing;
// the flag must come through CLIFlagTokens.
func TestController_PreviewAgentCommand_ACPAppendsCLIFlagsOnce(t *testing.T) {
agentList := map[string]agents.Agent{
"auggie": &testAgent{
id: "auggie",
name: "auggie",
enabled: true,
runtime: &agents.RuntimeConfig{
Cmd: agents.NewCommand("auggie", "--acp"),
},
// No permissionSettings: mirrors Auggie's real BuildCommand which
// does not emit --allow-indexing — it flows through CLIFlags only.
},
}
controller := newTestController(agentList)
req := CommandPreviewRequest{
CLIPassthrough: false,
CLIFlags: []dto.CLIFlagDTO{
{Flag: "--allow-indexing", Enabled: true},
},
}
result, err := controller.PreviewAgentCommand(context.Background(), "auggie", req)
if err != nil {
t.Fatalf("PreviewAgentCommand() error = %v", err)
}
count := 0
for _, part := range result.Command {
if part == "--allow-indexing" {
count++
}
}
if count != 1 {
t.Errorf("PreviewAgentCommand() emitted --allow-indexing %d times, want 1; got %v", count, 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: "",
},
{
name: "empty argument",
cmd: []string{"node", "-e", ""},
expected: `node -e ""`,
},
}
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)
}
}
// TestSyncAgentFromDiscovery_UnknownAgentSkipped verifies that
// syncAgentFromDiscovery returns nil (not an error) when the discovered agent
// is not in the registry. This is the expected behaviour when
// KANDEV_MOCK_AGENT=only suppresses all real agents: the filesystem scanner may
// still detect installed CLIs, but they should be silently skipped rather than
// aborting the entire profile-sync loop.
func TestSyncAgentFromDiscovery_UnknownAgentSkipped(t *testing.T) {
ctrl := newTestController(map[string]agents.Agent{}) // empty registry
result := discovery.Availability{
Name: "some-unregistered-agent",
Available: true,
}
if err := ctrl.syncAgentFromDiscovery(context.Background(), result); err != nil {
t.Errorf("expected nil error for unknown agent, got: %v", err)
}
}
// TestDetectAgents_E2EMockBypassesFilesystem verifies that when
// KANDEV_E2E_MOCK=true, detectAgents returns results synthesised from the
// registry without calling c.discovery.Detect (which would panic here
// because c.discovery is nil). Every enabled inference agent must appear as
// Available, while virtual families remain outside host discovery.
func TestDetectAgents_E2EMockBypassesFilesystem(t *testing.T) {
t.Setenv("KANDEV_E2E_MOCK", "true")
ctrl := newTestController(map[string]agents.Agent{
"mock-agent": &testAgent{id: "mock-agent", name: "mock-agent", enabled: true},
"other-mock": &testAgent{id: "other-mock", name: "other-mock", enabled: true},
agents.DynamicAgentID: agents.NewDynamicAgent(),
})
// c.discovery is nil — if detectAgents called it the test would panic.
results, err := ctrl.detectAgents(context.Background())
if err != nil {
t.Fatalf("detectAgents() error = %v", err)
}
if len(results) != 2 {
t.Fatalf("expected 2 inference results, got %d", len(results))
}
for _, r := range results {
if r.Name == agents.DynamicAgentID {
t.Fatal("virtual Dynamic family must not appear in host discovery")
}
if !r.Available {
t.Errorf("agent %q: Available = false, want true in E2E mock mode", r.Name)
}
}
}
// TestDetectAgents_E2EMockPropagatesSupportsMCP pins the regression
// that broke plan mode in every E2E test: synthAvailabilityFromRegistry
// was synthesising Availability records without calling IsInstalled, so
// SupportsMCP defaulted to false. The frontend's useSessionMcp then
// treated every agent as MCP-incapable, the plan-mode toggle took the
// layout-only path, and `togglePlanMode()` tests timed out waiting for
// `Continue working on the plan...`.
func TestDetectAgents_E2EMockPropagatesSupportsMCP(t *testing.T) {
t.Setenv("KANDEV_E2E_MOCK", "true")
ctrl := newTestController(map[string]agents.Agent{
"mock-mcp": &testAgent{
id: "mock-mcp",
name: "mock-mcp",
enabled: true,
supportsMCP: true,
mcpConfigPaths: []string{"/some/path/.mock-mcp.json"},
},
"mock-no-mcp": &testAgent{
id: "mock-no-mcp",
name: "mock-no-mcp",
enabled: true,
supportsMCP: false,
},
})
results, err := ctrl.detectAgents(context.Background())
if err != nil {
t.Fatalf("detectAgents() error = %v", err)
}
byID := make(map[string]struct {
supportsMCP bool
mcpConfigPath string
}, len(results))
for _, r := range results {
byID[r.Name] = struct {
supportsMCP bool
mcpConfigPath string
}{r.SupportsMCP, r.MCPConfigPath}
}
if got := byID["mock-mcp"]; !got.supportsMCP {
t.Error("mock-mcp: SupportsMCP = false, want true (IsInstalled said yes)")
} else if got.mcpConfigPath != "/some/path/.mock-mcp.json" {
t.Errorf("mock-mcp: MCPConfigPath = %q, want first entry from IsInstalled", got.mcpConfigPath)
}
if got := byID["mock-no-mcp"]; got.supportsMCP {
t.Error("mock-no-mcp: SupportsMCP = true, want false (IsInstalled said no)")
}
}
// TestController_PreviewAgentCommand_CopilotKeepsManagedPackage verifies that
// a globally installed Copilot CLI does not make the command preview bypass
// the managed npm runtime.
func TestController_PreviewAgentCommand_CopilotKeepsManagedPackage(t *testing.T) {
controller := newTestController(map[string]agents.Agent{
"copilot-acp": agents.NewCopilotACP(),
})
// A copilot binary on PATH must not change the previewed launch command.
dir := t.TempDir()
if err := os.WriteFile(filepath.Join(dir, "copilot"), []byte("#!/bin/sh\n"), 0o755); err != nil {
t.Fatalf("write fake copilot: %v", err)
}
t.Setenv("PATH", dir)
res, err := controller.PreviewAgentCommand(context.Background(), "copilot-acp", CommandPreviewRequest{})
if err != nil {
t.Fatalf("PreviewAgentCommand() error = %v", err)
}
managed := agents.NewCopilotACP()
want := []string{"npx", "--yes", "--prefer-offline", managed.ManagedNPMRuntime().PackageSpec(""), "--acp"}
if got := res.Command; !slices.Equal(got, want) {
t.Errorf("preview with copilot on PATH = %v, want %v", got, want)
}
}