KTD1 fifth step (behavior preserved):
- squad.AssetProvider{DocTemplate,RenderDoc,AgentInstruction,
AgentInstructionFile} injected via Options.Assets into Service,
WorkspaceEngine and AdaptivePlanner
- bizdev templateAssetProvider maps port to internal/template (assembly side)
- nil-provider paths degrade to the same fallbacks the old error branches
used (placeholder template, role header) — fail-visible, no guessing
- squad -> template import eliminated; arch edge moved bizdev->template
- verified: build/vet/arch green, squad+bizdev short tests pass
119 lines
3.6 KiB
Go
119 lines
3.6 KiB
Go
package squad_test
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"chat-agent-system/internal/agent"
|
|
"chat-agent-system/internal/squad"
|
|
)
|
|
|
|
// failFirstBackend 第一步失败、其余成功。
|
|
type failFirstBackend struct {
|
|
calls int
|
|
}
|
|
|
|
func (b *failFirstBackend) Name() string { return "fail-first" }
|
|
|
|
func (b *failFirstBackend) Execute(ctx context.Context, req agent.Request) (*agent.Result, error) {
|
|
b.calls++
|
|
if b.calls == 1 {
|
|
return &agent.Result{Success: false, ErrorReason: "boom"}, nil
|
|
}
|
|
body := []byte("# core deliverable\n")
|
|
_ = os.WriteFile(filepath.Join(req.WorkDir, "output.md"), body, 0o644)
|
|
return &agent.Result{Success: true, Patches: map[string][]byte{"output.md": body}}, nil
|
|
}
|
|
|
|
func prepareDegradeWS(t *testing.T) (string, *squad.WorkflowDriver) {
|
|
t.Helper()
|
|
tmpDir := t.TempDir()
|
|
engine := squad.NewWorkspaceEngine(nil)
|
|
ws, err := engine.PrepareWorkspace(squad.WorkspacePrepareRequest{
|
|
WorkDir: tmpDir,
|
|
TaskKey: "degrade_test",
|
|
StageTitle: "降级验收",
|
|
ProjectName: "degrade-audit",
|
|
Backend: "fail-first",
|
|
WorkflowSteps: []squad.StepSpec{
|
|
{ID: "step_01_optional", Role: "writer", Title: "可选分析", SubWorkspace: "tasks/01_optional", Mode: "execute"},
|
|
{ID: "step_02_core", Role: "writer", Title: "核心成文", SubWorkspace: "tasks/02_core", Mode: "execute"},
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("PrepareWorkspace: %v", err)
|
|
}
|
|
return ws.WorkDir, squad.NewWorkflowDriver(engine)
|
|
}
|
|
|
|
// A5:非关键步骤失败 → degraded 记录、后续步骤继续、整体 degraded + DegradedSteps。
|
|
func TestNonCriticalStepDegradesAndContinues(t *testing.T) {
|
|
wsDir, driver := prepareDegradeWS(t)
|
|
wf, err := driver.LoadWorkflow(wsDir)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
nonCritical := false
|
|
for i := range wf.Steps {
|
|
if wf.Steps[i].ID == "step_01_optional" {
|
|
wf.Steps[i].Critical = &nonCritical
|
|
}
|
|
}
|
|
_ = driver.SaveWorkflow(wsDir, wf)
|
|
|
|
backend := &failFirstBackend{}
|
|
res, err := driver.Execute(context.Background(), wsDir, backend, squad.ExecuteOptions{Timeout: 30 * time.Second})
|
|
if err != nil {
|
|
t.Fatalf("degraded run must not hard-fail: %v", err)
|
|
}
|
|
if !res.Success {
|
|
t.Fatalf("degraded run Success must be true, reason=%s", res.ErrorReason)
|
|
}
|
|
if len(res.DegradedSteps) != 1 || res.DegradedSteps[0] != "step_01_optional" {
|
|
t.Fatalf("DegradedSteps = %v", res.DegradedSteps)
|
|
}
|
|
after, _ := driver.LoadWorkflow(wsDir)
|
|
if after.Status != "degraded" {
|
|
t.Fatalf("wf.Status = %s, want degraded", after.Status)
|
|
}
|
|
found := false
|
|
for _, s := range after.Steps {
|
|
if s.ID == "step_01_optional" {
|
|
found = true
|
|
if s.Status != "degraded" || !strings.Contains(s.Error, "boom") {
|
|
t.Fatalf("step_01 = %s err=%s", s.Status, s.Error)
|
|
}
|
|
}
|
|
if s.ID == "step_02_core" && s.Status != "completed" {
|
|
t.Fatalf("step_02 must complete, got %s", s.Status)
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatal("step_01_optional missing")
|
|
}
|
|
if backend.calls != 2 {
|
|
t.Fatalf("second step must run, calls=%d", backend.calls)
|
|
}
|
|
}
|
|
|
|
// A5:关键步骤(默认 nil)失败行为不变——立即终止、不跑后续。
|
|
func TestCriticalStepFailureStillAborts(t *testing.T) {
|
|
wsDir, driver := prepareDegradeWS(t)
|
|
backend := &failFirstBackend{}
|
|
res, err := driver.Execute(context.Background(), wsDir, backend, squad.ExecuteOptions{Timeout: 30 * time.Second})
|
|
if err == nil || (res != nil && res.Success) {
|
|
t.Fatalf("critical failure must abort: err=%v res=%+v", err, res)
|
|
}
|
|
after, _ := driver.LoadWorkflow(wsDir)
|
|
if after.Status != "failed" {
|
|
t.Fatalf("wf.Status = %s, want failed", after.Status)
|
|
}
|
|
if backend.calls != 1 {
|
|
t.Fatalf("second step must not run after critical failure, calls=%d", backend.calls)
|
|
}
|
|
}
|