All checks were successful
CI / build (push) Successful in 2m37s
- Add AES-256-GCM encryption for API keys (internal/secret) - Add dangerous command detection in terminal - Add muyue doctor command for system health checks - Add scanner TTL cache, orchestrator history mutex, shared HTTP client - Deduplicate MCP config generation, refactor skills YAML parser - Add XDG-compliant config dir with legacy migration - Add cleanup on all TUI quit paths - Add 8 test files (config, workflow, skills, orchestrator, version, platform, scanner, secret) - Update CI to actions/setup-go@v5 - Add CHANGELOG.md, update README and Makefile 🤖 Generated with Crush Assisted-by: GLM-5.1 via Crush <crush@charm.land>
256 lines
5.9 KiB
Go
256 lines
5.9 KiB
Go
package workflow
|
|
|
|
import (
|
|
"testing"
|
|
)
|
|
|
|
func TestNew(t *testing.T) {
|
|
wf := New()
|
|
if wf.Phase != PhaseIdle {
|
|
t.Errorf("Expected PhaseIdle, got %s", wf.Phase)
|
|
}
|
|
if wf.Plan == nil {
|
|
t.Error("Plan should not be nil")
|
|
}
|
|
}
|
|
|
|
func TestStart(t *testing.T) {
|
|
wf := New()
|
|
wf.Start("Build a REST API")
|
|
if wf.Phase != PhaseGathering {
|
|
t.Errorf("Expected PhaseGathering, got %s", wf.Phase)
|
|
}
|
|
if wf.Plan.Goal != "Build a REST API" {
|
|
t.Errorf("Expected goal 'Build a REST API', got %s", wf.Plan.Goal)
|
|
}
|
|
}
|
|
|
|
func TestAddAnswer(t *testing.T) {
|
|
wf := New()
|
|
wf.Start("test goal")
|
|
wf.Plan.Questions = []string{"Q1?", "Q2?"}
|
|
|
|
wf.AddAnswer("A1")
|
|
if wf.Phase != PhaseGathering {
|
|
t.Errorf("Should still be gathering, got %s", wf.Phase)
|
|
}
|
|
|
|
wf.AddAnswer("A2")
|
|
if wf.Phase != PhasePlanning {
|
|
t.Errorf("Should move to planning, got %s", wf.Phase)
|
|
}
|
|
}
|
|
|
|
func TestSetPlan(t *testing.T) {
|
|
wf := New()
|
|
planJSON := `[{"id":"1","title":"Step 1","description":"Do something","agent":"crush","status":"pending"}]`
|
|
err := wf.SetPlan(planJSON)
|
|
if err != nil {
|
|
t.Fatalf("SetPlan failed: %v", err)
|
|
}
|
|
if len(wf.Plan.Steps) != 1 {
|
|
t.Errorf("Expected 1 step, got %d", len(wf.Plan.Steps))
|
|
}
|
|
if wf.Phase != PhaseReviewing {
|
|
t.Errorf("Expected PhaseReviewing, got %s", wf.Phase)
|
|
}
|
|
}
|
|
|
|
func TestApprove(t *testing.T) {
|
|
wf := New()
|
|
wf.Start("test")
|
|
wf.Plan.Steps = []Step{{ID: "1", Title: "Step 1", Status: "pending"}}
|
|
wf.Phase = PhaseReviewing
|
|
wf.Approve()
|
|
if wf.Phase != PhaseExecuting {
|
|
t.Errorf("Expected PhaseExecuting, got %s", wf.Phase)
|
|
}
|
|
if wf.Plan.StepIndex != 0 {
|
|
t.Errorf("Expected step index 0, got %d", wf.Plan.StepIndex)
|
|
}
|
|
}
|
|
|
|
func TestReject(t *testing.T) {
|
|
wf := New()
|
|
wf.Phase = PhaseReviewing
|
|
wf.Reject("too complex")
|
|
if wf.Phase != PhasePlanning {
|
|
t.Errorf("Expected PhasePlanning, got %s", wf.Phase)
|
|
}
|
|
}
|
|
|
|
func TestAdvanceStep(t *testing.T) {
|
|
wf := New()
|
|
wf.Plan.Steps = []Step{
|
|
{ID: "1", Title: "Step 1", Status: "pending"},
|
|
{ID: "2", Title: "Step 2", Status: "pending"},
|
|
}
|
|
wf.Phase = PhaseExecuting
|
|
|
|
wf.AdvanceStep("output1")
|
|
if wf.Plan.Steps[0].Status != "done" {
|
|
t.Error("First step should be done")
|
|
}
|
|
if wf.Plan.StepIndex != 1 {
|
|
t.Errorf("Expected step index 1, got %d", wf.Plan.StepIndex)
|
|
}
|
|
if wf.Phase != PhaseExecuting {
|
|
t.Errorf("Should still be executing, got %s", wf.Phase)
|
|
}
|
|
|
|
wf.AdvanceStep("output2")
|
|
if wf.Phase != PhaseDone {
|
|
t.Errorf("Expected PhaseDone, got %s", wf.Phase)
|
|
}
|
|
}
|
|
|
|
func TestFailStep(t *testing.T) {
|
|
wf := New()
|
|
wf.Plan.Steps = []Step{{ID: "1", Title: "Step 1"}}
|
|
wf.Phase = PhaseExecuting
|
|
|
|
wf.FailStep("something broke")
|
|
if wf.Phase != PhaseError {
|
|
t.Errorf("Expected PhaseError, got %s", wf.Phase)
|
|
}
|
|
if wf.Plan.Steps[0].Status != "error" {
|
|
t.Error("Step should have error status")
|
|
}
|
|
}
|
|
|
|
func TestReset(t *testing.T) {
|
|
wf := New()
|
|
wf.Start("test")
|
|
wf.Phase = PhaseExecuting
|
|
wf.Reset()
|
|
if wf.Phase != PhaseIdle {
|
|
t.Errorf("Expected PhaseIdle, got %s", wf.Phase)
|
|
}
|
|
}
|
|
|
|
func TestCurrentStep(t *testing.T) {
|
|
wf := New()
|
|
if wf.CurrentStep() != nil {
|
|
t.Error("Should be nil with no steps")
|
|
}
|
|
|
|
wf.Plan.Steps = []Step{{ID: "1"}, {ID: "2"}}
|
|
wf.Plan.StepIndex = 0
|
|
step := wf.CurrentStep()
|
|
if step == nil || step.ID != "1" {
|
|
t.Error("Should return first step")
|
|
}
|
|
|
|
wf.Plan.StepIndex = 2
|
|
if wf.CurrentStep() != nil {
|
|
t.Error("Should be nil when past all steps")
|
|
}
|
|
}
|
|
|
|
func TestProgress(t *testing.T) {
|
|
wf := New()
|
|
wf.Plan.Steps = []Step{
|
|
{ID: "1", Status: "done"},
|
|
{ID: "2", Status: "pending"},
|
|
{ID: "3", Status: "done"},
|
|
}
|
|
done, total := wf.Progress()
|
|
if done != 2 || total != 3 {
|
|
t.Errorf("Expected 2/3, got %d/%d", done, total)
|
|
}
|
|
}
|
|
|
|
func TestParsePlanResponse(t *testing.T) {
|
|
resp := `Here is the plan:
|
|
[
|
|
{"id": "1", "title": "Setup", "description": "Init project", "agent": "crush"},
|
|
{"id": "2", "title": "Build", "description": "Write code", "agent": "claude"}
|
|
]`
|
|
steps, err := ParsePlanResponse(resp)
|
|
if err != nil {
|
|
t.Fatalf("ParsePlanResponse failed: %v", err)
|
|
}
|
|
if len(steps) != 2 {
|
|
t.Errorf("Expected 2 steps, got %d", len(steps))
|
|
}
|
|
if steps[0].ID != "1" {
|
|
t.Errorf("Expected step ID 1, got %s", steps[0].ID)
|
|
}
|
|
for _, s := range steps {
|
|
if s.Status != "pending" {
|
|
t.Errorf("Steps should be pending, got %s", s.Status)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestParsePlanResponseInvalid(t *testing.T) {
|
|
_, err := ParsePlanResponse("no json here")
|
|
if err == nil {
|
|
t.Error("Should fail with no JSON")
|
|
}
|
|
}
|
|
|
|
func TestParseApproval(t *testing.T) {
|
|
tests := []struct {
|
|
input string
|
|
approved bool
|
|
}{
|
|
{"plan_approved", true},
|
|
{"approved", true},
|
|
{"yes", true},
|
|
{"ok", true},
|
|
{"oui", true},
|
|
{"go ahead", true},
|
|
{"no", false},
|
|
{"plan_rejected: too complex", false},
|
|
{"I don't like it", false},
|
|
}
|
|
for _, tt := range tests {
|
|
approved, feedback := ParseApproval(tt.input)
|
|
if approved != tt.approved {
|
|
t.Errorf("ParseApproval(%q) = %v, want %v", tt.input, approved, tt.approved)
|
|
}
|
|
if !approved && tt.input == "plan_rejected: too complex" {
|
|
if feedback != "too complex" {
|
|
t.Errorf("Expected feedback 'too complex', got %s", feedback)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestParsePreviewFiles(t *testing.T) {
|
|
resp := `Some text
|
|
<<<PREVIEW_JSON>>>
|
|
[{"filename":"test.html","content":"<h1>Hello</h1>","type":"html"}]
|
|
<<<END_PREVIEW>>>`
|
|
files := ParsePreviewFiles(resp)
|
|
if len(files) != 1 {
|
|
t.Fatalf("Expected 1 file, got %d", len(files))
|
|
}
|
|
if files[0].Filename != "test.html" {
|
|
t.Errorf("Expected test.html, got %s", files[0].Filename)
|
|
}
|
|
}
|
|
|
|
func TestParsePreviewFilesNone(t *testing.T) {
|
|
files := ParsePreviewFiles("no preview here")
|
|
if files != nil {
|
|
t.Error("Should return nil")
|
|
}
|
|
}
|
|
|
|
func TestBuildSystemPrompt(t *testing.T) {
|
|
prompt := BuildSystemPrompt(PhaseIdle, &Plan{})
|
|
if prompt == "" {
|
|
t.Error("Prompt should not be empty")
|
|
}
|
|
if len(prompt) < 100 {
|
|
t.Error("Prompt seems too short")
|
|
}
|
|
|
|
prompt = BuildSystemPrompt(PhaseGathering, &Plan{Goal: "test"})
|
|
if prompt == "" {
|
|
t.Error("Gathering prompt should not be empty")
|
|
}
|
|
}
|