package server_test import ( "bytes" "encoding/json" "fmt" "net/http" "net/http/httptest" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "chat-agent-system/internal/bizdev" "chat-agent-system/internal/config" "chat-agent-system/internal/projectworkflow" "chat-agent-system/internal/server" ) func setupTestServerForProjectWorkflow(t *testing.T) (*server.Server, http.Handler) { cfg := &config.Config{ DataDir: t.TempDir(), AgentBackend: "stub", AllowInMemory: true, } svc := bizdev.MustNewService(t, cfg) srv := server.NewServer(cfg, svc) handler := srv.Routes() return srv, handler } func TestProjectWorkflowAPI_Lifecycle(t *testing.T) { _, handler := setupTestServerForProjectWorkflow(t) // 1. 未认证创建项目 -> 401 bodyUnauth, _ := json.Marshal(projectworkflow.CreateProjectInput{ Name: "未授权项目", }) reqUnauth := httptest.NewRequest(http.MethodPost, "/api/v1/projects", bytes.NewReader(bodyUnauth)) recUnauth := httptest.NewRecorder() handler.ServeHTTP(recUnauth, reqUnauth) assert.Equal(t, http.StatusUnauthorized, recUnauth.Code) // 2. 认证用户创建项目 (User 2: 张工) -> 201 bodyCreate, _ := json.Marshal(projectworkflow.CreateProjectInput{ Name: "智能高频电刀 (ElectroCutter)", DeviceType: "高频手术设备", IntendedUse: "微创外科软组织切割与止血", TargetMarket: "NMPA Class III", }) reqCreate := httptest.NewRequest(http.MethodPost, "/api/v1/projects", bytes.NewReader(bodyCreate)) withUserIDAuth(reqCreate, loginTestUser(t, handler, "zhanggong", "123456")) recCreate := httptest.NewRecorder() handler.ServeHTTP(recCreate, reqCreate) require.Equal(t, http.StatusCreated, recCreate.Code) var createdProj projectworkflow.Project err := json.Unmarshal(recCreate.Body.Bytes(), &createdProj) require.NoError(t, err) assert.Equal(t, int64(2), createdProj.CreatorUserID) assert.Equal(t, "planning", createdProj.LifecycleStatus) assert.Equal(t, "", createdProj.ActiveWorkflowKey) assert.Equal(t, 1, createdProj.WorkflowSelectionVersion) // 3. 列出项目列表 -> 包含刚创建的项目 reqList := httptest.NewRequest(http.MethodGet, "/api/v1/projects", nil) withUserIDAuth(reqList, loginTestUser(t, handler, "zhanggong", "123456")) recList := httptest.NewRecorder() handler.ServeHTTP(recList, reqList) assert.Equal(t, http.StatusOK, recList.Code) var projList []*projectworkflow.Project err = json.Unmarshal(recList.Body.Bytes(), &projList) require.NoError(t, err) assert.NotEmpty(t, projList) // 4. 查询项目挂载的工作流清单 -> 包含 3 个默认工作流 reqWFs := httptest.NewRequest(http.MethodGet, "/api/v1/projects/1001/workflows", nil) recWFs := httptest.NewRecorder() handler.ServeHTTP(recWFs, reqWFs) assert.Equal(t, http.StatusOK, recWFs.Code) var wfList []*projectworkflow.ProjectWorkflow err = json.Unmarshal(recWFs.Body.Bytes(), &wfList) require.NoError(t, err) assert.Equal(t, 3, len(wfList)) // 5. planning 项目禁止旧式切换 -> 409 Conflict bodySwitchBlocked, _ := json.Marshal(projectworkflow.ActivateWorkflowInput{ WorkflowKey: "product_manager", ExpectedVersion: 1, Reason: "planning 项目应走设置激活", }) reqSwitchBlocked := httptest.NewRequest(http.MethodPut, fmt.Sprintf("/api/v1/projects/%d/active-workflow", createdProj.ID), bytes.NewReader(bodySwitchBlocked)) withUserIDAuth(reqSwitchBlocked, loginTestUser(t, handler, "zhanggong", "123456")) recSwitchBlocked := httptest.NewRecorder() handler.ServeHTTP(recSwitchBlocked, reqSwitchBlocked) assert.Equal(t, http.StatusConflict, recSwitchBlocked.Code) assert.Contains(t, recSwitchBlocked.Body.String(), "LEGACY_SWITCH_BLOCKED") // 6. 显式 initial_workflow_key 的 legacy 项目:ACL + 切换 bodyLegacyCreate, _ := json.Marshal(projectworkflow.CreateProjectInput{ Name: "Legacy 切换测试项目", InitialWorkflowKey: "main", }) reqLegacyCreate := httptest.NewRequest(http.MethodPost, "/api/v1/projects", bytes.NewReader(bodyLegacyCreate)) withUserIDAuth(reqLegacyCreate, loginTestUser(t, handler, "zhanggong", "123456")) recLegacyCreate := httptest.NewRecorder() handler.ServeHTTP(recLegacyCreate, reqLegacyCreate) require.Equal(t, http.StatusCreated, recLegacyCreate.Code) var legacyProj projectworkflow.Project require.NoError(t, json.Unmarshal(recLegacyCreate.Body.Bytes(), &legacyProj)) assert.Equal(t, "main", legacyProj.ActiveWorkflowKey) bodySwitchOther, _ := json.Marshal(projectworkflow.ActivateWorkflowInput{ WorkflowKey: "product_manager", ExpectedVersion: 1, Reason: "非法切换", }) reqSwitchOther := httptest.NewRequest(http.MethodPut, fmt.Sprintf("/api/v1/projects/%d/active-workflow", legacyProj.ID), bytes.NewReader(bodySwitchOther)) withUserIDAuth(reqSwitchOther, loginTestUser(t, handler, "lizhuguan", "123456")) recSwitchOther := httptest.NewRecorder() handler.ServeHTTP(recSwitchOther, reqSwitchOther) assert.Equal(t, http.StatusForbidden, recSwitchOther.Code) bodySwitchCreator, _ := json.Marshal(projectworkflow.ActivateWorkflowInput{ WorkflowKey: "product_manager", ExpectedVersion: 1, Reason: "切换至智能产品经理编制需求", }) reqSwitchCreator := httptest.NewRequest(http.MethodPut, fmt.Sprintf("/api/v1/projects/%d/active-workflow", legacyProj.ID), bytes.NewReader(bodySwitchCreator)) withUserIDAuth(reqSwitchCreator, loginTestUser(t, handler, "zhanggong", "123456")) recSwitchCreator := httptest.NewRecorder() handler.ServeHTTP(recSwitchCreator, reqSwitchCreator) require.Equal(t, http.StatusOK, recSwitchCreator.Code) var switchResult projectworkflow.ActivateWorkflowResult err = json.Unmarshal(recSwitchCreator.Body.Bytes(), &switchResult) require.NoError(t, err) assert.Equal(t, "product_manager", switchResult.ActiveWorkflowKey) assert.Equal(t, 2, switchResult.Version) // 7. 再次用旧版本 1 切换 -> 409 Conflict bodySwitchConflict, _ := json.Marshal(projectworkflow.ActivateWorkflowInput{ WorkflowKey: "test_manager", ExpectedVersion: 1, Reason: "并发冲突测试", }) reqSwitchConflict := httptest.NewRequest(http.MethodPut, fmt.Sprintf("/api/v1/projects/%d/active-workflow", legacyProj.ID), bytes.NewReader(bodySwitchConflict)) withUserIDAuth(reqSwitchConflict, loginTestUser(t, handler, "zhanggong", "123456")) recSwitchConflict := httptest.NewRecorder() handler.ServeHTTP(recSwitchConflict, reqSwitchConflict) assert.Equal(t, http.StatusConflict, recSwitchConflict.Code) // 8. 查询全局工作流规范目录 -> 200 OK reqDefs := httptest.NewRequest(http.MethodGet, "/api/v1/workflows", nil) recDefs := httptest.NewRecorder() handler.ServeHTTP(recDefs, reqDefs) assert.Equal(t, http.StatusOK, recDefs.Code) var defs []*projectworkflow.Definition err = json.Unmarshal(recDefs.Body.Bytes(), &defs) require.NoError(t, err) assert.Equal(t, 3, len(defs)) // 9. 触发工作流运行 -> 200 OK bodyRun, _ := json.Marshal(map[string]any{ "task_key": "intake", "project_name": "智能高频电刀", "device_type": "高频手术设备", "backend": "stub", }) reqRun := httptest.NewRequest(http.MethodPost, "/api/v1/projects/1001/workflows/main/run", bytes.NewReader(bodyRun)) withUserIDAuth(reqRun, loginTestUser(t, handler, "zhanggong", "123456")) recRun := httptest.NewRecorder() handler.ServeHTTP(recRun, reqRun) assert.Equal(t, http.StatusOK, recRun.Code) // 10. 查询运行历史 -> 200 OK 且包含刚运行的记录 reqRuns := httptest.NewRequest(http.MethodGet, "/api/v1/projects/1001/workflows/main/runs", nil) recRuns := httptest.NewRecorder() handler.ServeHTTP(recRuns, reqRuns) assert.Equal(t, http.StatusOK, recRuns.Code) var runsList []*projectworkflow.WorkflowRun err = json.Unmarshal(recRuns.Body.Bytes(), &runsList) require.NoError(t, err) assert.NotEmpty(t, runsList) assert.Contains(t, []string{"completed", "quality_failed"}, 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)) withUserIDAuth(reqCreate, loginTestUser(t, handler, "zhanggong", "123456")) recCreate := httptest.NewRecorder() handler.ServeHTTP(recCreate, reqCreate) require.Equal(t, http.StatusCreated, recCreate.Code) var proj projectworkflow.Project require.NoError(t, json.Unmarshal(recCreate.Body.Bytes(), &proj)) require.Positive(t, proj.ID) projectID := proj.ID // 2. 无已批准 SRS 时,test_manager 仍可独立启动(PM/TE 解耦) bodyRunTestNoSRS, _ := json.Marshal(map[string]any{ "task_key": "test_manager", "project_name": proj.Name, "backend": "stub", }) reqRunTestNoSRS := httptest.NewRequest(http.MethodPost, fmt.Sprintf("/api/v1/projects/%d/workflows/test_manager/run", projectID), bytes.NewReader(bodyRunTestNoSRS)) withUserIDAuth(reqRunTestNoSRS, loginTestUser(t, handler, "zhanggong", "123456")) recRunTestNoSRS := httptest.NewRecorder() handler.ServeHTTP(recRunTestNoSRS, reqRunTestNoSRS) assert.Equal(t, http.StatusOK, recRunTestNoSRS.Code) assert.NotContains(t, recRunTestNoSRS.Body.String(), "前置条件未满足") // 3. 在受控治理中创建并批准系统需求规格书 srDocID := fmt.Sprintf("doc_proj%d_system_reqs", projectID) _, err := srv.BizdevService().DocGov().CreateOrGetDocument( nil, srDocID, projectID, "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", "李主管", "同意") approvedDoc, err := srv.BizdevService().DocGov().ApproverApprove(nil, srDocID, 4, "approver", "王总监", "批准生效") require.NoError(t, err) assert.Equal(t, "approved", string(approvedDoc.CurrentState)) // 4. 前置已批准后,触发 test_manager 运行 -> 成功 200 bodyRunTestSuccess, _ := json.Marshal(map[string]any{ "task_key": "test_manager", "project_name": proj.Name, "backend": "stub", }) reqRunTestSuccess := httptest.NewRequest(http.MethodPost, fmt.Sprintf("/api/v1/projects/%d/workflows/test_manager/run", projectID), bytes.NewReader(bodyRunTestSuccess)) withUserIDAuth(reqRunTestSuccess, loginTestUser(t, handler, "zhanggong", "123456")) recRunTestSuccess := httptest.NewRecorder() handler.ServeHTTP(recRunTestSuccess, reqRunTestSuccess) if recRunTestSuccess.Code != http.StatusOK { t.Fatalf("test_manager run failed: %d %s", recRunTestSuccess.Code, recRunTestSuccess.Body.String()) } // 5. 查询文档血缘 -> 存在且 is_stale == false lineageDocID := fmt.Sprintf("doc_proj%d_test_manager", projectID) lineageToken := loginTestUser(t, handler, "zhanggong", "123456") reqLineage := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/api/v1/projects/%d/lineage?doc_id=%s", projectID, lineageDocID), nil) withUserIDAuth(reqLineage, lineageToken) 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) }