Files
Agent-System/internal/projectinstance/pg_integration_test.go
T
weichen 70882918e5 test(pg): isolate PG integration tests from shared dev database
修复 go test -short 在设置 TEST_DATABASE_URL 时的既有欠债:
- storage/notification/docgov:移除硬编码 agent_system_dev 回退 DSN,改为
  无 TEST_DATABASE_URL 即 skip;固定项目名改为 name||code 唯一化。
- docgov engine_db:改为隔离库 + 自建项目行,替换随机 projectID 撞外键的旧写法。
- server/*_pg、projectinstance/pg_integration:迁移到官方 main 工作流
  (BuildDefaultPlan official-only 治理后的正确前提),补齐在任成员
  (DUTY_ASSIGNEE_NOT_ACTIVE_MEMBER)、任务键 bd_intake、阶段键 V0.1_triage、
  项目作用域 run record(events.ValidateEnvelope 拒绝 project<=0)、
  task_run_resume 去除内嵌 dev 凭证的 config 硬编码。
- 新增 seedProjectForRun 测试辅助。
验证:fresh 隔离库(55432)上 go test -short -p 1 ./... 除 archtest
(另行注册表分支修复)外全部通过;包内并行迁移竞争需 -p 1。
2026-09-22 01:10:17 +00:00

165 lines
5.3 KiB
Go

package projectinstance_test
import (
"context"
"testing"
"time"
"github.com/jackc/pgx/v5/pgxpool"
"chat-agent-system/internal/db"
"chat-agent-system/internal/docgov"
"chat-agent-system/internal/projectinstance"
"chat-agent-system/internal/projectworkflow"
"chat-agent-system/internal/workflowdef"
)
func openPGPool(t *testing.T) *pgxpool.Pool {
t.Helper()
dsn := db.SkipIfNoTestDatabaseURL(t)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
pool, err := pgxpool.New(ctx, dsn)
if err != nil {
t.Skip(err)
}
if err := pool.Ping(ctx); err != nil {
pool.Close()
t.Skip(err)
}
migCtx, migCancel := context.WithTimeout(context.Background(), 30*time.Second)
defer migCancel()
if err := db.RunMigrations(migCtx, pool, db.ResolveMigrationsDir()); err != nil {
pool.Close()
t.Skipf("migration failed: %v", err)
}
if !projectinstance.TablesReady(ctx, pool) {
pool.Close()
t.Skip("project_workflow_instances not migrated")
}
if !workflowdef.TablesReady(ctx, pool) {
pool.Close()
t.Skip("workflow_definitions not migrated")
}
return pool
}
func TestPG_ActivationAndStageAdvance(t *testing.T) {
pool := openPGPool(t)
defer pool.Close()
ctx := context.Background()
// 治理链路仅接受官方工作流(BuildDefaultPlan official-only),seed 官方 main manifest。
def, err := workflowdef.LoadManifestDefinition(workflowdef.V2ManifestFiles[workflowdef.OfficialKeyMain])
if err != nil {
t.Fatalf("load manifest: %v", err)
}
wfStore := workflowdef.NewStore(pool)
if err := wfStore.UpsertPublishedSeed(ctx, workflowdef.CreateDefinitionInput{
Key: workflowdef.OfficialKeyMain,
Name: workflowdef.OfficialWorkflowName(workflowdef.OfficialKeyMain),
Definition: def,
}, 1); err != nil {
t.Fatalf("seed official workflow: %v", err)
}
wfKey := workflowdef.OfficialKeyMain
pwStore := projectworkflow.NewStore(pool)
instStore := projectinstance.NewStore(pool)
docChecker := projectinstance.NewDocGovChecker(docgov.NewGovernanceEngine(pool))
svc := projectinstance.NewService(instStore, wfStore, pwStore, docChecker)
var projectID int64
code := "pg-p3-" + time.Now().Format("150405.000000")
err = pool.QueryRow(ctx, `
INSERT INTO projects (code, name, status, creator_user_id, lifecycle_status, active_workflow_key, workflow_selection_version, workflow_plan_revision)
VALUES ($1, 'PG Phase3 Test ' || $1, 'active', 2, 'planning', '', 1, 0) RETURNING id
`, code).Scan(&projectID)
if err != nil {
t.Fatalf("insert project: %v", err)
}
t.Cleanup(func() {
_, _ = pool.Exec(context.Background(), `DELETE FROM projects WHERE id = $1`, projectID)
})
plan, err := svc.BuildDefaultPlan(ctx, wfKey, 0)
if err != nil {
t.Fatalf("default plan: %v", err)
}
for i := range plan.Stages {
for j := range plan.Stages[i].DocTasks {
if plan.Stages[i].DocTasks[j].TaskKey == "bd_intake" {
plan.Stages[i].DocTasks[j].Duties = projectinstance.DocDuty{
AuthorUserID: 2, ReviewerUserID: 3, ApproverUserID: 1,
}
}
}
}
_, err = svc.SaveWorkflowPlan(ctx, projectID, 2, true, projectinstance.SavePlanInput{
Plan: *plan, ExpectedPlanRevision: 0, ExpectedSelectionVersion: 1,
})
if err != nil {
t.Fatalf("save plan: %v", err)
}
preview, err := svc.ActivationPreview(ctx, projectID, 2, true)
if err != nil {
t.Fatalf("preview: %v", err)
}
activated, err := svc.Activate(ctx, projectID, 2, true, projectinstance.ActivateInput{
ConfirmationToken: preview.ConfirmationToken,
ExpectedProjectVersion: preview.ExpectedProjectVersion,
ExpectedPlanRevision: preview.ExpectedPlanRevision,
})
if err != nil {
t.Fatalf("activate: %v", err)
}
docID := "doc_pg_p3_intake_" + code
engine := docgov.NewGovernanceEngine(pool)
_, err = engine.CreateOrGetDocument(ctx, docID, projectID, "bd_intake", "用户初始需求收集表", "V0.1", "", 2, "张", 3, "李", 1, "王")
if err != nil {
t.Fatalf("create doc: %v", err)
}
_ = engine.MarkDocumentContentUpdated(ctx, docID, "hash_pg_p3", false)
_, _ = engine.SubmitForReview(ctx, docID, 2, "author", "张")
_, _ = engine.ReviewApprove(ctx, docID, 3, "reviewer", "李", "ok")
_, err = engine.ApproverApprove(ctx, docID, 1, "approver", "王", "ok")
if err != nil {
t.Fatalf("approve doc: %v", err)
}
approvedResult, err := svc.HandleDocumentApproved(ctx, projectID, "bd_intake")
if err != nil {
t.Fatalf("handle document approved: %v", err)
}
if approvedResult == nil || !approvedResult.StageReady {
t.Fatalf("expected stage ready after approval, got %+v", approvedResult)
}
readiness, err := svc.GetStageReadiness(ctx, activated.Instance.ID, "V0.1_triage", 2, true)
if err != nil {
t.Fatalf("readiness: %v", err)
}
if !readiness.Ready {
otherTasks := []string{}
for _, dt := range readiness.DocTasks {
if !dt.Approved {
otherTasks = append(otherTasks, dt.TaskKey+":"+dt.DocState)
}
}
t.Fatalf("expected ready stage, pending=%v issues=%+v", otherTasks, readiness.BlockingIssues)
}
advanced, err := svc.AdvanceStage(ctx, activated.Instance.ID, 2, true, projectinstance.AdvanceStageInput{
StageKey: "V0.1_triage", ExpectedConfigRevision: activated.Instance.ConfigRevision,
})
if err != nil {
t.Fatalf("advance: %v", err)
}
if advanced.NextStageKey == "" && !advanced.InstanceDone {
t.Fatalf("expected next stage or done, got %+v", advanced)
}
}