agent/pkg/runner/queue_test.go
Snider edfcb1bdfe feat(agent): unblock factory dispatch, runtime-aware containers, RFC gaps
- paths.go: resolve relative workspace_root against $HOME/Code so workspaces
  land in the conventional location regardless of launch cwd (MCP stdio vs CLI)
- dispatch.go: container mounts use /home/agent (matches DEV_USER), plus
  runtime-aware dispatch (apple/docker/podman) with GPU toggle per RFC §15.5
- queue.go / runner/queue.go: DispatchConfig adds Runtime/Image/GPU fields;
  AgentIdentity parsing for the agents: block (RFC §10/§11)
- pr.go / commands_forge.go / actions.go: agentic_delete_branch tool +
  branch/delete CLI (RFC §7)
- brain/tools.go / provider.go: Org + IndexedAt fields on Memory (RFC §4)
- config/agents.yaml: document new dispatch fields, fix identity table
- tests: dispatch_runtime_test.go (21), expanded pr_test.go + queue_test.go,
  new CLI fixtures for branch/delete and pr/list

Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-14 11:45:09 +01:00

366 lines
9.7 KiB
Go

// SPDX-License-Identifier: EUPL-1.2
package runner
import (
"testing"
"github.com/stretchr/testify/assert"
"gopkg.in/yaml.v3"
)
// --- ConcurrencyLimit UnmarshalYAML ---
func TestQueue_ConcurrencyLimit_Good_Int(t *testing.T) {
var cl ConcurrencyLimit
node := &yaml.Node{Kind: yaml.ScalarNode, Value: "5", Tag: "!!int"}
err := cl.UnmarshalYAML(node)
assert.NoError(t, err)
assert.Equal(t, 5, cl.Total)
assert.Nil(t, cl.Models)
}
func TestQueue_ConcurrencyLimit_Good_Map(t *testing.T) {
input := `
total: 5
gpt-5.4: 1
gpt-5.3-codex-spark: 3
`
var cl ConcurrencyLimit
err := yaml.Unmarshal([]byte(input), &cl)
assert.NoError(t, err)
assert.Equal(t, 5, cl.Total)
assert.Equal(t, 1, cl.Models["gpt-5.4"])
assert.Equal(t, 3, cl.Models["gpt-5.3-codex-spark"])
}
func TestQueue_ConcurrencyLimit_Bad_InvalidYAML(t *testing.T) {
node := &yaml.Node{Kind: yaml.SequenceNode}
var cl ConcurrencyLimit
err := cl.UnmarshalYAML(node)
assert.Error(t, err)
}
func TestQueue_ConcurrencyLimit_Ugly_ZeroTotal(t *testing.T) {
input := `
total: 0
gpt-5.4: 1
`
var cl ConcurrencyLimit
err := yaml.Unmarshal([]byte(input), &cl)
assert.NoError(t, err)
assert.Equal(t, 0, cl.Total)
}
// --- baseAgent ---
func TestQueue_BaseAgent_Good_Simple(t *testing.T) {
assert.Equal(t, "codex", baseAgent("codex"))
assert.Equal(t, "claude", baseAgent("claude"))
assert.Equal(t, "gemini", baseAgent("gemini"))
}
func TestQueue_BaseAgent_Good_WithModel(t *testing.T) {
assert.Equal(t, "codex", baseAgent("codex:gpt-5.4"))
assert.Equal(t, "claude", baseAgent("claude:haiku"))
}
func TestQueue_BaseAgent_Good_CodexSpark(t *testing.T) {
assert.Equal(t, "codex", baseAgent("codex:gpt-5.3-codex-spark"))
}
func TestQueue_BaseAgent_Ugly_Empty(t *testing.T) {
assert.Equal(t, "", baseAgent(""))
}
func TestQueue_BaseAgent_Ugly_MultipleColons(t *testing.T) {
assert.Equal(t, "codex", baseAgent("codex:model:extra"))
}
// --- modelVariant ---
func TestQueue_ModelVariant_Good(t *testing.T) {
assert.Equal(t, "gpt-5.4", modelVariant("codex:gpt-5.4"))
assert.Equal(t, "haiku", modelVariant("claude:haiku"))
}
func TestQueue_ModelVariant_Bad_NoColon(t *testing.T) {
assert.Equal(t, "", modelVariant("codex"))
assert.Equal(t, "", modelVariant("claude"))
}
func TestQueue_ModelVariant_Ugly_Empty(t *testing.T) {
assert.Equal(t, "", modelVariant(""))
}
// --- canDispatchAgent ---
func TestQueue_CanDispatchAgent_Good_NoConfig(t *testing.T) {
svc := New()
can, _ := svc.canDispatchAgent("codex")
assert.True(t, can, "no config → unlimited")
}
func TestQueue_CanDispatchAgent_Good_UnknownAgent(t *testing.T) {
svc := New()
can, _ := svc.canDispatchAgent("unknown-agent")
assert.True(t, can)
}
func TestQueue_CanDispatchAgent_Bad_AtLimit(t *testing.T) {
svc := New()
// Simulate 5 running codex workspaces
for i := 0; i < 5; i++ {
svc.TrackWorkspace("ws-"+string(rune('a'+i)), &WorkspaceStatus{
Status: "running", Agent: "codex", PID: 99999, // PID won't be alive
})
}
// Since PIDs aren't alive, count should be 0
can, _ := svc.canDispatchAgent("codex")
assert.True(t, can, "dead PIDs don't count")
}
func TestQueue_CanDispatchAgent_Ugly_ZeroLimit(t *testing.T) {
svc := New()
// Zero total means unlimited
can, _ := svc.canDispatchAgent("codex")
assert.True(t, can)
}
// --- countRunningByAgent ---
func TestQueue_CountRunningByAgent_Good_Empty(t *testing.T) {
svc := New()
// Add a non-running entry so Registry is non-empty (avoids disk fallback)
svc.TrackWorkspace("ws-seed", &WorkspaceStatus{Status: "completed", Agent: "claude"})
assert.Equal(t, 0, svc.countRunningByAgent("codex"))
}
func TestQueue_CountRunningByAgent_Good_SkipsNonRunning(t *testing.T) {
svc := New()
svc.TrackWorkspace("ws-1", &WorkspaceStatus{Status: "completed", Agent: "codex"})
svc.TrackWorkspace("ws-2", &WorkspaceStatus{Status: "queued", Agent: "codex"})
assert.Equal(t, 0, svc.countRunningByAgent("codex"))
}
func TestQueue_CountRunningByAgent_Bad_SkipsMismatchedAgent(t *testing.T) {
svc := New()
svc.TrackWorkspace("ws-1", &WorkspaceStatus{Status: "running", Agent: "claude", PID: 1})
assert.Equal(t, 0, svc.countRunningByAgent("codex"))
}
func TestQueue_CountRunningByAgent_Ugly_DeadPIDsIgnored(t *testing.T) {
svc := New()
svc.TrackWorkspace("ws-1", &WorkspaceStatus{
Status: "running", Agent: "codex", PID: 999999999, // almost certainly not alive
})
assert.Equal(t, 0, svc.countRunningByAgent("codex"))
}
// --- countRunningByModel ---
func TestQueue_CountRunningByModel_Good_Empty(t *testing.T) {
svc := New()
assert.Equal(t, 0, svc.countRunningByModel("codex:gpt-5.4"))
}
func TestQueue_CountRunningByModel_Bad_WrongModel(t *testing.T) {
svc := New()
svc.TrackWorkspace("ws-1", &WorkspaceStatus{
Status: "running", Agent: "codex:gpt-5.3", PID: 1,
})
assert.Equal(t, 0, svc.countRunningByModel("codex:gpt-5.4"))
}
func TestQueue_CountRunningByModel_Ugly_ExactMatch(t *testing.T) {
svc := New()
svc.TrackWorkspace("ws-1", &WorkspaceStatus{
Status: "running", Agent: "codex:gpt-5.4", PID: 999999999,
})
// PID is dead so count is 0
assert.Equal(t, 0, svc.countRunningByModel("codex:gpt-5.4"))
}
// --- drainQueue ---
func TestQueue_DrainQueue_Good_FrozenDoesNothing(t *testing.T) {
svc := New()
svc.frozen = true
assert.NotPanics(t, func() {
svc.drainQueue()
})
}
func TestQueue_DrainQueue_Bad_EmptyWorkspace(t *testing.T) {
svc := New()
svc.frozen = false
assert.NotPanics(t, func() {
svc.drainQueue()
})
}
func TestQueue_DrainQueue_Ugly_NoStatusFiles(t *testing.T) {
svc := New()
svc.frozen = false
// drainOne scans disk — with no workspace root, it finds nothing
assert.False(t, svc.drainOne())
}
// --- loadAgentsConfig ---
func TestQueue_LoadAgentsConfig_Good_ReadsFile(t *testing.T) {
svc := New()
cfg := svc.loadAgentsConfig()
assert.NotNil(t, cfg)
assert.GreaterOrEqual(t, cfg.Version, 0)
}
func TestQueue_LoadAgentsConfig_Bad_NoFile(t *testing.T) {
t.Setenv("CORE_WORKSPACE", t.TempDir())
svc := New()
cfg := svc.loadAgentsConfig()
assert.NotNil(t, cfg, "should return defaults when no file found")
}
func TestQueue_LoadAgentsConfig_Ugly_InvalidYAML(t *testing.T) {
dir := t.TempDir()
t.Setenv("CORE_WORKSPACE", dir)
fs.Write(dir+"/agents.yaml", "{{{{invalid yaml")
svc := New()
cfg := svc.loadAgentsConfig()
assert.NotNil(t, cfg, "should return defaults on parse failure")
}
// --- AgentsConfig full parse ---
func TestQueue_AgentsConfig_Good_FullParse(t *testing.T) {
input := `
version: 1
dispatch:
default_agent: claude
default_template: coding
concurrency:
claude: 3
codex:
total: 5
gpt-5.4: 1
rates:
gemini:
reset_utc: "06:00"
sustained_delay: 120
`
var cfg AgentsConfig
err := yaml.Unmarshal([]byte(input), &cfg)
assert.NoError(t, err)
assert.Equal(t, 1, cfg.Version)
assert.Equal(t, 3, cfg.Concurrency["claude"].Total)
assert.Equal(t, 5, cfg.Concurrency["codex"].Total)
assert.Equal(t, 1, cfg.Concurrency["codex"].Models["gpt-5.4"])
assert.Equal(t, 120, cfg.Rates["gemini"].SustainedDelay)
}
// --- DispatchConfig runtime/image/gpu ---
func TestQueue_DispatchConfig_Good_RuntimeImageGPU(t *testing.T) {
input := `
version: 1
dispatch:
default_agent: claude
runtime: apple
image: core-ml
gpu: true
`
var cfg AgentsConfig
err := yaml.Unmarshal([]byte(input), &cfg)
assert.NoError(t, err)
assert.Equal(t, "apple", cfg.Dispatch.Runtime)
assert.Equal(t, "core-ml", cfg.Dispatch.Image)
assert.True(t, cfg.Dispatch.GPU)
}
func TestQueue_DispatchConfig_Bad_OmittedRuntimeFields(t *testing.T) {
// When runtime / image / gpu are missing the yaml unmarshals into the
// struct's zero values. Callers treat empty runtime as "auto".
input := `
version: 1
dispatch:
default_agent: claude
`
var cfg AgentsConfig
err := yaml.Unmarshal([]byte(input), &cfg)
assert.NoError(t, err)
assert.Empty(t, cfg.Dispatch.Runtime)
assert.Empty(t, cfg.Dispatch.Image)
assert.False(t, cfg.Dispatch.GPU)
}
func TestQueue_DispatchConfig_Ugly_PartialRuntimeBlock(t *testing.T) {
// Only runtime is set; image keeps its zero value, gpu defaults to false.
input := `
version: 1
dispatch:
runtime: docker
`
var cfg AgentsConfig
err := yaml.Unmarshal([]byte(input), &cfg)
assert.NoError(t, err)
assert.Equal(t, "docker", cfg.Dispatch.Runtime)
assert.Empty(t, cfg.Dispatch.Image)
assert.False(t, cfg.Dispatch.GPU)
}
// --- AgentIdentity ---
func TestQueue_AgentIdentity_Good_FullParse(t *testing.T) {
input := `
version: 1
agents:
cladius:
host: local
runner: claude
active: true
roles: [dispatch, review, plan]
codex:
host: cloud
runner: openai
active: true
roles: [worker]
`
var cfg AgentsConfig
err := yaml.Unmarshal([]byte(input), &cfg)
assert.NoError(t, err)
assert.Equal(t, "local", cfg.Agents["cladius"].Host)
assert.Equal(t, "claude", cfg.Agents["cladius"].Runner)
assert.True(t, cfg.Agents["cladius"].Active)
assert.Contains(t, cfg.Agents["cladius"].Roles, "dispatch")
assert.Contains(t, cfg.Agents["cladius"].Roles, "review")
assert.Equal(t, "cloud", cfg.Agents["codex"].Host)
}
func TestQueue_AgentIdentity_Bad_MissingAgentsBlock(t *testing.T) {
input := `
version: 1
`
var cfg AgentsConfig
err := yaml.Unmarshal([]byte(input), &cfg)
assert.NoError(t, err)
assert.Empty(t, cfg.Agents)
}
func TestQueue_AgentIdentity_Ugly_OnlyHostSet(t *testing.T) {
// An identity with only host set populates host and leaves zero values
// for runner / active / roles. Routing code treats Active=false as a
// disabled identity and SHOULD NOT crash on missing fields.
input := `
agents:
ghost:
host: 192.168.0.42
`
var cfg AgentsConfig
err := yaml.Unmarshal([]byte(input), &cfg)
assert.NoError(t, err)
assert.Equal(t, "192.168.0.42", cfg.Agents["ghost"].Host)
assert.Empty(t, cfg.Agents["ghost"].Runner)
assert.False(t, cfg.Agents["ghost"].Active)
assert.Empty(t, cfg.Agents["ghost"].Roles)
}