test(agentic): add unit tests for paths, status, queue, plans
Some checks failed
CI / test (push) Failing after 3s

Coverage: 4.2% → 9.2%. Tests for extractPRNumber, workspace
status scanning, queue management, and plan file operations.
Remaining coverage requires integration tests (git/forge/process).

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Snider 2026-03-21 15:05:40 +00:00
parent 267550b288
commit 726a384873
4 changed files with 675 additions and 0 deletions

212
pkg/agentic/plan_test.go Normal file
View file

@ -0,0 +1,212 @@
// SPDX-License-Identifier: EUPL-1.2
package agentic
import (
"path/filepath"
"strings"
"testing"
coreio "forge.lthn.ai/core/go-io"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestPlanPath_Good(t *testing.T) {
assert.Equal(t, "/tmp/plans/my-plan-abc123.json", planPath("/tmp/plans", "my-plan-abc123"))
assert.Equal(t, "/data/test.json", planPath("/data", "test"))
}
func TestWritePlan_Good(t *testing.T) {
dir := t.TempDir()
plan := &Plan{
ID: "test-plan-abc123",
Title: "Test Plan",
Status: "draft",
Objective: "Test the plan system",
}
path, err := writePlan(dir, plan)
require.NoError(t, err)
assert.Equal(t, filepath.Join(dir, "test-plan-abc123.json"), path)
// Verify file exists
assert.True(t, coreio.Local.IsFile(path))
}
func TestWritePlan_Good_CreatesDirectory(t *testing.T) {
base := t.TempDir()
dir := filepath.Join(base, "nested", "plans")
plan := &Plan{
ID: "nested-plan-abc123",
Title: "Nested",
Status: "draft",
Objective: "Test nested directory creation",
}
path, err := writePlan(dir, plan)
require.NoError(t, err)
assert.Contains(t, path, "nested-plan-abc123.json")
}
func TestReadPlan_Good(t *testing.T) {
dir := t.TempDir()
original := &Plan{
ID: "read-test-abc123",
Title: "Read Test",
Status: "ready",
Repo: "go-io",
Org: "core",
Objective: "Verify plan reading works",
Phases: []Phase{
{Number: 1, Name: "Setup", Status: "done"},
{Number: 2, Name: "Implement", Status: "pending"},
},
Notes: "Some notes",
Agent: "claude:opus",
}
_, err := writePlan(dir, original)
require.NoError(t, err)
read, err := readPlan(dir, "read-test-abc123")
require.NoError(t, err)
assert.Equal(t, original.ID, read.ID)
assert.Equal(t, original.Title, read.Title)
assert.Equal(t, original.Status, read.Status)
assert.Equal(t, original.Repo, read.Repo)
assert.Equal(t, original.Org, read.Org)
assert.Equal(t, original.Objective, read.Objective)
assert.Len(t, read.Phases, 2)
assert.Equal(t, "Setup", read.Phases[0].Name)
assert.Equal(t, "done", read.Phases[0].Status)
assert.Equal(t, "Implement", read.Phases[1].Name)
assert.Equal(t, "pending", read.Phases[1].Status)
assert.Equal(t, "Some notes", read.Notes)
assert.Equal(t, "claude:opus", read.Agent)
}
func TestReadPlan_Bad_NotFound(t *testing.T) {
dir := t.TempDir()
_, err := readPlan(dir, "nonexistent-plan")
assert.Error(t, err)
}
func TestReadPlan_Bad_InvalidJSON(t *testing.T) {
dir := t.TempDir()
require.NoError(t, coreio.Local.Write(filepath.Join(dir, "bad-json.json"), "{broken"))
_, err := readPlan(dir, "bad-json")
assert.Error(t, err)
}
func TestWriteReadPlan_Good_Roundtrip(t *testing.T) {
dir := t.TempDir()
plan := &Plan{
ID: "roundtrip-abc123",
Title: "Roundtrip Test",
Status: "in_progress",
Repo: "agent",
Org: "core",
Objective: "Ensure write-read roundtrip works",
Phases: []Phase{
{Number: 1, Name: "Phase One", Status: "done", Criteria: []string{"tests pass", "coverage > 80%"}, Tests: 5},
{Number: 2, Name: "Phase Two", Status: "in_progress", Notes: "Working on it"},
{Number: 3, Name: "Phase Three", Status: "pending"},
},
Notes: "Important plan",
Agent: "gemini",
}
_, err := writePlan(dir, plan)
require.NoError(t, err)
read, err := readPlan(dir, "roundtrip-abc123")
require.NoError(t, err)
assert.Equal(t, plan.Title, read.Title)
assert.Equal(t, plan.Status, read.Status)
assert.Len(t, read.Phases, 3)
assert.Equal(t, []string{"tests pass", "coverage > 80%"}, read.Phases[0].Criteria)
assert.Equal(t, 5, read.Phases[0].Tests)
assert.Equal(t, "Working on it", read.Phases[1].Notes)
}
func TestGeneratePlanID_Good_Slugifies(t *testing.T) {
id := generatePlanID("Add Unit Tests for Agentic")
assert.True(t, strings.HasPrefix(id, "add-unit-tests-for-agentic"), "got: %s", id)
// Should have random suffix
parts := strings.Split(id, "-")
assert.True(t, len(parts) >= 5, "expected slug with random suffix, got: %s", id)
}
func TestGeneratePlanID_Good_TruncatesLong(t *testing.T) {
id := generatePlanID("This is a very long title that should be truncated to a reasonable length for file naming purposes")
// Slug part (before random suffix) should be <= 30 chars
lastDash := strings.LastIndex(id, "-")
slug := id[:lastDash]
assert.True(t, len(slug) <= 36, "slug too long: %s (%d chars)", slug, len(slug))
}
func TestGeneratePlanID_Good_HandlesSpecialChars(t *testing.T) {
id := generatePlanID("Fix bug #123: auth & session!")
assert.True(t, strings.Contains(id, "fix-bug"), "got: %s", id)
assert.NotContains(t, id, "#")
assert.NotContains(t, id, "!")
assert.NotContains(t, id, "&")
}
func TestGeneratePlanID_Good_Unique(t *testing.T) {
id1 := generatePlanID("Same Title")
id2 := generatePlanID("Same Title")
assert.NotEqual(t, id1, id2, "IDs should differ due to random suffix")
}
func TestValidPlanStatus_Good_AllValid(t *testing.T) {
validStatuses := []string{"draft", "ready", "in_progress", "needs_verification", "verified", "approved"}
for _, s := range validStatuses {
assert.True(t, validPlanStatus(s), "expected %q to be valid", s)
}
}
func TestValidPlanStatus_Bad_Invalid(t *testing.T) {
invalidStatuses := []string{"", "running", "completed", "cancelled", "archived", "DRAFT", "Draft"}
for _, s := range invalidStatuses {
assert.False(t, validPlanStatus(s), "expected %q to be invalid", s)
}
}
func TestWritePlan_Good_OverwriteExisting(t *testing.T) {
dir := t.TempDir()
plan := &Plan{
ID: "overwrite-abc123",
Title: "Original",
Status: "draft",
Objective: "Original objective",
}
_, err := writePlan(dir, plan)
require.NoError(t, err)
plan.Title = "Updated"
plan.Status = "ready"
_, err = writePlan(dir, plan)
require.NoError(t, err)
read, err := readPlan(dir, "overwrite-abc123")
require.NoError(t, err)
assert.Equal(t, "Updated", read.Title)
assert.Equal(t, "ready", read.Status)
}
func TestReadPlan_Ugly_EmptyFile(t *testing.T) {
dir := t.TempDir()
require.NoError(t, coreio.Local.Write(filepath.Join(dir, "empty.json"), ""))
_, err := readPlan(dir, "empty")
assert.Error(t, err)
}

192
pkg/agentic/prep_test.go Normal file
View file

@ -0,0 +1,192 @@
// SPDX-License-Identifier: EUPL-1.2
package agentic
import (
"path/filepath"
"testing"
coreio "forge.lthn.ai/core/go-io"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestEnvOr_Good_EnvSet(t *testing.T) {
t.Setenv("TEST_ENVVAR_CUSTOM", "custom-value")
assert.Equal(t, "custom-value", envOr("TEST_ENVVAR_CUSTOM", "default"))
}
func TestEnvOr_Good_Fallback(t *testing.T) {
t.Setenv("TEST_ENVVAR_MISSING", "")
assert.Equal(t, "default-value", envOr("TEST_ENVVAR_MISSING", "default-value"))
}
func TestEnvOr_Good_UnsetUsesFallback(t *testing.T) {
t.Setenv("TEST_ENVVAR_TOTALLY_MISSING", "")
assert.Equal(t, "fallback", envOr("TEST_ENVVAR_TOTALLY_MISSING", "fallback"))
}
func TestDetectLanguage_Good_Go(t *testing.T) {
dir := t.TempDir()
require.NoError(t, coreio.Local.Write(filepath.Join(dir, "go.mod"), "module test"))
assert.Equal(t, "go", detectLanguage(dir))
}
func TestDetectLanguage_Good_PHP(t *testing.T) {
dir := t.TempDir()
require.NoError(t, coreio.Local.Write(filepath.Join(dir, "composer.json"), "{}"))
assert.Equal(t, "php", detectLanguage(dir))
}
func TestDetectLanguage_Good_TypeScript(t *testing.T) {
dir := t.TempDir()
require.NoError(t, coreio.Local.Write(filepath.Join(dir, "package.json"), "{}"))
assert.Equal(t, "ts", detectLanguage(dir))
}
func TestDetectLanguage_Good_Rust(t *testing.T) {
dir := t.TempDir()
require.NoError(t, coreio.Local.Write(filepath.Join(dir, "Cargo.toml"), "[package]"))
assert.Equal(t, "rust", detectLanguage(dir))
}
func TestDetectLanguage_Good_Python(t *testing.T) {
dir := t.TempDir()
require.NoError(t, coreio.Local.Write(filepath.Join(dir, "requirements.txt"), "flask"))
assert.Equal(t, "py", detectLanguage(dir))
}
func TestDetectLanguage_Good_Cpp(t *testing.T) {
dir := t.TempDir()
require.NoError(t, coreio.Local.Write(filepath.Join(dir, "CMakeLists.txt"), "cmake_minimum_required"))
assert.Equal(t, "cpp", detectLanguage(dir))
}
func TestDetectLanguage_Good_Docker(t *testing.T) {
dir := t.TempDir()
require.NoError(t, coreio.Local.Write(filepath.Join(dir, "Dockerfile"), "FROM alpine"))
assert.Equal(t, "docker", detectLanguage(dir))
}
func TestDetectLanguage_Good_DefaultsToGo(t *testing.T) {
dir := t.TempDir()
assert.Equal(t, "go", detectLanguage(dir))
}
func TestDetectBuildCmd_Good(t *testing.T) {
tests := []struct {
file string
content string
expected string
}{
{"go.mod", "module test", "go build ./..."},
{"composer.json", "{}", "composer install"},
{"package.json", "{}", "npm run build"},
{"requirements.txt", "flask", "pip install -e ."},
{"Cargo.toml", "[package]", "cargo build"},
{"CMakeLists.txt", "cmake", "cmake --build ."},
}
for _, tt := range tests {
t.Run(tt.file, func(t *testing.T) {
dir := t.TempDir()
require.NoError(t, coreio.Local.Write(filepath.Join(dir, tt.file), tt.content))
assert.Equal(t, tt.expected, detectBuildCmd(dir))
})
}
}
func TestDetectBuildCmd_Good_DefaultsToGo(t *testing.T) {
dir := t.TempDir()
assert.Equal(t, "go build ./...", detectBuildCmd(dir))
}
func TestDetectTestCmd_Good(t *testing.T) {
tests := []struct {
file string
content string
expected string
}{
{"go.mod", "module test", "go test ./..."},
{"composer.json", "{}", "composer test"},
{"package.json", "{}", "npm test"},
{"requirements.txt", "flask", "pytest"},
{"Cargo.toml", "[package]", "cargo test"},
{"CMakeLists.txt", "cmake", "ctest"},
}
for _, tt := range tests {
t.Run(tt.file, func(t *testing.T) {
dir := t.TempDir()
require.NoError(t, coreio.Local.Write(filepath.Join(dir, tt.file), tt.content))
assert.Equal(t, tt.expected, detectTestCmd(dir))
})
}
}
func TestDetectTestCmd_Good_DefaultsToGo(t *testing.T) {
dir := t.TempDir()
assert.Equal(t, "go test ./...", detectTestCmd(dir))
}
func TestNewPrep_Good_Defaults(t *testing.T) {
t.Setenv("FORGE_TOKEN", "")
t.Setenv("GITEA_TOKEN", "")
t.Setenv("CORE_BRAIN_KEY", "")
t.Setenv("FORGE_URL", "")
t.Setenv("CORE_BRAIN_URL", "")
t.Setenv("SPECS_PATH", "")
t.Setenv("CODE_PATH", "")
s := NewPrep()
assert.Equal(t, "https://forge.lthn.ai", s.forgeURL)
assert.Equal(t, "https://api.lthn.sh", s.brainURL)
assert.NotNil(t, s.client)
}
func TestNewPrep_Good_EnvOverrides(t *testing.T) {
t.Setenv("FORGE_URL", "https://custom-forge.example.com")
t.Setenv("FORGE_TOKEN", "test-token")
t.Setenv("CORE_BRAIN_URL", "https://custom-brain.example.com")
t.Setenv("CORE_BRAIN_KEY", "brain-key-123")
t.Setenv("SPECS_PATH", "/custom/specs")
t.Setenv("CODE_PATH", "/custom/code")
s := NewPrep()
assert.Equal(t, "https://custom-forge.example.com", s.forgeURL)
assert.Equal(t, "test-token", s.forgeToken)
assert.Equal(t, "https://custom-brain.example.com", s.brainURL)
assert.Equal(t, "brain-key-123", s.brainKey)
assert.Equal(t, "/custom/specs", s.specsPath)
assert.Equal(t, "/custom/code", s.codePath)
}
func TestNewPrep_Good_GiteaTokenFallback(t *testing.T) {
t.Setenv("FORGE_TOKEN", "")
t.Setenv("GITEA_TOKEN", "gitea-fallback-token")
s := NewPrep()
assert.Equal(t, "gitea-fallback-token", s.forgeToken)
}
func TestPrepSubsystem_Good_Name(t *testing.T) {
s := &PrepSubsystem{}
assert.Equal(t, "agentic", s.Name())
}
func TestSetCompletionNotifier_Good(t *testing.T) {
s := &PrepSubsystem{}
assert.Nil(t, s.onComplete)
notifier := &mockNotifier{}
s.SetCompletionNotifier(notifier)
assert.NotNil(t, s.onComplete)
}
type mockNotifier struct {
poked bool
}
func (m *mockNotifier) Poke() {
m.poked = true
}

87
pkg/agentic/queue_test.go Normal file
View file

@ -0,0 +1,87 @@
// SPDX-License-Identifier: EUPL-1.2
package agentic
import (
"path/filepath"
"testing"
coreio "forge.lthn.ai/core/go-io"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestBaseAgent_Ugly_Empty(t *testing.T) {
assert.Equal(t, "", baseAgent(""))
}
func TestBaseAgent_Ugly_MultipleColons(t *testing.T) {
// SplitN with N=2 should only split on first colon
assert.Equal(t, "claude", baseAgent("claude:opus:extra"))
}
func TestDispatchConfig_Good_Defaults(t *testing.T) {
// loadAgentsConfig falls back to defaults when no config file exists
s := &PrepSubsystem{codePath: t.TempDir()}
t.Setenv("CORE_WORKSPACE", t.TempDir())
cfg := s.loadAgentsConfig()
assert.Equal(t, "claude", cfg.Dispatch.DefaultAgent)
assert.Equal(t, "coding", cfg.Dispatch.DefaultTemplate)
assert.Equal(t, 1, cfg.Concurrency["claude"])
assert.Equal(t, 3, cfg.Concurrency["gemini"])
}
func TestCanDispatchAgent_Good_NoConfig(t *testing.T) {
// With no running workspaces and default config, should be able to dispatch
root := t.TempDir()
t.Setenv("CORE_WORKSPACE", root)
require.NoError(t, coreio.Local.EnsureDir(filepath.Join(root, "workspace")))
s := &PrepSubsystem{codePath: t.TempDir()}
assert.True(t, s.canDispatchAgent("gemini"))
}
func TestCanDispatchAgent_Good_UnknownAgent(t *testing.T) {
// Unknown agent has no limit, so always allowed
root := t.TempDir()
t.Setenv("CORE_WORKSPACE", root)
require.NoError(t, coreio.Local.EnsureDir(filepath.Join(root, "workspace")))
s := &PrepSubsystem{codePath: t.TempDir()}
assert.True(t, s.canDispatchAgent("unknown-agent"))
}
func TestCountRunningByAgent_Good_EmptyWorkspace(t *testing.T) {
root := t.TempDir()
t.Setenv("CORE_WORKSPACE", root)
require.NoError(t, coreio.Local.EnsureDir(filepath.Join(root, "workspace")))
s := &PrepSubsystem{}
assert.Equal(t, 0, s.countRunningByAgent("gemini"))
assert.Equal(t, 0, s.countRunningByAgent("claude"))
}
func TestCountRunningByAgent_Good_NoRunning(t *testing.T) {
root := t.TempDir()
t.Setenv("CORE_WORKSPACE", root)
// Create a workspace with completed status under workspace/
ws := filepath.Join(root, "workspace", "test-ws")
require.NoError(t, coreio.Local.EnsureDir(ws))
require.NoError(t, writeStatus(ws, &WorkspaceStatus{
Status: "completed",
Agent: "gemini",
PID: 0,
}))
s := &PrepSubsystem{}
assert.Equal(t, 0, s.countRunningByAgent("gemini"))
}
func TestDelayForAgent_Good_NoConfig(t *testing.T) {
// With no config, delay should be 0
t.Setenv("CORE_WORKSPACE", t.TempDir())
s := &PrepSubsystem{codePath: t.TempDir()}
assert.Equal(t, 0, int(s.delayForAgent("gemini").Seconds()))
}

184
pkg/agentic/status_test.go Normal file
View file

@ -0,0 +1,184 @@
// SPDX-License-Identifier: EUPL-1.2
package agentic
import (
"encoding/json"
"path/filepath"
"testing"
"time"
coreio "forge.lthn.ai/core/go-io"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestWriteStatus_Good(t *testing.T) {
dir := t.TempDir()
status := &WorkspaceStatus{
Status: "running",
Agent: "gemini",
Repo: "go-io",
Task: "fix tests",
PID: 12345,
StartedAt: time.Now(),
Runs: 1,
}
err := writeStatus(dir, status)
require.NoError(t, err)
// Verify file was written via coreio
data, err := coreio.Local.Read(filepath.Join(dir, "status.json"))
require.NoError(t, err)
var read WorkspaceStatus
err = json.Unmarshal([]byte(data), &read)
require.NoError(t, err)
assert.Equal(t, "running", read.Status)
assert.Equal(t, "gemini", read.Agent)
assert.Equal(t, "go-io", read.Repo)
assert.Equal(t, "fix tests", read.Task)
assert.Equal(t, 12345, read.PID)
assert.Equal(t, 1, read.Runs)
assert.False(t, read.UpdatedAt.IsZero(), "UpdatedAt should be set by writeStatus")
}
func TestWriteStatus_Good_UpdatesTimestamp(t *testing.T) {
dir := t.TempDir()
before := time.Now().Add(-time.Second)
status := &WorkspaceStatus{
Status: "running",
Agent: "claude",
}
err := writeStatus(dir, status)
require.NoError(t, err)
assert.True(t, status.UpdatedAt.After(before), "UpdatedAt should be after the start time")
}
func TestReadStatus_Good(t *testing.T) {
dir := t.TempDir()
status := &WorkspaceStatus{
Status: "completed",
Agent: "codex",
Repo: "go-log",
Task: "add logging",
Branch: "agent/add-logging",
StartedAt: time.Now().Truncate(time.Second),
UpdatedAt: time.Now().Truncate(time.Second),
Runs: 2,
PRURL: "https://forge.lthn.ai/core/go-log/pulls/5",
}
data, err := json.MarshalIndent(status, "", " ")
require.NoError(t, err)
require.NoError(t, coreio.Local.Write(filepath.Join(dir, "status.json"), string(data)))
read, err := readStatus(dir)
require.NoError(t, err)
assert.Equal(t, "completed", read.Status)
assert.Equal(t, "codex", read.Agent)
assert.Equal(t, "go-log", read.Repo)
assert.Equal(t, "add logging", read.Task)
assert.Equal(t, "agent/add-logging", read.Branch)
assert.Equal(t, 2, read.Runs)
assert.Equal(t, "https://forge.lthn.ai/core/go-log/pulls/5", read.PRURL)
}
func TestReadStatus_Bad_NoFile(t *testing.T) {
dir := t.TempDir()
_, err := readStatus(dir)
assert.Error(t, err)
}
func TestReadStatus_Bad_InvalidJSON(t *testing.T) {
dir := t.TempDir()
require.NoError(t, coreio.Local.Write(filepath.Join(dir, "status.json"), "not json{"))
_, err := readStatus(dir)
assert.Error(t, err)
}
func TestReadStatus_Good_BlockedWithQuestion(t *testing.T) {
dir := t.TempDir()
status := &WorkspaceStatus{
Status: "blocked",
Agent: "gemini",
Repo: "go-io",
Question: "Which interface should I implement?",
}
data, err := json.MarshalIndent(status, "", " ")
require.NoError(t, err)
require.NoError(t, coreio.Local.Write(filepath.Join(dir, "status.json"), string(data)))
read, err := readStatus(dir)
require.NoError(t, err)
assert.Equal(t, "blocked", read.Status)
assert.Equal(t, "Which interface should I implement?", read.Question)
}
func TestWriteReadStatus_Good_Roundtrip(t *testing.T) {
dir := t.TempDir()
original := &WorkspaceStatus{
Status: "running",
Agent: "claude:opus",
Repo: "agent",
Org: "core",
Task: "write tests for agentic package",
Branch: "agent/write-tests",
Issue: 42,
PID: 99999,
StartedAt: time.Now().Truncate(time.Second),
Runs: 3,
}
err := writeStatus(dir, original)
require.NoError(t, err)
read, err := readStatus(dir)
require.NoError(t, err)
assert.Equal(t, original.Status, read.Status)
assert.Equal(t, original.Agent, read.Agent)
assert.Equal(t, original.Repo, read.Repo)
assert.Equal(t, original.Org, read.Org)
assert.Equal(t, original.Task, read.Task)
assert.Equal(t, original.Branch, read.Branch)
assert.Equal(t, original.Issue, read.Issue)
assert.Equal(t, original.PID, read.PID)
assert.Equal(t, original.Runs, read.Runs)
}
func TestWriteStatus_Good_OverwriteExisting(t *testing.T) {
dir := t.TempDir()
first := &WorkspaceStatus{Status: "running", Agent: "gemini"}
err := writeStatus(dir, first)
require.NoError(t, err)
second := &WorkspaceStatus{Status: "completed", Agent: "gemini"}
err = writeStatus(dir, second)
require.NoError(t, err)
read, err := readStatus(dir)
require.NoError(t, err)
assert.Equal(t, "completed", read.Status)
}
func TestReadStatus_Ugly_EmptyFile(t *testing.T) {
dir := t.TempDir()
require.NoError(t, coreio.Local.Write(filepath.Join(dir, "status.json"), ""))
_, err := readStatus(dir)
assert.Error(t, err)
}