Files
Agent-System/internal/server/project_workflow_api_test.go
weichenandCursor 01a783b8d5 test(swboot): SWBOOT-09 L1 suite and acceptance report
Update catalog size expectations to five workflows and archive the
unit-test report; sync FEATURE-MATRIX and task docs without merging to
dev.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-16 15:45:08 +00:00

259 lines
11 KiB
Go
Raw Permalink 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. 查询项目挂载的工作流清单 -> 包含医疗三 + 软件两默认工作流
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, 5, 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 仍停留 planning,目录已挂,不可直切 active
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, "planning", legacyProj.LifecycleStatus)
assert.Equal(t, "", legacyProj.ActiveWorkflowKey)
reqKeyedWFs := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/api/v1/projects/%d/workflows", legacyProj.ID), nil)
recKeyedWFs := httptest.NewRecorder()
handler.ServeHTTP(recKeyedWFs, reqKeyedWFs)
require.Equal(t, http.StatusOK, recKeyedWFs.Code)
var keyedWFs []*projectworkflow.ProjectWorkflow
require.NoError(t, json.Unmarshal(recKeyedWFs.Body.Bytes(), &keyedWFs))
assert.Equal(t, 5, len(keyedWFs))
bodySwitchCreator, _ := json.Marshal(projectworkflow.ActivateWorkflowInput{
WorkflowKey: "product_manager",
ExpectedVersion: 1,
Reason: "planning 项目不得靠创建旁路直切",
})
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)
assert.Equal(t, http.StatusConflict, recSwitchCreator.Code)
assert.Contains(t, recSwitchCreator.Body.String(), "LEGACY_SWITCH_BLOCKED")
// 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, 5, 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 := bizdev.Snapshot(srv.BizdevService()).Gov.CreateOrGetDocument(
nil, srDocID, projectID, "system_reqs", "系统需求规格书",
"V0.5_feasibility", "",
2, "张工", 3, "李主管", 4, "王总监",
)
require.NoError(t, err)
_ = bizdev.Snapshot(srv.BizdevService()).Gov.MarkDocumentContentUpdated(nil, srDocID, "hash_v1", false)
_, _ = bizdev.Snapshot(srv.BizdevService()).Gov.SubmitForReview(nil, srDocID, 2, "author", "张工")
_, _ = bizdev.Snapshot(srv.BizdevService()).Gov.ReviewApprove(nil, srDocID, 3, "reviewer", "李主管", "同意")
approvedDoc, err := bizdev.Snapshot(srv.BizdevService()).Gov.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)
}