diff --git a/internal/bizdev/service.go b/internal/bizdev/service.go index f6dd319a..5d94e000 100644 --- a/internal/bizdev/service.go +++ b/internal/bizdev/service.go @@ -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, diff --git a/internal/server/handlers.go b/internal/server/handlers.go index 4f5833d2..7f245097 100644 --- a/internal/server/handlers.go +++ b/internal/server/handlers.go @@ -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) diff --git a/internal/server/project_handlers.go b/internal/server/project_handlers.go index 2ea00234..703dbf3e 100644 --- a/internal/server/project_handlers.go +++ b/internal/server/project_handlers.go @@ -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) +} + diff --git a/internal/server/project_workflow_api_test.go b/internal/server/project_workflow_api_test.go index 601daca2..83b53f77 100644 --- a/internal/server/project_workflow_api_test.go +++ b/internal/server/project_workflow_api_test.go @@ -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) +} + diff --git a/internal/server/server.go b/internal/server/server.go index e8d439a8..417a59ac 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -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) diff --git a/templates/docs/test_manager.md b/templates/docs/test_manager.md new file mode 100644 index 00000000..4d9c9818 --- /dev/null +++ b/templates/docs/test_manager.md @@ -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),必须由测试经理重新触发验证方案评审或回归编制。