test: batch 1 — add 80 Bad/Ugly tests for paths, plan, status, shutdown, forge cmds
Fill missing Good/Bad/Ugly categories for: - paths.go: LocalFs, WorkspaceRoot, CoreRoot, PlansRoot, AgentName, GitHubOrg, parseInt, DefaultBranch - plan.go: planCreate/Read/Update/Delete/List Ugly, planPath Ugly, validPlanStatus Ugly - status.go: writeStatus Bad, status Good/Bad - shutdown.go: dispatchStart/shutdownGraceful Bad/Ugly, shutdownNow Ugly - commands_forge.go: all 9 cmd* functions Ugly (with httptest mocks) - sanitise.go: Bad/Ugly for all 5 functions - prep.go: various lifecycle Bad/Ugly Gap: 260 → 208 missing categories Tests: 566 → 646 (+80) Coverage: 74.4% Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
97d06c1e90
commit
a5afad870c
6 changed files with 749 additions and 2 deletions
|
|
@ -3,6 +3,8 @@
|
|||
package agentic
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
|
|
@ -58,3 +60,136 @@ func TestCommandsForge_FmtIndex_Good(t *testing.T) {
|
|||
assert.Equal(t, "0", fmtIndex(0))
|
||||
assert.Equal(t, "999999", fmtIndex(999999))
|
||||
}
|
||||
|
||||
// --- parseForgeArgs Ugly ---
|
||||
|
||||
func TestCommandsForge_ParseForgeArgs_Ugly_OrgSetButNoRepo(t *testing.T) {
|
||||
opts := core.NewOptions(
|
||||
core.Option{Key: "org", Value: "custom-org"},
|
||||
)
|
||||
org, repo, num := parseForgeArgs(opts)
|
||||
assert.Equal(t, "custom-org", org)
|
||||
assert.Empty(t, repo, "repo should be empty when only org is set")
|
||||
assert.Equal(t, int64(0), num)
|
||||
}
|
||||
|
||||
func TestCommandsForge_ParseForgeArgs_Ugly_NegativeNumber(t *testing.T) {
|
||||
opts := core.NewOptions(
|
||||
core.Option{Key: "_arg", Value: "go-io"},
|
||||
core.Option{Key: "number", Value: "-5"},
|
||||
)
|
||||
_, _, num := parseForgeArgs(opts)
|
||||
assert.Equal(t, int64(-5), num, "negative numbers parse but are semantically invalid")
|
||||
}
|
||||
|
||||
// --- fmtIndex Bad/Ugly ---
|
||||
|
||||
func TestCommandsForge_FmtIndex_Bad_Negative(t *testing.T) {
|
||||
result := fmtIndex(-1)
|
||||
assert.Equal(t, "-1", result, "negative should format as negative string")
|
||||
}
|
||||
|
||||
func TestCommandsForge_FmtIndex_Ugly_VeryLarge(t *testing.T) {
|
||||
result := fmtIndex(9999999999)
|
||||
assert.Equal(t, "9999999999", result)
|
||||
}
|
||||
|
||||
func TestCommandsForge_FmtIndex_Ugly_MaxInt64(t *testing.T) {
|
||||
result := fmtIndex(9223372036854775807) // math.MaxInt64
|
||||
assert.NotEmpty(t, result)
|
||||
assert.Equal(t, "9223372036854775807", result)
|
||||
}
|
||||
|
||||
// --- Forge commands Ugly (special chars → API returns 404/error) ---
|
||||
|
||||
func TestCommandsForge_CmdIssueGet_Ugly(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(404) }))
|
||||
t.Cleanup(srv.Close)
|
||||
s, _ := testPrepWithCore(t, srv)
|
||||
r := s.cmdIssueGet(core.NewOptions(
|
||||
core.Option{Key: "_arg", Value: "go-io/<script>"},
|
||||
core.Option{Key: "number", Value: "1"},
|
||||
))
|
||||
assert.False(t, r.OK)
|
||||
}
|
||||
|
||||
func TestCommandsForge_CmdIssueList_Ugly(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(500) }))
|
||||
t.Cleanup(srv.Close)
|
||||
s, _ := testPrepWithCore(t, srv)
|
||||
r := s.cmdIssueList(core.NewOptions(core.Option{Key: "_arg", Value: "repo&evil=true"}))
|
||||
assert.False(t, r.OK)
|
||||
}
|
||||
|
||||
func TestCommandsForge_CmdIssueComment_Ugly(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(500) }))
|
||||
t.Cleanup(srv.Close)
|
||||
s, _ := testPrepWithCore(t, srv)
|
||||
r := s.cmdIssueComment(core.NewOptions(
|
||||
core.Option{Key: "_arg", Value: "go-io"},
|
||||
core.Option{Key: "number", Value: "1"},
|
||||
core.Option{Key: "body", Value: "Hello <b>world</b> & \"quotes\""},
|
||||
))
|
||||
assert.False(t, r.OK)
|
||||
}
|
||||
|
||||
func TestCommandsForge_CmdIssueCreate_Ugly(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(500) }))
|
||||
t.Cleanup(srv.Close)
|
||||
s, _ := testPrepWithCore(t, srv)
|
||||
r := s.cmdIssueCreate(core.NewOptions(
|
||||
core.Option{Key: "_arg", Value: "go-io"},
|
||||
core.Option{Key: "title", Value: "Fix <b>bug</b> #123"},
|
||||
))
|
||||
assert.False(t, r.OK)
|
||||
}
|
||||
|
||||
func TestCommandsForge_CmdPRGet_Ugly(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(404) }))
|
||||
t.Cleanup(srv.Close)
|
||||
s, _ := testPrepWithCore(t, srv)
|
||||
r := s.cmdPRGet(core.NewOptions(
|
||||
core.Option{Key: "_arg", Value: "../../../etc/passwd"},
|
||||
core.Option{Key: "number", Value: "1"},
|
||||
))
|
||||
assert.False(t, r.OK)
|
||||
}
|
||||
|
||||
func TestCommandsForge_CmdPRList_Ugly(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(500) }))
|
||||
t.Cleanup(srv.Close)
|
||||
s, _ := testPrepWithCore(t, srv)
|
||||
r := s.cmdPRList(core.NewOptions(core.Option{Key: "_arg", Value: "repo%00null"}))
|
||||
assert.False(t, r.OK)
|
||||
}
|
||||
|
||||
func TestCommandsForge_CmdPRMerge_Ugly(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(422) }))
|
||||
t.Cleanup(srv.Close)
|
||||
s, _ := testPrepWithCore(t, srv)
|
||||
r := s.cmdPRMerge(core.NewOptions(
|
||||
core.Option{Key: "_arg", Value: "go-io"},
|
||||
core.Option{Key: "number", Value: "1"},
|
||||
core.Option{Key: "method", Value: "invalid-method"},
|
||||
))
|
||||
assert.False(t, r.OK)
|
||||
}
|
||||
|
||||
func TestCommandsForge_CmdRepoGet_Ugly(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(404) }))
|
||||
t.Cleanup(srv.Close)
|
||||
s, _ := testPrepWithCore(t, srv)
|
||||
r := s.cmdRepoGet(core.NewOptions(
|
||||
core.Option{Key: "_arg", Value: "go-io"},
|
||||
core.Option{Key: "org", Value: "org/with/slashes"},
|
||||
))
|
||||
assert.False(t, r.OK)
|
||||
}
|
||||
|
||||
func TestCommandsForge_CmdRepoList_Ugly(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(500) }))
|
||||
t.Cleanup(srv.Close)
|
||||
s, _ := testPrepWithCore(t, srv)
|
||||
r := s.cmdRepoList(core.NewOptions(core.Option{Key: "org", Value: "<script>alert(1)</script>"}))
|
||||
assert.False(t, r.OK)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ import (
|
|||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
)
|
||||
|
||||
func TestPaths_CoreRoot_Good_EnvVar(t *testing.T) {
|
||||
|
|
@ -197,3 +199,143 @@ func TestPaths_DefaultBranch_Ugly(t *testing.T) {
|
|||
branch := DefaultBranch(dir)
|
||||
assert.Equal(t, "master", branch)
|
||||
}
|
||||
|
||||
// --- LocalFs Bad/Ugly ---
|
||||
|
||||
func TestPaths_LocalFs_Bad_ReadNonExistent(t *testing.T) {
|
||||
f := LocalFs()
|
||||
r := f.Read("/tmp/nonexistent-path-" + strings.Repeat("x", 20) + "/file.txt")
|
||||
assert.False(t, r.OK, "reading a non-existent file should fail")
|
||||
}
|
||||
|
||||
func TestPaths_LocalFs_Ugly_EmptyPath(t *testing.T) {
|
||||
f := LocalFs()
|
||||
assert.NotPanics(t, func() {
|
||||
f.Read("")
|
||||
})
|
||||
}
|
||||
|
||||
// --- WorkspaceRoot Bad/Ugly ---
|
||||
|
||||
func TestPaths_WorkspaceRoot_Bad_EmptyEnv(t *testing.T) {
|
||||
t.Setenv("CORE_WORKSPACE", "")
|
||||
home, _ := os.UserHomeDir()
|
||||
// Should fall back to ~/Code/.core/workspace
|
||||
assert.Equal(t, home+"/Code/.core/workspace", WorkspaceRoot())
|
||||
}
|
||||
|
||||
func TestPaths_WorkspaceRoot_Ugly_TrailingSlash(t *testing.T) {
|
||||
t.Setenv("CORE_WORKSPACE", "/tmp/test-core/")
|
||||
// Verify it still constructs a valid path (JoinPath handles trailing slash)
|
||||
ws := WorkspaceRoot()
|
||||
assert.NotEmpty(t, ws)
|
||||
assert.Contains(t, ws, "workspace")
|
||||
}
|
||||
|
||||
// --- CoreRoot Bad/Ugly ---
|
||||
|
||||
func TestPaths_CoreRoot_Bad_WhitespaceEnv(t *testing.T) {
|
||||
t.Setenv("CORE_WORKSPACE", " ")
|
||||
// Non-empty string (whitespace) will be used as-is
|
||||
root := CoreRoot()
|
||||
assert.Equal(t, " ", root)
|
||||
}
|
||||
|
||||
func TestPaths_CoreRoot_Ugly_UnicodeEnv(t *testing.T) {
|
||||
t.Setenv("CORE_WORKSPACE", "/tmp/\u00e9\u00e0\u00fc")
|
||||
assert.NotPanics(t, func() {
|
||||
root := CoreRoot()
|
||||
assert.Equal(t, "/tmp/\u00e9\u00e0\u00fc", root)
|
||||
})
|
||||
}
|
||||
|
||||
// --- PlansRoot Bad/Ugly ---
|
||||
|
||||
func TestPaths_PlansRoot_Bad_EmptyEnv(t *testing.T) {
|
||||
t.Setenv("CORE_WORKSPACE", "")
|
||||
home, _ := os.UserHomeDir()
|
||||
assert.Equal(t, home+"/Code/.core/plans", PlansRoot())
|
||||
}
|
||||
|
||||
func TestPaths_PlansRoot_Ugly_NestedPath(t *testing.T) {
|
||||
t.Setenv("CORE_WORKSPACE", "/a/b/c/d/e/f")
|
||||
assert.Equal(t, "/a/b/c/d/e/f/plans", PlansRoot())
|
||||
}
|
||||
|
||||
// --- AgentName Bad/Ugly ---
|
||||
|
||||
func TestPaths_AgentName_Bad_WhitespaceEnv(t *testing.T) {
|
||||
t.Setenv("AGENT_NAME", " ")
|
||||
// Whitespace is non-empty, so returned as-is
|
||||
assert.Equal(t, " ", AgentName())
|
||||
}
|
||||
|
||||
func TestPaths_AgentName_Ugly_UnicodeEnv(t *testing.T) {
|
||||
t.Setenv("AGENT_NAME", "\u00e9nchantr\u00efx")
|
||||
assert.NotPanics(t, func() {
|
||||
name := AgentName()
|
||||
assert.Equal(t, "\u00e9nchantr\u00efx", name)
|
||||
})
|
||||
}
|
||||
|
||||
// --- GitHubOrg Bad/Ugly ---
|
||||
|
||||
func TestPaths_GitHubOrg_Bad_WhitespaceEnv(t *testing.T) {
|
||||
t.Setenv("GITHUB_ORG", " ")
|
||||
assert.Equal(t, " ", GitHubOrg())
|
||||
}
|
||||
|
||||
func TestPaths_GitHubOrg_Ugly_SpecialChars(t *testing.T) {
|
||||
t.Setenv("GITHUB_ORG", "org/with/slashes")
|
||||
assert.NotPanics(t, func() {
|
||||
org := GitHubOrg()
|
||||
assert.Equal(t, "org/with/slashes", org)
|
||||
})
|
||||
}
|
||||
|
||||
// --- parseInt Bad/Ugly ---
|
||||
|
||||
func TestPaths_ParseInt_Bad_EmptyString(t *testing.T) {
|
||||
assert.Equal(t, 0, parseInt(""))
|
||||
}
|
||||
|
||||
func TestPaths_ParseInt_Bad_NonNumeric(t *testing.T) {
|
||||
assert.Equal(t, 0, parseInt("abc"))
|
||||
assert.Equal(t, 0, parseInt("12.5"))
|
||||
assert.Equal(t, 0, parseInt("0xff"))
|
||||
}
|
||||
|
||||
func TestPaths_ParseInt_Bad_WhitespaceOnly(t *testing.T) {
|
||||
assert.Equal(t, 0, parseInt(" "))
|
||||
}
|
||||
|
||||
func TestPaths_ParseInt_Ugly_NegativeNumber(t *testing.T) {
|
||||
assert.Equal(t, -42, parseInt("-42"))
|
||||
}
|
||||
|
||||
func TestPaths_ParseInt_Ugly_VeryLargeNumber(t *testing.T) {
|
||||
assert.Equal(t, 0, parseInt("99999999999999999999999"))
|
||||
}
|
||||
|
||||
func TestPaths_ParseInt_Ugly_LeadingTrailingWhitespace(t *testing.T) {
|
||||
assert.Equal(t, 42, parseInt(" 42 "))
|
||||
}
|
||||
|
||||
// --- newFs Bad/Ugly ---
|
||||
|
||||
func TestPaths_NewFs_Bad_EmptyRoot(t *testing.T) {
|
||||
f := newFs("")
|
||||
assert.NotNil(t, f, "newFs with empty root should not return nil")
|
||||
}
|
||||
|
||||
func TestPaths_NewFs_Ugly_UnicodeRoot(t *testing.T) {
|
||||
assert.NotPanics(t, func() {
|
||||
f := newFs("/tmp/\u00e9\u00e0\u00fc/\u00f1o\u00f0\u00e9s")
|
||||
assert.NotNil(t, f)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPaths_NewFs_Ugly_VerifyIsFs(t *testing.T) {
|
||||
f := newFs("/tmp")
|
||||
assert.IsType(t, &core.Fs{}, f)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ package agentic
|
|||
import (
|
||||
"context"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
|
|
@ -351,3 +352,157 @@ func TestPlan_PlanPath_Bad_Dot(t *testing.T) {
|
|||
assert.Contains(t, planPath("/tmp", ".."), "invalid")
|
||||
assert.Contains(t, planPath("/tmp", ""), "invalid")
|
||||
}
|
||||
|
||||
// --- planCreate Ugly ---
|
||||
|
||||
func TestPlan_PlanCreate_Ugly_VeryLongTitle(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
t.Setenv("CORE_WORKSPACE", dir)
|
||||
|
||||
s := newTestPrep(t)
|
||||
longTitle := strings.Repeat("Long Title With Many Words ", 20)
|
||||
_, out, err := s.planCreate(context.Background(), nil, PlanCreateInput{
|
||||
Title: longTitle,
|
||||
Objective: "Test very long title handling",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, out.Success)
|
||||
assert.NotEmpty(t, out.ID)
|
||||
// The slug portion should be truncated
|
||||
assert.LessOrEqual(t, len(out.ID), 50, "ID should be reasonably short")
|
||||
}
|
||||
|
||||
func TestPlan_PlanCreate_Ugly_UnicodeTitle(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
t.Setenv("CORE_WORKSPACE", dir)
|
||||
|
||||
s := newTestPrep(t)
|
||||
_, out, err := s.planCreate(context.Background(), nil, PlanCreateInput{
|
||||
Title: "\u00e9\u00e0\u00fc\u00f1\u00f0 Plan \u2603\u2764\u270c",
|
||||
Objective: "Handle unicode gracefully",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, out.Success)
|
||||
assert.NotEmpty(t, out.ID)
|
||||
// Should be readable from disk
|
||||
_, statErr := os.Stat(out.Path)
|
||||
assert.NoError(t, statErr)
|
||||
}
|
||||
|
||||
// --- planRead Ugly ---
|
||||
|
||||
func TestPlan_PlanRead_Ugly_SpecialCharsInID(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
t.Setenv("CORE_WORKSPACE", dir)
|
||||
|
||||
s := newTestPrep(t)
|
||||
// Try to read with special chars — should safely not find it
|
||||
_, _, err := s.planRead(context.Background(), nil, PlanReadInput{ID: "plan-with-<script>-chars"})
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "not found")
|
||||
}
|
||||
|
||||
func TestPlan_PlanRead_Ugly_UnicodeID(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
t.Setenv("CORE_WORKSPACE", dir)
|
||||
|
||||
s := newTestPrep(t)
|
||||
_, _, err := s.planRead(context.Background(), nil, PlanReadInput{ID: "\u00e9\u00e0\u00fc-plan"})
|
||||
assert.Error(t, err, "unicode ID should not find a file")
|
||||
}
|
||||
|
||||
// --- planUpdate Ugly ---
|
||||
|
||||
func TestPlan_PlanUpdate_Ugly_EmptyPhasesArray(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
t.Setenv("CORE_WORKSPACE", dir)
|
||||
|
||||
s := newTestPrep(t)
|
||||
_, createOut, _ := s.planCreate(context.Background(), nil, PlanCreateInput{
|
||||
Title: "Phase Test",
|
||||
Objective: "Test empty phases",
|
||||
Phases: []Phase{{Name: "Phase 1", Status: "pending"}},
|
||||
})
|
||||
|
||||
// Update with empty phases array — should replace with no phases
|
||||
_, updateOut, err := s.planUpdate(context.Background(), nil, PlanUpdateInput{
|
||||
ID: createOut.ID,
|
||||
Phases: []Phase{},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
// Empty slice is still non-nil, so it replaces
|
||||
assert.Empty(t, updateOut.Plan.Phases)
|
||||
}
|
||||
|
||||
func TestPlan_PlanUpdate_Ugly_UnicodeNotes(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
t.Setenv("CORE_WORKSPACE", dir)
|
||||
|
||||
s := newTestPrep(t)
|
||||
_, createOut, _ := s.planCreate(context.Background(), nil, PlanCreateInput{
|
||||
Title: "Unicode Notes",
|
||||
Objective: "Test unicode in notes",
|
||||
})
|
||||
|
||||
_, updateOut, err := s.planUpdate(context.Background(), nil, PlanUpdateInput{
|
||||
ID: createOut.ID,
|
||||
Notes: "\u00e9\u00e0\u00fc\u00f1 notes with \u2603 snowman and \u00a3 pound sign",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, updateOut.Plan.Notes, "\u2603")
|
||||
}
|
||||
|
||||
// --- planDelete Ugly ---
|
||||
|
||||
func TestPlan_PlanDelete_Ugly_PathTraversalAttempt(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
t.Setenv("CORE_WORKSPACE", dir)
|
||||
|
||||
s := newTestPrep(t)
|
||||
// Path traversal attempt should be sanitised and not find anything
|
||||
_, _, err := s.planDelete(context.Background(), nil, PlanDeleteInput{ID: "../../etc/passwd"})
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "not found")
|
||||
}
|
||||
|
||||
func TestPlan_PlanDelete_Ugly_UnicodeID(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
t.Setenv("CORE_WORKSPACE", dir)
|
||||
|
||||
s := newTestPrep(t)
|
||||
_, _, err := s.planDelete(context.Background(), nil, PlanDeleteInput{ID: "\u00e9\u00e0\u00fc-to-delete"})
|
||||
assert.Error(t, err, "unicode ID should not match a real plan")
|
||||
}
|
||||
|
||||
// --- planPath Ugly ---
|
||||
|
||||
func TestPlan_PlanPath_Ugly_UnicodeID(t *testing.T) {
|
||||
result := planPath("/tmp/plans", "\u00e9\u00e0\u00fc-plan-\u2603")
|
||||
assert.NotPanics(t, func() {
|
||||
_ = planPath("/tmp", "\u00e9\u00e0\u00fc")
|
||||
})
|
||||
assert.Contains(t, result, ".json")
|
||||
}
|
||||
|
||||
func TestPlan_PlanPath_Ugly_VeryLongID(t *testing.T) {
|
||||
longID := strings.Repeat("a", 500)
|
||||
result := planPath("/tmp/plans", longID)
|
||||
assert.Contains(t, result, ".json")
|
||||
assert.NotEmpty(t, result)
|
||||
}
|
||||
|
||||
// --- validPlanStatus Ugly ---
|
||||
|
||||
func TestPlan_ValidPlanStatus_Ugly_UnicodeStatus(t *testing.T) {
|
||||
assert.False(t, validPlanStatus("\u00e9\u00e0\u00fc"))
|
||||
assert.False(t, validPlanStatus("\u2603"))
|
||||
assert.False(t, validPlanStatus("\u0000"))
|
||||
}
|
||||
|
||||
func TestPlan_ValidPlanStatus_Ugly_NearMissStatus(t *testing.T) {
|
||||
assert.False(t, validPlanStatus("Draft")) // capital D
|
||||
assert.False(t, validPlanStatus("DRAFT")) // all caps
|
||||
assert.False(t, validPlanStatus("in-progress")) // hyphen instead of underscore
|
||||
assert.False(t, validPlanStatus(" draft")) // leading space
|
||||
assert.False(t, validPlanStatus("draft ")) // trailing space
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ package agentic
|
|||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
|
|
@ -194,3 +195,135 @@ func TestPrep_SetCore_Good(t *testing.T) {
|
|||
s.SetCore(c)
|
||||
assert.NotNil(t, s.core)
|
||||
}
|
||||
|
||||
// --- sanitiseBranchSlug Bad/Ugly ---
|
||||
|
||||
func TestSanitise_SanitiseBranchSlug_Bad_EmptyString(t *testing.T) {
|
||||
assert.Equal(t, "", sanitiseBranchSlug("", 40))
|
||||
}
|
||||
|
||||
func TestSanitise_SanitiseBranchSlug_Bad_OnlySpecialChars(t *testing.T) {
|
||||
assert.Equal(t, "", sanitiseBranchSlug("!@#$%^&*()", 40))
|
||||
}
|
||||
|
||||
func TestSanitise_SanitiseBranchSlug_Bad_OnlyDashes(t *testing.T) {
|
||||
assert.Equal(t, "", sanitiseBranchSlug("------", 40))
|
||||
}
|
||||
|
||||
func TestSanitise_SanitiseBranchSlug_Ugly_VeryLongString(t *testing.T) {
|
||||
long := strings.Repeat("abcdefghij", 100)
|
||||
result := sanitiseBranchSlug(long, 50)
|
||||
assert.LessOrEqual(t, len(result), 50)
|
||||
}
|
||||
|
||||
func TestSanitise_SanitiseBranchSlug_Ugly_Unicode(t *testing.T) {
|
||||
// Unicode chars should be replaced with dashes, then edges trimmed
|
||||
result := sanitiseBranchSlug("\u00e9\u00e0\u00fc\u00f1\u00f0", 40)
|
||||
assert.NotContains(t, result, "\u00e9")
|
||||
// All replaced with dashes, then trimmed = empty
|
||||
assert.Equal(t, "", result)
|
||||
}
|
||||
|
||||
func TestSanitise_SanitiseBranchSlug_Ugly_ZeroMax(t *testing.T) {
|
||||
// max=0 means no limit
|
||||
result := sanitiseBranchSlug("hello-world", 0)
|
||||
assert.Equal(t, "hello-world", result)
|
||||
}
|
||||
|
||||
// --- sanitisePlanSlug Bad/Ugly ---
|
||||
|
||||
func TestSanitise_SanitisePlanSlug_Bad_EmptyString(t *testing.T) {
|
||||
assert.Equal(t, "", sanitisePlanSlug(""))
|
||||
}
|
||||
|
||||
func TestSanitise_SanitisePlanSlug_Bad_OnlySpecialChars(t *testing.T) {
|
||||
assert.Equal(t, "", sanitisePlanSlug("!@#$%^&*()"))
|
||||
}
|
||||
|
||||
func TestSanitise_SanitisePlanSlug_Bad_OnlySpaces(t *testing.T) {
|
||||
// Spaces become dashes, then collapsed, then trimmed
|
||||
assert.Equal(t, "", sanitisePlanSlug(" "))
|
||||
}
|
||||
|
||||
func TestSanitise_SanitisePlanSlug_Ugly_VeryLongString(t *testing.T) {
|
||||
long := strings.Repeat("abcdefghij ", 20)
|
||||
result := sanitisePlanSlug(long)
|
||||
assert.LessOrEqual(t, len(result), 30)
|
||||
}
|
||||
|
||||
func TestSanitise_SanitisePlanSlug_Ugly_Unicode(t *testing.T) {
|
||||
result := sanitisePlanSlug("\u00e9\u00e0\u00fc\u00f1\u00f0")
|
||||
assert.Equal(t, "", result, "unicode chars should be stripped, leaving empty string")
|
||||
}
|
||||
|
||||
func TestSanitise_SanitisePlanSlug_Ugly_AllDashInput(t *testing.T) {
|
||||
assert.Equal(t, "", sanitisePlanSlug("---"))
|
||||
}
|
||||
|
||||
// --- sanitiseFilename Bad/Ugly ---
|
||||
|
||||
func TestSanitise_SanitiseFilename_Bad_EmptyString(t *testing.T) {
|
||||
assert.Equal(t, "", sanitiseFilename(""))
|
||||
}
|
||||
|
||||
func TestSanitise_SanitiseFilename_Bad_OnlySpecialChars(t *testing.T) {
|
||||
result := sanitiseFilename("!@#$%^&*()")
|
||||
// All replaced with dashes
|
||||
assert.Equal(t, "----------", result)
|
||||
}
|
||||
|
||||
func TestSanitise_SanitiseFilename_Ugly_VeryLongString(t *testing.T) {
|
||||
long := strings.Repeat("a", 1000)
|
||||
result := sanitiseFilename(long)
|
||||
assert.Equal(t, 1000, len(result))
|
||||
}
|
||||
|
||||
func TestSanitise_SanitiseFilename_Ugly_Unicode(t *testing.T) {
|
||||
result := sanitiseFilename("\u00e9\u00e0\u00fc\u00f1\u00f0")
|
||||
// All replaced with dashes
|
||||
for _, r := range result {
|
||||
assert.Equal(t, '-', r)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitise_SanitiseFilename_Ugly_PreservesDotsUnderscores(t *testing.T) {
|
||||
assert.Equal(t, "my_file.test.txt", sanitiseFilename("my_file.test.txt"))
|
||||
}
|
||||
|
||||
// --- collapseRepeatedRune Bad/Ugly ---
|
||||
|
||||
func TestSanitise_CollapseRepeatedRune_Bad_EmptyString(t *testing.T) {
|
||||
assert.Equal(t, "", collapseRepeatedRune("", '-'))
|
||||
}
|
||||
|
||||
func TestSanitise_CollapseRepeatedRune_Bad_AllTarget(t *testing.T) {
|
||||
assert.Equal(t, "-", collapseRepeatedRune("-----", '-'))
|
||||
}
|
||||
|
||||
func TestSanitise_CollapseRepeatedRune_Ugly_Unicode(t *testing.T) {
|
||||
assert.Equal(t, "h\u00e9llo", collapseRepeatedRune("h\u00e9\u00e9\u00e9llo", '\u00e9'))
|
||||
}
|
||||
|
||||
func TestSanitise_CollapseRepeatedRune_Ugly_VeryLong(t *testing.T) {
|
||||
long := strings.Repeat("--a", 500)
|
||||
result := collapseRepeatedRune(long, '-')
|
||||
assert.NotContains(t, result, "--")
|
||||
}
|
||||
|
||||
// --- trimRuneEdges Bad/Ugly ---
|
||||
|
||||
func TestSanitise_TrimRuneEdges_Bad_EmptyString(t *testing.T) {
|
||||
assert.Equal(t, "", trimRuneEdges("", '-'))
|
||||
}
|
||||
|
||||
func TestSanitise_TrimRuneEdges_Bad_AllTarget(t *testing.T) {
|
||||
assert.Equal(t, "", trimRuneEdges("-----", '-'))
|
||||
}
|
||||
|
||||
func TestSanitise_TrimRuneEdges_Ugly_Unicode(t *testing.T) {
|
||||
assert.Equal(t, "hello", trimRuneEdges("\u00e9hello\u00e9\u00e9", '\u00e9'))
|
||||
}
|
||||
|
||||
func TestSanitise_TrimRuneEdges_Ugly_NoMatch(t *testing.T) {
|
||||
assert.Equal(t, "hello", trimRuneEdges("hello", '-'))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -569,3 +569,116 @@ func TestPrep_Shutdown_ShutdownNow_Ugly(t *testing.T) {
|
|||
assert.Equal(t, "failed", st.Status)
|
||||
assert.Contains(t, st.Question, "cleared by shutdown_now")
|
||||
}
|
||||
|
||||
// --- dispatchStart Bad/Ugly ---
|
||||
|
||||
func TestShutdown_DispatchStart_Bad_NilPokeCh(t *testing.T) {
|
||||
s := &PrepSubsystem{
|
||||
frozen: true,
|
||||
pokeCh: nil,
|
||||
backoff: make(map[string]time.Time),
|
||||
failCount: make(map[string]int),
|
||||
}
|
||||
|
||||
// Should not panic even with nil pokeCh (Poke is nil-safe)
|
||||
_, out, err := s.dispatchStart(context.Background(), nil, ShutdownInput{})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, out.Success)
|
||||
assert.False(t, s.frozen, "frozen should be cleared even with nil pokeCh")
|
||||
}
|
||||
|
||||
func TestShutdown_DispatchStart_Ugly_AlreadyUnfrozen(t *testing.T) {
|
||||
s := &PrepSubsystem{
|
||||
frozen: false, // already unfrozen
|
||||
pokeCh: make(chan struct{}, 1),
|
||||
backoff: make(map[string]time.Time),
|
||||
failCount: make(map[string]int),
|
||||
}
|
||||
|
||||
_, out, err := s.dispatchStart(context.Background(), nil, ShutdownInput{})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, out.Success)
|
||||
assert.False(t, s.frozen, "should remain unfrozen")
|
||||
assert.Contains(t, out.Message, "started")
|
||||
}
|
||||
|
||||
// --- shutdownGraceful Bad/Ugly ---
|
||||
|
||||
func TestShutdown_ShutdownGraceful_Bad_AlreadyFrozen(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
t.Setenv("CORE_WORKSPACE", root)
|
||||
|
||||
s := &PrepSubsystem{
|
||||
frozen: true, // already frozen
|
||||
backoff: make(map[string]time.Time),
|
||||
failCount: make(map[string]int),
|
||||
}
|
||||
|
||||
_, out, err := s.shutdownGraceful(context.Background(), nil, ShutdownInput{})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, out.Success)
|
||||
assert.True(t, s.frozen, "should remain frozen")
|
||||
assert.Contains(t, out.Message, "frozen")
|
||||
}
|
||||
|
||||
func TestShutdown_ShutdownGraceful_Ugly_WithWorkspaces(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
t.Setenv("CORE_WORKSPACE", root)
|
||||
wsRoot := filepath.Join(root, "workspace")
|
||||
|
||||
// Create workspaces with various statuses
|
||||
for _, name := range []string{"ws-completed", "ws-failed", "ws-blocked"} {
|
||||
ws := filepath.Join(wsRoot, name)
|
||||
require.True(t, fs.EnsureDir(ws).OK)
|
||||
require.NoError(t, writeStatus(ws, &WorkspaceStatus{
|
||||
Status: "completed",
|
||||
Repo: "go-io",
|
||||
Agent: "codex",
|
||||
}))
|
||||
}
|
||||
|
||||
s := &PrepSubsystem{
|
||||
frozen: false,
|
||||
backoff: make(map[string]time.Time),
|
||||
failCount: make(map[string]int),
|
||||
}
|
||||
|
||||
_, out, err := s.shutdownGraceful(context.Background(), nil, ShutdownInput{})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, out.Success)
|
||||
assert.True(t, s.frozen)
|
||||
// Running count should be 0 (no live PIDs)
|
||||
assert.Equal(t, 0, out.Running)
|
||||
}
|
||||
|
||||
// --- shutdownNow Bad ---
|
||||
|
||||
func TestShutdown_ShutdownNow_Bad_NoRunningPIDs(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
t.Setenv("CORE_WORKSPACE", root)
|
||||
wsRoot := filepath.Join(root, "workspace")
|
||||
|
||||
// Create completed workspaces only (no running PIDs to kill)
|
||||
for i := 1; i <= 2; i++ {
|
||||
ws := filepath.Join(wsRoot, "task-"+itoa(i))
|
||||
require.True(t, fs.EnsureDir(ws).OK)
|
||||
require.NoError(t, writeStatus(ws, &WorkspaceStatus{
|
||||
Status: "completed",
|
||||
Repo: "go-io",
|
||||
Agent: "codex",
|
||||
}))
|
||||
}
|
||||
|
||||
s := &PrepSubsystem{
|
||||
frozen: false,
|
||||
backoff: make(map[string]time.Time),
|
||||
failCount: make(map[string]int),
|
||||
}
|
||||
|
||||
_, out, err := s.shutdownNow(context.Background(), nil, ShutdownInput{})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, out.Success)
|
||||
assert.True(t, s.frozen)
|
||||
assert.Contains(t, out.Message, "killed 0")
|
||||
assert.Contains(t, out.Message, "cleared 0")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,12 +3,14 @@
|
|||
package agentic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestStatus_WriteStatus_Good(t *testing.T) {
|
||||
|
|
@ -299,3 +301,70 @@ func TestStatus_WriteStatus_Ugly(t *testing.T) {
|
|||
assert.Equal(t, "https://forge.lthn.ai/core/go-mcp/pulls/12", read.PRURL)
|
||||
assert.False(t, read.UpdatedAt.IsZero(), "UpdatedAt must survive roundtrip")
|
||||
}
|
||||
|
||||
// --- writeStatus Bad ---
|
||||
|
||||
func TestStatus_WriteStatus_Bad_ReadOnlyPath(t *testing.T) {
|
||||
// go-io fs.Write auto-creates dirs, so test with /dev/null parent
|
||||
st := &WorkspaceStatus{Status: "running", Agent: "codex"}
|
||||
err := writeStatus("/dev/null/impossible", st)
|
||||
assert.Error(t, err, "writeStatus to an impossible path should fail")
|
||||
}
|
||||
|
||||
// --- status() MCP handler Good/Bad ---
|
||||
|
||||
func TestStatus_Status_Good_PopulatedWorkspaces(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
t.Setenv("CORE_WORKSPACE", root)
|
||||
wsRoot := filepath.Join(root, "workspace")
|
||||
|
||||
// Create a running workspace with a live PID (our own PID)
|
||||
ws1 := filepath.Join(wsRoot, "task-running")
|
||||
require.True(t, fs.EnsureDir(filepath.Join(ws1, "repo")).OK)
|
||||
require.NoError(t, writeStatus(ws1, &WorkspaceStatus{
|
||||
Status: "completed",
|
||||
Repo: "go-io",
|
||||
Agent: "codex",
|
||||
Task: "fix tests",
|
||||
}))
|
||||
|
||||
// Create a blocked workspace
|
||||
ws2 := filepath.Join(wsRoot, "task-blocked")
|
||||
require.True(t, fs.EnsureDir(filepath.Join(ws2, "repo")).OK)
|
||||
require.NoError(t, writeStatus(ws2, &WorkspaceStatus{
|
||||
Status: "blocked",
|
||||
Repo: "go-log",
|
||||
Agent: "gemini",
|
||||
Question: "Which log format?",
|
||||
}))
|
||||
|
||||
s := &PrepSubsystem{
|
||||
backoff: make(map[string]time.Time),
|
||||
failCount: make(map[string]int),
|
||||
}
|
||||
|
||||
_, out, err := s.status(context.Background(), nil, StatusInput{})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 2, out.Total)
|
||||
assert.Equal(t, 1, out.Completed)
|
||||
assert.Len(t, out.Blocked, 1)
|
||||
assert.Equal(t, "go-log", out.Blocked[0].Repo)
|
||||
assert.Equal(t, "Which log format?", out.Blocked[0].Question)
|
||||
}
|
||||
|
||||
func TestStatus_Status_Bad_EmptyWorkspaceRoot(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
t.Setenv("CORE_WORKSPACE", root)
|
||||
// Do NOT create the workspace/ subdirectory
|
||||
|
||||
s := &PrepSubsystem{
|
||||
backoff: make(map[string]time.Time),
|
||||
failCount: make(map[string]int),
|
||||
}
|
||||
|
||||
_, out, err := s.status(context.Background(), nil, StatusInput{})
|
||||
require.NoError(t, err, "status on missing workspace dir should not error")
|
||||
assert.Equal(t, 0, out.Total)
|
||||
assert.Equal(t, 0, out.Running)
|
||||
assert.Equal(t, 0, out.Completed)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue