Files
Agent-System/internal/squad/quality_gate_test.go
T
weichenandCursor 107f898362 fix(docgov): tighten prohibited-phrase exemption; lock stage request fields
自检豁免改为同行且须 meta+否定紧邻或引号引用,避免正文「无」误放过;buildBizdevStageRequest 单测锁住身份/职责字段与 AutoRepair 契约。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-13 15:16:12 +00:00

291 lines
10 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
}
}