package squad import ( "bytes" "context" "os" "path/filepath" "strings" "testing" "time" "chat-agent-system/internal/agent" ) func TestStepEvaluator_InferAssumptionsFromGaps(t *testing.T) { evaluator := NewStepEvaluator() step := StepSpec{ ID: "step_01_organize", Role: "organizer", AllowDynamicAdjustment: true, } outputContent := "材料整理完成。目前客户未明确灭菌方式与包装材料,仅注明为一次性无菌介入类医疗器械。" bb := &BlackboardState{ Gaps: []GapItem{ {ID: "g1", Item: "未提供产品灭菌方式", Blocking: false}, }, Assumptions: []AssumptionItem{}, } res, err := evaluator.EvaluateStepOutput("", step, outputContent, bb) if err != nil { t.Fatalf("EvaluateStepOutput failed: %v", err) } foundSterilization := false for _, a := range res.NewAssumptions { if a.Topic == "sterilization_method" { foundSterilization = true if !strings.Contains(a.Statement, "环氧乙烷") { t.Errorf("assumption statement should infer EO sterilization, got %s", a.Statement) } if a.Source != "inferred" { t.Errorf("assumption source should be inferred, got %s", a.Source) } break } } if !foundSterilization { t.Errorf("expected baseline assumption for sterilization_method to be inferred") } } func TestStepEvaluator_SkipStepDetection(t *testing.T) { evaluator := NewStepEvaluator() step := StepSpec{ ID: "step_01_organize", Role: "organizer", AllowDynamicAdjustment: true, } outputContent := "输入材料中已完整包含国内外3款主要竞品型号、市场份额与终端招标定价,竞品数据已全面锁定,无需重复开展市场竞品检索。" bb := &BlackboardState{} res, err := evaluator.EvaluateStepOutput("", step, outputContent, bb) if err != nil { t.Fatalf("EvaluateStepOutput failed: %v", err) } if res.Action != StepActionSkipStep { t.Errorf("expected action %s, got %s", StepActionSkipStep, res.Action) } hasMarketSkip := false for _, s := range res.StepsToSkip { if s == "step_02_market" || strings.Contains(s, "market") { hasMarketSkip = true break } } if !hasMarketSkip { t.Errorf("expected step_02_market to be in StepsToSkip, got %v", res.StepsToSkip) } } func TestStepEvaluator_AddStepDetection(t *testing.T) { evaluator := NewStepEvaluator() step := StepSpec{ ID: "step_01_organize", Role: "organizer", AllowDynamicAdjustment: true, } outputContent := "梳理发现本项目采用微波射频双极消融技术,输出功率达150W,涉及高频电磁兼容性(EMC)与热灼伤防护特殊工程要求,需重点论证。" bb := &BlackboardState{} res, err := evaluator.EvaluateStepOutput("", step, outputContent, bb) if err != nil { t.Fatalf("EvaluateStepOutput failed: %v", err) } if res.Action != StepActionAddStep { t.Errorf("expected action %s, got %s", StepActionAddStep, res.Action) } if len(res.DynamicStepsToAdd) == 0 { t.Fatalf("expected at least 1 dynamic step to be added") } added := res.DynamicStepsToAdd[0] if !strings.Contains(added.ID, "emc") && !strings.Contains(added.Title, "电磁兼容") { t.Errorf("expected added step related to EMC/RF, got %s / %s", added.ID, added.Title) } } func TestWorkflowDriver_DynamicAdjustmentIntegration(t *testing.T) { tmpDir, err := os.MkdirTemp("", "wf_eval_test_*") if err != nil { t.Fatalf("create temp dir: %v", err) } defer os.RemoveAll(tmpDir) engine := NewWorkspaceEngine(nil) driver := NewWorkflowDriver(engine) evaluator := NewStepEvaluator() driver.SetEvaluator(evaluator) // 构建一个包含 3 步的工作流 steps := []StepSpec{ { ID: "step_01", Role: "organizer", Title: "整理输入材料", SubWorkspace: "tasks/01_organize", Mode: "execute", AllowDynamicAdjustment: true, }, { ID: "step_02", Role: "market", Title: "市场竞品调研", SubWorkspace: "tasks/02_market", Mode: "execute", }, { ID: "step_03", Role: "writer", Title: "整合撰写交付初稿", SubWorkspace: "tasks/03_draft", Mode: "execute", }, } req := WorkspacePrepareRequest{ WorkDir: tmpDir, TaskKey: "intake", StageTitle: "需求收集", ProjectName: "射频消融介入系统", WorkflowSteps: steps, } _, err = engine.PrepareWorkspace(req) if err != nil { t.Fatalf("prepare workspace failed: %v", err) } // 模拟让 step 1 产出触发跳过 step 2 并添加电磁兼容专题 step1Dir := filepath.Join(tmpDir, "tasks", "01_organize") _ = os.MkdirAll(step1Dir, 0o755) _ = os.WriteFile(filepath.Join(step1Dir, "output.md"), []byte( "材料梳理完毕。竞品数据已全面锁定,无需重复开展市场竞品检索。同时发现输出功率达150W微波射频高频,需重点论证高频电磁兼容性(EMC)。另外,客户未明确灭菌方式,需做工程假设推演。", ), 0o644) // 创建一个仅执行并模拟写入 output.md 的后端 backend := &testEchoBackend{} res, err := driver.Execute(context.Background(), tmpDir, backend, ExecuteOptions{Timeout: 5 * time.Second}) if err != nil { t.Fatalf("driver.Execute failed: %v", err) } if !res.Success { t.Errorf("driver.Execute expected success") } // 检验工作流状态:step_02 应被置为 skipped,且动态追加了 emc 步骤 wf, err := driver.LoadWorkflow(tmpDir) if err != nil { t.Fatalf("load workflow failed: %v", err) } foundSkipped := false foundEMCAdded := false for _, s := range wf.Steps { if s.ID == "step_02" && s.Status == "skipped" { foundSkipped = true } if strings.Contains(s.ID, "emc") && s.Status == "completed" { foundEMCAdded = true } } if !foundSkipped { t.Errorf("step_02 should have been marked as skipped") } if !foundEMCAdded { t.Errorf("dynamic EMC step should have been added and executed") } // 检验黑板是否成功写入了 baseline assumption bb, err := driver.LoadBlackboard(tmpDir) if err != nil { t.Fatalf("load blackboard failed: %v", err) } if len(bb.Assumptions) == 0 { t.Errorf("expected at least 1 baseline assumption in blackboard") } else { t.Logf("inferred assumption: %s (%s)", bb.Assumptions[0].Topic, bb.Assumptions[0].Statement) } } type testEchoBackend struct{} func (b *testEchoBackend) Name() string { return "test-echo" } func (b *testEchoBackend) Execute(ctx context.Context, req agent.Request) (*agent.Result, error) { // 如果目标文件已存在(如手动预置的 step 1),不覆盖 target := "output.md" if len(req.Targets) > 0 { target = req.Targets[0] } p := filepath.Join(req.WorkDir, target) if _, err := os.Stat(p); os.IsNotExist(err) { _ = os.WriteFile(p, []byte("## 步骤执行交付产物\n\n已完成任务要求论证与数据沉淀。\n"), 0o644) } content, err := os.ReadFile(p) if err != nil || len(bytes.TrimSpace(content)) == 0 { return &agent.Result{Success: false, ErrorReason: "missing target deliverable " + target}, nil } return &agent.Result{Success: true, Patches: map[string][]byte{target: content}}, nil }