feat(workflow): Phase 5 智能测试经理工作流、前置强依赖校验与文档血缘级联失效

This commit is contained in:
WeiCHEN
2026-09-13 02:50:21 +00:00
committed by weichen
parent f994c45d67
commit abeab90388
6 changed files with 241 additions and 0 deletions
+53
View File
@@ -113,6 +113,10 @@ func NewService(cfg *config.Config) *Service {
}
}
}
if pwSvc != nil && doc.TaskKey == "system_reqs" {
_, _ = pwSvc.CheckAndPropagateStale(ctx, doc.DocID, doc.ContentSHA256, fmt.Sprintf("上游系统需求规格书 V%d 已正式批准,已标记关联下游验证方案为待复核(stale)", doc.Version))
}
})
return svc
@@ -280,6 +284,38 @@ func (s *Service) ExecuteTask(ctx context.Context, req TaskRunRequest) (*TaskRun
if docID == "" {
docID = fmt.Sprintf("doc_proj%d_%s", req.ProjectID, req.TaskKey)
}
// 智能测试经理前置检查:必须依赖同项目已批准的系统需求规格书 (system_reqs)
var upstreamSRDoc *docgov.DocumentRecord
if req.TaskKey == "test_manager" {
upstreamDocID := fmt.Sprintf("doc_proj%d_system_reqs", req.ProjectID)
if s.docGov != nil && req.ProjectID > 0 {
doc, err := s.docGov.GetDocument(ctx, upstreamDocID)
if err == nil && doc != nil && doc.CurrentState == docgov.StateApproved {
upstreamSRDoc = doc
}
}
if upstreamSRDoc == nil {
return &TaskRunResult{
TaskKey: req.TaskKey,
ArtifactTitle: artifactTitle,
WorkDir: req.WorkDir,
Success: false,
ErrorReason: "智能测试经理工作流前置条件未满足:必须先完成同项目的《系统需求规格书》并经批准 (approved) 后方可启动系统验证编制",
}, projectworkflow.ErrPrerequisiteNotMet
}
// 自动将已批准系统需求规格书注入为测试方案编制的依据输入
if len(materials) == 0 && upstreamSRDoc.WorkDir != "" {
contentPath := filepath.Join(upstreamSRDoc.WorkDir, "output.md")
if b, err := os.ReadFile(contentPath); err == nil && len(b) > 0 {
materials = append(materials, squad.MaterialItem{
Filename: "upstream_system_requirements.md",
Content: b,
})
}
}
}
authorID := req.AuthorID
if authorID == 0 {
authorID = req.UserID
@@ -561,6 +597,23 @@ func (s *Service) ExecuteTask(ctx context.Context, req TaskRunRequest) (*TaskRun
}
}
if res.Success && req.TaskKey == "test_manager" && upstreamSRDoc != nil && s.projectWorkflow != nil && req.ProjectID > 0 {
_ = s.projectWorkflow.RecordLineage(ctx, &projectworkflow.DocumentLineage{
ProjectID: req.ProjectID,
DownstreamDocID: docID,
DownstreamVersion: 1,
UpstreamDocID: upstreamSRDoc.DocID,
UpstreamVersion: upstreamSRDoc.Version,
UpstreamSHA256: upstreamSRDoc.ContentSHA256,
IsStale: false,
})
}
if res.Success && req.TaskKey == "system_reqs" && s.projectWorkflow != nil && req.ProjectID > 0 {
sha := sha256.Sum256([]byte(res.MarkdownBody))
shaStr := hex.EncodeToString(sha[:])
_, _ = s.projectWorkflow.CheckAndPropagateStale(ctx, docID, shaStr, "上游系统需求规格书已更新,下游验证方案需复核")
}
return &TaskRunResult{
TaskKey: res.TaskKey,
ArtifactTitle: res.ArtifactTitle,
+2
View File
@@ -267,6 +267,8 @@ func writeTaskError(w http.ResponseWriter, err error, payload any) {
status = http.StatusConflict
} else if errors.Is(err, docgov.ErrDocumentLocked) || errors.Is(err, docgov.ErrForceEditNotAllowed) {
status = http.StatusConflict
} else if errors.Is(err, projectworkflow.ErrPrerequisiteNotMet) {
status = http.StatusBadRequest
}
if payload != nil {
writeJSONResponse(w, status, payload)
+28
View File
@@ -4,9 +4,11 @@ import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strconv"
"strings"
"chat-agent-system/internal/projectworkflow"
)
@@ -228,3 +230,29 @@ func (s *Server) handleListProjectWorkflowRuns(w http.ResponseWriter, r *http.Re
}
writeJSONResponse(w, http.StatusOK, runs)
}
// handleGetProjectLineage 查询项目文档血缘关系及失效状态
func (s *Server) handleGetProjectLineage(w http.ResponseWriter, r *http.Request) {
docID := strings.TrimSpace(r.URL.Query().Get("doc_id"))
if docID == "" {
idStr := r.PathValue("id")
id, _ := strconv.ParseInt(idStr, 10, 64)
if id > 0 {
docID = fmt.Sprintf("doc_proj%d_test_manager", id)
}
}
if docID == "" {
http.Error(w, `{"error":"ERR_INVALID_PARAM","message":"doc_id is required"}`, http.StatusBadRequest)
return
}
lineages, err := s.pwSvc.GetLineageForDownstream(r.Context(), docID, 0)
if err != nil {
http.Error(w, `{"error":"ERR_INTERNAL","message":"`+err.Error()+`"}`, http.StatusInternalServerError)
return
}
if lineages == nil {
lineages = make([]*projectworkflow.DocumentLineage, 0)
}
writeJSONResponse(w, http.StatusOK, lineages)
}
@@ -156,3 +156,90 @@ func TestProjectWorkflowAPI_Lifecycle(t *testing.T) {
assert.NotEmpty(t, runsList)
assert.Equal(t, "completed", runsList[0].Status)
}
func TestProjectWorkflow_TestManagerAndLineageStale(t *testing.T) {
srv, handler := setupTestServerForProjectWorkflow(t)
// 1. 创建测试项目
bodyCreate, _ := json.Marshal(projectworkflow.CreateProjectInput{
Name: "智能肺功能仪",
DeviceType: "呼吸诊疗设备",
IntendedUse: "肺通气功能评测",
TargetMarket: "NMPA Class II",
})
reqCreate := httptest.NewRequest(http.MethodPost, "/api/v1/projects", bytes.NewReader(bodyCreate))
reqCreate.Header.Set("X-User-ID", "2")
recCreate := httptest.NewRecorder()
handler.ServeHTTP(recCreate, reqCreate)
require.Equal(t, http.StatusCreated, recCreate.Code)
var proj projectworkflow.Project
_ = json.Unmarshal(recCreate.Body.Bytes(), &proj)
// 2. 在未批准系统需求规格书时,直接触发 test_manager -> 应触发前置阻断 400 / ERR_PRECONDITION_FAILED
bodyRunTestFail, _ := json.Marshal(map[string]any{
"task_key": "test_manager",
"project_name": proj.Name,
})
reqRunTestFail := httptest.NewRequest(http.MethodPost, "/api/v1/projects/1001/workflows/test_manager/run", bytes.NewReader(bodyRunTestFail))
reqRunTestFail.Header.Set("X-User-ID", "2")
recRunTestFail := httptest.NewRecorder()
handler.ServeHTTP(recRunTestFail, reqRunTestFail)
assert.Equal(t, http.StatusBadRequest, recRunTestFail.Code)
assert.Contains(t, recRunTestFail.Body.String(), "前置条件未满足")
// 3. 在受控治理中创建并批准系统需求规格书
srDocID := "doc_proj1001_system_reqs"
srDoc, err := srv.BizdevService().DocGov().CreateOrGetDocument(
nil, srDocID, 1001, "system_reqs", "系统需求规格书",
"V0.5_feasibility", "",
2, "张工", 3, "李主管", 4, "王总监",
)
require.NoError(t, err)
_ = srv.BizdevService().DocGov().MarkDocumentContentUpdated(nil, srDocID, "hash_v1", false)
_, _ = srv.BizdevService().DocGov().SubmitForReview(nil, srDocID, 2, "author", "张工")
_, _ = srv.BizdevService().DocGov().ReviewApprove(nil, srDocID, 3, "reviewer", "李主管", "同意")
_, err = srv.BizdevService().DocGov().ApproverApprove(nil, srDocID, 4, "approver", "王总监", "批准生效")
require.NoError(t, err)
assert.Equal(t, "approved", string(srDoc.CurrentState))
// 4. 前置已批准后,触发 test_manager 运行 -> 成功 200
bodyRunTestSuccess, _ := json.Marshal(map[string]any{
"task_key": "test_manager",
"project_name": proj.Name,
"materials": []map[string]string{
{
"filename": "upstream_system_requirements.md",
"content": "# 系统需求\n| REQ-01 | 测量流量误差 <= 3% | 测试 | 高 |\n",
},
},
})
reqRunTestSuccess := httptest.NewRequest(http.MethodPost, "/api/v1/projects/1001/workflows/test_manager/run", bytes.NewReader(bodyRunTestSuccess))
reqRunTestSuccess.Header.Set("X-User-ID", "2")
recRunTestSuccess := httptest.NewRecorder()
handler.ServeHTTP(recRunTestSuccess, reqRunTestSuccess)
assert.Equal(t, http.StatusOK, recRunTestSuccess.Code)
// 5. 查询文档血缘 -> 存在且 is_stale == false
reqLineage := httptest.NewRequest(http.MethodGet, "/api/v1/projects/1001/lineage?doc_id=doc_proj1001_test_manager", nil)
recLineage := httptest.NewRecorder()
handler.ServeHTTP(recLineage, reqLineage)
assert.Equal(t, http.StatusOK, recLineage.Code)
var lineages []*projectworkflow.DocumentLineage
_ = json.Unmarshal(recLineage.Body.Bytes(), &lineages)
require.NotEmpty(t, lineages)
assert.Equal(t, srDocID, lineages[0].UpstreamDocID)
assert.False(t, lineages[0].IsStale)
// 6. 上游 system_reqs 发生更新(模拟触发 CheckAndPropagateStale)
_, err = srv.ProjectWorkflowService().CheckAndPropagateStale(nil, srDocID, "hash_v2_changed", "需求发生重大变更")
require.NoError(t, err)
// 再次查询下游血缘 -> is_stale 应变为 true
recLineage2 := httptest.NewRecorder()
handler.ServeHTTP(recLineage2, reqLineage)
var lineagesAfter []*projectworkflow.DocumentLineage
_ = json.Unmarshal(recLineage2.Body.Bytes(), &lineagesAfter)
require.NotEmpty(t, lineagesAfter)
assert.True(t, lineagesAfter[0].IsStale)
}
+11
View File
@@ -70,6 +70,16 @@ func (s *Server) AuthService() *auth.Service {
return s.authSvc
}
// BizdevService 获取业务交付服务
func (s *Server) BizdevService() *bizdev.Service {
return s.svc
}
// ProjectWorkflowService 获取项目多工作流控制服务
func (s *Server) ProjectWorkflowService() *projectworkflow.Service {
return s.pwSvc
}
// Routes 构建 HTTP 路由分发器
func (s *Server) Routes() http.Handler {
mux := http.NewServeMux()
@@ -87,6 +97,7 @@ func (s *Server) Routes() http.Handler {
mux.HandleFunc("GET /api/v1/workflows", s.handleListWorkflowDefinitions)
mux.HandleFunc("POST /api/v1/projects/{id}/workflows/{workflow_key}/run", s.handleRunProjectWorkflow)
mux.HandleFunc("GET /api/v1/projects/{id}/workflows/{workflow_key}/runs", s.handleListProjectWorkflowRuns)
mux.HandleFunc("GET /api/v1/projects/{id}/lineage", s.handleGetProjectLineage)
// 3. 对话协同服务 (标准 JSON 阻塞)
mux.HandleFunc("POST /api/v1/chat", s.handleChat)
+60
View File
@@ -0,0 +1,60 @@
# 系统验证方案及计划(SVP)
- 项目:{{project_name}}
- 生成时间:{{generated_at}}
- 快照:{{snapshot_id}}
- 文件编号:RD-SVP
- 记录归属:DHF
- 版本:A
- 阶段:04 设计验证
## 结论
{{conclusion}}
## 验证范围与策略
- 产品名称:{{project_name}}
- 验证类型:系统级功能、性能、电气安全、环境适应性与电磁兼容测试
- 上游依据:系统需求规格书 (RD-SR)
- 验证原则:需求 100% 全覆盖,关键风险与基本安全项目双重验证
## 测试环境与工具
| 仪器设备/软件工具 | 规格型号/版本 | 校验状态/有效期限 | 用途说明 |
| --- | --- | --- | --- |
| 模拟生理信号源 | 标准多参源 V2.1 | 计量校准合格 | 产生心率/血压模拟基准信号 |
| 医用示波器 | 100MHz 数字存储 | 检定合格 | 接口电平及波形质量测量 |
| 自动化测试平台 | AutoTest Runner 1.0 | 已确认验证 | 协议与长周期稳定性测试 |
## 系统测试用例及判定准则
| 用例 ID | 追溯需求 ID | 测试项与方法说明 | 判定准则与接受限值 | 测试结果 |
| --- | --- | --- | --- | --- |
| TC-01 | REQ-01 | 按规程输入标准心率模拟信号,采样并计算误差 | 心率测量误差不超过 ±2bpm 或 ±2% | 符合 |
| TC-02 | REQ-02 | 连续工作 72 小时无故障与数据漂移测试 | 72 小时内无死机、内存泄漏与通信中断 | 符合 |
{{test_cases_table}}
## 需求覆盖率与追踪矩阵
| 系统需求 ID | 需求陈述摘要 | 对应测试用例 ID | 覆盖结论 |
| --- | --- | --- | --- |
| REQ-01 | 心率与生理参数监测精度 | TC-01 | 100% 已覆盖 |
| REQ-02 | 系统长周期运行稳定性与连续性 | TC-02 | 100% 已覆盖 |
{{traceability_matrix_table}}
## 待确认清单
{{pending_items}}
## 签署
| 角色 | 姓名 | 结论 | 日期 |
| --- | --- | --- | --- |
| 编制 | {{author_name}} | 已编制 | {{generated_at}} |
| 审核 | {{reviewer_name}} | 待审核 | |
| 批准 | {{approver_name}} | 待批准 | |
## 说明
本文档由智能测试经理小队基于同项目已批准的《系统需求规格书》自动关联生成。若上游系统需求规格书发生修订或版本变更,本文档血缘将自动标记为失效(stale),必须由测试经理重新触发验证方案评审或回归编制。