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:
parent
36c29cd0af
commit
42e558ed38
4 changed files with 702 additions and 0 deletions
175
pkg/agentic/plan_logic_test.go
Normal file
175
pkg/agentic/plan_logic_test.go
Normal 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)
|
||||
}
|
||||
220
pkg/agentic/queue_logic_test.go
Normal file
220
pkg/agentic/queue_logic_test.go
Normal 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
|
||||
}
|
||||
131
pkg/agentic/register_test.go
Normal file
131
pkg/agentic/register_test.go
Normal 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)
|
||||
}
|
||||
176
pkg/agentic/status_logic_test.go
Normal file
176
pkg/agentic/status_logic_test.go
Normal 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"`)
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue