test(agentic): add queue/status/plan/register test files

Adds four new test files covering previously untested functions:
- queue_logic_test.go: countRunningByModel, drainQueue, Poke, StartRunner, DefaultBranch, LocalFs
- status_logic_test.go: ReadStatus/writeStatus field coverage + WorkspaceStatus JSON round-trip
- plan_logic_test.go: planPath sanitisation + readPlan/writePlan round-trips with phases
- register_test.go: Register (service discovery, core wiring, config loading), OnStartup, OnShutdown

All 56 new tests follow _Good/_Bad/_Ugly convention and pass with go test ./pkg/agentic/...

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Snider 2026-03-24 23:07:02 +00:00
parent 36c29cd0af
commit 42e558ed38
4 changed files with 702 additions and 0 deletions

View file

@ -0,0 +1,175 @@
// SPDX-License-Identifier: EUPL-1.2
package agentic
import (
"path/filepath"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// --- planPath ---
func TestPlanPath_Good_BasicFormat(t *testing.T) {
result := planPath("/tmp/plans", "my-plan-abc123")
assert.Equal(t, "/tmp/plans/my-plan-abc123.json", result)
}
func TestPlanPath_Good_NestedIDStripped(t *testing.T) {
// PathBase strips directory component — prevents path traversal
result := planPath("/plans", "../../../etc/passwd")
assert.Equal(t, "/plans/passwd.json", result)
}
func TestPlanPath_Good_SimpleID(t *testing.T) {
assert.Equal(t, "/data/test.json", planPath("/data", "test"))
}
func TestPlanPath_Good_SlugWithDashes(t *testing.T) {
assert.Equal(t, "/root/migrate-core-abc123.json", planPath("/root", "migrate-core-abc123"))
}
func TestPlanPath_Bad_DotID(t *testing.T) {
// "." is sanitised to "invalid" to prevent exploiting the root directory
result := planPath("/plans", ".")
assert.Equal(t, "/plans/invalid.json", result)
}
func TestPlanPath_Bad_DoubleDotID(t *testing.T) {
result := planPath("/plans", "..")
assert.Equal(t, "/plans/invalid.json", result)
}
func TestPlanPath_Bad_EmptyID(t *testing.T) {
result := planPath("/plans", "")
assert.Equal(t, "/plans/invalid.json", result)
}
// --- readPlan / writePlan ---
func TestReadWritePlan_Good_BasicRoundtrip(t *testing.T) {
dir := t.TempDir()
now := time.Now().Truncate(time.Second)
plan := &Plan{
ID: "basic-plan-abc",
Title: "Basic Plan",
Status: "draft",
Repo: "go-io",
Org: "core",
Objective: "Verify round-trip works",
Agent: "claude:opus",
CreatedAt: now,
UpdatedAt: now,
}
path, err := writePlan(dir, plan)
require.NoError(t, err)
assert.Equal(t, filepath.Join(dir, "basic-plan-abc.json"), path)
read, err := readPlan(dir, "basic-plan-abc")
require.NoError(t, err)
assert.Equal(t, plan.ID, read.ID)
assert.Equal(t, plan.Title, read.Title)
assert.Equal(t, plan.Status, read.Status)
assert.Equal(t, plan.Repo, read.Repo)
assert.Equal(t, plan.Org, read.Org)
assert.Equal(t, plan.Objective, read.Objective)
assert.Equal(t, plan.Agent, read.Agent)
}
func TestReadWritePlan_Good_WithPhases(t *testing.T) {
dir := t.TempDir()
plan := &Plan{
ID: "phase-plan-abc",
Title: "Phased Work",
Status: "in_progress",
Objective: "Multi-phase plan",
Phases: []Phase{
{Number: 1, Name: "Setup", Status: "done", Criteria: []string{"repo cloned", "deps installed"}, Tests: 3},
{Number: 2, Name: "Implement", Status: "in_progress", Notes: "WIP"},
{Number: 3, Name: "Verify", Status: "pending"},
},
}
_, err := writePlan(dir, plan)
require.NoError(t, err)
read, err := readPlan(dir, "phase-plan-abc")
require.NoError(t, err)
require.Len(t, read.Phases, 3)
assert.Equal(t, "Setup", read.Phases[0].Name)
assert.Equal(t, "done", read.Phases[0].Status)
assert.Equal(t, []string{"repo cloned", "deps installed"}, read.Phases[0].Criteria)
assert.Equal(t, 3, read.Phases[0].Tests)
assert.Equal(t, "WIP", read.Phases[1].Notes)
assert.Equal(t, "pending", read.Phases[2].Status)
}
func TestReadPlan_Bad_MissingFile(t *testing.T) {
dir := t.TempDir()
_, err := readPlan(dir, "nonexistent-plan")
assert.Error(t, err)
}
func TestReadPlan_Bad_CorruptJSON(t *testing.T) {
dir := t.TempDir()
require.True(t, fs.Write(filepath.Join(dir, "bad.json"), `{broken`).OK)
_, err := readPlan(dir, "bad")
assert.Error(t, err)
}
func TestWritePlan_Good_CreatesNestedDir(t *testing.T) {
base := t.TempDir()
nested := filepath.Join(base, "deep", "nested", "plans")
plan := &Plan{
ID: "deep-plan-xyz",
Title: "Deep",
Status: "draft",
Objective: "Test nested dir creation",
}
path, err := writePlan(nested, plan)
require.NoError(t, err)
assert.Equal(t, filepath.Join(nested, "deep-plan-xyz.json"), path)
assert.True(t, fs.IsFile(path))
}
func TestWritePlan_Good_OverwriteExistingLogic(t *testing.T) {
dir := t.TempDir()
plan := &Plan{
ID: "overwrite-plan-abc",
Title: "First Title",
Status: "draft",
Objective: "Initial",
}
_, err := writePlan(dir, plan)
require.NoError(t, err)
plan.Title = "Second Title"
plan.Status = "approved"
_, err = writePlan(dir, plan)
require.NoError(t, err)
read, err := readPlan(dir, "overwrite-plan-abc")
require.NoError(t, err)
assert.Equal(t, "Second Title", read.Title)
assert.Equal(t, "approved", read.Status)
}
func TestReadPlan_Ugly_EmptyFileLogic(t *testing.T) {
dir := t.TempDir()
require.True(t, fs.Write(filepath.Join(dir, "empty.json"), "").OK)
_, err := readPlan(dir, "empty")
assert.Error(t, err)
}

View file

@ -0,0 +1,220 @@
// SPDX-License-Identifier: EUPL-1.2
package agentic
import (
"os/exec"
"path/filepath"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// --- countRunningByModel ---
func TestCountRunningByModel_Good_Empty(t *testing.T) {
root := t.TempDir()
t.Setenv("CORE_WORKSPACE", root)
s := &PrepSubsystem{}
assert.Equal(t, 0, s.countRunningByModel("claude:opus"))
}
func TestCountRunningByModel_Good_SkipsNonRunning(t *testing.T) {
root := t.TempDir()
t.Setenv("CORE_WORKSPACE", root)
// Completed workspace — must not be counted
ws := filepath.Join(root, "workspace", "test-ws")
require.True(t, fs.EnsureDir(ws).OK)
require.NoError(t, writeStatus(ws, &WorkspaceStatus{
Status: "completed",
Agent: "codex:gpt-5.4",
PID: 0,
}))
s := &PrepSubsystem{}
assert.Equal(t, 0, s.countRunningByModel("codex:gpt-5.4"))
}
func TestCountRunningByModel_Good_SkipsMismatchedModel(t *testing.T) {
root := t.TempDir()
t.Setenv("CORE_WORKSPACE", root)
ws := filepath.Join(root, "workspace", "model-ws")
require.True(t, fs.EnsureDir(ws).OK)
require.NoError(t, writeStatus(ws, &WorkspaceStatus{
Status: "running",
Agent: "gemini:flash",
PID: 0,
}))
s := &PrepSubsystem{}
// Asking for gemini:pro — must not count gemini:flash
assert.Equal(t, 0, s.countRunningByModel("gemini:pro"))
}
func TestCountRunningByModel_Good_DeepLayout(t *testing.T) {
root := t.TempDir()
t.Setenv("CORE_WORKSPACE", root)
// Deep layout: workspace/org/repo/task-N/status.json
ws := filepath.Join(root, "workspace", "core", "go-io", "task-1")
require.True(t, fs.EnsureDir(ws).OK)
require.NoError(t, writeStatus(ws, &WorkspaceStatus{
Status: "completed",
Agent: "codex:gpt-5.4",
}))
s := &PrepSubsystem{}
// Completed, so count is still 0
assert.Equal(t, 0, s.countRunningByModel("codex:gpt-5.4"))
}
// --- drainQueue ---
func TestDrainQueue_Good_FrozenReturnsImmediately(t *testing.T) {
root := t.TempDir()
t.Setenv("CORE_WORKSPACE", root)
s := &PrepSubsystem{frozen: true, backoff: make(map[string]time.Time), failCount: make(map[string]int)}
// Must not panic and must not block
assert.NotPanics(t, func() {
s.drainQueue()
})
}
func TestDrainQueue_Good_EmptyWorkspace(t *testing.T) {
root := t.TempDir()
t.Setenv("CORE_WORKSPACE", root)
s := &PrepSubsystem{frozen: false, backoff: make(map[string]time.Time), failCount: make(map[string]int)}
// No workspaces — must return without error/panic
assert.NotPanics(t, func() {
s.drainQueue()
})
}
// --- Poke ---
func TestPoke_Good_NilChannel(t *testing.T) {
s := &PrepSubsystem{pokeCh: nil}
// Must not panic when pokeCh is nil
assert.NotPanics(t, func() {
s.Poke()
})
}
func TestPoke_Good_ChannelReceivesSignal(t *testing.T) {
s := &PrepSubsystem{}
s.pokeCh = make(chan struct{}, 1)
s.Poke()
assert.Len(t, s.pokeCh, 1, "poke should enqueue one signal")
}
func TestPoke_Good_NonBlockingWhenFull(t *testing.T) {
s := &PrepSubsystem{}
s.pokeCh = make(chan struct{}, 1)
// Pre-fill the channel
s.pokeCh <- struct{}{}
// Second poke must not block or panic
assert.NotPanics(t, func() {
s.Poke()
})
assert.Len(t, s.pokeCh, 1, "channel length should remain 1")
}
// --- StartRunner ---
func TestStartRunner_Good_CreatesPokeCh(t *testing.T) {
root := t.TempDir()
t.Setenv("CORE_WORKSPACE", root)
t.Setenv("CORE_AGENT_DISPATCH", "")
s := NewPrep()
assert.Nil(t, s.pokeCh)
s.StartRunner()
assert.NotNil(t, s.pokeCh, "StartRunner should initialise pokeCh")
}
func TestStartRunner_Good_FrozenByDefault(t *testing.T) {
root := t.TempDir()
t.Setenv("CORE_WORKSPACE", root)
t.Setenv("CORE_AGENT_DISPATCH", "")
s := NewPrep()
s.StartRunner()
assert.True(t, s.frozen, "queue should be frozen by default")
}
func TestStartRunner_Good_AutoStartEnvVar(t *testing.T) {
root := t.TempDir()
t.Setenv("CORE_WORKSPACE", root)
t.Setenv("CORE_AGENT_DISPATCH", "1")
s := NewPrep()
s.StartRunner()
assert.False(t, s.frozen, "CORE_AGENT_DISPATCH=1 should unfreeze the queue")
}
// --- DefaultBranch ---
func TestDefaultBranch_Good_DefaultsToMain(t *testing.T) {
// Non-git temp dir — git commands fail, fallback is "main"
dir := t.TempDir()
branch := DefaultBranch(dir)
assert.Equal(t, "main", branch)
}
func TestDefaultBranch_Good_RealGitRepo(t *testing.T) {
dir := t.TempDir()
// Init a real git repo with a main branch
require.NoError(t, runGitInit(dir))
branch := DefaultBranch(dir)
// Any valid branch name — just must not panic or be empty
assert.NotEmpty(t, branch)
}
// --- LocalFs ---
func TestLocalFs_Good_NonNil(t *testing.T) {
f := LocalFs()
assert.NotNil(t, f, "LocalFs should return a non-nil *core.Fs")
}
func TestLocalFs_Good_CanRead(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "hello.txt")
require.True(t, fs.Write(path, "hello").OK)
f := LocalFs()
r := f.Read(path)
assert.True(t, r.OK)
assert.Equal(t, "hello", r.Value.(string))
}
// --- helpers ---
// runGitInit initialises a bare git repo with one commit so branch detection works.
func runGitInit(dir string) error {
cmds := [][]string{
{"git", "init", "-b", "main"},
{"git", "config", "user.email", "test@test.com"},
{"git", "config", "user.name", "Test"},
{"git", "commit", "--allow-empty", "-m", "init"},
}
for _, args := range cmds {
cmd := exec.Command(args[0], args[1:]...)
cmd.Dir = dir
if err := cmd.Run(); err != nil {
return err
}
}
return nil
}

View file

@ -0,0 +1,131 @@
// SPDX-License-Identifier: EUPL-1.2
package agentic
import (
"context"
"testing"
core "dappco.re/go/core"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// --- Register ---
func TestRegister_Good_ServiceRegistered(t *testing.T) {
t.Setenv("CORE_WORKSPACE", t.TempDir())
t.Setenv("FORGE_TOKEN", "")
t.Setenv("FORGE_URL", "")
t.Setenv("CORE_BRAIN_KEY", "")
t.Setenv("CORE_BRAIN_URL", "")
c := core.New(core.WithService(Register))
require.NotNil(t, c)
// Service auto-registered under the last segment of the package path: "agentic"
prep, ok := core.ServiceFor[*PrepSubsystem](c, "agentic")
assert.True(t, ok, "PrepSubsystem must be registered as \"agentic\"")
assert.NotNil(t, prep)
}
func TestRegister_Good_CoreWired(t *testing.T) {
t.Setenv("CORE_WORKSPACE", t.TempDir())
t.Setenv("FORGE_TOKEN", "")
t.Setenv("FORGE_URL", "")
c := core.New(core.WithService(Register))
prep, ok := core.ServiceFor[*PrepSubsystem](c, "agentic")
require.True(t, ok)
// Register must wire s.core — service needs it for config access
assert.NotNil(t, prep.core, "Register must set prep.core")
assert.Equal(t, c, prep.core)
}
func TestRegister_Good_AgentsConfigLoaded(t *testing.T) {
t.Setenv("CORE_WORKSPACE", t.TempDir())
t.Setenv("FORGE_TOKEN", "")
t.Setenv("FORGE_URL", "")
c := core.New(core.WithService(Register))
// Register stores agents.concurrency into Core Config — verify it is present
concurrency := core.ConfigGet[map[string]ConcurrencyLimit](c.Config(), "agents.concurrency")
assert.NotNil(t, concurrency, "Register must store agents.concurrency in Core Config")
}
// --- OnStartup ---
func TestOnStartup_Good_CreatesPokeCh(t *testing.T) {
t.Setenv("CORE_WORKSPACE", t.TempDir())
t.Setenv("CORE_AGENT_DISPATCH", "")
c := core.New(core.WithOption("name", "test"))
s := NewPrep()
s.SetCore(c)
assert.Nil(t, s.pokeCh, "pokeCh should be nil before OnStartup")
err := s.OnStartup(context.Background())
require.NoError(t, err)
assert.NotNil(t, s.pokeCh, "OnStartup must initialise pokeCh via StartRunner")
}
func TestOnStartup_Good_FrozenByDefault(t *testing.T) {
t.Setenv("CORE_WORKSPACE", t.TempDir())
t.Setenv("CORE_AGENT_DISPATCH", "")
c := core.New(core.WithOption("name", "test"))
s := NewPrep()
s.SetCore(c)
require.NoError(t, s.OnStartup(context.Background()))
assert.True(t, s.frozen, "queue must be frozen after OnStartup without CORE_AGENT_DISPATCH=1")
}
func TestOnStartup_Good_NoError(t *testing.T) {
t.Setenv("CORE_WORKSPACE", t.TempDir())
t.Setenv("CORE_AGENT_DISPATCH", "")
c := core.New(core.WithOption("name", "test"))
s := NewPrep()
s.SetCore(c)
err := s.OnStartup(context.Background())
assert.NoError(t, err)
}
// --- OnShutdown ---
func TestOnShutdown_Good_FreezesQueue(t *testing.T) {
t.Setenv("CORE_WORKSPACE", t.TempDir())
s := &PrepSubsystem{frozen: false}
err := s.OnShutdown(context.Background())
require.NoError(t, err)
assert.True(t, s.frozen, "OnShutdown must set frozen=true")
}
func TestOnShutdown_Good_AlreadyFrozen(t *testing.T) {
// Calling OnShutdown twice must be idempotent
s := &PrepSubsystem{frozen: true}
err := s.OnShutdown(context.Background())
require.NoError(t, err)
assert.True(t, s.frozen)
}
func TestOnShutdown_Good_NoError(t *testing.T) {
s := &PrepSubsystem{}
assert.NoError(t, s.OnShutdown(context.Background()))
}
func TestOnShutdown_Ugly_NilCore(t *testing.T) {
// OnShutdown must not panic even if s.core is nil
s := &PrepSubsystem{core: nil, frozen: false}
assert.NotPanics(t, func() {
_ = s.OnShutdown(context.Background())
})
assert.True(t, s.frozen)
}

View file

@ -0,0 +1,176 @@
// SPDX-License-Identifier: EUPL-1.2
package agentic
import (
"encoding/json"
"path/filepath"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// --- ReadStatus ---
func TestReadStatus_Good_AllFields(t *testing.T) {
dir := t.TempDir()
now := time.Now().Truncate(time.Second)
original := WorkspaceStatus{
Status: "running",
Agent: "claude:opus",
Repo: "go-io",
Org: "core",
Task: "add observability",
Branch: "agent/add-observability",
Issue: 7,
PID: 42100,
StartedAt: now,
UpdatedAt: now,
Question: "",
Runs: 2,
PRURL: "",
}
data, err := json.MarshalIndent(original, "", " ")
require.NoError(t, err)
require.True(t, fs.Write(filepath.Join(dir, "status.json"), string(data)).OK)
st, err := ReadStatus(dir)
require.NoError(t, err)
assert.Equal(t, original.Status, st.Status)
assert.Equal(t, original.Agent, st.Agent)
assert.Equal(t, original.Repo, st.Repo)
assert.Equal(t, original.Org, st.Org)
assert.Equal(t, original.Task, st.Task)
assert.Equal(t, original.Branch, st.Branch)
assert.Equal(t, original.Issue, st.Issue)
assert.Equal(t, original.PID, st.PID)
assert.Equal(t, original.Runs, st.Runs)
}
func TestReadStatus_Bad_MissingFile(t *testing.T) {
dir := t.TempDir()
_, err := ReadStatus(dir)
assert.Error(t, err, "missing status.json must return an error")
}
func TestReadStatus_Bad_CorruptJSON(t *testing.T) {
dir := t.TempDir()
require.True(t, fs.Write(filepath.Join(dir, "status.json"), `{"status": "running", broken`).OK)
_, err := ReadStatus(dir)
assert.Error(t, err, "corrupt JSON must return an error")
}
func TestReadStatus_Bad_NullJSON(t *testing.T) {
dir := t.TempDir()
require.True(t, fs.Write(filepath.Join(dir, "status.json"), "null").OK)
// null is valid JSON — ReadStatus returns a zero-value struct, not an error
st, err := ReadStatus(dir)
require.NoError(t, err)
assert.Equal(t, "", st.Status)
}
// --- writeStatus ---
func TestWriteStatus_Good_WritesAndReadsBack(t *testing.T) {
dir := t.TempDir()
st := &WorkspaceStatus{
Status: "queued",
Agent: "gemini:pro",
Repo: "go-log",
Task: "improve logging",
Runs: 0,
}
err := writeStatus(dir, st)
require.NoError(t, err)
read, err := ReadStatus(dir)
require.NoError(t, err)
assert.Equal(t, "queued", read.Status)
assert.Equal(t, "gemini:pro", read.Agent)
assert.Equal(t, "go-log", read.Repo)
assert.Equal(t, "improve logging", read.Task)
}
func TestWriteStatus_Good_SetsUpdatedAt(t *testing.T) {
dir := t.TempDir()
before := time.Now().Add(-time.Millisecond)
st := &WorkspaceStatus{Status: "failed", Agent: "codex"}
err := writeStatus(dir, st)
require.NoError(t, err)
assert.True(t, st.UpdatedAt.After(before), "writeStatus must set UpdatedAt to a recent time")
}
func TestWriteStatus_Good_Overwrites(t *testing.T) {
dir := t.TempDir()
require.NoError(t, writeStatus(dir, &WorkspaceStatus{Status: "running", Agent: "gemini"}))
require.NoError(t, writeStatus(dir, &WorkspaceStatus{Status: "completed", Agent: "gemini"}))
st, err := ReadStatus(dir)
require.NoError(t, err)
assert.Equal(t, "completed", st.Status)
}
// --- WorkspaceStatus JSON round-trip ---
func TestWorkspaceStatus_Good_JSONRoundTrip(t *testing.T) {
now := time.Now().Truncate(time.Second)
original := WorkspaceStatus{
Status: "blocked",
Agent: "codex:gpt-5.4",
Repo: "agent",
Org: "core",
Task: "write more tests",
Branch: "agent/write-more-tests",
Issue: 15,
PID: 99001,
StartedAt: now,
UpdatedAt: now,
Question: "Which pattern should I use?",
Runs: 3,
PRURL: "https://forge.lthn.ai/core/agent/pulls/10",
}
data, err := json.Marshal(original)
require.NoError(t, err)
var decoded WorkspaceStatus
require.NoError(t, json.Unmarshal(data, &decoded))
assert.Equal(t, original.Status, decoded.Status)
assert.Equal(t, original.Agent, decoded.Agent)
assert.Equal(t, original.Repo, decoded.Repo)
assert.Equal(t, original.Org, decoded.Org)
assert.Equal(t, original.Task, decoded.Task)
assert.Equal(t, original.Branch, decoded.Branch)
assert.Equal(t, original.Issue, decoded.Issue)
assert.Equal(t, original.PID, decoded.PID)
assert.Equal(t, original.Question, decoded.Question)
assert.Equal(t, original.Runs, decoded.Runs)
assert.Equal(t, original.PRURL, decoded.PRURL)
}
func TestWorkspaceStatus_Good_OmitemptyFields(t *testing.T) {
st := WorkspaceStatus{Status: "queued", Agent: "claude"}
data, err := json.Marshal(st)
require.NoError(t, err)
// Optional fields with omitempty must be absent when zero
jsonStr := string(data)
assert.NotContains(t, jsonStr, `"org"`)
assert.NotContains(t, jsonStr, `"branch"`)
assert.NotContains(t, jsonStr, `"question"`)
assert.NotContains(t, jsonStr, `"pr_url"`)
assert.NotContains(t, jsonStr, `"pid"`)
assert.NotContains(t, jsonStr, `"issue"`)
}