自检豁免改为同行且须 meta+否定紧邻或引号引用,避免正文「无」误放过;buildBizdevStageRequest 单测锁住身份/职责字段与 AutoRepair 契约。 Co-authored-by: Cursor <cursoragent@cursor.com>
291 lines
10 KiB
Go
291 lines
10 KiB
Go
package squad_test
|
||
|
||
import (
|
||
"context"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
"testing"
|
||
"time"
|
||
|
||
"chat-agent-system/internal/agent"
|
||
"chat-agent-system/internal/squad"
|
||
)
|
||
|
||
func TestQualityGate_Validate(t *testing.T) {
|
||
tmpDir := t.TempDir()
|
||
|
||
engine := squad.NewWorkspaceEngine()
|
||
ws, err := engine.PrepareWorkspace(squad.WorkspacePrepareRequest{
|
||
WorkDir: tmpDir,
|
||
TaskKey: "intake",
|
||
StageTitle: "用户初始需求收集表",
|
||
ProjectName: "医疗设备",
|
||
TemplateContent: "# 用户初始需求收集表\n\n## 1. 客户与项目画像\n\n## 2. 预期用途\n\n## 3. 待确认清单\n",
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("PrepareWorkspace failed: %v", err)
|
||
}
|
||
|
||
gate := squad.NewQualityGate()
|
||
|
||
// 1. 测试违规文本:包含敷衍词「不做展开」与缺少第 2 节
|
||
badContent := "# 用户初始需求收集表\n\n## 1. 客户与项目画像\n此处篇幅有限,不做展开。\n\n## 3. 待确认清单\n- 待确认事项\n"
|
||
report1, err := gate.Validate(ws.WorkDir, "intake", badContent)
|
||
if err != nil {
|
||
t.Fatalf("Validate failed: %v", err)
|
||
}
|
||
if report1.Passed {
|
||
t.Errorf("expected report1 to fail quality gate")
|
||
}
|
||
if len(report1.ProhibitedPhrases) == 0 {
|
||
t.Errorf("expected prohibited phrase '不做展开' to be detected")
|
||
}
|
||
if len(report1.MissingSections) == 0 {
|
||
t.Errorf("expected missing section '## 2. 预期用途' to be detected")
|
||
}
|
||
|
||
// 2. 测试合规文本:包含所有章节且无违规词
|
||
goodContent := `# 用户初始需求收集表
|
||
|
||
## 1. 客户与项目画像
|
||
本项目客户为专业医疗机构〔材料: 需求表 §1〕。
|
||
(四段式展开详细论述)
|
||
|
||
## 2. 预期用途
|
||
用于临床辅助诊断与生理信号监测。
|
||
|
||
## 3. 待确认清单
|
||
- 供电模块认证周期待窗口确认。
|
||
`
|
||
report2, err := gate.Validate(ws.WorkDir, "intake", goodContent)
|
||
if err != nil {
|
||
t.Fatalf("Validate failed: %v", err)
|
||
}
|
||
if !report2.Passed {
|
||
t.Errorf("expected report2 to pass quality gate, violations: %v", report2.Violations)
|
||
}
|
||
if report2.Score < 80.0 {
|
||
t.Errorf("expected high score, got %f", report2.Score)
|
||
}
|
||
|
||
// 3. 测试 Manifest 生成
|
||
manifest, err := gate.GenerateManifest(ws.WorkDir, "intake", *report2)
|
||
if err != nil {
|
||
t.Fatalf("GenerateManifest failed: %v", err)
|
||
}
|
||
if manifest.DocKey != "intake" {
|
||
t.Errorf("expected DocKey 'intake', got %s", manifest.DocKey)
|
||
}
|
||
if len(manifest.Files) == 0 {
|
||
t.Errorf("expected tracked files in manifest")
|
||
}
|
||
if _, err := os.Stat(filepath.Join(ws.WorkDir, "manifest.json")); err != nil {
|
||
t.Errorf("manifest.json not written to disk")
|
||
}
|
||
}
|
||
|
||
func TestQualityGate_AutoRepair(t *testing.T) {
|
||
tmpDir := t.TempDir()
|
||
|
||
engine := squad.NewWorkspaceEngine()
|
||
ws, err := engine.PrepareWorkspace(squad.WorkspacePrepareRequest{
|
||
WorkDir: tmpDir,
|
||
TaskKey: "intake",
|
||
StageTitle: "用户初始需求收集表",
|
||
TemplateContent: "# 用户初始需求收集表\n\n## 1. 结论\n\n## 2. 详细方案\n",
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("PrepareWorkspace failed: %v", err)
|
||
}
|
||
|
||
driver := squad.NewWorkflowDriver(engine)
|
||
gate := squad.NewQualityGate()
|
||
|
||
// 模拟不合格草稿
|
||
badDraft := "# 用户初始需求收集表\n\n## 1. 结论\n由于篇幅有限,不做展开。\n"
|
||
_ = os.WriteFile(filepath.Join(ws.WorkDir, "output.md"), []byte(badDraft), 0o644)
|
||
|
||
// 触发自动修复:门禁检测到不合格,向 workflow 追加修复步骤并执行
|
||
stubBackend := &agent.StubBackend{}
|
||
report, err := gate.EnsureQualityOrRepair(context.Background(), ws.WorkDir, "intake", driver, stubBackend, 1, squad.ExecuteOptions{
|
||
Timeout: 5 * time.Second,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("EnsureQualityOrRepair failed: %v", err)
|
||
}
|
||
|
||
// 验证工作流中已追加过修复步骤
|
||
wf, _ := driver.LoadWorkflow(ws.WorkDir)
|
||
hasRepairStep := false
|
||
for _, s := range wf.Steps {
|
||
if s.ID == "step_repair_1" {
|
||
hasRepairStep = true
|
||
break
|
||
}
|
||
}
|
||
if !hasRepairStep {
|
||
t.Errorf("expected step_repair_1 to be appended to workflow")
|
||
}
|
||
|
||
if report == nil {
|
||
t.Errorf("expected non-nil quality report")
|
||
}
|
||
}
|
||
|
||
func TestQualityGate_SystemReqsAndTestManager(t *testing.T) {
|
||
tmpDir := t.TempDir()
|
||
engine := squad.NewWorkspaceEngine()
|
||
gate := squad.NewQualityGate()
|
||
|
||
// 1. 测试 system_reqs
|
||
ws1, err := engine.PrepareWorkspace(squad.WorkspacePrepareRequest{
|
||
WorkDir: filepath.Join(tmpDir, "ws_reqs"),
|
||
TaskKey: "system_reqs",
|
||
StageTitle: "系统需求规格书",
|
||
ProjectName: "医疗监护仪",
|
||
TemplateContent: "# 系统需求规格书\n\n## 1. 结论\n\n## 2. 系统需求清单\n",
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("PrepareWorkspace failed: %v", err)
|
||
}
|
||
|
||
// 缺少需求编号和验证方式
|
||
badReqContent := "# 系统需求规格书\n\n## 1. 结论\n结论完整\n\n## 2. 系统需求清单\n系统应支持心率采集。\n"
|
||
rep1, err := gate.Validate(ws1.WorkDir, "system_reqs", badReqContent)
|
||
if err != nil {
|
||
t.Fatalf("Validate failed: %v", err)
|
||
}
|
||
if rep1.Passed {
|
||
t.Errorf("expected system_reqs missing REQ- and verification to fail")
|
||
}
|
||
|
||
// 合规文本
|
||
goodReqContent := "# 系统需求规格书\n\n## 1. 结论\n结论完整\n\n## 2. 系统需求清单\n| REQ-01 | 系统应支持心率监测,测量误差不超过 ±2bpm | 测试 | 高 |\n"
|
||
rep2, err := gate.Validate(ws1.WorkDir, "system_reqs", goodReqContent)
|
||
if err != nil {
|
||
t.Fatalf("Validate failed: %v", err)
|
||
}
|
||
if !rep2.Passed {
|
||
t.Errorf("expected compliant system_reqs to pass, got violations: %v", rep2.Violations)
|
||
}
|
||
|
||
// 2. 测试 test_manager
|
||
ws2, err := engine.PrepareWorkspace(squad.WorkspacePrepareRequest{
|
||
WorkDir: filepath.Join(tmpDir, "ws_test"),
|
||
TaskKey: "test_manager",
|
||
StageTitle: "系统验证方案及计划",
|
||
ProjectName: "医疗监护仪",
|
||
TemplateContent: "# 系统验证方案及计划\n\n## 1. 结论\n\n## 2. 用例与验证规程\n",
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("PrepareWorkspace failed: %v", err)
|
||
}
|
||
|
||
// 缺少用例和上游追溯
|
||
badTestContent := "# 系统验证方案及计划\n\n## 1. 结论\n结论\n\n## 2. 用例与验证规程\n验证功能正常。\n"
|
||
rep3, err := gate.Validate(ws2.WorkDir, "test_manager", badTestContent)
|
||
if err != nil {
|
||
t.Fatalf("Validate failed: %v", err)
|
||
}
|
||
if rep3.Passed {
|
||
t.Errorf("expected test_manager missing TC- and REQ- to fail")
|
||
}
|
||
|
||
// 合规文本
|
||
goodTestContent := "# 系统验证方案及计划\n\n## 1. 结论\n结论\n\n## 2. 用例与验证规程\n| TC-01 | 验证心率测量精度 | 追溯 REQ-01 | 判定准则误差 <= ±2bpm | 通过 |\n"
|
||
rep4, err := gate.Validate(ws2.WorkDir, "test_manager", goodTestContent)
|
||
if err != nil {
|
||
t.Fatalf("Validate failed: %v", err)
|
||
}
|
||
if !rep4.Passed {
|
||
t.Errorf("expected compliant test_manager to pass, got violations: %v", rep4.Violations)
|
||
}
|
||
}
|
||
|
||
// 现场证据 output.md:204 — 终稿自检表否定性引用禁词,不得误报为敷衍违规。
|
||
func TestQualityGate_NegatedSelfCheckCitation_NoFalsePositive(t *testing.T) {
|
||
tmpDir := t.TempDir()
|
||
engine := squad.NewWorkspaceEngine()
|
||
ws, err := engine.PrepareWorkspace(squad.WorkspacePrepareRequest{
|
||
WorkDir: tmpDir,
|
||
TaskKey: "intake",
|
||
StageTitle: "用户初始需求收集表",
|
||
ProjectName: "医疗设备",
|
||
TemplateContent: "# 用户初始需求收集表\n\n## 1. 客户与项目画像\n\n## 2. 预期用途\n\n## 3. 待确认清单\n",
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("PrepareWorkspace: %v", err)
|
||
}
|
||
gate := squad.NewQualityGate()
|
||
|
||
// 与 data/dev/workspaces/bizdev/intake_1789308825666278807/output.md 第204行同构
|
||
realChecklistLine := `| 写作禁令遵守情况 | ✅ 通过 | 无"不做展开""从略""篇幅有限"等敷衍套话 |`
|
||
content := `# 用户初始需求收集表
|
||
|
||
## 1. 客户与项目画像
|
||
本项目客户为专业医疗机构〔材料: 需求表 §1〕。四段式展开详细论述客户背景与项目边界。
|
||
|
||
## 2. 预期用途
|
||
用于临床辅助诊断与生理信号监测,覆盖病房与门诊场景。
|
||
|
||
## 3. 待确认清单
|
||
- 供电模块认证周期待窗口确认。
|
||
|
||
### 附录C:审核记录(Review Record)
|
||
|
||
| 审核项 | 审核结论 | 说明 |
|
||
|-------|---------|------|
|
||
| 全文逻辑一致性 | ✅ 通过 | 结论→正文→解读完整 |
|
||
` + realChecklistLine + "\n"
|
||
|
||
report, err := gate.Validate(ws.WorkDir, "intake", content)
|
||
if err != nil {
|
||
t.Fatalf("Validate: %v", err)
|
||
}
|
||
if !report.Passed {
|
||
t.Fatalf("negated self-check must not fail gate; violations=%v phrases=%v score=%.0f",
|
||
report.Violations, report.ProhibitedPhrases, report.Score)
|
||
}
|
||
if len(report.ProhibitedPhrases) != 0 {
|
||
t.Fatalf("expected no prohibited hits on meta citation, got %v", report.ProhibitedPhrases)
|
||
}
|
||
|
||
// 真敷衍仍须命中
|
||
hedging := "# 用户初始需求收集表\n\n## 1. 客户与项目画像\n此处篇幅有限,不做展开。\n\n## 2. 预期用途\nok\n\n## 3. 待确认清单\n- x\n"
|
||
bad, err := gate.Validate(ws.WorkDir, "intake", hedging)
|
||
if err != nil {
|
||
t.Fatalf("Validate hedging: %v", err)
|
||
}
|
||
if bad.Passed || len(bad.ProhibitedPhrases) == 0 {
|
||
t.Fatalf("real hedging must fail; passed=%v phrases=%v", bad.Passed, bad.ProhibitedPhrases)
|
||
}
|
||
for _, v := range bad.Violations {
|
||
for _, p := range squad.ProhibitedPhrases {
|
||
if strings.Contains(v, p) {
|
||
t.Fatalf("violation text must not echo banned phrase %q: %s", p, v)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 正文出现「无」但非自检引用:不得因 48 字窗口豁免
|
||
falseNeg := "# 用户初始需求收集表\n\n## 1. 客户与项目画像\n此处无更多数据,不做展开。\n\n## 2. 预期用途\nok\n\n## 3. 待确认清单\n- x\n"
|
||
fn, err := gate.Validate(ws.WorkDir, "intake", falseNeg)
|
||
if err != nil {
|
||
t.Fatalf("Validate false-neg: %v", err)
|
||
}
|
||
if fn.Passed || len(fn.ProhibitedPhrases) == 0 {
|
||
t.Fatalf("body hedging with 无 must still fail; passed=%v phrases=%v", fn.Passed, fn.ProhibitedPhrases)
|
||
}
|
||
|
||
// 仅有审核栏标题、禁词作正文省略:不得豁免
|
||
metaOnly := "# 用户初始需求收集表\n\n## 1. 客户与项目画像\n审核结论:本节不做展开。\n\n## 2. 预期用途\nok\n\n## 3. 待确认清单\n- x\n"
|
||
mo, err := gate.Validate(ws.WorkDir, "intake", metaOnly)
|
||
if err != nil {
|
||
t.Fatalf("Validate meta-only: %v", err)
|
||
}
|
||
if mo.Passed || len(mo.ProhibitedPhrases) == 0 {
|
||
t.Fatalf("meta heading plus real hedging must fail; passed=%v phrases=%v", mo.Passed, mo.ProhibitedPhrases)
|
||
}
|
||
}
|