Files
Agent-System/internal/server/workflow_governance_piagent_pg_test.go
T
weichen 185c42fc94 test(governance): 补跑 product_manager PG+PI 全链路并通过
抽取 runPiAgentGovernanceE2E 共用 harness,新增 system_reqs 真实 Agent 验收并更新测试报告。
2026-09-13 02:50:23 +00:00

291 lines
11 KiB
Go

package server_test
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/jackc/pgx/v5/pgxpool"
"chat-agent-system/internal/bizdev"
"chat-agent-system/internal/notification"
"chat-agent-system/internal/projectinstance"
"chat-agent-system/internal/projectworkflow"
"chat-agent-system/internal/server"
"chat-agent-system/internal/workflowdef"
)
func piAgentRunnerHealthy() bool {
client := &http.Client{Timeout: 3 * time.Second}
resp, err := client.Get("http://127.0.0.1:18865/health")
if err != nil {
return false
}
resp.Body.Close()
return resp.StatusCode == http.StatusOK
}
func setupPGPiAgentServer(t *testing.T, pool *pgxpool.Pool) (http.Handler, *server.Server) {
t.Helper()
cfg := configFromTestDatabaseURL(t)
cfg.DataDir = piAgentSharedDataDir(t)
cfg.AgentBackend = "pi-agent"
cfg.AgentTimeout = 60 * time.Minute
cfg.PiAgentRunnerURL = envOr("PI_AGENT_RUNNER_URL", "http://127.0.0.1:18865")
cfg.PiAgentRunnerAuthToken = envOr("PI_AGENT_RUNNER_AUTH_TOKEN", envOr("RUNNER_AUTH_TOKEN", ""))
cfg.PiAgentModel = "MiniMax-M3"
cfg.PiAgentProvider = "minimax"
if cfg.PiAgentRunnerAuthToken == "" {
t.Skip("RUNNER_AUTH_TOKEN not set; skipping pi-agent PG E2E")
}
mergePiAgentE2EKbConfig(cfg)
svc := bizdev.MustNewService(t, cfg)
if svc.Pool() == nil {
t.Skip("bizdev service not connected to postgres")
}
srv := server.NewServer(cfg, svc)
return srv.Routes(), srv
}
type piAgentGovernanceCase struct {
WfKey string
TaskKey string
StageKey string
ProjectName string
ExpectPlanningDone bool
Materials []bizdev.MaterialInput
}
func runPiAgentGovernanceE2E(t *testing.T, spec piAgentGovernanceCase) {
t.Helper()
if testing.Short() {
t.Skip("skipping slow pi-agent PG E2E in short mode")
}
if !piAgentRunnerHealthy() {
t.Skip("pi-agent runner not available at http://127.0.0.1:18865/health")
}
pool := openPGPoolWithMigrations(t)
if pool == nil {
return
}
defer pool.Close()
ctx := context.Background()
handler, srv := setupPGPiAgentServer(t, pool)
ts := httptest.NewServer(handler)
defer ts.Close()
client := &http.Client{Timeout: 50 * time.Minute}
seedOfficialWorkflowManifest(t, pool, spec.WfKey)
creatorToken := loginTestUser(t, handler, "zhanggong", "123456")
reviewerToken := loginTestUser(t, handler, "lizhuguan", "123456")
approverToken := loginTestUser(t, handler, "wangzong", "123456")
creatorID := fetchAuthUserID(t, handler, creatorToken)
reviewerID := fetchAuthUserID(t, handler, reviewerToken)
approverID := fetchAuthUserID(t, handler, approverToken)
createBody, _ := json.Marshal(projectworkflow.CreateProjectInput{
Name: spec.ProjectName,
DeviceType: "Ultrasonic Device",
IntendedUse: "Tissue resection",
TargetMarket: "NMPA Class III",
})
reqCreate := httptest.NewRequest(http.MethodPost, "/api/v1/projects", bytes.NewReader(createBody))
withBearerAuth(reqCreate, creatorToken)
recCreate := httptest.NewRecorder()
handler.ServeHTTP(recCreate, reqCreate)
if recCreate.Code != http.StatusCreated {
t.Fatalf("create project: %d %s", recCreate.Code, recCreate.Body.String())
}
var proj projectworkflow.Project
if err := json.Unmarshal(recCreate.Body.Bytes(), &proj); err != nil {
t.Fatalf("decode project: %v", err)
}
t.Cleanup(func() { cleanupProject(t, pool, proj.ID) })
projectID := proj.ID
t.Logf("✓ [1. 项目创建] id=%d code=%s wf=%s", projectID, proj.Code, spec.WfKey)
reqDefault := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/api/v1/projects/%d/workflow-plan/default?workflow_key=%s&version=0", projectID, spec.WfKey), nil)
withBearerAuth(reqDefault, creatorToken)
recDefault := httptest.NewRecorder()
handler.ServeHTTP(recDefault, reqDefault)
if recDefault.Code != http.StatusOK {
t.Fatalf("default plan: %d %s", recDefault.Code, recDefault.Body.String())
}
var plan projectinstance.WorkflowPlan
_ = json.Unmarshal(recDefault.Body.Bytes(), &plan)
for i := range plan.Stages {
for j := range plan.Stages[i].DocTasks {
plan.Stages[i].DocTasks[j].Duties = projectinstance.DocDuty{
AuthorUserID: creatorID, ReviewerUserID: reviewerID, ApproverUserID: approverID,
}
}
}
saveBody, _ := json.Marshal(projectinstance.SavePlanInput{Plan: plan, ExpectedPlanRevision: 0, ExpectedSelectionVersion: 1})
reqSave := httptest.NewRequest(http.MethodPut, fmt.Sprintf("/api/v1/projects/%d/workflow-plan", projectID), bytes.NewReader(saveBody))
withBearerAuth(reqSave, creatorToken)
recSave := httptest.NewRecorder()
handler.ServeHTTP(recSave, reqSave)
if recSave.Code != http.StatusOK {
t.Fatalf("save plan: %d %s", recSave.Code, recSave.Body.String())
}
reqPreview := httptest.NewRequest(http.MethodPost, fmt.Sprintf("/api/v1/projects/%d/activation-preview", projectID), nil)
withBearerAuth(reqPreview, creatorToken)
recPreview := httptest.NewRecorder()
handler.ServeHTTP(recPreview, reqPreview)
var preview projectinstance.ActivationPreview
_ = json.Unmarshal(recPreview.Body.Bytes(), &preview)
activateBody, _ := json.Marshal(projectinstance.ActivateInput{
ConfirmationToken: preview.ConfirmationToken, ExpectedProjectVersion: preview.ExpectedProjectVersion,
ExpectedPlanRevision: preview.ExpectedPlanRevision,
})
reqActivate := httptest.NewRequest(http.MethodPost, fmt.Sprintf("/api/v1/projects/%d/workflow-instances/activate", projectID), bytes.NewReader(activateBody))
withBearerAuth(reqActivate, creatorToken)
recActivate := httptest.NewRecorder()
handler.ServeHTTP(recActivate, reqActivate)
if recActivate.Code != http.StatusOK {
t.Fatalf("activate: %d %s", recActivate.Code, recActivate.Body.String())
}
var activated projectinstance.ActivateResult
_ = json.Unmarshal(recActivate.Body.Bytes(), &activated)
instanceID := activated.Instance.ID
t.Logf("✓ [2. 工作流激活] instance_id=%d", instanceID)
taskStart := time.Now()
taskBody, _ := json.Marshal(server.TaskRunRequestDTO{
TaskKey: spec.TaskKey,
ProjectName: proj.Name,
DeviceType: proj.DeviceType,
IntendedUse: "Tissue resection",
TargetMarket: "NMPA Class III",
Backend: "pi-agent",
ProjectID: projectID,
AuthorID: creatorID,
ReviewerID: reviewerID,
ApproverID: approverID,
Fields: map[string]string{
"customer_name": "PG PiAgent 治理链客户",
"device_type": proj.DeviceType,
"intended_use": "Tissue resection",
"target_market": "NMPA Class III",
},
Materials: spec.Materials,
})
reqTask, _ := http.NewRequest(http.MethodPost, ts.URL+"/api/v1/tasks/run", bytes.NewReader(taskBody))
withBearerAuth(reqTask, creatorToken)
respTask, err := client.Do(reqTask)
if err != nil {
t.Fatalf("pi-agent task run: %v", err)
}
defer respTask.Body.Close()
if respTask.StatusCode != http.StatusOK {
var buf bytes.Buffer
_, _ = buf.ReadFrom(respTask.Body)
t.Fatalf("pi-agent task HTTP %d: %s", respTask.StatusCode, buf.String())
}
var taskRes struct {
Success bool `json:"success"`
DocRecord *struct{ DocID string `json:"doc_id"` } `json:"doc_record"`
ArtifactTitle string `json:"artifact_title"`
}
_ = json.NewDecoder(respTask.Body).Decode(&taskRes)
if !taskRes.Success {
t.Fatalf("pi-agent %s task failed", spec.TaskKey)
}
docID := fmt.Sprintf("doc_proj%d_%s", projectID, spec.TaskKey)
if taskRes.DocRecord != nil && taskRes.DocRecord.DocID != "" {
docID = taskRes.DocRecord.DocID
}
t.Logf("✓ [3. pi-agent %s] doc=%s title=%s elapsed=%s", spec.TaskKey, docID, taskRes.ArtifactTitle, time.Since(taskStart).Round(time.Second))
docEngine := srv.BizdevService().DocGov()
_ = docEngine.MarkDocumentContentUpdated(ctx, docID, "hash_pi_pg", false)
_, _ = docEngine.SubmitForReview(ctx, docID, creatorID, "author", "Author")
_, _ = docEngine.ReviewApprove(ctx, docID, reviewerID, "reviewer", "Reviewer", "ok")
_, err = docEngine.ApproverApprove(ctx, docID, approverID, "approver", "Approver", "ok")
if err != nil {
t.Fatalf("approve doc: %v", err)
}
t.Log("✓ [4. 文档批准] stage ready hook 已触发")
reqNotify := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/api/v1/projects/%d/notifications", projectID), nil)
withBearerAuth(reqNotify, creatorToken)
recNotify := httptest.NewRecorder()
handler.ServeHTTP(recNotify, reqNotify)
var notifyResp struct {
Items []map[string]any `json:"items"`
}
_ = json.Unmarshal(recNotify.Body.Bytes(), &notifyResp)
foundReady := false
for _, n := range notifyResp.Items {
if n["type"] == notification.TypeStageReadyToAdvance {
foundReady = true
break
}
}
if !foundReady {
t.Fatalf("expected stage_ready notification, got %+v", notifyResp.Items)
}
advanceBody, _ := json.Marshal(projectinstance.AdvanceStageInput{
StageKey: spec.StageKey, ExpectedConfigRevision: activated.Instance.ConfigRevision,
})
reqAdvance := httptest.NewRequest(http.MethodPost, fmt.Sprintf("/api/v1/workflow-instances/%d/stages/%s/advance", instanceID, spec.StageKey), bytes.NewReader(advanceBody))
withBearerAuth(reqAdvance, creatorToken)
recAdvance := httptest.NewRecorder()
handler.ServeHTTP(recAdvance, reqAdvance)
if recAdvance.Code != http.StatusOK {
t.Fatalf("advance: %d %s", recAdvance.Code, recAdvance.Body.String())
}
if spec.ExpectPlanningDone {
reqSettings := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/api/v1/projects/%d/settings", projectID), nil)
withBearerAuth(reqSettings, creatorToken)
recSettings := httptest.NewRecorder()
handler.ServeHTTP(recSettings, reqSettings)
var settings projectinstance.ProjectSettings
_ = json.Unmarshal(recSettings.Body.Bytes(), &settings)
if settings.LifecycleStatus != projectinstance.LifecyclePlanning {
t.Fatalf("expected planning after completion, got %q", settings.LifecycleStatus)
}
}
t.Logf("✓ [5. 阶段推进] PG + pi-agent 治理全链路完成 wf=%s", spec.WfKey)
}
// TestWorkflowGovernance_PiAgent_PG_FullChain — 正式 main + pi-agent intake
func TestWorkflowGovernance_PiAgent_PG_FullChain(t *testing.T) {
runPiAgentGovernanceE2E(t, piAgentGovernanceCase{
WfKey: workflowdef.OfficialKeyMain,
TaskKey: "intake",
StageKey: "V0.1_triage",
ProjectName: "PG PiAgent 治理链中文项目",
Materials: []bizdev.MaterialInput{{
Filename: "project_brief.md",
Content: []byte("# 项目简报\n\n- 产品:超声软组织手术系统\n- 目标:NMPA III 类注册路径摸底\n- 关键需求:双频刀头、灌注与负压吸引\n"),
}},
})
}
// TestWorkflowGovernance_PiAgent_PG_ProductManager_FullChain — 正式 product_manager + pi-agent system_reqs
func TestWorkflowGovernance_PiAgent_PG_ProductManager_FullChain(t *testing.T) {
runPiAgentGovernanceE2E(t, piAgentGovernanceCase{
WfKey: workflowdef.OfficialKeyProductManager,
TaskKey: "system_reqs",
StageKey: "requirements_baseline",
ProjectName: "PG PiAgent 产品经理治理链",
ExpectPlanningDone: true,
Materials: []bizdev.MaterialInput{{
Filename: "intake_summary.md",
Content: []byte("# 立项输入\n\n| REQ-01 | 双频超声刀头 24/35kHz | 高 |\n| REQ-02 | 灌注流量 10-50 mL/min | 高 |\n| REQ-03 | NMPA III 类注册路径 | 高 |\n"),
}},
})
}