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
245 lines
7.1 KiB
Go
245 lines
7.1 KiB
Go
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
|
|
}
|