246 lines
10 KiB
Go
246 lines
10 KiB
Go
package server_test
|
||
|
||
import (
|
||
"bytes"
|
||
"encoding/json"
|
||
"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(),
|
||
}
|
||
svc := bizdev.NewService(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))
|
||
reqCreate.Header.Set("X-User-ID", "2")
|
||
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, "main", createdProj.ActiveWorkflowKey)
|
||
assert.Equal(t, 1, createdProj.WorkflowSelectionVersion)
|
||
|
||
// 3. 列出项目列表 -> 包含刚创建的项目
|
||
reqList := httptest.NewRequest(http.MethodGet, "/api/v1/projects", nil)
|
||
reqList.Header.Set("X-User-ID", "2")
|
||
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. 非创建人尝试切换工作流 (User 3) -> 403 Forbidden
|
||
bodySwitch, _ := json.Marshal(projectworkflow.ActivateWorkflowInput{
|
||
WorkflowKey: "product_manager",
|
||
ExpectedVersion: 1,
|
||
Reason: "非法切换",
|
||
})
|
||
reqSwitchOther := httptest.NewRequest(http.MethodPut, "/api/v1/projects/1001/active-workflow", bytes.NewReader(bodySwitch))
|
||
reqSwitchOther.Header.Set("X-User-ID", "3") // Other user
|
||
recSwitchOther := httptest.NewRecorder()
|
||
handler.ServeHTTP(recSwitchOther, reqSwitchOther)
|
||
assert.Equal(t, http.StatusForbidden, recSwitchOther.Code)
|
||
|
||
// 6. 创建人切换工作流 (User 2) -> 200 OK
|
||
bodySwitchCreator, _ := json.Marshal(projectworkflow.ActivateWorkflowInput{
|
||
WorkflowKey: "product_manager",
|
||
ExpectedVersion: 1,
|
||
Reason: "切换至智能产品经理编制需求",
|
||
})
|
||
reqSwitchCreator := httptest.NewRequest(http.MethodPut, "/api/v1/projects/1001/active-workflow", bytes.NewReader(bodySwitchCreator))
|
||
reqSwitchCreator.Header.Set("X-User-ID", "2") // Creator
|
||
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, "/api/v1/projects/1001/active-workflow", bytes.NewReader(bodySwitchConflict))
|
||
reqSwitchConflict.Header.Set("X-User-ID", "2")
|
||
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": "高频手术设备",
|
||
})
|
||
reqRun := httptest.NewRequest(http.MethodPost, "/api/v1/projects/1001/workflows/main/run", bytes.NewReader(bodyRun))
|
||
reqRun.Header.Set("X-User-ID", "2")
|
||
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.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)
|
||
}
|
||
|