Files
Agent-System/internal/squad/noncritical_degrade_test.go
T
agent-bot 2c75dcaf69
CI / go-vet (pull_request) Successful in 13s
CI / short-tests-report (pull_request) Failing after 35s
CI / backup-restore-drill (pull_request) Successful in 36s
CI / web (pull_request) Successful in 32s
refactor(squad): AssetProvider port replaces direct template dependency
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
2026-09-29 06:29:51 +00:00

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)
}
}