agent/pkg/agentic/dispatch_test.go
Snider 537226bd4d feat: AX v0.8.0 upgrade — Core features + quality gates
AX Quality Gates (RFC-025):
- Eliminate os/exec from all test + production code (12+ files)
- Eliminate encoding/json from all test files (15 files, 66 occurrences)
- Eliminate os from all test files except TestMain (Go runtime contract)
- Eliminate path/filepath, net/url from all files
- String concat: 39 violations replaced with core.Concat()
- Test naming AX-7: 264 test functions renamed across all 6 packages
- Example test 1:1 coverage complete

Core Features Adopted:
- Task Composition: agent.completion pipeline (QA → PR → Verify → Ingest → Poke)
- PerformAsync: completion pipeline runs with WaitGroup + progress tracking
- Config: agents.yaml loaded once, feature flags (auto-qa/pr/merge/ingest)
- Named Locks: c.Lock("drain") for queue serialisation
- Registry: workspace state with cross-package QUERY access
- QUERY: c.QUERY(WorkspaceQuery{Status: "running"}) for cross-service queries
- Action descriptions: 25+ Actions self-documenting
- Data mounts: prompts/tasks/flows/personas/workspaces via c.Data()
- Content Actions: agentic.prompt/task/flow/persona callable via IPC
- Drive endpoints: forge + brain registered with tokens
- Drive REST helpers: DriveGet/DrivePost/DriveDo for Drive-aware HTTP
- HandleIPCEvents: auto-discovered by WithService (no manual wiring)
- Entitlement: frozen-queue gate on write Actions
- CLI dispatch: workspace dispatch wired to real dispatch method
- CLI: --quiet/-q and --debug/-d global flags
- CLI: banner, version, check (with service/action/command counts), env
- main.go: minimal — 5 services + c.Run(), no os import
- cmd tests: 84.2% coverage (was 0%)

Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-26 06:38:02 +00:00

491 lines
17 KiB
Go

