A later standards table in the same section was counted as issue rows and failed the saved P3 sample. Co-authored-by: Cursor <cursoragent@cursor.com>
153 lines
5.6 KiB
Go
153 lines
5.6 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()
|
|
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)
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|