agent/pkg/runner/queue_test.go
Snider 6bb4fb8d57 fix(dispatch): concurrency, queue runner, and path improvements
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-29 20:40:20 +01:00

256 lines
6.8 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()
assert.True(t, svc.canDispatchAgent("codex"), "no config → unlimited")
}
func TestQueue_CanDispatchAgent_Good_UnknownAgent(t *testing.T) {
svc := New()
assert.True(t, svc.canDispatchAgent("unknown-agent"))
}
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
assert.True(t, svc.canDispatchAgent("codex"), "dead PIDs don't count")
}
func TestQueue_CanDispatchAgent_Ugly_ZeroLimit(t *testing.T) {
svc := New()
// Zero total means unlimited
assert.True(t, svc.canDispatchAgent("codex"))
}
// --- 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)
}