260 lines
6.9 KiB
Go
260 lines
6.9 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)
|
|
}
|