// SPDX-License-Identifier: EUPL-1.2 package agentic import ( "testing" "time" core "dappco.re/go/core" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // --- planPath --- func TestPlan_PlanPath_Good_BasicFormat(t *testing.T) { result := planPath("/tmp/plans", "my-plan-abc123") assert.Equal(t, "/tmp/plans/my-plan-abc123.json", result) } func TestPlan_PlanPath_Good_NestedIDStripped(t *testing.T) { // SanitisePath strips directory components — prevents path traversal result := planPath("/plans", "../../../etc/passwd") assert.Equal(t, "/plans/passwd.json", result) } func TestPlan_PlanPath_Good_SimpleID(t *testing.T) { assert.Equal(t, "/data/test.json", planPath("/data", "test")) } func TestPlan_PlanPath_Good_SlugWithDashes(t *testing.T) { assert.Equal(t, "/root/migrate-core-abc123.json", planPath("/root", "migrate-core-abc123")) } func TestPlan_PlanPath_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 TestPlan_PlanPath_Bad_DoubleDotID(t *testing.T) { result := planPath("/plans", "..") assert.Equal(t, "/plans/invalid.json", result) } func TestPlan_PlanPath_Bad_EmptyID(t *testing.T) { result := planPath("/plans", "") assert.Equal(t, "/plans/invalid.json", result) } // --- readPlan / writePlan --- func TestPlan_ReadWrite_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, core.JoinPath(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 TestPlan_ReadWrite_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 TestPlan_ReadPlan_Bad_MissingFile(t *testing.T) { dir := t.TempDir() _, err := readPlan(dir, "nonexistent-plan") assert.Error(t, err) } func TestPlan_ReadPlan_Bad_CorruptJSON(t *testing.T) { dir := t.TempDir() require.True(t, fs.Write(core.JoinPath(dir, "bad.json"), `{broken`).OK) _, err := readPlan(dir, "bad") assert.Error(t, err) } func TestPlan_WritePlan_Good_CreatesNestedDir(t *testing.T) { base := t.TempDir() nested := core.JoinPath(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, core.JoinPath(nested, "deep-plan-xyz.json"), path) assert.True(t, fs.IsFile(path)) } func TestPlan_WritePlan_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 TestPlan_ReadPlan_Ugly_EmptyFileLogic(t *testing.T) { dir := t.TempDir() require.True(t, fs.Write(core.JoinPath(dir, "empty.json"), "").OK) _, err := readPlan(dir, "empty") assert.Error(t, err) } func TestPlan_PhaseValue_Good_CompletionCriteriaAlias(t *testing.T) { phase, ok := phaseValue(map[string]any{ "name": "Setup", "completion_criteria": []any{"repo cloned", "dependencies installed"}, }) require.True(t, ok) assert.Equal(t, []string{"repo cloned", "dependencies installed"}, phase.Criteria) assert.Equal(t, []string{"repo cloned", "dependencies installed"}, phase.CompletionCriteria) normalised := normalisePhase(phase, 1) assert.Equal(t, []string{"repo cloned", "dependencies installed"}, normalised.Criteria) assert.Equal(t, []string{"repo cloned", "dependencies installed"}, normalised.CompletionCriteria) tasks := phaseTaskList(normalised) require.Len(t, tasks, 2) assert.Equal(t, "repo cloned", tasks[0].Title) assert.Equal(t, "dependencies installed", tasks[1].Title) }