Files
Agent-System/internal/server/project_workflow_api_test.go
T
weichenandCursor ad6e38d11a test(workflow): add governance regression and activation PG coverage
Cover public auth config, catalog policy, official main workflow activation on Postgres, and conn-busy metadata writeback fixes.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-13 02:50:23 +00:00

279 lines
12 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
}