// SPDX-License-Identifier: EUPL-1.2
package agentic
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"time"
core "dappco.re/go/core"
"dappco.re/go/core/forge"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// --- agentCommand ---
// Good: tested in logic_test.go (TestAgentCommand_Good_*)
// Bad: tested in logic_test.go (TestAgentCommand_Bad_Unknown)
// Ugly: tested in logic_test.go (TestAgentCommand_Ugly_EmptyAgent)
// --- containerCommand ---
// Good: tested in logic_test.go (TestContainerCommand_Good_*)
// --- agentOutputFile ---
func TestDispatch_AgentOutputFile_Good(t *testing.T) {
assert.Contains(t, agentOutputFile("/ws", "codex"), ".meta/agent-codex.log")
assert.Contains(t, agentOutputFile("/ws", "claude:opus"), ".meta/agent-claude.log")
assert.Contains(t, agentOutputFile("/ws", "gemini:flash"), ".meta/agent-gemini.log")
}
func TestDispatch_AgentOutputFile_Bad(t *testing.T) {
// Empty agent — still produces a path (no crash)
result := agentOutputFile("/ws", "")
assert.Contains(t, result, ".meta/agent-.log")
}
func TestDispatch_AgentOutputFile_Ugly(t *testing.T) {
// Agent with multiple colons — only splits on first
result := agentOutputFile("/ws", "claude:opus:latest")
assert.Contains(t, result, "agent-claude.log")
}
// --- detectFinalStatus ---
func TestDispatch_DetectFinalStatus_Good(t *testing.T) {
dir := t.TempDir()
// Clean exit = completed
status, question := detectFinalStatus(dir, 0, "completed")
assert.Equal(t, "completed", status)
assert.Empty(t, question)
}
func TestDispatch_DetectFinalStatus_Bad(t *testing.T) {
dir := t.TempDir()
// Non-zero exit code
status, question := detectFinalStatus(dir, 1, "completed")
assert.Equal(t, "failed", status)
assert.Contains(t, question, "code 1")
// Process killed
status2, _ := detectFinalStatus(dir, 0, "killed")
assert.Equal(t, "failed", status2)
// Process status "failed"
status3, _ := detectFinalStatus(dir, 0, "failed")
assert.Equal(t, "failed", status3)
}
func TestDispatch_DetectFinalStatus_Ugly(t *testing.T) {
dir := t.TempDir()
// BLOCKED.md exists but is whitespace only — NOT blocked
fs.Write(core.JoinPath(dir, "BLOCKED.md"), " \n ")
status, _ := detectFinalStatus(dir, 0, "completed")
assert.Equal(t, "completed", status)
// BLOCKED.md takes precedence over non-zero exit
fs.Write(core.JoinPath(dir, "BLOCKED.md"), "Need credentials")
status2, question2 := detectFinalStatus(dir, 1, "failed")
assert.Equal(t, "blocked", status2)
assert.Equal(t, "Need credentials", question2)
}
// --- trackFailureRate ---
func TestDispatch_TrackFailureRate_Good(t *testing.T) {
s := &PrepSubsystem{ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}), backoff: make(map[string]time.Time), failCount: map[string]int{"codex": 2}}
// Success resets count
triggered := s.trackFailureRate("codex", "completed", time.Now().Add(-10*time.Second))
assert.False(t, triggered)
assert.Equal(t, 0, s.failCount["codex"])
}
func TestDispatch_TrackFailureRate_Bad(t *testing.T) {
s := &PrepSubsystem{ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}), backoff: make(map[string]time.Time), failCount: map[string]int{"codex": 2}}
// 3rd fast failure triggers backoff
triggered := s.trackFailureRate("codex", "failed", time.Now().Add(-10*time.Second))
assert.True(t, triggered)
assert.True(t, time.Now().Before(s.backoff["codex"]))
}
func TestDispatch_TrackFailureRate_Ugly(t *testing.T) {
s := newPrepWithProcess()
// Slow failure (>60s) resets count instead of incrementing
s.failCount["codex"] = 2
s.trackFailureRate("codex", "failed", time.Now().Add(-5*time.Minute))
assert.Equal(t, 0, s.failCount["codex"])
// Model variant tracks by base pool
s.trackFailureRate("codex:gpt-5.4", "failed", time.Now().Add(-10*time.Second))
assert.Equal(t, 1, s.failCount["codex"])
}
// --- startIssueTracking ---
func TestDispatch_StartIssueTracking_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(201)
}))
t.Cleanup(srv.Close)
dir := t.TempDir()
st := &WorkspaceStatus{Status: "running", Repo: "go-io", Org: "core", Issue: 15}
fs.Write(core.JoinPath(dir, "status.json"), core.JSONMarshalString(st))
s := &PrepSubsystem{ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}), forge: forge.NewForge(srv.URL, "tok"), backoff: make(map[string]time.Time), failCount: make(map[string]int)}
s.startIssueTracking(dir)
}
func TestDispatch_StartIssueTracking_Bad(t *testing.T) {
// No forge — returns early
s := &PrepSubsystem{ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}), forge: nil, backoff: make(map[string]time.Time), failCount: make(map[string]int)}
s.startIssueTracking(t.TempDir())
// No status file
s2 := &PrepSubsystem{ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}), forge: nil, backoff: make(map[string]time.Time), failCount: make(map[string]int)}
s2.startIssueTracking(t.TempDir())
}
func TestDispatch_StartIssueTracking_Ugly(t *testing.T) {
// Status has no issue — early return
dir := t.TempDir()
st := &WorkspaceStatus{Status: "running", Repo: "test"}
fs.Write(core.JoinPath(dir, "status.json"), core.JSONMarshalString(st))
s := &PrepSubsystem{ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}), forge: forge.NewForge("http://invalid", "tok"), backoff: make(map[string]time.Time), failCount: make(map[string]int)}
s.startIssueTracking(dir) // no issue → skips API call
}
// --- stopIssueTracking ---
func TestDispatch_StopIssueTracking_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(204)
}))
t.Cleanup(srv.Close)
dir := t.TempDir()
st := &WorkspaceStatus{Status: "completed", Repo: "go-io", Issue: 10}
fs.Write(core.JoinPath(dir, "status.json"), core.JSONMarshalString(st))
s := &PrepSubsystem{ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}), forge: forge.NewForge(srv.URL, "tok"), backoff: make(map[string]time.Time), failCount: make(map[string]int)}
s.stopIssueTracking(dir)
}
func TestDispatch_StopIssueTracking_Bad(t *testing.T) {
s := &PrepSubsystem{ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}), forge: nil, backoff: make(map[string]time.Time), failCount: make(map[string]int)}
s.stopIssueTracking(t.TempDir())
}
func TestDispatch_StopIssueTracking_Ugly(t *testing.T) {
// Status has no issue
dir := t.TempDir()
st := &WorkspaceStatus{Status: "completed", Repo: "test"}
fs.Write(core.JoinPath(dir, "status.json"), core.JSONMarshalString(st))
s := &PrepSubsystem{ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}), forge: forge.NewForge("http://invalid", "tok"), backoff: make(map[string]time.Time), failCount: make(map[string]int)}
s.stopIssueTracking(dir)
}
// --- broadcastStart ---
func TestDispatch_BroadcastStart_Good(t *testing.T) {
root := t.TempDir()
t.Setenv("CORE_WORKSPACE", root)
wsDir := core.JoinPath(root, "workspace", "ws-test")
fs.EnsureDir(wsDir)
fs.Write(core.JoinPath(wsDir, "status.json"), core.JSONMarshalString(WorkspaceStatus{Repo: "go-io", Agent: "codex"}))
c := core.New()
s := &PrepSubsystem{ServiceRuntime: core.NewServiceRuntime(c, AgentOptions{}), backoff: make(map[string]time.Time), failCount: make(map[string]int)}
s.broadcastStart("codex", wsDir)
}
func TestDispatch_BroadcastStart_Bad(t *testing.T) {
// No Core — should not panic
s := &PrepSubsystem{ServiceRuntime: nil, backoff: make(map[string]time.Time), failCount: make(map[string]int)}
s.broadcastStart("codex", t.TempDir())
}
func TestDispatch_BroadcastStart_Ugly(t *testing.T) {
// No status file — broadcasts with empty repo
c := core.New()
s := &PrepSubsystem{ServiceRuntime: core.NewServiceRuntime(c, AgentOptions{}), backoff: make(map[string]time.Time), failCount: make(map[string]int)}
s.broadcastStart("codex", t.TempDir())
}
// --- broadcastComplete ---
func TestDispatch_BroadcastComplete_Good(t *testing.T) {
root := t.TempDir()
t.Setenv("CORE_WORKSPACE", root)
wsDir := core.JoinPath(root, "workspace", "ws-test")
fs.EnsureDir(wsDir)
fs.Write(core.JoinPath(wsDir, "status.json"), core.JSONMarshalString(WorkspaceStatus{Repo: "go-io", Agent: "codex"}))
c := core.New()
s := &PrepSubsystem{ServiceRuntime: core.NewServiceRuntime(c, AgentOptions{}), backoff: make(map[string]time.Time), failCount: make(map[string]int)}
s.broadcastComplete("codex", wsDir, "completed")
}
func TestDispatch_BroadcastComplete_Bad(t *testing.T) {
s := &PrepSubsystem{ServiceRuntime: nil, backoff: make(map[string]time.Time), failCount: make(map[string]int)}
s.broadcastComplete("codex", t.TempDir(), "failed")
}
func TestDispatch_BroadcastComplete_Ugly(t *testing.T) {
// No status file
c := core.New()
s := &PrepSubsystem{ServiceRuntime: core.NewServiceRuntime(c, AgentOptions{}), backoff: make(map[string]time.Time), failCount: make(map[string]int)}
s.broadcastComplete("codex", t.TempDir(), "completed")
}
// --- onAgentComplete ---
func TestDispatch_OnAgentComplete_Good(t *testing.T) {
root := t.TempDir()
t.Setenv("CORE_WORKSPACE", root)
wsDir := core.JoinPath(root, "ws-test")
repoDir := core.JoinPath(wsDir, "repo")
metaDir := core.JoinPath(wsDir, ".meta")
fs.EnsureDir(repoDir)
fs.EnsureDir(metaDir)
st := &WorkspaceStatus{Status: "running", Repo: "go-io", Agent: "codex", StartedAt: time.Now()}
fs.Write(core.JoinPath(wsDir, "status.json"), core.JSONMarshalString(st))
s := newPrepWithProcess()
outputFile := core.JoinPath(metaDir, "agent-codex.log")
s.onAgentComplete("codex", wsDir, outputFile, 0, "completed", "test output")
updated, err := ReadStatus(wsDir)
require.NoError(t, err)
assert.Equal(t, "completed", updated.Status)
assert.Equal(t, 0, updated.PID)
r := fs.Read(outputFile)
assert.True(t, r.OK)
assert.Equal(t, "test output", r.Value.(string))
}
func TestDispatch_OnAgentComplete_Bad(t *testing.T) {
root := t.TempDir()
t.Setenv("CORE_WORKSPACE", root)
wsDir := core.JoinPath(root, "ws-fail")
repoDir := core.JoinPath(wsDir, "repo")
metaDir := core.JoinPath(wsDir, ".meta")
fs.EnsureDir(repoDir)
fs.EnsureDir(metaDir)
st := &WorkspaceStatus{Status: "running", Repo: "go-io", Agent: "codex", StartedAt: time.Now()}
fs.Write(core.JoinPath(wsDir, "status.json"), core.JSONMarshalString(st))
s := newPrepWithProcess()
s.onAgentComplete("codex", wsDir, core.JoinPath(metaDir, "agent-codex.log"), 1, "failed", "error")
updated, _ := ReadStatus(wsDir)
assert.Equal(t, "failed", updated.Status)
assert.Contains(t, updated.Question, "code 1")
}
func TestDispatch_OnAgentComplete_Ugly(t *testing.T) {
root := t.TempDir()
t.Setenv("CORE_WORKSPACE", root)
wsDir := core.JoinPath(root, "ws-blocked")
repoDir := core.JoinPath(wsDir, "repo")
metaDir := core.JoinPath(wsDir, ".meta")
fs.EnsureDir(repoDir)
fs.EnsureDir(metaDir)
fs.Write(core.JoinPath(repoDir, "BLOCKED.md"), "Need credentials")
st := &WorkspaceStatus{Status: "running", Repo: "go-io", Agent: "codex", StartedAt: time.Now()}
fs.Write(core.JoinPath(wsDir, "status.json"), core.JSONMarshalString(st))
s := newPrepWithProcess()
s.onAgentComplete("codex", wsDir, core.JoinPath(metaDir, "agent-codex.log"), 0, "completed", "")
updated, _ := ReadStatus(wsDir)
assert.Equal(t, "blocked", updated.Status)
assert.Equal(t, "Need credentials", updated.Question)
// Empty output should NOT create log file
assert.False(t, fs.Exists(core.JoinPath(metaDir, "agent-codex.log")))
}
// --- runQA ---
func TestDispatch_RunQA_Good(t *testing.T) {
wsDir := t.TempDir()
repoDir := core.JoinPath(wsDir, "repo")
fs.EnsureDir(repoDir)
fs.Write(core.JoinPath(repoDir, "go.mod"), "module testmod\n\ngo 1.22\n")
fs.Write(core.JoinPath(repoDir, "main.go"), "package main\nfunc main() {}\n")
s := newPrepWithProcess()
assert.True(t, s.runQA(wsDir))
}
func TestDispatch_RunQA_Bad(t *testing.T) {
wsDir := t.TempDir()
repoDir := core.JoinPath(wsDir, "repo")
fs.EnsureDir(repoDir)
// Broken Go code
fs.Write(core.JoinPath(repoDir, "go.mod"), "module testmod\n\ngo 1.22\n")
fs.Write(core.JoinPath(repoDir, "main.go"), "package main\nfunc main( {\n}\n")
s := newPrepWithProcess()
assert.False(t, s.runQA(wsDir))
// PHP project — composer not available
wsDir2 := t.TempDir()
repoDir2 := core.JoinPath(wsDir2, "repo")
fs.EnsureDir(repoDir2)
fs.Write(core.JoinPath(repoDir2, "composer.json"), `{"name":"test"}`)
assert.False(t, s.runQA(wsDir2))
}
func TestDispatch_RunQA_Ugly(t *testing.T) {
// Unknown language — passes QA (no checks)
wsDir := t.TempDir()
fs.EnsureDir(core.JoinPath(wsDir, "repo"))
s := newPrepWithProcess()
assert.True(t, s.runQA(wsDir))
// Go vet failure (compiles but bad printf)
wsDir2 := t.TempDir()
repoDir2 := core.JoinPath(wsDir2, "repo")
fs.EnsureDir(repoDir2)
fs.Write(core.JoinPath(repoDir2, "go.mod"), "module testmod\n\ngo 1.22\n")
fs.Write(core.JoinPath(repoDir2, "main.go"), "package main\nimport \"fmt\"\nfunc main() { fmt.Printf(\"%d\", \"x\") }\n")
assert.False(t, s.runQA(wsDir2))
// Node project — npm install likely fails
wsDir3 := t.TempDir()
repoDir3 := core.JoinPath(wsDir3, "repo")
fs.EnsureDir(repoDir3)
fs.Write(core.JoinPath(repoDir3, "package.json"), `{"name":"test","scripts":{"test":"echo ok"}}`)
_ = s.runQA(wsDir3) // exercises the node path
}
// --- dispatch ---
func TestDispatch_Dispatch_Good(t *testing.T) {
root := t.TempDir()
t.Setenv("CORE_WORKSPACE", root)
forgeSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(core.JSONMarshalString(map[string]any{"title": "Issue", "body": "Fix"})))
}))
t.Cleanup(forgeSrv.Close)
srcRepo := core.JoinPath(t.TempDir(), "core", "go-io")
testCore.Process().Run(context.Background(), "git", "init", "-b", "main", srcRepo)
testCore.Process().RunIn(context.Background(), srcRepo, "git", "config", "user.name", "T")
testCore.Process().RunIn(context.Background(), srcRepo, "git", "config", "user.email", "t@t.com")
fs.Write(core.JoinPath(srcRepo, "go.mod"), "module test\ngo 1.22\n")
testCore.Process().RunIn(context.Background(), srcRepo, "git", "add", ".")
testCore.Process().RunIn(context.Background(), srcRepo, "git", "commit", "-m", "init")
s := newPrepWithProcess()
s.forge = forge.NewForge(forgeSrv.URL, "tok")
s.codePath = core.PathDir(core.PathDir(srcRepo))
_, out, err := s.dispatch(context.Background(), nil, DispatchInput{
Repo: "go-io", Task: "Fix stuff", Issue: 42, DryRun: true,
})
require.NoError(t, err)
assert.True(t, out.Success)
assert.Equal(t, "codex", out.Agent)
assert.NotEmpty(t, out.Prompt)
}
func TestDispatch_Dispatch_Bad(t *testing.T) {
s := newPrepWithProcess()
// No repo
_, _, err := s.dispatch(context.Background(), nil, DispatchInput{Task: "do"})
assert.Error(t, err)
assert.Contains(t, err.Error(), "repo is required")
// No task
_, _, err = s.dispatch(context.Background(), nil, DispatchInput{Repo: "go-io"})
assert.Error(t, err)
assert.Contains(t, err.Error(), "task is required")
}
func TestDispatch_Dispatch_Ugly(t *testing.T) {
root := t.TempDir()
t.Setenv("CORE_WORKSPACE", root)
// Prep fails (no local clone)
s := &PrepSubsystem{ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}), codePath: t.TempDir(), backoff: make(map[string]time.Time), failCount: make(map[string]int)}
_, _, err := s.dispatch(context.Background(), nil, DispatchInput{
Repo: "nonexistent", Task: "do", Issue: 1,
})
assert.Error(t, err)
assert.Contains(t, err.Error(), "prep workspace failed")
}
// --- workspaceDir ---
func TestDispatch_WorkspaceDir_Good(t *testing.T) {
root := t.TempDir()
t.Setenv("CORE_WORKSPACE", root)
dir, err := workspaceDir("core", "go-io", PrepInput{Issue: 42})
require.NoError(t, err)
assert.Contains(t, dir, "task-42")
dir2, _ := workspaceDir("core", "go-io", PrepInput{PR: 7})
assert.Contains(t, dir2, "pr-7")
dir3, _ := workspaceDir("core", "go-io", PrepInput{Branch: "feat/new"})
assert.Contains(t, dir3, "feat/new")
dir4, _ := workspaceDir("core", "go-io", PrepInput{Tag: "v1.0.0"})
assert.Contains(t, dir4, "v1.0.0")
}
func TestDispatch_WorkspaceDir_Bad(t *testing.T) {
_, err := workspaceDir("core", "go-io", PrepInput{})
assert.Error(t, err)
assert.Contains(t, err.Error(), "one of issue, pr, branch, or tag")
}
func TestDispatch_WorkspaceDir_Ugly(t *testing.T) {
root := t.TempDir()
t.Setenv("CORE_WORKSPACE", root)
// PR takes precedence when multiple set (first match)
dir, err := workspaceDir("core", "go-io", PrepInput{PR: 3, Issue: 5})
require.NoError(t, err)
assert.Contains(t, dir, "pr-3")
}
// --- containerCommand ---
func TestDispatch_ContainerCommand_Bad(t *testing.T) {
t.Setenv("AGENT_DOCKER_IMAGE", "")
t.Setenv("DIR_HOME", "/home/dev")
// Empty command string — docker still runs, just with no command after image
cmd, args := containerCommand("codex", "", []string{}, "/ws/repo", "/ws/.meta")
assert.Equal(t, "docker", cmd)
assert.Contains(t, args, "run")
// The image should still be present in args
assert.Contains(t, args, defaultDockerImage)
}
// --- canDispatchAgent ---
// Good: tested in queue_test.go
// Bad: tested in queue_test.go
// Ugly: see queue_extra_test.go