Files
kandev/apps/backend/internal/agent/agents/mock.go
T
2026-05-16 20:07:57 +01:00

214 lines
6.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package agents
import (
"context"
_ "embed"
"time"
"github.com/kandev/kandev/internal/agent/usage"
"github.com/kandev/kandev/pkg/agent"
)
//go:embed logos/mock_light.svg
var mockLogoLight []byte
//go:embed logos/mock_dark.svg
var mockLogoDark []byte
var (
_ Agent = (*MockAgent)(nil)
_ PassthroughAgent = (*MockAgent)(nil)
_ InferenceAgent = (*MockAgent)(nil)
)
const (
mockTUIReadyPromptPattern = "\x1b\\[1;32m❯\x1b\\[0m $"
// mockAgentDefaultID is the canonical ID for the default MockAgent
// instance. When NewMockAgentWithID() supplies a non-empty override
// the override wins; otherwise this is returned by ID() and used as
// the default binary basename across BuildCommand/Runtime/Inference.
mockAgentDefaultID = "mock-agent"
)
type MockAgent struct {
StandardPassthrough
// id/name/displayName are per-instance overrides so the same mock
// binary can be registered under multiple canonical routing provider
// IDs (e.g. "claude-acp", "codex-acp") in E2E mode. When empty the
// defaults ("mock-agent", "Mock Agent", "Mock") are returned.
id string
name string
displayName string
enabled bool
binaryPath string
supportsMCP bool
}
func NewMockAgent() *MockAgent {
return &MockAgent{
StandardPassthrough: StandardPassthrough{
Cfg: PassthroughConfig{
Supported: true,
Label: "TUI Passthrough",
Description: "Terminal UI mode for testing",
PassthroughCmd: NewCommand(mockAgentDefaultID, "--tui"),
ModelFlag: NewParam("--model", "{model}"),
PromptFlag: NewParam("--prompt", "{prompt}"),
SessionResumeFlag: NewParam("--resume"),
ResumeFlag: NewParam("-c"),
PromptPattern: mockTUIReadyPromptPattern,
IdleTimeout: 2 * time.Second,
BufferMaxBytes: DefaultBufferMaxBytes,
},
},
supportsMCP: true,
}
}
// NewMockAgentWithID returns a MockAgent identical to NewMockAgent but
// with its ID, Name and DisplayName pre-set to the supplied values.
// Used in E2E mode (KANDEV_MOCK_PROVIDERS) to register the same mock
// binary under multiple canonical routing provider IDs so routing
// validation accepts real-looking provider_order lists while every
// launch still routes through the mock binary.
func NewMockAgentWithID(id, name, displayName string) *MockAgent {
a := NewMockAgent()
a.id = id
a.name = name
a.displayName = displayName
return a
}
// SetEnabled enables or disables the mock agent at runtime.
func (a *MockAgent) SetEnabled(enabled bool) { a.enabled = enabled }
// SetBinaryPath overrides the mock-agent binary path.
// Also updates the passthrough command to use the same binary.
func (a *MockAgent) SetBinaryPath(path string) {
a.binaryPath = path
a.Cfg.PassthroughCmd = NewCommand(path, "--tui")
}
// SetSupportsMCP controls whether the mock agent reports MCP support.
// Defaults to true so plan mode workflow events work in E2E tests.
func (a *MockAgent) SetSupportsMCP(v bool) { a.supportsMCP = v }
// SupportsMCPEnabled reports the current MCP support setting.
func (a *MockAgent) SupportsMCPEnabled() bool { return a.supportsMCP }
func (a *MockAgent) ID() string {
if a.id != "" {
return a.id
}
return mockAgentDefaultID
}
func (a *MockAgent) Name() string {
if a.name != "" {
return a.name
}
return "Mock Agent"
}
func (a *MockAgent) DisplayName() string {
if a.displayName != "" {
return a.displayName
}
return "Mock"
}
func (a *MockAgent) Description() string {
return "Mock agent for testing. Generates simulated responses with all message types."
}
func (a *MockAgent) Enabled() bool { return a.enabled }
func (a *MockAgent) DisplayOrder() int { return 99 }
func (a *MockAgent) Logo(v LogoVariant) []byte {
if v == LogoDark {
return mockLogoDark
}
return mockLogoLight
}
func (a *MockAgent) IsInstalled(ctx context.Context) (*DiscoveryResult, error) {
// Mock agent is always "available" when enabled (forced by settings controller).
return &DiscoveryResult{Available: true, SupportsMCP: a.supportsMCP}, nil
}
func (a *MockAgent) BuildCommand(opts CommandOptions) Command {
// For container-bound runtimes (Docker, Sprites, remote Docker) the
// agent process exec's inside a Linux container, where the host path
// computed by configureMockAgent doesn't exist. The container's image
// has /usr/local/bin on PATH and container.go bind-mounts the
// linux/amd64 mock-agent there, so the bare name resolves correctly.
//
// For host runtimes (standalone) we prefer the absolute host path
// that configureMockAgent computed via os.Executable() — that's the
// only way the host path works in dev mode (where the shell's $PATH
// doesn't include apps/backend/bin). Fall back to the bare name when
// no path is set: e2e fixtures prepend the kandev bin dir to PATH.
if opts.Runtime.IsContainerized() {
return Cmd(mockAgentDefaultID).Build()
}
binary := mockAgentDefaultID
if a.binaryPath != "" {
binary = a.binaryPath
}
return Cmd(binary).Build()
}
func (a *MockAgent) Runtime() *RuntimeConfig {
canRecover := true
return &RuntimeConfig{
Cmd: Cmd(mockAgentDefaultID).Build(),
WorkingDir: "{workspace}",
Env: map[string]string{},
ResourceLimits: ResourceLimits{MemoryMB: 512, CPUCores: 0.5, Timeout: time.Hour},
Protocol: agent.ProtocolACP,
ProjectSkillDir: DefaultProjectSkillDir,
UserSkillDir: ".mock-agent/skills",
SessionConfig: SessionConfig{
CanRecover: &canRecover,
},
}
}
// RemoteAuth returns a non-nil empty spec so the mock agent counts as a
// remote-capable agent (it's runnable inside Docker for E2E tests) but
// requires no credentials. The empty methods list signals "no auth needed".
func (a *MockAgent) RemoteAuth() *RemoteAuth { return &RemoteAuth{} }
// InstallScript returns a deterministic short-lived shell script so e2e tests
// can exercise the install streaming endpoint without depending on npm.
func (a *MockAgent) InstallScript() string {
return "echo mock-install: step 1 && echo mock-install: step 2 && echo mock-install: done"
}
// LoginCommand exposes a deterministic interactive command for e2e PTY tests.
// `cat` echoes any input back to the user; tests send "exit\n" or kill the
// session to terminate.
func (a *MockAgent) LoginCommand() *LoginCommand {
return &LoginCommand{
Cmd: []string{"cat"},
Description: "Mock login (echoes input).",
}
}
func (a *MockAgent) BillingType() usage.BillingType { return defaultBillingType() }
func (a *MockAgent) PermissionSettings() map[string]PermissionSetting {
return emptyPermSettings
}
// InferenceConfig enables one-shot inference via ACP. The mock-agent binary
// advertises its available models in the session/new response, so the host
// utility capability probe populates them into the cache without any static
// model list here.
func (a *MockAgent) InferenceConfig() *InferenceConfig {
binary := mockAgentDefaultID
if a.binaryPath != "" {
binary = a.binaryPath
}
return &InferenceConfig{
Supported: true,
Command: NewCommand(binary),
}
}