Files
multica/server/pkg/agent/agent_test.go
Bohan Jiang 3708fb0f07 fix(daemon): inactivity-based agent run timeout, no wall-clock guillotine (MUL-3064)
Active long-running sessions are no longer killed by a fixed wall-clock deadline. Liveness is delegated to the idle watchdog (MULTICA_AGENT_IDLE_WATCHDOG, default 30m) with a larger in-flight-tool budget (MULTICA_AGENT_TOOL_WATCHDOG, default 2h). MULTICA_AGENT_TIMEOUT is an opt-in absolute cap (default 0 = no cap). The server-side 2.5h sweeper is unchanged as a coarse backstop.

Fixes #3745.
2026-06-05 15:06:07 +08:00

136 lines
3.8 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 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", "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)
}
}