mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-05 13:29:44 +02:00
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.
136 lines
3.8 KiB
Go
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)
|
|
}
|
|
}
|