Files
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

169 lines
6.7 KiB
Go

package squad
import (
"os"
"path/filepath"
"strings"
"testing"
)
func TestRDevOverlayFiltersRoleAndComplexity(t *testing.T) {
writer := RDevStageOverlay("rd_planning_review", "writer", "")
if !strings.Contains(writer, "策划评审") {
t.Fatal("planning review writer overlay must include phase 02 discipline")
}
if !strings.Contains(writer, "role:writer") {
t.Fatal("writer overlay must keep the writer section")
}
if strings.Contains(writer, "role:engineer") {
t.Fatal("writer overlay must not include the engineer section")
}
high := RDevStageOverlay("rd_sw_plan", "leader", "high")
if !strings.Contains(high, "高复杂度") {
t.Fatal("high tier must include complexity_high overlay")
}
if !strings.Contains(high, "不适用理由") {
t.Fatal("software stage must include the software overlay")
}
low := RDevStageOverlay("rd_sw_plan", "leader", "low")
if strings.Contains(low, "高复杂度项目策略") {
t.Fatal("low tier must not include the high-complexity overlay")
}
}
func TestRDevWorkspaceWritesOverlay(t *testing.T) {
tmpDir := t.TempDir()
engine := NewWorkspaceEngine(nil)
ws, err := engine.PrepareWorkspace(WorkspacePrepareRequest{
WorkDir: filepath.Join(tmpDir, "rd_planning_review"),
TaskKey: "rd_planning_review",
StageTitle: "设计和开发策划评审记录",
ProjectName: "rdev-overlay",
Backend: "stub",
InitialFields: map[string]string{
"complexity_tier": "medium",
},
})
if err != nil {
t.Fatal(err)
}
body, err := os.ReadFile(filepath.Join(ws.WorkDir, "tasks", "02_writer", "OVERLAY.md"))
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(body), "评审记录") {
t.Fatal("writer OVERLAY.md must carry review-minutes discipline")
}
task, err := os.ReadFile(filepath.Join(ws.WorkDir, "tasks", "02_writer", "TASK.md"))
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(task), "OVERLAY.md") {
t.Fatal("TASK.md must point at the overlay")
}
}
func TestRDevQualityGates(t *testing.T) {
gate := NewQualityGate()
ws := t.TempDir()
if err := os.MkdirAll(filepath.Join(ws, "artifacts"), 0o755); err != nil {
t.Fatal(err)
}
badReq := "本规格随机抽取部分需求。\n\nUR-01 能开机。\nUR-03 能关机。\n"
rep, err := gate.Validate(ws, "rd_user_needs", badReq)
if err != nil {
t.Fatal(err)
}
if rep.Passed {
t.Fatalf("gapped and sampled requirements must fail: %+v", rep.Violations)
}
goodReq := "UR-01 能开机,验证准则为通电检查。\nUR-02 能关机,验证准则为断电检查。\n"
raw := []byte(`[
{"id":"UR-01","verification_method":"测试","acceptance_criteria":"通电后进入主界面"},
{"id":"UR-02","verification_method":"测试","acceptance_criteria":"断电后安全停止"}
]`)
if err := os.WriteFile(filepath.Join(ws, "artifacts", "requirements.json"), raw, 0o644); err != nil {
t.Fatal(err)
}
rep, err = gate.Validate(ws, "rd_user_needs", goodReq)
if err != nil {
t.Fatal(err)
}
if !rep.Passed {
t.Fatalf("consecutive requirements must pass: %+v", rep.Violations)
}
hollow := "## 问题与决议\n\n同意通过。\n\n| 编号 | 问题 | 决议 | 责任人 | 期限 | 关闭准则 |\n| --- | --- | --- | --- | --- | --- |\n| Q-00 | 无 | 同意通过 | 待确认 | 待确认 | 无 |\n\n## 遗留行动项跟踪\n\n| 行动项 ID | 内容 | 责任人 | 期限 | 状态 | 阻塞影响 |\n| --- | --- | --- | --- | --- | --- |\n| AI-00 | 无 | 待确认 | 待确认 | 关闭 | 无 |\n"
rep, err = gate.Validate(ws, "rd_planning_review", hollow)
if err != nil {
t.Fatal(err)
}
if rep.Passed {
t.Fatal("hollow 同意通过 must fail")
}
closed := "## 问题与决议\n\n| 编号 | 问题 | 决议 | 责任人 | 期限 | 关闭准则 |\n| --- | --- | --- | --- | --- | --- |\n| Q-01 | 计划未覆盖软件 | 补充软件开发计划 | 张三 | 2026-10-01 | 文件批准 |\n\n## 遗留行动项跟踪\n\n| 行动项 ID | 内容 | 责任人 | 期限 | 状态 | 阻塞影响 |\n| --- | --- | --- | --- | --- | --- |\n| AI-01 | 补软件计划 | 张三 | 2026-10-01 | 打开 | 阻塞设计输入 |\n"
rep, err = gate.Validate(ws, "rd_planning_review", closed)
if err != nil {
t.Fatal(err)
}
if !rep.Passed {
t.Fatalf("closed review must pass: %+v", rep.Violations)
}
// 问题表之后、同一 ## 节里的法规对照表不能被当成缺责任人的问题行。
laterTable := strings.TrimSuffix(closed, "## 遗留行动项跟踪\n") +
"### 决议解读\n\n| 法规/标准 | 相关条款 | 本次评审要求 |\n| --- | --- | --- |\n| ISO 13485:2016 | §7.3.1 | 策划应保持记录 |\n\n## 遗留行动项跟踪\n" +
strings.SplitN(closed, "## 遗留行动项跟踪\n", 2)[1]
rep, err = gate.Validate(ws, "rd_planning_review", laterTable)
if err != nil {
t.Fatal(err)
}
if !rep.Passed {
t.Fatalf("later reference table must not fail the issue table: %+v", rep.Violations)
}
sample, err := os.ReadFile("../../.doc/test-reports/assets/2026-09-19-rdev-p3-real-pi/rd_planning_review-final.md")
if err != nil {
t.Fatal(err)
}
for _, issue := range rdevReviewMinutesIssues(string(sample)) {
if strings.Contains(issue, "问题行缺少") {
t.Fatalf("saved P3 sample still flags issue rows: %s", issue)
}
}
proposalSample, err := os.ReadFile("../../.doc/test-reports/assets/2026-09-19-rdev-p3-real-pi/rd_proposal-final.md")
if err != nil {
t.Fatal(err)
}
if issues := rdevProposalHeadingIssues(string(proposalSample)); len(issues) == 0 {
t.Fatal("P3 rd_proposal dry fixture must fail title alignment (self-authored headings)")
}
aligned := "## 结论\n\nok\n\n## 适用性\n\nok\n\n## 项目背景与立项依据\n\nok\n\n## 产品定义与范围\n\nok\n\n## 目标、里程碑与资源假设\n\nok\n\n## 主要风险与对策概览\n\nok\n\n## 追溯\n\n| RD- | |\n\n## 已有材料摘要\n\nok\n\n## 待确认清单\n\n- 无\n\n## 签署\n\nok\n\n## 说明\n\nok\n"
if issues := rdevProposalHeadingIssues(aligned); len(issues) != 0 {
t.Fatalf("template-aligned proposal must pass heading gate: %v", issues)
}
fuzzy := strings.ReplaceAll(aligned, "## 主要风险与对策概览", "## 风险清单")
if got := rdevProposalHeadingIssues(fuzzy); len(got) == 0 {
t.Fatal("renamed risk heading must fail exact title alignment")
}
thin := "已收照片 1 张。"
rep, err = gate.Validate(ws, "rd_prototype_photos", thin)
if err != nil {
t.Fatal(err)
}
if rep.Passed {
t.Fatal("upload index without a table must fail")
}
indexed := "| 附件 | 状态 | 缺失影响 |\n| --- | --- | --- |\n| 正面照 | 缺失 | 缺失影响验证拍照,待上传后更新 |\n"
rep, err = gate.Validate(ws, "rd_prototype_photos", indexed)
if err != nil {
t.Fatal(err)
}
if !rep.Passed {
t.Fatalf("indexed upload must pass: %+v", rep.Violations)
}
}