mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-06 14:00:09 +02:00
* feat(agent): add codebuddyBackend struct and buildCodebuddyArgs Introduces the codebuddy agent backend skeleton with args builder that mirrors claudeBackend's protocol flags (stream-json, bypass permissions, blocked args filtering) for the codebuddy CLI fork. * feat(agent): implement codebuddyBackend.Execute with stream-json parsing * feat(agent): wire codebuddy into New() factory and launchHeaders * feat(agent): add codebuddy dynamic model discovery from --help * feat(agent): add codebuddy thinking/effort discovery and providerThinkingEnums * feat(daemon): add codebuddy CLI probe, env vars, and args support * fix(agent): use len(models)==0 for default model instead of loop index * fix(agent): increase codebuddy --help timeout to 35s for slow CLI startup * fix(agent): address codebuddy PR review feedback - Wire codebuddy into execenv: reuse claude's CLAUDE.md, .claude/skills, and ~/.claude/skills paths since CodeBuddy is a Claude Code fork - Replace hardcoded 20-min timeout with runContext for zero-timeout = no-deadline semantics matching all other backends - Restore runContext regression tests lost in rebase merge - Mirror claude.go execution model: concurrent stdin write to prevent pipe deadlock, sync.Once for stdin closure, keep stdin open for control_request auto-approval mid-run - Add control_request handling with auto-approve behavior - Add RequestID/Request fields to codebuddySDKMessage - Add codebuddy to metrics knownRuntimeProviders - Add codebuddy to provider-logo.tsx (reuses ClaudeLogo) - Consolidate --help discovery: shared codebuddyHelpOutput cache eliminates duplicate cold-start invocations --------- Co-authored-by: krislliu <krislliu@tencent.com>
147 lines
4.1 KiB
Go
147 lines
4.1 KiB
Go
package agent
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestNewReturnsClaudeBackend(t *testing.T) {
|
|
t.Parallel()
|
|
b, err := New("claude", Config{ExecutablePath: "/nonexistent/claude"})
|
|
if err != nil {
|
|
t.Fatalf("New(claude) error: %v", err)
|
|
}
|
|
if _, ok := b.(*claudeBackend); !ok {
|
|
t.Fatalf("expected *claudeBackend, got %T", b)
|
|
}
|
|
}
|
|
|
|
func TestNewReturnsCodexBackend(t *testing.T) {
|
|
t.Parallel()
|
|
b, err := New("codex", Config{ExecutablePath: "/nonexistent/codex"})
|
|
if err != nil {
|
|
t.Fatalf("New(codex) error: %v", err)
|
|
}
|
|
if _, ok := b.(*codexBackend); !ok {
|
|
t.Fatalf("expected *codexBackend, got %T", b)
|
|
}
|
|
}
|
|
|
|
func TestNewReturnsCodebuddyBackend(t *testing.T) {
|
|
t.Parallel()
|
|
b, err := New("codebuddy", Config{ExecutablePath: "/nonexistent/codebuddy"})
|
|
if err != nil {
|
|
t.Fatalf("New(codebuddy) error: %v", err)
|
|
}
|
|
if _, ok := b.(*codebuddyBackend); !ok {
|
|
t.Fatalf("expected *codebuddyBackend, got %T", b)
|
|
}
|
|
}
|
|
|
|
func TestNewReturnsCopilotBackend(t *testing.T) {
|
|
t.Parallel()
|
|
b, err := New("copilot", Config{ExecutablePath: "/nonexistent/copilot"})
|
|
if err != nil {
|
|
t.Fatalf("New(copilot) error: %v", err)
|
|
}
|
|
if _, ok := b.(*copilotBackend); !ok {
|
|
t.Fatalf("expected *copilotBackend, got %T", b)
|
|
}
|
|
}
|
|
|
|
func TestNewReturnsAntigravityBackend(t *testing.T) {
|
|
t.Parallel()
|
|
b, err := New("antigravity", Config{ExecutablePath: "/nonexistent/agy"})
|
|
if err != nil {
|
|
t.Fatalf("New(antigravity) error: %v", err)
|
|
}
|
|
if _, ok := b.(*antigravityBackend); !ok {
|
|
t.Fatalf("expected *antigravityBackend, got %T", b)
|
|
}
|
|
}
|
|
|
|
func TestNewRejectsUnknownType(t *testing.T) {
|
|
t.Parallel()
|
|
_, err := New("gpt", Config{})
|
|
if err == nil {
|
|
t.Fatal("expected error for unknown agent type")
|
|
}
|
|
}
|
|
|
|
func TestNewDefaultsLogger(t *testing.T) {
|
|
t.Parallel()
|
|
b, _ := New("claude", Config{})
|
|
cb := b.(*claudeBackend)
|
|
if cb.cfg.Logger == nil {
|
|
t.Fatal("expected non-nil logger")
|
|
}
|
|
}
|
|
|
|
func TestDetectVersionFailsForMissingBinary(t *testing.T) {
|
|
t.Parallel()
|
|
_, err := DetectVersion(context.Background(), "/nonexistent/binary")
|
|
if err == nil {
|
|
t.Fatal("expected error for missing binary")
|
|
}
|
|
}
|
|
|
|
func TestLaunchHeaderCoversAllSupportedBackends(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// The factory in New() enumerates every supported agent type; LaunchHeader
|
|
// must stay in sync so the UI preview never shows an empty skeleton for a
|
|
// runtime the daemon actually spawns. If a new backend is added, add an
|
|
// entry to launchHeaders in agent.go and extend this list.
|
|
supported := []string{
|
|
"antigravity", "claude", "codebuddy", "codex", "copilot", "cursor", "gemini",
|
|
"hermes", "kimi", "kiro", "openclaw", "opencode", "pi",
|
|
}
|
|
for _, t_ := range supported {
|
|
if header := LaunchHeader(t_); header == "" {
|
|
t.Errorf("LaunchHeader(%q) returned empty string — add it to launchHeaders", t_)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLaunchHeaderReturnsEmptyForUnknownType(t *testing.T) {
|
|
t.Parallel()
|
|
if header := LaunchHeader("made-up-agent"); header != "" {
|
|
t.Errorf("expected empty header for unknown type, got %q", header)
|
|
}
|
|
}
|
|
|
|
func TestRunContextZeroTimeoutHasNoDeadline(t *testing.T) {
|
|
t.Parallel()
|
|
// A zero (or negative) timeout must NOT impose a wall-clock deadline:
|
|
// liveness is delegated to the daemon's inactivity watchdog so an actively
|
|
// streaming long-running session is never killed merely for running long
|
|
// (MUL-3064).
|
|
for _, d := range []time.Duration{0, -time.Second} {
|
|
ctx, cancel := runContext(context.Background(), d)
|
|
if _, ok := ctx.Deadline(); ok {
|
|
cancel()
|
|
t.Fatalf("runContext(%s) imposed a deadline; want none", d)
|
|
}
|
|
cancel()
|
|
if ctx.Err() == nil {
|
|
t.Fatalf("runContext(%s): context should be cancelled after cancel()", d)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRunContextPositiveTimeoutHasDeadline(t *testing.T) {
|
|
t.Parallel()
|
|
// A positive timeout keeps the hard wall-clock deadline (the opt-in
|
|
// absolute cap operators can still set via MULTICA_AGENT_TIMEOUT).
|
|
ctx, cancel := runContext(context.Background(), time.Hour)
|
|
defer cancel()
|
|
deadline, ok := ctx.Deadline()
|
|
if !ok {
|
|
t.Fatal("runContext(1h) should impose a deadline")
|
|
}
|
|
if remaining := time.Until(deadline); remaining <= 0 || remaining > time.Hour+time.Minute {
|
|
t.Fatalf("unexpected deadline remaining: %s", remaining)
|
|
}
|
|
}
|