1827 lines
57 KiB
Go
1827 lines
57 KiB
Go
// SPDX-License-Identifier: EUPL-1.2
|
|
|
|
package agentic
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
|
|
core "dappco.re/go/core"
|
|
"dappco.re/go/core/forge"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// testPrepWithCore creates a PrepSubsystem backed by a real Core + Forge mock.
|
|
func testPrepWithCore(t *testing.T, srv *httptest.Server) (*PrepSubsystem, *core.Core) {
|
|
t.Helper()
|
|
root := t.TempDir()
|
|
t.Setenv("CORE_WORKSPACE", root)
|
|
|
|
c := core.New()
|
|
|
|
var f *forge.Forge
|
|
if srv != nil {
|
|
f = forge.NewForge(srv.URL, "test-token")
|
|
}
|
|
|
|
s := &PrepSubsystem{
|
|
ServiceRuntime: core.NewServiceRuntime(c, AgentOptions{}),
|
|
forge: f,
|
|
forgeURL: "",
|
|
forgeToken: "test-token",
|
|
codePath: t.TempDir(),
|
|
pokeCh: make(chan struct{}, 1),
|
|
backoff: make(map[string]time.Time),
|
|
failCount: make(map[string]int),
|
|
}
|
|
if srv != nil {
|
|
s.forgeURL = srv.URL
|
|
}
|
|
|
|
return s, c
|
|
}
|
|
|
|
func captureStdout(t *testing.T, run func()) string {
|
|
t.Helper()
|
|
|
|
old := os.Stdout
|
|
reader, writer, err := os.Pipe()
|
|
if err != nil {
|
|
t.Fatalf("pipe stdout: %v", err)
|
|
}
|
|
os.Stdout = writer
|
|
defer func() {
|
|
os.Stdout = old
|
|
}()
|
|
|
|
run()
|
|
|
|
if err := writer.Close(); err != nil {
|
|
t.Fatalf("close writer: %v", err)
|
|
}
|
|
data, err := io.ReadAll(reader)
|
|
if err != nil {
|
|
t.Fatalf("read stdout: %v", err)
|
|
}
|
|
if err := reader.Close(); err != nil {
|
|
t.Fatalf("close reader: %v", err)
|
|
}
|
|
|
|
return string(data)
|
|
}
|
|
|
|
// --- Forge command methods (extracted from closures) ---
|
|
|
|
func TestCommandsforge_CmdIssueGet_Bad_MissingArgs(t *testing.T) {
|
|
s, _ := testPrepWithCore(t, nil)
|
|
r := s.cmdIssueGet(core.NewOptions())
|
|
assert.False(t, r.OK)
|
|
}
|
|
|
|
func TestCommandsforge_CmdIssueGet_Good_Success(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Write([]byte(core.JSONMarshalString(map[string]any{
|
|
"number": 42, "title": "Fix tests", "state": "open",
|
|
"html_url": "https://forge.test/core/go-io/issues/42", "body": "broken",
|
|
})))
|
|
}))
|
|
t.Cleanup(srv.Close)
|
|
|
|
s, _ := testPrepWithCore(t, srv)
|
|
r := s.cmdIssueGet(core.NewOptions(
|
|
core.Option{Key: "_arg", Value: "go-io"},
|
|
core.Option{Key: "number", Value: "42"},
|
|
))
|
|
assert.True(t, r.OK)
|
|
}
|
|
|
|
func TestCommandsforge_CmdIssueGet_Bad_APIError(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.cmdIssueGet(core.NewOptions(
|
|
core.Option{Key: "_arg", Value: "go-io"},
|
|
core.Option{Key: "number", Value: "42"},
|
|
))
|
|
assert.False(t, r.OK)
|
|
}
|
|
|
|
func TestCommandsforge_CmdIssueList_Bad_MissingRepo(t *testing.T) {
|
|
s, _ := testPrepWithCore(t, nil)
|
|
r := s.cmdIssueList(core.NewOptions())
|
|
assert.False(t, r.OK)
|
|
}
|
|
|
|
func TestCommandsforge_CmdIssueList_Good_Success(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Write([]byte(core.JSONMarshalString([]map[string]any{
|
|
{"number": 1, "title": "Bug", "state": "open"},
|
|
{"number": 2, "title": "Feature", "state": "closed"},
|
|
})))
|
|
}))
|
|
t.Cleanup(srv.Close)
|
|
|
|
s, _ := testPrepWithCore(t, srv)
|
|
r := s.cmdIssueList(core.NewOptions(core.Option{Key: "_arg", Value: "go-io"}))
|
|
assert.True(t, r.OK)
|
|
}
|
|
|
|
func TestCommandsforge_CmdIssueList_Good_Empty(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Write([]byte(core.JSONMarshalString([]map[string]any{})))
|
|
}))
|
|
t.Cleanup(srv.Close)
|
|
|
|
s, _ := testPrepWithCore(t, srv)
|
|
r := s.cmdIssueList(core.NewOptions(core.Option{Key: "_arg", Value: "go-io"}))
|
|
assert.True(t, r.OK)
|
|
}
|
|
|
|
func TestCommandsforge_CmdIssueComment_Bad_MissingArgs(t *testing.T) {
|
|
s, _ := testPrepWithCore(t, nil)
|
|
r := s.cmdIssueComment(core.NewOptions())
|
|
assert.False(t, r.OK)
|
|
}
|
|
|
|
func TestCommandsforge_CmdIssueComment_Good_Success(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Write([]byte(core.JSONMarshalString(map[string]any{"id": 99})))
|
|
}))
|
|
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: "5"},
|
|
core.Option{Key: "body", Value: "LGTM"},
|
|
))
|
|
assert.True(t, r.OK)
|
|
}
|
|
|
|
func TestCommandsforge_CmdIssueCreate_Bad_MissingTitle(t *testing.T) {
|
|
s, _ := testPrepWithCore(t, nil)
|
|
r := s.cmdIssueCreate(core.NewOptions(core.Option{Key: "_arg", Value: "go-io"}))
|
|
assert.False(t, r.OK)
|
|
}
|
|
|
|
func TestCommandsforge_CmdIssueCreate_Good_Success(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Write([]byte(core.JSONMarshalString(map[string]any{
|
|
"number": 10, "title": "New bug", "html_url": "https://forge.test/issues/10",
|
|
})))
|
|
}))
|
|
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: "New bug"},
|
|
core.Option{Key: "body", Value: "Details here"},
|
|
core.Option{Key: "assignee", Value: "virgil"},
|
|
))
|
|
assert.True(t, r.OK)
|
|
}
|
|
|
|
func TestCommandsforge_CmdIssueCreate_Good_WithLabelsAndMilestone(t *testing.T) {
|
|
callPaths := []string{}
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
callPaths = append(callPaths, r.URL.Path)
|
|
switch {
|
|
case r.URL.Path == "/api/v1/repos/core/go-io/milestones":
|
|
w.Write([]byte(core.JSONMarshalString([]map[string]any{
|
|
{"id": 1, "title": "v0.8.0"},
|
|
{"id": 2, "title": "v0.9.0"},
|
|
})))
|
|
case r.URL.Path == "/api/v1/repos/core/go-io/labels":
|
|
w.Write([]byte(core.JSONMarshalString([]map[string]any{
|
|
{"id": 10, "name": "agentic"},
|
|
{"id": 11, "name": "bug"},
|
|
})))
|
|
default:
|
|
w.Write([]byte(core.JSONMarshalString(map[string]any{
|
|
"number": 15, "title": "Full issue", "html_url": "https://forge.test/issues/15",
|
|
})))
|
|
}
|
|
}))
|
|
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: "Full issue"},
|
|
core.Option{Key: "labels", Value: "agentic,bug"},
|
|
core.Option{Key: "milestone", Value: "v0.8.0"},
|
|
core.Option{Key: "ref", Value: "dev"},
|
|
))
|
|
assert.True(t, r.OK)
|
|
}
|
|
|
|
func TestCommands_RegisterCommands_Good_BrainRecall(t *testing.T) {
|
|
s, c := testPrepWithCore(t, nil)
|
|
|
|
s.registerCommands(context.Background())
|
|
|
|
assert.Contains(t, c.Commands(), "brain/recall")
|
|
assert.Contains(t, c.Commands(), "brain:recall")
|
|
assert.Contains(t, c.Commands(), "brain/remember")
|
|
assert.Contains(t, c.Commands(), "brain:remember")
|
|
}
|
|
|
|
func TestCommands_CmdBrainList_Good(t *testing.T) {
|
|
s, c := testPrepWithCore(t, nil)
|
|
c.Action("brain.list", func(_ context.Context, options core.Options) core.Result {
|
|
assert.Equal(t, "agent", options.String("project"))
|
|
assert.Equal(t, "architecture", options.String("type"))
|
|
assert.Equal(t, "virgil", options.String("agent_id"))
|
|
return core.Result{Value: map[string]any{
|
|
"success": true,
|
|
"count": 1,
|
|
"memories": []any{
|
|
map[string]any{
|
|
"id": "mem-1",
|
|
"type": "architecture",
|
|
"content": "Use named actions.",
|
|
"project": "agent",
|
|
"agent_id": "virgil",
|
|
"confidence": 0.9,
|
|
"supersedes_count": 3,
|
|
"deleted_at": "2026-03-31T12:30:00Z",
|
|
"tags": []any{"architecture", "convention"},
|
|
},
|
|
},
|
|
}, OK: true}
|
|
})
|
|
|
|
output := captureStdout(t, func() {
|
|
result := s.cmdBrainList(core.NewOptions(
|
|
core.Option{Key: "project", Value: "agent"},
|
|
core.Option{Key: "type", Value: "architecture"},
|
|
core.Option{Key: "agent", Value: "virgil"},
|
|
))
|
|
require.True(t, result.OK)
|
|
})
|
|
|
|
assert.Contains(t, output, "count: 1")
|
|
assert.Contains(t, output, "mem-1 architecture")
|
|
assert.Contains(t, output, "supersedes: 3")
|
|
assert.Contains(t, output, "deleted_at: 2026-03-31T12:30:00Z")
|
|
assert.Contains(t, output, "Use named actions.")
|
|
}
|
|
|
|
func TestCommands_CmdBrainRemember_Good(t *testing.T) {
|
|
s, c := testPrepWithCore(t, nil)
|
|
c.Action("brain.remember", func(_ context.Context, options core.Options) core.Result {
|
|
assert.Equal(t, "Use named actions.", options.String("content"))
|
|
assert.Equal(t, "convention", options.String("type"))
|
|
assert.Equal(t, []string{"architecture", "history"}, optionStringSliceValue(options, "tags"))
|
|
assert.Equal(t, "agent", options.String("project"))
|
|
assert.Equal(t, "0.9", options.String("confidence"))
|
|
assert.Equal(t, "mem-1", options.String("supersedes"))
|
|
assert.Equal(t, 24, options.Int("expires_in"))
|
|
return core.Result{Value: map[string]any{
|
|
"success": true,
|
|
"memoryId": "mem-1",
|
|
"timestamp": "2026-03-31T12:00:00Z",
|
|
}, OK: true}
|
|
})
|
|
|
|
output := captureStdout(t, func() {
|
|
result := s.cmdBrainRemember(core.NewOptions(
|
|
core.Option{Key: "_arg", Value: "Use named actions."},
|
|
core.Option{Key: "type", Value: "convention"},
|
|
core.Option{Key: "tags", Value: []string{"architecture", "history"}},
|
|
core.Option{Key: "project", Value: "agent"},
|
|
core.Option{Key: "confidence", Value: "0.9"},
|
|
core.Option{Key: "supersedes", Value: "mem-1"},
|
|
core.Option{Key: "expires_in", Value: 24},
|
|
))
|
|
require.True(t, result.OK)
|
|
})
|
|
|
|
assert.Contains(t, output, "remembered: mem-1")
|
|
assert.Contains(t, output, "timestamp: 2026-03-31T12:00:00Z")
|
|
}
|
|
|
|
func TestCommands_CmdBrainRemember_Bad_MissingContent(t *testing.T) {
|
|
s, _ := testPrepWithCore(t, nil)
|
|
|
|
result := s.cmdBrainRemember(core.NewOptions(
|
|
core.Option{Key: "type", Value: "convention"},
|
|
))
|
|
|
|
require.False(t, result.OK)
|
|
err, ok := result.Value.(error)
|
|
require.True(t, ok)
|
|
assert.Contains(t, err.Error(), "content and type are required")
|
|
}
|
|
|
|
func TestCommands_CmdBrainRemember_Ugly_InvalidOutput(t *testing.T) {
|
|
s, c := testPrepWithCore(t, nil)
|
|
c.Action("brain.remember", func(_ context.Context, _ core.Options) core.Result {
|
|
return core.Result{Value: 123, OK: true}
|
|
})
|
|
|
|
result := s.cmdBrainRemember(core.NewOptions(
|
|
core.Option{Key: "_arg", Value: "Use named actions."},
|
|
core.Option{Key: "type", Value: "convention"},
|
|
))
|
|
|
|
require.False(t, result.OK)
|
|
err, ok := result.Value.(error)
|
|
require.True(t, ok)
|
|
assert.Contains(t, err.Error(), "invalid brain remember output")
|
|
}
|
|
|
|
func TestCommands_CmdBrainList_Bad_MissingAction(t *testing.T) {
|
|
s, _ := testPrepWithCore(t, nil)
|
|
|
|
result := s.cmdBrainList(core.NewOptions())
|
|
|
|
require.False(t, result.OK)
|
|
err, ok := result.Value.(error)
|
|
require.True(t, ok)
|
|
assert.Contains(t, err.Error(), "action not registered")
|
|
}
|
|
|
|
func TestCommands_CmdBrainList_Ugly_InvalidOutput(t *testing.T) {
|
|
s, c := testPrepWithCore(t, nil)
|
|
c.Action("brain.list", func(_ context.Context, _ core.Options) core.Result {
|
|
return core.Result{Value: 123, OK: true}
|
|
})
|
|
|
|
result := s.cmdBrainList(core.NewOptions())
|
|
|
|
require.False(t, result.OK)
|
|
err, ok := result.Value.(error)
|
|
require.True(t, ok)
|
|
assert.Contains(t, err.Error(), "invalid brain list output")
|
|
}
|
|
|
|
func TestCommands_CmdBrainRecall_Good(t *testing.T) {
|
|
s, c := testPrepWithCore(t, nil)
|
|
c.Action("brain.recall", func(_ context.Context, options core.Options) core.Result {
|
|
assert.Equal(t, "workspace handoff context", options.String("query"))
|
|
assert.Equal(t, 3, options.Int("top_k"))
|
|
assert.Equal(t, "agent", options.String("project"))
|
|
assert.Equal(t, "architecture", options.String("type"))
|
|
assert.Equal(t, "virgil", options.String("agent_id"))
|
|
assert.Equal(t, "0.75", options.String("min_confidence"))
|
|
return core.Result{Value: map[string]any{
|
|
"success": true,
|
|
"count": 1,
|
|
"memories": []any{
|
|
map[string]any{
|
|
"id": "mem-1",
|
|
"type": "architecture",
|
|
"content": "Use named actions.",
|
|
"project": "agent",
|
|
"agent_id": "virgil",
|
|
"confidence": 0.75,
|
|
"tags": []any{"architecture", "convention"},
|
|
},
|
|
},
|
|
}, OK: true}
|
|
})
|
|
|
|
output := captureStdout(t, func() {
|
|
result := s.cmdBrainRecall(core.NewOptions(
|
|
core.Option{Key: "_arg", Value: "workspace handoff context"},
|
|
core.Option{Key: "top_k", Value: 3},
|
|
core.Option{Key: "project", Value: "agent"},
|
|
core.Option{Key: "type", Value: "architecture"},
|
|
core.Option{Key: "agent", Value: "virgil"},
|
|
core.Option{Key: "min_confidence", Value: "0.75"},
|
|
))
|
|
require.True(t, result.OK)
|
|
})
|
|
|
|
assert.Contains(t, output, "count: 1")
|
|
assert.Contains(t, output, "mem-1 architecture")
|
|
assert.Contains(t, output, "Use named actions.")
|
|
}
|
|
|
|
func TestCommands_CmdBrainRecall_Bad_MissingQuery(t *testing.T) {
|
|
s, _ := testPrepWithCore(t, nil)
|
|
|
|
result := s.cmdBrainRecall(core.NewOptions())
|
|
|
|
require.False(t, result.OK)
|
|
err, ok := result.Value.(error)
|
|
require.True(t, ok)
|
|
assert.Contains(t, err.Error(), "query is required")
|
|
}
|
|
|
|
func TestCommands_CmdBrainRecall_Ugly_InvalidOutput(t *testing.T) {
|
|
s, c := testPrepWithCore(t, nil)
|
|
c.Action("brain.recall", func(_ context.Context, _ core.Options) core.Result {
|
|
return core.Result{Value: 123, OK: true}
|
|
})
|
|
|
|
result := s.cmdBrainRecall(core.NewOptions(core.Option{Key: "_arg", Value: "workspace handoff context"}))
|
|
|
|
require.False(t, result.OK)
|
|
err, ok := result.Value.(error)
|
|
require.True(t, ok)
|
|
assert.Contains(t, err.Error(), "invalid brain recall output")
|
|
}
|
|
|
|
func TestCommands_CmdBrainForget_Good(t *testing.T) {
|
|
s, c := testPrepWithCore(t, nil)
|
|
c.Action("brain.forget", func(_ context.Context, options core.Options) core.Result {
|
|
assert.Equal(t, "mem-1", options.String("id"))
|
|
assert.Equal(t, "superseded", options.String("reason"))
|
|
return core.Result{Value: map[string]any{
|
|
"success": true,
|
|
"forgotten": "mem-1",
|
|
}, OK: true}
|
|
})
|
|
|
|
output := captureStdout(t, func() {
|
|
result := s.cmdBrainForget(core.NewOptions(
|
|
core.Option{Key: "_arg", Value: "mem-1"},
|
|
core.Option{Key: "reason", Value: "superseded"},
|
|
))
|
|
require.True(t, result.OK)
|
|
})
|
|
|
|
assert.Contains(t, output, "forgotten: mem-1")
|
|
assert.Contains(t, output, "reason: superseded")
|
|
}
|
|
|
|
func TestCommands_CmdBrainForget_Bad_MissingID(t *testing.T) {
|
|
s, _ := testPrepWithCore(t, nil)
|
|
|
|
result := s.cmdBrainForget(core.NewOptions())
|
|
|
|
require.False(t, result.OK)
|
|
err, ok := result.Value.(error)
|
|
require.True(t, ok)
|
|
assert.Contains(t, err.Error(), "memory id is required")
|
|
}
|
|
|
|
func TestCommands_CmdBrainForget_Ugly_ActionFailure(t *testing.T) {
|
|
s, c := testPrepWithCore(t, nil)
|
|
c.Action("brain.forget", func(_ context.Context, _ core.Options) core.Result {
|
|
return core.Result{Value: core.E("brain.forget", "failed to forget memory", nil), OK: false}
|
|
})
|
|
|
|
result := s.cmdBrainForget(core.NewOptions(core.Option{Key: "_arg", Value: "mem-1"}))
|
|
|
|
require.False(t, result.OK)
|
|
err, ok := result.Value.(error)
|
|
require.True(t, ok)
|
|
assert.Contains(t, err.Error(), "failed to forget memory")
|
|
}
|
|
|
|
func TestCommandsforge_CmdIssueCreate_Bad_APIError(t *testing.T) {
|
|
callCount := 0
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
callCount++
|
|
if callCount <= 2 {
|
|
w.Write([]byte(core.JSONMarshalString([]map[string]any{}))) // milestones/labels
|
|
} else {
|
|
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: "Fail"},
|
|
))
|
|
assert.False(t, r.OK)
|
|
}
|
|
|
|
func TestCommandsforge_CmdPRGet_Bad_MissingArgs(t *testing.T) {
|
|
s, _ := testPrepWithCore(t, nil)
|
|
r := s.cmdPRGet(core.NewOptions())
|
|
assert.False(t, r.OK)
|
|
}
|
|
|
|
func TestCommandsforge_CmdPRGet_Good_Success(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Write([]byte(core.JSONMarshalString(map[string]any{
|
|
"number": 3, "title": "Fix", "state": "open", "mergeable": true,
|
|
"html_url": "https://forge.test/pulls/3", "body": "PR body here",
|
|
"head": map[string]any{"ref": "fix/it"}, "base": map[string]any{"ref": "dev"},
|
|
})))
|
|
}))
|
|
t.Cleanup(srv.Close)
|
|
|
|
s, _ := testPrepWithCore(t, srv)
|
|
r := s.cmdPRGet(core.NewOptions(
|
|
core.Option{Key: "_arg", Value: "go-io"},
|
|
core.Option{Key: "number", Value: "3"},
|
|
))
|
|
assert.True(t, r.OK)
|
|
}
|
|
|
|
func TestCommandsforge_CmdPRGet_Bad_APIError(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: "go-io"},
|
|
core.Option{Key: "number", Value: "99"},
|
|
))
|
|
assert.False(t, r.OK)
|
|
}
|
|
|
|
func TestCommandsforge_CmdPRList_Good_WithPRs(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Write([]byte(core.JSONMarshalString([]map[string]any{
|
|
{"number": 1, "title": "Fix", "state": "open",
|
|
"head": map[string]any{"ref": "fix/a"}, "base": map[string]any{"ref": "dev"}},
|
|
{"number": 2, "title": "Feat", "state": "closed",
|
|
"head": map[string]any{"ref": "feat/b"}, "base": map[string]any{"ref": "dev"}},
|
|
})))
|
|
}))
|
|
t.Cleanup(srv.Close)
|
|
|
|
s, _ := testPrepWithCore(t, srv)
|
|
r := s.cmdPRList(core.NewOptions(core.Option{Key: "_arg", Value: "go-io"}))
|
|
assert.True(t, r.OK)
|
|
}
|
|
|
|
func TestCommandsforge_CmdPRList_Bad_APIError(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: "go-io"}))
|
|
assert.False(t, r.OK)
|
|
}
|
|
|
|
func TestCommandsforge_CmdPRMerge_Bad_APIError(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(409)
|
|
w.Write([]byte(core.JSONMarshalString(map[string]any{"message": "conflict"})))
|
|
}))
|
|
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: "5"},
|
|
))
|
|
assert.False(t, r.OK)
|
|
}
|
|
|
|
func TestCommandsforge_CmdPRMerge_Good_CustomMethod(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(200)
|
|
}))
|
|
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: "5"},
|
|
core.Option{Key: "method", Value: "squash"},
|
|
))
|
|
assert.True(t, r.OK)
|
|
}
|
|
|
|
func TestCommandsforge_CmdPRClose_Bad_MissingArgs(t *testing.T) {
|
|
s, _ := testPrepWithCore(t, nil)
|
|
r := s.cmdPRClose(core.NewOptions())
|
|
assert.False(t, r.OK)
|
|
}
|
|
|
|
func TestCommandsforge_CmdPRClose_Good_Success(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
assert.Equal(t, http.MethodPatch, r.Method)
|
|
assert.Equal(t, "/api/v1/repos/core/go-io/pulls/5", r.URL.Path)
|
|
|
|
bodyResult := core.ReadAll(r.Body)
|
|
assert.True(t, bodyResult.OK)
|
|
assert.Contains(t, bodyResult.Value.(string), `"state":"closed"`)
|
|
|
|
w.Write([]byte(core.JSONMarshalString(map[string]any{
|
|
"number": 5,
|
|
"state": "closed",
|
|
})))
|
|
}))
|
|
t.Cleanup(srv.Close)
|
|
|
|
s, _ := testPrepWithCore(t, srv)
|
|
r := s.cmdPRClose(core.NewOptions(
|
|
core.Option{Key: "_arg", Value: "go-io"},
|
|
core.Option{Key: "number", Value: "5"},
|
|
))
|
|
assert.True(t, r.OK)
|
|
}
|
|
|
|
func TestCommandsforge_CmdPRClose_Ugly_APIError(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.cmdPRClose(core.NewOptions(
|
|
core.Option{Key: "_arg", Value: "go-io"},
|
|
core.Option{Key: "number", Value: "5"},
|
|
))
|
|
assert.False(t, r.OK)
|
|
}
|
|
|
|
func TestCommandsforge_CmdIssueGet_Good_WithBody(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Write([]byte(core.JSONMarshalString(map[string]any{
|
|
"number": 1, "title": "Bug", "state": "open",
|
|
"html_url": "https://forge.test/issues/1", "body": "Detailed description",
|
|
})))
|
|
}))
|
|
t.Cleanup(srv.Close)
|
|
|
|
s, _ := testPrepWithCore(t, srv)
|
|
r := s.cmdIssueGet(core.NewOptions(
|
|
core.Option{Key: "_arg", Value: "go-io"},
|
|
core.Option{Key: "number", Value: "1"},
|
|
))
|
|
assert.True(t, r.OK)
|
|
}
|
|
|
|
func TestCommandsforge_CmdIssueList_Bad_APIError(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: "go-io"}))
|
|
assert.False(t, r.OK)
|
|
}
|
|
|
|
func TestCommandsforge_CmdIssueComment_Bad_APIError(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: "test"},
|
|
))
|
|
assert.False(t, r.OK)
|
|
}
|
|
|
|
func TestCommandsforge_CmdRepoGet_Bad_APIError(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.cmdRepoGet(core.NewOptions(core.Option{Key: "_arg", Value: "go-io"}))
|
|
assert.False(t, r.OK)
|
|
}
|
|
|
|
func TestCommandsforge_CmdRepoList_Bad_APIError(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())
|
|
assert.False(t, r.OK)
|
|
}
|
|
|
|
func TestCommandsforge_CmdPRList_Bad_MissingRepo(t *testing.T) {
|
|
s, _ := testPrepWithCore(t, nil)
|
|
r := s.cmdPRList(core.NewOptions())
|
|
assert.False(t, r.OK)
|
|
}
|
|
|
|
func TestCommandsforge_CmdPRList_Good_Empty(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Write([]byte(core.JSONMarshalString([]map[string]any{})))
|
|
}))
|
|
t.Cleanup(srv.Close)
|
|
|
|
s, _ := testPrepWithCore(t, srv)
|
|
r := s.cmdPRList(core.NewOptions(core.Option{Key: "_arg", Value: "go-io"}))
|
|
assert.True(t, r.OK)
|
|
}
|
|
|
|
func TestCommandsforge_CmdPRMerge_Bad_MissingArgs(t *testing.T) {
|
|
s, _ := testPrepWithCore(t, nil)
|
|
r := s.cmdPRMerge(core.NewOptions())
|
|
assert.False(t, r.OK)
|
|
}
|
|
|
|
func TestCommandsforge_CmdPRMerge_Good_DefaultMethod(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(200)
|
|
}))
|
|
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: "5"},
|
|
))
|
|
assert.True(t, r.OK)
|
|
}
|
|
|
|
func TestCommandsforge_CmdRepoGet_Bad_MissingRepo(t *testing.T) {
|
|
s, _ := testPrepWithCore(t, nil)
|
|
r := s.cmdRepoGet(core.NewOptions())
|
|
assert.False(t, r.OK)
|
|
}
|
|
|
|
func TestCommandsforge_CmdRepoGet_Good_Success(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Write([]byte(core.JSONMarshalString(map[string]any{
|
|
"name": "go-io", "description": "IO", "default_branch": "dev",
|
|
"private": false, "archived": false, "html_url": "https://forge.test/go-io",
|
|
"owner": map[string]any{"login": "core"},
|
|
})))
|
|
}))
|
|
t.Cleanup(srv.Close)
|
|
|
|
s, _ := testPrepWithCore(t, srv)
|
|
r := s.cmdRepoGet(core.NewOptions(core.Option{Key: "_arg", Value: "go-io"}))
|
|
assert.True(t, r.OK)
|
|
}
|
|
|
|
func TestCommandsforge_CmdRepoList_Good_Success(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Write([]byte(core.JSONMarshalString([]map[string]any{
|
|
{"name": "go-io", "description": "IO", "archived": false, "owner": map[string]any{"login": "core"}},
|
|
{"name": "go-log", "description": "Logging", "archived": true, "owner": map[string]any{"login": "core"}},
|
|
})))
|
|
}))
|
|
t.Cleanup(srv.Close)
|
|
|
|
s, _ := testPrepWithCore(t, srv)
|
|
r := s.cmdRepoList(core.NewOptions())
|
|
assert.True(t, r.OK)
|
|
}
|
|
|
|
// --- Workspace command methods ---
|
|
|
|
func TestCommandsworkspace_CmdWorkspaceList_Good_Empty(t *testing.T) {
|
|
s, _ := testPrepWithCore(t, nil)
|
|
r := s.cmdWorkspaceList(core.NewOptions())
|
|
assert.True(t, r.OK)
|
|
}
|
|
|
|
func TestCommandsworkspace_CmdWorkspaceList_Good_WithEntries(t *testing.T) {
|
|
s, _ := testPrepWithCore(t, nil)
|
|
|
|
wsRoot := WorkspaceRoot()
|
|
ws := core.JoinPath(wsRoot, "ws-1")
|
|
fs.EnsureDir(ws)
|
|
fs.Write(core.JoinPath(ws, "status.json"), core.JSONMarshalString(WorkspaceStatus{Status: "running", Repo: "go-io", Agent: "codex"}))
|
|
|
|
r := s.cmdWorkspaceList(core.NewOptions())
|
|
assert.True(t, r.OK)
|
|
}
|
|
|
|
func TestCommandsworkspace_CmdWorkspaceClean_Good_Empty(t *testing.T) {
|
|
s, _ := testPrepWithCore(t, nil)
|
|
r := s.cmdWorkspaceClean(core.NewOptions())
|
|
assert.True(t, r.OK)
|
|
}
|
|
|
|
func TestCommandsworkspace_CmdWorkspaceClean_Good_RemovesCompleted(t *testing.T) {
|
|
s, _ := testPrepWithCore(t, nil)
|
|
|
|
wsRoot := WorkspaceRoot()
|
|
ws := core.JoinPath(wsRoot, "ws-done")
|
|
fs.EnsureDir(ws)
|
|
fs.Write(core.JoinPath(ws, "status.json"), core.JSONMarshalString(WorkspaceStatus{Status: "completed", Repo: "go-io", Agent: "codex"}))
|
|
|
|
r := s.cmdWorkspaceClean(core.NewOptions())
|
|
assert.True(t, r.OK)
|
|
|
|
assert.False(t, fs.Exists(ws))
|
|
}
|
|
|
|
func TestCommandsworkspace_CmdWorkspaceClean_Good_FilterFailed(t *testing.T) {
|
|
s, _ := testPrepWithCore(t, nil)
|
|
|
|
wsRoot := WorkspaceRoot()
|
|
for _, ws := range []struct{ name, status string }{
|
|
{"ws-ok", "completed"},
|
|
{"ws-bad", "failed"},
|
|
} {
|
|
d := core.JoinPath(wsRoot, ws.name)
|
|
fs.EnsureDir(d)
|
|
fs.Write(core.JoinPath(d, "status.json"), core.JSONMarshalString(WorkspaceStatus{Status: ws.status, Repo: "test", Agent: "codex"}))
|
|
}
|
|
|
|
r := s.cmdWorkspaceClean(core.NewOptions(core.Option{Key: "_arg", Value: "failed"}))
|
|
assert.True(t, r.OK)
|
|
|
|
assert.False(t, fs.Exists(core.JoinPath(wsRoot, "ws-bad")))
|
|
assert.True(t, fs.Exists(core.JoinPath(wsRoot, "ws-ok")))
|
|
}
|
|
|
|
func TestCommandsworkspace_CmdWorkspaceClean_Good_FilterBlocked(t *testing.T) {
|
|
s, _ := testPrepWithCore(t, nil)
|
|
|
|
wsRoot := WorkspaceRoot()
|
|
d := core.JoinPath(wsRoot, "ws-stuck")
|
|
fs.EnsureDir(d)
|
|
fs.Write(core.JoinPath(d, "status.json"), core.JSONMarshalString(WorkspaceStatus{Status: "blocked", Repo: "test", Agent: "codex"}))
|
|
|
|
r := s.cmdWorkspaceClean(core.NewOptions(core.Option{Key: "_arg", Value: "blocked"}))
|
|
assert.True(t, r.OK)
|
|
|
|
assert.False(t, fs.Exists(d))
|
|
}
|
|
|
|
func TestCommandsworkspace_CmdWorkspaceDispatch_Bad_MissingRepo(t *testing.T) {
|
|
s, _ := testPrepWithCore(t, nil)
|
|
r := s.cmdWorkspaceDispatch(core.NewOptions())
|
|
assert.False(t, r.OK)
|
|
}
|
|
|
|
func TestCommandsworkspace_CmdWorkspaceDispatch_Bad_MissingTask(t *testing.T) {
|
|
s, _ := testPrepWithCore(t, nil)
|
|
r := s.cmdWorkspaceDispatch(core.NewOptions(core.Option{Key: "_arg", Value: "go-io"}))
|
|
assert.False(t, r.OK) // task is required
|
|
}
|
|
|
|
// --- commands.go extracted methods ---
|
|
|
|
func TestCommands_CmdPrep_Bad_MissingRepo(t *testing.T) {
|
|
s, _ := testPrepWithCore(t, nil)
|
|
r := s.cmdPrep(core.NewOptions())
|
|
assert.False(t, r.OK)
|
|
}
|
|
|
|
func TestCommands_CmdPrep_Good_DefaultsToDev(t *testing.T) {
|
|
s, _ := testPrepWithCore(t, nil)
|
|
// Will fail (no local clone) but exercises the default branch logic
|
|
r := s.cmdPrep(core.NewOptions(core.Option{Key: "_arg", Value: "nonexistent-repo"}))
|
|
assert.False(t, r.OK) // expected — no local repo
|
|
}
|
|
|
|
func TestCommands_CmdStatus_Good_Empty(t *testing.T) {
|
|
s, _ := testPrepWithCore(t, nil)
|
|
r := s.cmdStatus(core.NewOptions())
|
|
assert.True(t, r.OK)
|
|
}
|
|
|
|
func TestCommands_CmdStatus_Good_WithWorkspaces(t *testing.T) {
|
|
s, _ := testPrepWithCore(t, nil)
|
|
|
|
wsRoot := WorkspaceRoot()
|
|
ws := core.JoinPath(wsRoot, "ws-1")
|
|
fs.EnsureDir(ws)
|
|
fs.Write(core.JoinPath(ws, "status.json"), core.JSONMarshalString(WorkspaceStatus{Status: "completed", Repo: "test", Agent: "codex"}))
|
|
|
|
r := s.cmdStatus(core.NewOptions())
|
|
assert.True(t, r.OK)
|
|
}
|
|
|
|
func TestCommands_CmdStatus_Good_DeepWorkspace(t *testing.T) {
|
|
s, _ := testPrepWithCore(t, nil)
|
|
|
|
ws := core.JoinPath(WorkspaceRoot(), "core", "go-io", "task-5")
|
|
fs.EnsureDir(ws)
|
|
fs.Write(core.JoinPath(ws, "status.json"), core.JSONMarshalString(WorkspaceStatus{
|
|
Status: "completed",
|
|
Repo: "go-io",
|
|
Agent: "codex",
|
|
}))
|
|
|
|
output := captureStdout(t, func() {
|
|
r := s.cmdStatus(core.NewOptions())
|
|
assert.True(t, r.OK)
|
|
})
|
|
|
|
assert.Contains(t, output, "completed")
|
|
assert.Contains(t, output, "codex")
|
|
assert.Contains(t, output, "go-io")
|
|
assert.Contains(t, output, "core/go-io/task-5")
|
|
}
|
|
|
|
func TestCommands_CmdStatus_Good_BranchWorkspace(t *testing.T) {
|
|
s, _ := testPrepWithCore(t, nil)
|
|
|
|
ws := core.JoinPath(WorkspaceRoot(), "core", "go-io", "feature", "new-ui")
|
|
fs.EnsureDir(WorkspaceRepoDir(ws))
|
|
fs.EnsureDir(WorkspaceMetaDir(ws))
|
|
fs.Write(core.JoinPath(ws, "status.json"), core.JSONMarshalString(WorkspaceStatus{
|
|
Status: "completed",
|
|
Repo: "go-io",
|
|
Agent: "codex",
|
|
}))
|
|
|
|
output := captureStdout(t, func() {
|
|
r := s.cmdStatus(core.NewOptions())
|
|
assert.True(t, r.OK)
|
|
})
|
|
|
|
assert.Contains(t, output, "completed")
|
|
assert.Contains(t, output, "core/go-io/feature/new-ui")
|
|
}
|
|
|
|
func TestCommands_CmdStatus_Good_BlockedQuestion(t *testing.T) {
|
|
s, _ := testPrepWithCore(t, nil)
|
|
|
|
ws := core.JoinPath(WorkspaceRoot(), "core", "go-io", "task-9")
|
|
fs.EnsureDir(ws)
|
|
fs.Write(core.JoinPath(ws, "status.json"), core.JSONMarshalString(WorkspaceStatus{
|
|
Status: "blocked",
|
|
Repo: "go-io",
|
|
Agent: "gemini",
|
|
Question: "Which API version?",
|
|
}))
|
|
|
|
output := captureStdout(t, func() {
|
|
r := s.cmdStatus(core.NewOptions())
|
|
assert.True(t, r.OK)
|
|
})
|
|
|
|
assert.Contains(t, output, "blocked")
|
|
assert.Contains(t, output, "gemini")
|
|
assert.Contains(t, output, "go-io")
|
|
assert.Contains(t, output, "Which API version?")
|
|
}
|
|
|
|
func TestCommands_CmdStatus_Good_WorkspaceFilter(t *testing.T) {
|
|
s, _ := testPrepWithCore(t, nil)
|
|
|
|
wsA := core.JoinPath(WorkspaceRoot(), "core", "go-io", "task-9")
|
|
fs.EnsureDir(wsA)
|
|
fs.Write(core.JoinPath(wsA, "status.json"), core.JSONMarshalString(WorkspaceStatus{
|
|
Status: "blocked",
|
|
Repo: "go-io",
|
|
Agent: "gemini",
|
|
}))
|
|
|
|
wsB := core.JoinPath(WorkspaceRoot(), "core", "go-log", "task-4")
|
|
fs.EnsureDir(wsB)
|
|
fs.Write(core.JoinPath(wsB, "status.json"), core.JSONMarshalString(WorkspaceStatus{
|
|
Status: "completed",
|
|
Repo: "go-log",
|
|
Agent: "codex",
|
|
}))
|
|
|
|
output := captureStdout(t, func() {
|
|
r := s.cmdStatus(core.NewOptions(core.Option{Key: "workspace", Value: "core/go-io/task-9"}))
|
|
assert.True(t, r.OK)
|
|
})
|
|
|
|
assert.Contains(t, output, "core/go-io/task-9")
|
|
assert.NotContains(t, output, "core/go-log/task-4")
|
|
}
|
|
|
|
func TestCommands_CmdStatus_Good_StatusFilterAndLimit(t *testing.T) {
|
|
s, _ := testPrepWithCore(t, nil)
|
|
|
|
for _, workspace := range []struct {
|
|
name string
|
|
status string
|
|
repo string
|
|
}{
|
|
{name: "ws-1", status: "blocked", repo: "go-io"},
|
|
{name: "ws-2", status: "blocked", repo: "go-log"},
|
|
{name: "ws-3", status: "completed", repo: "go-scm"},
|
|
} {
|
|
ws := core.JoinPath(WorkspaceRoot(), workspace.name)
|
|
fs.EnsureDir(ws)
|
|
fs.Write(core.JoinPath(ws, "status.json"), core.JSONMarshalString(WorkspaceStatus{
|
|
Status: workspace.status,
|
|
Repo: workspace.repo,
|
|
Agent: "codex",
|
|
}))
|
|
}
|
|
|
|
output := captureStdout(t, func() {
|
|
r := s.cmdStatus(core.NewOptions(
|
|
core.Option{Key: "status", Value: "blocked"},
|
|
core.Option{Key: "limit", Value: 1},
|
|
))
|
|
assert.True(t, r.OK)
|
|
})
|
|
|
|
assert.Contains(t, output, "ws-1")
|
|
assert.NotContains(t, output, "ws-2")
|
|
assert.NotContains(t, output, "ws-3")
|
|
}
|
|
|
|
func TestCommands_CmdPrompt_Bad_MissingRepo(t *testing.T) {
|
|
s, _ := testPrepWithCore(t, nil)
|
|
r := s.cmdPrompt(core.NewOptions())
|
|
assert.False(t, r.OK)
|
|
}
|
|
|
|
func TestCommands_CmdPrompt_Good_DefaultTask(t *testing.T) {
|
|
s, _ := testPrepWithCore(t, nil)
|
|
r := s.cmdPrompt(core.NewOptions(core.Option{Key: "_arg", Value: "go-io"}))
|
|
assert.True(t, r.OK)
|
|
}
|
|
|
|
func TestCommands_CmdPrompt_Good_PlanTemplateVariables(t *testing.T) {
|
|
home := t.TempDir()
|
|
t.Setenv("CORE_HOME", home)
|
|
|
|
repoDir := core.JoinPath(home, "Code", "core", "go-io")
|
|
fs.EnsureDir(repoDir)
|
|
fs.Write(core.JoinPath(repoDir, "go.mod"), "module example.com/go-io\n\ngo 1.24\n")
|
|
|
|
s, _ := testPrepWithCore(t, nil)
|
|
output := captureStdout(t, func() {
|
|
r := s.cmdPrompt(core.NewOptions(
|
|
core.Option{Key: "_arg", Value: "go-io"},
|
|
core.Option{Key: "task", Value: "Add a login flow"},
|
|
core.Option{Key: "plan_template", Value: "new-feature"},
|
|
core.Option{Key: "variables", Value: `{"feature_name":"Authentication"}`},
|
|
))
|
|
assert.True(t, r.OK)
|
|
})
|
|
|
|
assert.Contains(t, output, "PLAN:")
|
|
assert.Contains(t, output, "Authentication")
|
|
}
|
|
|
|
func TestCommands_CmdGenerate_Bad_MissingPrompt(t *testing.T) {
|
|
s, _ := testPrepWithCore(t, nil)
|
|
r := s.cmdGenerate(core.NewOptions())
|
|
assert.False(t, r.OK)
|
|
}
|
|
|
|
func TestCommands_CmdGenerate_Good(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
assert.Equal(t, "/v1/content/generate", r.URL.Path)
|
|
assert.Equal(t, http.MethodPost, r.Method)
|
|
_, _ = w.Write([]byte(`{"data":{"id":"gen_1","provider":"claude","model":"claude-3.7-sonnet","content":"Release notes draft","input_tokens":12,"output_tokens":48,"status":"completed"}}`))
|
|
}))
|
|
defer server.Close()
|
|
|
|
s := testPrepWithPlatformServer(t, server, "secret-token")
|
|
output := captureStdout(t, func() {
|
|
r := s.cmdGenerate(core.NewOptions(
|
|
core.Option{Key: "_arg", Value: "Draft a release note"},
|
|
core.Option{Key: "provider", Value: "claude"},
|
|
))
|
|
assert.True(t, r.OK)
|
|
})
|
|
|
|
assert.Contains(t, output, "provider: claude")
|
|
assert.Contains(t, output, "model: claude-3.7-sonnet")
|
|
assert.Contains(t, output, "status: completed")
|
|
assert.Contains(t, output, "content: Release notes draft")
|
|
}
|
|
|
|
func TestCommands_CmdGenerate_Good_BriefTemplate(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
assert.Equal(t, "/v1/content/generate", r.URL.Path)
|
|
assert.Equal(t, http.MethodPost, r.Method)
|
|
|
|
bodyResult := core.ReadAll(r.Body)
|
|
require.True(t, bodyResult.OK)
|
|
|
|
var payload map[string]any
|
|
parseResult := core.JSONUnmarshalString(bodyResult.Value.(string), &payload)
|
|
require.True(t, parseResult.OK)
|
|
assert.Equal(t, "brief_1", payload["brief_id"])
|
|
assert.Equal(t, "help-article", payload["template"])
|
|
|
|
_, _ = w.Write([]byte(`{"data":{"id":"gen_2","provider":"claude","model":"claude-3.7-sonnet","content":"Template draft","status":"completed"}}`))
|
|
}))
|
|
defer server.Close()
|
|
|
|
s := testPrepWithPlatformServer(t, server, "secret-token")
|
|
output := captureStdout(t, func() {
|
|
r := s.cmdGenerate(core.NewOptions(
|
|
core.Option{Key: "brief_id", Value: "brief_1"},
|
|
core.Option{Key: "template", Value: "help-article"},
|
|
core.Option{Key: "provider", Value: "claude"},
|
|
))
|
|
assert.True(t, r.OK)
|
|
})
|
|
|
|
assert.Contains(t, output, "provider: claude")
|
|
assert.Contains(t, output, "model: claude-3.7-sonnet")
|
|
assert.Contains(t, output, "status: completed")
|
|
assert.Contains(t, output, "content: Template draft")
|
|
}
|
|
|
|
func TestCommands_CmdContentSchemaGenerate_Good(t *testing.T) {
|
|
s, _ := testPrepWithCore(t, nil)
|
|
output := captureStdout(t, func() {
|
|
r := s.cmdContentSchemaGenerate(core.NewOptions(
|
|
core.Option{Key: "type", Value: "howto"},
|
|
core.Option{Key: "title", Value: "Set up the workspace"},
|
|
core.Option{Key: "description", Value: "Prepare a fresh workspace for an agent."},
|
|
core.Option{Key: "url", Value: "https://example.test/workspace"},
|
|
core.Option{Key: "steps", Value: `[{"name":"Clone","text":"Clone the repository."},{"name":"Prepare","text":"Build the prompt."}]`},
|
|
))
|
|
assert.True(t, r.OK)
|
|
})
|
|
|
|
assert.Contains(t, output, "schema type: HowTo")
|
|
assert.Contains(t, output, `"@type":"HowTo"`)
|
|
assert.Contains(t, output, `"name":"Set up the workspace"`)
|
|
}
|
|
|
|
func TestCommands_CmdContentSchemaGenerate_Bad_MissingTitle(t *testing.T) {
|
|
s, _ := testPrepWithCore(t, nil)
|
|
r := s.cmdContentSchemaGenerate(core.NewOptions(
|
|
core.Option{Key: "type", Value: "howto"},
|
|
))
|
|
assert.False(t, r.OK)
|
|
}
|
|
|
|
func TestCommands_CmdContentSchemaGenerate_Ugly_InvalidSchemaType(t *testing.T) {
|
|
s, _ := testPrepWithCore(t, nil)
|
|
r := s.cmdContentSchemaGenerate(core.NewOptions(
|
|
core.Option{Key: "type", Value: "toast"},
|
|
core.Option{Key: "title", Value: "Set up the workspace"},
|
|
))
|
|
assert.False(t, r.OK)
|
|
}
|
|
|
|
func TestCommands_CmdComplete_Good(t *testing.T) {
|
|
s, c := testPrepWithCore(t, nil)
|
|
|
|
c.Action("noop", func(_ context.Context, _ core.Options) core.Result {
|
|
return core.Result{OK: true}
|
|
})
|
|
c.Task("agent.completion", core.Task{
|
|
Description: "QA → PR → Verify → Commit → Ingest → Poke",
|
|
Steps: []core.Step{
|
|
{Action: "noop"},
|
|
},
|
|
})
|
|
|
|
r := s.cmdComplete(core.NewOptions(
|
|
core.Option{Key: "workspace", Value: core.JoinPath(WorkspaceRoot(), "core/go-io/task-42")},
|
|
))
|
|
assert.True(t, r.OK)
|
|
}
|
|
|
|
func TestCommands_CmdComplete_Bad_MissingTask(t *testing.T) {
|
|
s, _ := testPrepWithCore(t, nil)
|
|
|
|
r := s.cmdComplete(core.NewOptions())
|
|
assert.False(t, r.OK)
|
|
}
|
|
|
|
func TestCommands_CmdScan_Good(t *testing.T) {
|
|
server := mockScanServer(t)
|
|
s := &PrepSubsystem{
|
|
ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}),
|
|
forge: forge.NewForge(server.URL, "secret-token"),
|
|
forgeURL: server.URL,
|
|
forgeToken: "secret-token",
|
|
backoff: make(map[string]time.Time),
|
|
failCount: make(map[string]int),
|
|
}
|
|
|
|
output := captureStdout(t, func() {
|
|
r := s.cmdScan(core.NewOptions(
|
|
core.Option{Key: "org", Value: "core"},
|
|
core.Option{Key: "labels", Value: "agentic,bug"},
|
|
core.Option{Key: "limit", Value: 5},
|
|
))
|
|
assert.True(t, r.OK)
|
|
})
|
|
|
|
assert.Contains(t, output, "count:")
|
|
assert.Contains(t, output, "go-io#10")
|
|
assert.Contains(t, output, "Add missing tests")
|
|
}
|
|
|
|
func TestCommands_CmdScan_Bad_NoForgeToken(t *testing.T) {
|
|
s, _ := testPrepWithCore(t, nil)
|
|
s.forgeToken = ""
|
|
|
|
r := s.cmdScan(core.NewOptions())
|
|
assert.False(t, r.OK)
|
|
}
|
|
|
|
func TestCommands_CmdScan_Ugly_EmptyResults(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch {
|
|
case r.URL.Path == "/api/v1/orgs/core/repos":
|
|
_, _ = w.Write([]byte(core.JSONMarshalString([]map[string]any{
|
|
{"name": "go-io"},
|
|
})))
|
|
default:
|
|
_, _ = w.Write([]byte(core.JSONMarshalString([]map[string]any{})))
|
|
}
|
|
}))
|
|
defer server.Close()
|
|
|
|
s := &PrepSubsystem{
|
|
ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}),
|
|
forge: forge.NewForge(server.URL, "secret-token"),
|
|
forgeURL: server.URL,
|
|
forgeToken: "secret-token",
|
|
backoff: make(map[string]time.Time),
|
|
failCount: make(map[string]int),
|
|
}
|
|
output := captureStdout(t, func() {
|
|
r := s.cmdScan(core.NewOptions(
|
|
core.Option{Key: "org", Value: "core"},
|
|
core.Option{Key: "limit", Value: 1},
|
|
))
|
|
assert.True(t, r.OK)
|
|
})
|
|
|
|
assert.Contains(t, output, "count: 0")
|
|
}
|
|
|
|
func TestCommands_CmdPlanCreate_Good(t *testing.T) {
|
|
s, _ := testPrepWithCore(t, nil)
|
|
|
|
r := s.cmdPlanCreate(core.NewOptions(
|
|
core.Option{Key: "slug", Value: "migrate-core"},
|
|
core.Option{Key: "title", Value: "Migrate Core"},
|
|
core.Option{Key: "objective", Value: "Use Core.Process everywhere"},
|
|
))
|
|
|
|
assert.True(t, r.OK)
|
|
|
|
output, ok := r.Value.(PlanCreateOutput)
|
|
require.True(t, ok)
|
|
require.NotEmpty(t, output.ID)
|
|
require.NotEmpty(t, output.Path)
|
|
assert.True(t, fs.Exists(output.Path))
|
|
|
|
plan, err := readPlan(PlansRoot(), output.ID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "Migrate Core", plan.Title)
|
|
assert.Equal(t, "Use Core.Process everywhere", plan.Objective)
|
|
assert.Equal(t, "draft", plan.Status)
|
|
assert.Equal(t, "migrate-core", plan.Slug)
|
|
}
|
|
|
|
func TestCommands_CmdPlanStatus_Good_GetAndSet(t *testing.T) {
|
|
s, _ := testPrepWithCore(t, nil)
|
|
|
|
_, created, err := s.planCreate(context.Background(), nil, PlanCreateInput{
|
|
Title: "Status Plan",
|
|
Objective: "Exercise plan status management",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
getOutput := captureStdout(t, func() {
|
|
r := s.cmdPlanStatus(core.NewOptions(core.Option{Key: "_arg", Value: created.ID}))
|
|
assert.True(t, r.OK)
|
|
})
|
|
assert.Contains(t, getOutput, "status:")
|
|
assert.Contains(t, getOutput, "draft")
|
|
|
|
setOutput := captureStdout(t, func() {
|
|
r := s.cmdPlanStatus(core.NewOptions(
|
|
core.Option{Key: "_arg", Value: created.ID},
|
|
core.Option{Key: "set", Value: "ready"},
|
|
))
|
|
assert.True(t, r.OK)
|
|
})
|
|
assert.Contains(t, setOutput, "status:")
|
|
assert.Contains(t, setOutput, "ready")
|
|
|
|
plan, err := readPlan(PlansRoot(), created.ID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "ready", plan.Status)
|
|
}
|
|
|
|
func TestCommands_CmdPlanArchive_Good(t *testing.T) {
|
|
s, _ := testPrepWithCore(t, nil)
|
|
|
|
_, created, err := s.planCreate(context.Background(), nil, PlanCreateInput{
|
|
Title: "Archive Plan",
|
|
Objective: "Exercise archive command",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
output := captureStdout(t, func() {
|
|
r := s.cmdPlanArchive(core.NewOptions(
|
|
core.Option{Key: "_arg", Value: created.ID},
|
|
))
|
|
assert.True(t, r.OK)
|
|
})
|
|
|
|
assert.Contains(t, output, "archived:")
|
|
|
|
plan, err := readPlan(PlansRoot(), created.ID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "archived", plan.Status)
|
|
assert.False(t, plan.ArchivedAt.IsZero())
|
|
}
|
|
|
|
func TestCommands_CmdPlanDelete_Good(t *testing.T) {
|
|
s, _ := testPrepWithCore(t, nil)
|
|
|
|
_, created, err := s.planCreate(context.Background(), nil, PlanCreateInput{
|
|
Title: "Delete Plan",
|
|
Objective: "Exercise delete command",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
output := captureStdout(t, func() {
|
|
r := s.cmdPlanDelete(core.NewOptions(
|
|
core.Option{Key: "_arg", Value: created.ID},
|
|
core.Option{Key: "reason", Value: "RFC contract says soft delete"},
|
|
))
|
|
assert.True(t, r.OK)
|
|
})
|
|
|
|
assert.Contains(t, output, "deleted:")
|
|
|
|
assert.False(t, fs.Exists(created.Path))
|
|
|
|
_, readErr := readPlan(PlansRoot(), created.ID)
|
|
require.Error(t, readErr)
|
|
}
|
|
|
|
func TestCommands_CmdExtract_Good(t *testing.T) {
|
|
s, _ := testPrepWithCore(t, nil)
|
|
target := core.JoinPath(t.TempDir(), "extract-test")
|
|
r := s.cmdExtract(core.NewOptions(
|
|
core.Option{Key: "_arg", Value: "default"},
|
|
core.Option{Key: "target", Value: target},
|
|
))
|
|
assert.True(t, r.OK)
|
|
}
|
|
|
|
func TestCommands_CmdExtract_Good_FromAgentOutput(t *testing.T) {
|
|
s, _ := testPrepWithCore(t, nil)
|
|
dir := t.TempDir()
|
|
source := core.JoinPath(dir, "agent-output.md")
|
|
target := core.JoinPath(dir, "extracted.json")
|
|
require.True(t, fs.Write(source, "Agent run complete.\n\n```json\n{\"summary\":\"done\",\"findings\":2}\n```\n").OK)
|
|
|
|
output := captureStdout(t, func() {
|
|
r := s.cmdExtract(core.NewOptions(
|
|
core.Option{Key: "source", Value: source},
|
|
core.Option{Key: "target", Value: target},
|
|
))
|
|
assert.True(t, r.OK)
|
|
assert.Equal(t, "{\"summary\":\"done\",\"findings\":2}", r.Value)
|
|
})
|
|
|
|
assert.Contains(t, output, "written: ")
|
|
assert.True(t, fs.Exists(target))
|
|
written := fs.Read(target)
|
|
require.True(t, written.OK)
|
|
assert.Equal(t, "{\"summary\":\"done\",\"findings\":2}", written.Value)
|
|
}
|
|
|
|
func TestCommands_CmdExtract_Bad_NoExtractableContent(t *testing.T) {
|
|
s, _ := testPrepWithCore(t, nil)
|
|
dir := t.TempDir()
|
|
source := core.JoinPath(dir, "agent-output.md")
|
|
require.True(t, fs.Write(source, "Agent run complete.\nNothing structured here.\n").OK)
|
|
|
|
r := s.cmdExtract(core.NewOptions(core.Option{Key: "source", Value: source}))
|
|
assert.False(t, r.OK)
|
|
}
|
|
|
|
func TestCommands_CmdRunTask_Bad_MissingArgs(t *testing.T) {
|
|
s, _ := testPrepWithCore(t, nil)
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
s.startupContext = ctx
|
|
r := s.cmdRunTask(core.NewOptions())
|
|
assert.False(t, r.OK)
|
|
}
|
|
|
|
func TestCommands_CmdDispatchSync_Bad_MissingArgs(t *testing.T) {
|
|
s, _ := testPrepWithCore(t, nil)
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
s.startupContext = ctx
|
|
r := s.cmdDispatchSync(core.NewOptions())
|
|
assert.False(t, r.OK)
|
|
}
|
|
|
|
func TestCommands_CmdRunTask_Bad_MissingTask(t *testing.T) {
|
|
s, _ := testPrepWithCore(t, nil)
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
s.startupContext = ctx
|
|
r := s.cmdRunTask(core.NewOptions(core.Option{Key: "repo", Value: "go-io"}))
|
|
assert.False(t, r.OK)
|
|
}
|
|
|
|
func TestCommands_CmdOrchestrator_Good_CancelledCtx(t *testing.T) {
|
|
s, _ := testPrepWithCore(t, nil)
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel() // cancel immediately
|
|
s.startupContext = ctx
|
|
r := s.cmdOrchestrator(core.NewOptions())
|
|
assert.True(t, r.OK)
|
|
}
|
|
|
|
func TestCommands_CmdDispatch_Good_CancelledCtx(t *testing.T) {
|
|
s, _ := testPrepWithCore(t, nil)
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
s.startupContext = ctx
|
|
r := s.cmdDispatch(core.NewOptions())
|
|
assert.True(t, r.OK)
|
|
}
|
|
|
|
func TestCommands_CmdDispatchStart_Good(t *testing.T) {
|
|
s, c := testPrepWithCore(t, nil)
|
|
called := false
|
|
c.Action("runner.start", func(_ context.Context, _ core.Options) core.Result {
|
|
called = true
|
|
return core.Result{OK: true}
|
|
})
|
|
|
|
output := captureStdout(t, func() {
|
|
r := s.cmdDispatchStart(core.NewOptions())
|
|
assert.True(t, r.OK)
|
|
})
|
|
|
|
assert.True(t, called)
|
|
assert.Contains(t, output, "dispatch started")
|
|
}
|
|
|
|
func TestCommands_CmdDispatchShutdown_Good(t *testing.T) {
|
|
s, c := testPrepWithCore(t, nil)
|
|
called := false
|
|
c.Action("runner.stop", func(_ context.Context, _ core.Options) core.Result {
|
|
called = true
|
|
return core.Result{OK: true}
|
|
})
|
|
|
|
output := captureStdout(t, func() {
|
|
r := s.cmdDispatchShutdown(core.NewOptions())
|
|
assert.True(t, r.OK)
|
|
})
|
|
|
|
assert.True(t, called)
|
|
assert.Contains(t, output, "queue frozen")
|
|
}
|
|
|
|
func TestCommands_CmdDispatchShutdownNow_Good(t *testing.T) {
|
|
s, c := testPrepWithCore(t, nil)
|
|
called := false
|
|
c.Action("runner.kill", func(_ context.Context, _ core.Options) core.Result {
|
|
called = true
|
|
return core.Result{OK: true}
|
|
})
|
|
|
|
output := captureStdout(t, func() {
|
|
r := s.cmdDispatchShutdownNow(core.NewOptions())
|
|
assert.True(t, r.OK)
|
|
})
|
|
|
|
assert.True(t, called)
|
|
assert.Contains(t, output, "killed all agents")
|
|
}
|
|
|
|
func TestCommands_CmdPoke_Good(t *testing.T) {
|
|
s, c := testPrepWithCore(t, nil)
|
|
called := false
|
|
c.Action("runner.poke", func(_ context.Context, _ core.Options) core.Result {
|
|
called = true
|
|
return core.Result{OK: true}
|
|
})
|
|
|
|
output := captureStdout(t, func() {
|
|
r := s.cmdPoke(core.NewOptions())
|
|
assert.True(t, r.OK)
|
|
})
|
|
|
|
assert.True(t, called)
|
|
assert.Contains(t, output, "queue poke requested")
|
|
}
|
|
|
|
func TestCommands_ParseIntStr_Good(t *testing.T) {
|
|
assert.Equal(t, 42, parseIntString("42"))
|
|
assert.Equal(t, 123, parseIntString("issue-123"))
|
|
assert.Equal(t, 0, parseIntString(""))
|
|
assert.Equal(t, 0, parseIntString("abc"))
|
|
assert.Equal(t, 7, parseIntString("#7"))
|
|
}
|
|
|
|
// --- Registration verification ---
|
|
|
|
func TestCommands_RegisterCommands_Good_AllRegistered(t *testing.T) {
|
|
s, c := testPrepWithCore(t, nil)
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
s.registerCommands(ctx)
|
|
|
|
cmds := c.Commands()
|
|
assert.Contains(t, cmds, "run/task")
|
|
assert.Contains(t, cmds, "run/flow")
|
|
assert.Contains(t, cmds, "flow/preview")
|
|
assert.Contains(t, cmds, "agentic:flow/preview")
|
|
assert.Contains(t, cmds, "dispatch/sync")
|
|
assert.Contains(t, cmds, "run/orchestrator")
|
|
assert.Contains(t, cmds, "dispatch")
|
|
assert.Contains(t, cmds, "agentic:dispatch")
|
|
assert.Contains(t, cmds, "dispatch/start")
|
|
assert.Contains(t, cmds, "agentic:dispatch/start")
|
|
assert.Contains(t, cmds, "dispatch/shutdown")
|
|
assert.Contains(t, cmds, "agentic:dispatch/shutdown")
|
|
assert.Contains(t, cmds, "dispatch/shutdown-now")
|
|
assert.Contains(t, cmds, "agentic:dispatch/shutdown-now")
|
|
assert.Contains(t, cmds, "poke")
|
|
assert.Contains(t, cmds, "agentic:poke")
|
|
assert.Contains(t, cmds, "prep")
|
|
assert.Contains(t, cmds, "agentic:prep-workspace")
|
|
assert.Contains(t, cmds, "resume")
|
|
assert.Contains(t, cmds, "agentic:resume")
|
|
assert.Contains(t, cmds, "content/generate")
|
|
assert.Contains(t, cmds, "agentic:content/generate")
|
|
assert.Contains(t, cmds, "content/schema/generate")
|
|
assert.Contains(t, cmds, "agentic:content/schema/generate")
|
|
assert.Contains(t, cmds, "complete")
|
|
assert.Contains(t, cmds, "agentic:complete")
|
|
assert.Contains(t, cmds, "scan")
|
|
assert.Contains(t, cmds, "agentic:scan")
|
|
assert.Contains(t, cmds, "mirror")
|
|
assert.Contains(t, cmds, "agentic:mirror")
|
|
assert.Contains(t, cmds, "brain/ingest")
|
|
assert.Contains(t, cmds, "brain:ingest")
|
|
assert.Contains(t, cmds, "brain/seed-memory")
|
|
assert.Contains(t, cmds, "brain:seed-memory")
|
|
assert.Contains(t, cmds, "brain/list")
|
|
assert.Contains(t, cmds, "brain:list")
|
|
assert.Contains(t, cmds, "brain/forget")
|
|
assert.Contains(t, cmds, "brain:forget")
|
|
assert.Contains(t, cmds, "status")
|
|
assert.Contains(t, cmds, "agentic:status")
|
|
assert.Contains(t, cmds, "prompt")
|
|
assert.Contains(t, cmds, "agentic:prompt")
|
|
assert.Contains(t, cmds, "prompt_version")
|
|
assert.Contains(t, cmds, "agentic:prompt_version")
|
|
assert.Contains(t, cmds, "prompt/version")
|
|
assert.Contains(t, cmds, "extract")
|
|
assert.Contains(t, cmds, "lang/detect")
|
|
assert.Contains(t, cmds, "lang/list")
|
|
assert.Contains(t, cmds, "epic")
|
|
assert.Contains(t, cmds, "agentic:epic")
|
|
assert.Contains(t, cmds, "plan")
|
|
assert.Contains(t, cmds, "agentic:plan/create")
|
|
assert.Contains(t, cmds, "plan/create")
|
|
assert.Contains(t, cmds, "agentic:plan/list")
|
|
assert.Contains(t, cmds, "plan/list")
|
|
assert.Contains(t, cmds, "agentic:plan/read")
|
|
assert.Contains(t, cmds, "plan/read")
|
|
assert.Contains(t, cmds, "agentic:plan/show")
|
|
assert.Contains(t, cmds, "plan/show")
|
|
assert.Contains(t, cmds, "agentic:plan/status")
|
|
assert.Contains(t, cmds, "plan/update")
|
|
assert.Contains(t, cmds, "agentic:plan/update")
|
|
assert.Contains(t, cmds, "agentic:plan/check")
|
|
assert.Contains(t, cmds, "plan/status")
|
|
assert.Contains(t, cmds, "plan/check")
|
|
assert.Contains(t, cmds, "agentic:plan/archive")
|
|
assert.Contains(t, cmds, "plan/archive")
|
|
assert.Contains(t, cmds, "agentic:plan/delete")
|
|
assert.Contains(t, cmds, "plan/delete")
|
|
assert.Contains(t, cmds, "agentic:plan-cleanup")
|
|
assert.Contains(t, cmds, "commit")
|
|
assert.Contains(t, cmds, "agentic:commit")
|
|
assert.Contains(t, cmds, "session/start")
|
|
assert.Contains(t, cmds, "session/get")
|
|
assert.Contains(t, cmds, "agentic:session/get")
|
|
assert.Contains(t, cmds, "session/list")
|
|
assert.Contains(t, cmds, "agentic:session/list")
|
|
assert.Contains(t, cmds, "agentic:session/start")
|
|
assert.Contains(t, cmds, "session/continue")
|
|
assert.Contains(t, cmds, "agentic:session/continue")
|
|
assert.Contains(t, cmds, "session/end")
|
|
assert.Contains(t, cmds, "agentic:session/end")
|
|
assert.Contains(t, cmds, "pr-manage")
|
|
assert.Contains(t, cmds, "agentic:pr-manage")
|
|
assert.Contains(t, cmds, "review-queue")
|
|
assert.Contains(t, cmds, "agentic:review-queue")
|
|
assert.Contains(t, cmds, "task")
|
|
assert.Contains(t, cmds, "task/update")
|
|
assert.Contains(t, cmds, "task/toggle")
|
|
assert.Contains(t, cmds, "sprint")
|
|
assert.Contains(t, cmds, "sprint/create")
|
|
}
|
|
|
|
func TestCommands_CmdPRManage_Good_NoCandidates(t *testing.T) {
|
|
s, _ := testPrepWithCore(t, nil)
|
|
r := s.cmdPRManage(core.NewOptions())
|
|
assert.True(t, r.OK)
|
|
}
|
|
|
|
func TestCommands_CmdMirror_Good_NoRepos(t *testing.T) {
|
|
s, _ := testPrepWithCore(t, nil)
|
|
output := captureStdout(t, func() {
|
|
r := s.cmdMirror(core.NewOptions())
|
|
assert.True(t, r.OK)
|
|
})
|
|
|
|
assert.Contains(t, output, "count: 0")
|
|
}
|
|
|
|
// --- CmdExtract Bad/Ugly ---
|
|
|
|
func TestCommands_CmdExtract_Bad_TargetDirAlreadyHasFiles(t *testing.T) {
|
|
s, _ := testPrepWithCore(t, nil)
|
|
target := core.JoinPath(t.TempDir(), "extract-existing")
|
|
fs.EnsureDir(target)
|
|
fs.Write(core.JoinPath(target, "existing.txt"), "data")
|
|
|
|
// Missing template arg uses "default", target already has files — still succeeds (overwrites)
|
|
r := s.cmdExtract(core.NewOptions(
|
|
core.Option{Key: "target", Value: target},
|
|
))
|
|
assert.True(t, r.OK)
|
|
}
|
|
|
|
func TestCommands_CmdExtract_Ugly_TargetIsFile(t *testing.T) {
|
|
s, _ := testPrepWithCore(t, nil)
|
|
target := core.JoinPath(t.TempDir(), "not-a-dir")
|
|
fs.Write(target, "I am a file")
|
|
|
|
r := s.cmdExtract(core.NewOptions(
|
|
core.Option{Key: "_arg", Value: "default"},
|
|
core.Option{Key: "target", Value: target},
|
|
))
|
|
// Extraction should fail because target is a file, not a directory
|
|
assert.False(t, r.OK)
|
|
}
|
|
|
|
// --- CmdOrchestrator Bad/Ugly ---
|
|
|
|
func TestCommands_CmdOrchestrator_Bad_DoneContext(t *testing.T) {
|
|
s, _ := testPrepWithCore(t, nil)
|
|
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(-1*time.Second))
|
|
defer cancel()
|
|
s.startupContext = ctx
|
|
r := s.cmdOrchestrator(core.NewOptions())
|
|
assert.True(t, r.OK) // returns OK after ctx.Done()
|
|
}
|
|
|
|
func TestCommands_CmdOrchestrator_Ugly_CancelledImmediately(t *testing.T) {
|
|
s, _ := testPrepWithCore(t, nil)
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
s.startupContext = ctx
|
|
r := s.cmdOrchestrator(core.NewOptions())
|
|
assert.True(t, r.OK) // exits immediately when context is already cancelled
|
|
}
|
|
|
|
// --- CmdPrep Ugly ---
|
|
|
|
func TestCommands_CmdPrep_Ugly_AllOptionalFields(t *testing.T) {
|
|
s, _ := testPrepWithCore(t, nil)
|
|
r := s.cmdPrep(core.NewOptions(
|
|
core.Option{Key: "_arg", Value: "nonexistent-repo"},
|
|
core.Option{Key: "issue", Value: "42"},
|
|
core.Option{Key: "pr", Value: "7"},
|
|
core.Option{Key: "branch", Value: "feat/test"},
|
|
core.Option{Key: "tag", Value: "v1.0.0"},
|
|
core.Option{Key: "task", Value: "do stuff"},
|
|
core.Option{Key: "template", Value: "coding"},
|
|
core.Option{Key: "plan_template", Value: "new-feature"},
|
|
core.Option{Key: "variables", Value: `{"feature_name":"Authentication"}`},
|
|
core.Option{Key: "persona", Value: "engineering"},
|
|
core.Option{Key: "dry-run", Value: "true"},
|
|
))
|
|
// Will fail (no local clone) but exercises all option parsing paths
|
|
assert.False(t, r.OK)
|
|
}
|
|
|
|
// --- CmdPrompt Ugly ---
|
|
|
|
func TestCommands_CmdPrompt_Ugly_AllOptionalFields(t *testing.T) {
|
|
s, _ := testPrepWithCore(t, nil)
|
|
r := s.cmdPrompt(core.NewOptions(
|
|
core.Option{Key: "_arg", Value: "go-io"},
|
|
core.Option{Key: "org", Value: "core"},
|
|
core.Option{Key: "task", Value: "review security"},
|
|
core.Option{Key: "template", Value: "verify"},
|
|
core.Option{Key: "persona", Value: "engineering/security"},
|
|
))
|
|
assert.True(t, r.OK)
|
|
}
|
|
|
|
// --- CmdRunTask Good/Ugly ---
|
|
|
|
func TestCommands_CmdRunTask_Good_DefaultsApplied(t *testing.T) {
|
|
s, _ := testPrepWithCore(t, nil)
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
s.startupContext = ctx
|
|
// Provide repo + task but omit agent + org — tests that defaults (codex, core) are applied
|
|
r := s.cmdRunTask(core.NewOptions(
|
|
core.Option{Key: "repo", Value: "go-io"},
|
|
core.Option{Key: "task", Value: "run all tests"},
|
|
))
|
|
// Will fail on dispatch but exercises the default-filling path
|
|
assert.False(t, r.OK)
|
|
}
|
|
|
|
func TestCommands_CmdRunTask_Ugly_MixedIssueString(t *testing.T) {
|
|
s, _ := testPrepWithCore(t, nil)
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
s.startupContext = ctx
|
|
r := s.cmdRunTask(core.NewOptions(
|
|
core.Option{Key: "repo", Value: "go-io"},
|
|
core.Option{Key: "task", Value: "fix it"},
|
|
core.Option{Key: "issue", Value: "issue-42abc"},
|
|
))
|
|
// Will fail on dispatch but exercises parseIntString with mixed chars
|
|
assert.False(t, r.OK)
|
|
}
|
|
|
|
// --- CommandContext Good/Bad/Ugly ---
|
|
|
|
func TestCommands_CommandContext_Good_StoredStartupContext(t *testing.T) {
|
|
s, _ := testPrepWithCore(t, nil)
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
s.registerCommands(ctx)
|
|
assert.Same(t, ctx, s.commandContext())
|
|
}
|
|
|
|
func TestCommands_CommandContext_Bad_FallsBackToBackground(t *testing.T) {
|
|
s, _ := testPrepWithCore(t, nil)
|
|
assert.NotNil(t, s.commandContext())
|
|
assert.NoError(t, s.commandContext().Err())
|
|
}
|
|
|
|
func TestCommands_CommandContext_Ugly_CancelledStartupContext(t *testing.T) {
|
|
s, _ := testPrepWithCore(t, nil)
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel() // pre-cancelled
|
|
s.startupContext = ctx
|
|
r := s.cmdOrchestrator(core.NewOptions())
|
|
assert.True(t, r.OK)
|
|
}
|
|
|
|
// --- CmdStatus Bad/Ugly ---
|
|
|
|
func TestCommands_CmdStatus_Bad_NoWorkspaceDir(t *testing.T) {
|
|
root := t.TempDir()
|
|
t.Setenv("CORE_WORKSPACE", root)
|
|
// Don't create workspace dir — WorkspaceRoot() returns root+"/workspace" which won't exist
|
|
|
|
c := core.New()
|
|
s := &PrepSubsystem{
|
|
ServiceRuntime: core.NewServiceRuntime(c, AgentOptions{}),
|
|
backoff: make(map[string]time.Time),
|
|
failCount: make(map[string]int),
|
|
}
|
|
|
|
r := s.cmdStatus(core.NewOptions())
|
|
assert.True(t, r.OK) // returns OK with "no workspaces found"
|
|
}
|
|
|
|
func TestCommands_CmdStatus_Ugly_NonDirEntries(t *testing.T) {
|
|
s, _ := testPrepWithCore(t, nil)
|
|
wsRoot := WorkspaceRoot()
|
|
fs.EnsureDir(wsRoot)
|
|
|
|
// Create a file (not a dir) inside workspace root
|
|
fs.Write(core.JoinPath(wsRoot, "not-a-workspace.txt"), "junk")
|
|
|
|
// Also create a proper workspace
|
|
ws := core.JoinPath(wsRoot, "ws-valid")
|
|
fs.EnsureDir(ws)
|
|
fs.Write(core.JoinPath(ws, "status.json"), core.JSONMarshalString(WorkspaceStatus{Status: "running", Repo: "test", Agent: "codex"}))
|
|
|
|
r := s.cmdStatus(core.NewOptions())
|
|
assert.True(t, r.OK)
|
|
}
|
|
|
|
// --- ParseIntStr Bad/Ugly ---
|
|
|
|
func TestCommands_ParseIntStr_Bad_NegativeAndOverflow(t *testing.T) {
|
|
// parseIntString extracts digits only, ignoring minus signs
|
|
assert.Equal(t, 5, parseIntString("-5")) // extracts "5", ignores "-"
|
|
assert.Equal(t, 0, parseIntString("-")) // no digits
|
|
assert.Equal(t, 0, parseIntString("---")) // no digits
|
|
}
|
|
|
|
func TestCommands_ParseIntStr_Ugly_UnicodeAndMixed(t *testing.T) {
|
|
// Unicode digits (e.g. Arabic-Indic) are NOT ASCII 0-9 so ignored
|
|
assert.Equal(t, 0, parseIntString("\u0661\u0662\u0663")) // ١٢٣ — not ASCII digits
|
|
assert.Equal(t, 42, parseIntString("abc42xyz")) // mixed chars
|
|
assert.Equal(t, 123, parseIntString("1a2b3c")) // interleaved
|
|
assert.Equal(t, 0, parseIntString(" \t\n")) // whitespace only
|
|
}
|