diff --git a/cmd/bizdev-cli/main_test.go b/cmd/bizdev-cli/main_test.go index 1df55f3c..a07afe8d 100644 --- a/cmd/bizdev-cli/main_test.go +++ b/cmd/bizdev-cli/main_test.go @@ -30,7 +30,13 @@ func TestBizdevCLIHelpAndRun(t *testing.T) { } func TestBizdevCLIListTasks(t *testing.T) { - cmd := exec.Command("go", "run", "./cmd/bizdev-cli", "-list-tasks") + tmpDir := t.TempDir() + envContent := "ENV=test\nDATA_DIR=" + tmpDir + "\nAGENT_BACKEND=stub\nALLOW_IN_MEMORY=true\n" + envPath := filepath.Join(tmpDir, ".env") + if err := os.WriteFile(envPath, []byte(envContent), 0o644); err != nil { + t.Fatal(err) + } + cmd := exec.Command("go", "run", "./cmd/bizdev-cli", "-list-tasks", "-env", envPath) cmd.Dir = moduleRoot(t) out, err := cmd.CombinedOutput() if err != nil { @@ -43,7 +49,7 @@ func TestBizdevCLIListTasks(t *testing.T) { func TestBizdevCLIExecuteAndInspect(t *testing.T) { tmpDir := t.TempDir() - envContent := "ENV=dev\nDATA_DIR=" + tmpDir + "\nAGENT_BACKEND=stub\n" + envContent := "ENV=test\nDATA_DIR=" + tmpDir + "\nAGENT_BACKEND=stub\nALLOW_IN_MEMORY=true\n" envPath := filepath.Join(tmpDir, ".env") _ = os.WriteFile(envPath, []byte(envContent), 0o644) @@ -108,7 +114,7 @@ func TestBizdevCLIExecuteAndInspect(t *testing.T) { func TestBizdevCLIStageExecution(t *testing.T) { tmpDir := t.TempDir() - envContent := "ENV=dev\nDATA_DIR=" + tmpDir + "\nAGENT_BACKEND=stub\n" + envContent := "ENV=test\nDATA_DIR=" + tmpDir + "\nAGENT_BACKEND=stub\nALLOW_IN_MEMORY=true\n" envPath := filepath.Join(tmpDir, ".env") _ = os.WriteFile(envPath, []byte(envContent), 0o644) @@ -152,7 +158,7 @@ func TestBizdevCLIStageExecution(t *testing.T) { func TestBizdevCLIChatQueries(t *testing.T) { tmpDir := t.TempDir() - envContent := "ENV=dev\nDATA_DIR=" + tmpDir + "\nAGENT_BACKEND=stub\n" + envContent := "ENV=test\nDATA_DIR=" + tmpDir + "\nAGENT_BACKEND=stub\nALLOW_IN_MEMORY=true\n" envPath := filepath.Join(tmpDir, ".env") _ = os.WriteFile(envPath, []byte(envContent), 0o644) @@ -238,7 +244,7 @@ func TestBizdevCLIChatQueries(t *testing.T) { func TestBizdevCLIThreeBranchQueries(t *testing.T) { tmpDir := t.TempDir() - envContent := "ENV=dev\nDATA_DIR=" + tmpDir + "\nAGENT_BACKEND=stub\n" + envContent := "ENV=test\nDATA_DIR=" + tmpDir + "\nAGENT_BACKEND=stub\nALLOW_IN_MEMORY=true\n" envPath := filepath.Join(tmpDir, ".env") _ = os.WriteFile(envPath, []byte(envContent), 0o644) @@ -339,7 +345,7 @@ func TestBizdevCLIThreeBranchQueries(t *testing.T) { func TestBizdevCLIExecuteAdhocTask(t *testing.T) { tmpDir := t.TempDir() - envContent := "ENV=dev\nDATA_DIR=" + tmpDir + "\nAGENT_BACKEND=stub\n" + envContent := "ENV=test\nDATA_DIR=" + tmpDir + "\nAGENT_BACKEND=stub\nALLOW_IN_MEMORY=true\n" envPath := filepath.Join(tmpDir, ".env") _ = os.WriteFile(envPath, []byte(envContent), 0o644) diff --git a/internal/projectinstance/access_governing_pg_test.go b/internal/projectinstance/access_governing_pg_test.go index 02e31ae4..0d788118 100644 --- a/internal/projectinstance/access_governing_pg_test.go +++ b/internal/projectinstance/access_governing_pg_test.go @@ -29,8 +29,8 @@ func TestPG_GoverningAccess_NeverActiveUnrestricted(t *testing.T) { code := "pg-access-none-" + time.Now().Format("150405.000") 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, 'Access Never Active', 'active', $2, 'planning', '', 1, 0) RETURNING id - `, code, creatorID).Scan(&projectID) + VALUES ($1, $2, 'active', $3, 'planning', '', 1, 0) RETURNING id + `, code, "Access Never Active "+code, creatorID).Scan(&projectID) if err != nil { t.Fatalf("insert project: %v", err) } diff --git a/internal/projectinstance/activation_official_pg_test.go b/internal/projectinstance/activation_official_pg_test.go index 8596f8c3..ba50efe6 100644 --- a/internal/projectinstance/activation_official_pg_test.go +++ b/internal/projectinstance/activation_official_pg_test.go @@ -40,8 +40,8 @@ func TestPG_ActivateOfficialWorkflowRuntimeMetadata(t *testing.T) { code := "pg-official-" + time.Now().Format("150405.000") 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 Official Activation Test', 'active', 1, 'planning', '', 1, 0) RETURNING id - `, code).Scan(&projectID) + VALUES ($1, $2, 'active', 1, 'planning', '', 1, 0) RETURNING id + `, code, "PG Official Activation Test "+code).Scan(&projectID) if err != nil { t.Fatalf("insert project: %v", err) } diff --git a/internal/projectinstance/pg_integration_test.go b/internal/projectinstance/pg_integration_test.go index 58ef79d6..8bb9451d 100644 --- a/internal/projectinstance/pg_integration_test.go +++ b/internal/projectinstance/pg_integration_test.go @@ -50,39 +50,50 @@ func TestPG_ActivationAndStageAdvance(t *testing.T) { defer pool.Close() ctx := context.Background() - wfSvc := workflowdef.NewService(pool, nil) - wfKey := "pg_p3_wf_" + time.Now().Format("150405") - _, ver, err := wfSvc.CreateDefinition(ctx, workflowdef.CreateDefinitionInput{ - Key: wfKey, Name: "PG P3 Test WF", - Definition: workflowdef.VersionDefinition{ - SchemaVersion: 1, - Stages: []workflowdef.StageDef{{ - Key: "biz_intake", Name: "商务摸底", Position: 0, - DocTasks: []workflowdef.DocTaskDef{{ - Key: "intake", Required: true, Blocking: true, DefaultEnabled: true, - OutputDocType: "intake", OutputDocTitle: "客户需求采集", - }}, - }}, - }, - }, 1) - if err != nil { - t.Fatalf("create workflow def: %v", err) - } - if _, err := wfSvc.PublishVersion(ctx, ver.WorkflowID, ver.Version, 1); err != nil { - t.Fatalf("publish workflow: %v", err) - } + // BuildDefaultPlan only accepts official keys; seed main.v2 like sibling PG tests. wfStore := workflowdef.NewStore(pool) + def, err := workflowdef.LoadManifestDefinition("main.v2.json") + if err != nil { + t.Fatalf("load main.v2 manifest: %v", err) + } + if err := wfStore.UpsertPublishedSeed(ctx, workflowdef.CreateDefinitionInput{ + Key: workflowdef.OfficialKeyMain, + Name: "商务对接流程", + Definition: def, + }, 1); err != nil { + t.Fatalf("seed official main workflow: %v", err) + } + + var authorID, reviewerID, approverID int64 + rows, err := pool.Query(ctx, `SELECT id FROM users ORDER BY id ASC LIMIT 3`) + if err != nil { + t.Fatalf("list users: %v", err) + } + defer rows.Close() + ids := make([]int64, 0, 3) + for rows.Next() { + var id int64 + if scanErr := rows.Scan(&id); scanErr != nil { + t.Fatalf("scan user: %v", scanErr) + } + ids = append(ids, id) + } + if len(ids) < 3 { + t.Skip("need at least 3 user rows for duty assignees") + } + authorID, reviewerID, approverID = ids[0], ids[1], ids[2] + 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.000") + 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', 'active', 2, 'planning', '', 1, 0) RETURNING id - `, code).Scan(&projectID) + VALUES ($1, $2, 'active', $3, 'planning', '', 1, 0) RETURNING id + `, code, "PG Phase3 Test "+code, authorID).Scan(&projectID) if err != nil { t.Fatalf("insert project: %v", err) } @@ -90,32 +101,41 @@ func TestPG_ActivationAndStageAdvance(t *testing.T) { _, _ = pool.Exec(context.Background(), `DELETE FROM projects WHERE id = $1`, projectID) }) - plan, err := svc.BuildDefaultPlan(ctx, wfKey, 0) + plan, err := svc.BuildDefaultPlan(ctx, workflowdef.OfficialKeyMain, 0) if err != nil { t.Fatalf("default plan: %v", err) } + intakeTask := "" for i := range plan.Stages { for j := range plan.Stages[i].DocTasks { - if plan.Stages[i].DocTasks[j].TaskKey == "intake" || plan.Stages[i].DocTasks[j].TaskKey == "bd_intake" { - plan.Stages[i].DocTasks[j].Duties = projectinstance.DocDuty{ - AuthorUserID: 2, ReviewerUserID: 3, ApproverUserID: 4, - } + tk := plan.Stages[i].DocTasks[j].TaskKey + plan.Stages[i].DocTasks[j].Duties = projectinstance.DocDuty{ + AuthorUserID: authorID, ReviewerUserID: reviewerID, ApproverUserID: approverID, + } + if tk == "intake" || tk == "bd_intake" { + intakeTask = tk + } else { + // Keep a single blocking intake path so stage readiness is deterministic. + plan.Stages[i].DocTasks[j].Enabled = false } } } + if intakeTask == "" { + t.Fatal("official main plan missing intake/bd_intake task") + } - _, err = svc.SaveWorkflowPlan(ctx, projectID, 2, true, projectinstance.SavePlanInput{ + _, err = svc.SaveWorkflowPlan(ctx, projectID, authorID, 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) + preview, err := svc.ActivationPreview(ctx, projectID, authorID, true) if err != nil { t.Fatalf("preview: %v", err) } - activated, err := svc.Activate(ctx, projectID, 2, true, projectinstance.ActivateInput{ - ConfirmationToken: preview.ConfirmationToken, + activated, err := svc.Activate(ctx, projectID, authorID, true, projectinstance.ActivateInput{ + ConfirmationToken: preview.ConfirmationToken, ExpectedProjectVersion: preview.ExpectedProjectVersion, ExpectedPlanRevision: preview.ExpectedPlanRevision, }) @@ -125,19 +145,19 @@ func TestPG_ActivationAndStageAdvance(t *testing.T) { docID := "doc_pg_p3_intake_" + code engine := docgov.NewGovernanceEngine(pool) - _, err = engine.CreateOrGetDocument(ctx, docID, projectID, "intake", "客户需求采集", "V0.1", "", 2, "张", 3, "李", 4, "王") + _, err = engine.CreateOrGetDocument(ctx, docID, projectID, intakeTask, "客户需求采集", "V0.1", "", authorID, "张", reviewerID, "李", approverID, "王") 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, 4, "approver", "王", "ok") + _, _ = engine.SubmitForReview(ctx, docID, authorID, "author", "张") + _, _ = engine.ReviewApprove(ctx, docID, reviewerID, "reviewer", "李", "ok") + _, err = engine.ApproverApprove(ctx, docID, approverID, "approver", "王", "ok") if err != nil { t.Fatalf("approve doc: %v", err) } - approvedResult, err := svc.HandleDocumentApproved(ctx, projectID, "intake") + approvedResult, err := svc.HandleDocumentApproved(ctx, projectID, intakeTask) if err != nil { t.Fatalf("handle document approved: %v", err) } @@ -147,7 +167,7 @@ func TestPG_ActivationAndStageAdvance(t *testing.T) { notifySvc := notification.NewService(pool) if notifySvc.Available() { - created, emitErr := notifySvc.EmitStageReadyToAdvance(ctx, 2, notification.StageReadyPayload{ + created, emitErr := notifySvc.EmitStageReadyToAdvance(ctx, authorID, notification.StageReadyPayload{ ProjectID: projectID, WorkflowInstanceID: approvedResult.InstanceID, StageInstanceID: approvedResult.StageInstanceID, @@ -165,7 +185,11 @@ func TestPG_ActivationAndStageAdvance(t *testing.T) { } } - readiness, err := svc.GetStageReadiness(ctx, activated.Instance.ID, "biz_intake", 2, true) + stageKey := approvedResult.StageKey + if stageKey == "" { + stageKey = "V0.1_triage" + } + readiness, err := svc.GetStageReadiness(ctx, activated.Instance.ID, stageKey, authorID, true) if err != nil { t.Fatalf("readiness: %v", err) } @@ -173,8 +197,8 @@ func TestPG_ActivationAndStageAdvance(t *testing.T) { t.Fatalf("expected ready stage, issues=%+v tasks=%+v", readiness.BlockingIssues, readiness.DocTasks) } - advanced, err := svc.AdvanceStage(ctx, activated.Instance.ID, 2, true, projectinstance.AdvanceStageInput{ - StageKey: "biz_intake", ExpectedConfigRevision: activated.Instance.ConfigRevision, + advanced, err := svc.AdvanceStage(ctx, activated.Instance.ID, authorID, true, projectinstance.AdvanceStageInput{ + StageKey: stageKey, ExpectedConfigRevision: activated.Instance.ConfigRevision, }) if err != nil { t.Fatalf("advance: %v", err) diff --git a/internal/projectinstance/rdev_pg_test.go b/internal/projectinstance/rdev_pg_test.go index 29639701..d514c585 100644 --- a/internal/projectinstance/rdev_pg_test.go +++ b/internal/projectinstance/rdev_pg_test.go @@ -37,8 +37,8 @@ func TestPG_RDevActivateWithNAClose(t *testing.T) { code := "pg-rdev-" + time.Now().Format("150405.000") 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 RDev Activation', 'active', 1, 'planning', '', 1, 0) RETURNING id - `, code).Scan(&projectID) + VALUES ($1, $2, 'active', 1, 'planning', '', 1, 0) RETURNING id + `, code, "PG RDev Activation "+code).Scan(&projectID) if err != nil { t.Fatalf("insert project: %v", err) } diff --git a/internal/server/chat_history_refresh_pg_test.go b/internal/server/chat_history_refresh_pg_test.go index 9cc3ce25..e3df7c67 100644 --- a/internal/server/chat_history_refresh_pg_test.go +++ b/internal/server/chat_history_refresh_pg_test.go @@ -40,8 +40,8 @@ func TestChatHistoryRefresh_PG_PreActivationAndTaskScope(t *testing.T) { code := "pg-refresh-" + time.Now().Format("150405.000") if 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 Refresh Test', 'active', $2, 'planning', '', 1, 0) RETURNING id - `, code, creatorID).Scan(&projectID); err != nil { + VALUES ($1, $2, 'active', $3, 'planning', '', 1, 0) RETURNING id + `, code, "PG Refresh Test "+code, creatorID).Scan(&projectID); err != nil { t.Fatalf("insert project: %v", err) } _, _ = pool.Exec(ctx, ` @@ -70,6 +70,11 @@ func TestChatHistoryRefresh_PG_PreActivationAndTaskScope(t *testing.T) { _ = json.Unmarshal(recDefault.Body.Bytes(), &plan) reviewerID := fetchAuthUserID(t, handler, loginTestUser(t, handler, "lizhuguan", "123456")) approverID := fetchAuthUserID(t, handler, loginTestUser(t, handler, "wangzong", "123456")) + ensureProjectMembers(t, pool, projectID, map[int64]string{ + creatorID: "owner", + reviewerID: "member", + approverID: "member", + }) for i := range plan.Stages { for j := range plan.Stages[i].DocTasks { plan.Stages[i].DocTasks[j].Duties = projectinstance.DocDuty{ diff --git a/internal/server/handlers.go b/internal/server/handlers.go index efe5eb72..1b99a19c 100644 --- a/internal/server/handlers.go +++ b/internal/server/handlers.go @@ -591,6 +591,12 @@ func (s *Server) handleGetProjectContext(w http.ResponseWriter, r *http.Request) return } + // Membership first: allowAnyStageAccess assumes the caller already passed + // project membership (see ResolveStageAccess). Without this guard any + // authenticated user can read an activated project's collaborative context. + if !s.enforceChatProjectAccess(w, r, user, projectID) { + return + } if !s.allowAnyStageAccess(r.Context(), projectID, user.ID) { s.writeWorkflowAccessDenied(w) return diff --git a/internal/server/phase5_acceptance_pg_test.go b/internal/server/phase5_acceptance_pg_test.go index 347bc1a1..efd232e0 100644 --- a/internal/server/phase5_acceptance_pg_test.go +++ b/internal/server/phase5_acceptance_pg_test.go @@ -107,35 +107,20 @@ func TestPhase5_ActivateIdempotencyConflict_PG(t *testing.T) { } defer pool.Close() - ctx := context.Background() handler, _ := setupPGGovernanceServer(t, pool) creatorToken := loginTestUser(t, handler, "zhanggong", "123456") + wfKey := workflowdef.OfficialKeyMain + seedOfficialWorkflowManifest(t, pool, wfKey) - wfSvc := workflowdef.NewService(pool, nil) - wfKey := "pg_p5_idem_" + time.Now().Format("150405.000") - _, ver, err := wfSvc.CreateDefinition(ctx, workflowdef.CreateDefinitionInput{ - Key: wfKey, Name: "Idempotency WF", - Definition: workflowdef.VersionDefinition{ - SchemaVersion: 1, - Stages: []workflowdef.StageDef{{ - Key: "biz_intake", Name: "Intake", Position: 0, - DocTasks: []workflowdef.DocTaskDef{{ - Key: "intake", Required: true, Blocking: true, DefaultEnabled: true, - OutputDocType: "intake", OutputDocTitle: "Customer Intake", - }}, - }}, - }, - }, 1) - if err != nil { - t.Fatalf("create wf: %v", err) - } - _, _ = wfSvc.PublishVersion(ctx, ver.WorkflowID, ver.Version, 1) - - createBody, _ := json.Marshal(projectworkflow.CreateProjectInput{Name: "Phase5 Idempotency Test"}) + uniqueName := "Phase5 Idempotency Test " + time.Now().Format("150405.000000") + createBody, _ := json.Marshal(projectworkflow.CreateProjectInput{Name: uniqueName}) 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.StatusOK && recCreate.Code != http.StatusCreated { + t.Fatalf("create project: %d %s", recCreate.Code, recCreate.Body.String()) + } var proj projectworkflow.Project _ = json.Unmarshal(recCreate.Body.Bytes(), &proj) t.Cleanup(func() { cleanupProject(t, pool, proj.ID) }) @@ -143,10 +128,18 @@ func TestPhase5_ActivateIdempotencyConflict_PG(t *testing.T) { creatorID := fetchAuthUserID(t, handler, creatorToken) reviewerID := fetchAuthUserID(t, handler, loginTestUser(t, handler, "lizhuguan", "123456")) approverID := fetchAuthUserID(t, handler, loginTestUser(t, handler, "wangzong", "123456")) - reqDefault := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/api/v1/projects/%d/workflow-plan/default?workflow_key=%s&version=1", proj.ID, wfKey), nil) + ensureProjectMembers(t, pool, proj.ID, map[int64]string{ + creatorID: "owner", + reviewerID: "member", + approverID: "member", + }) + reqDefault := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/api/v1/projects/%d/workflow-plan/default?workflow_key=%s&version=0", proj.ID, 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 { @@ -161,11 +154,17 @@ func TestPhase5_ActivateIdempotencyConflict_PG(t *testing.T) { 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", proj.ID), nil) withBearerAuth(reqPreview, creatorToken) recPreview := httptest.NewRecorder() handler.ServeHTTP(recPreview, reqPreview) + if recPreview.Code != http.StatusOK { + t.Fatalf("activation preview: %d %s", recPreview.Code, recPreview.Body.String()) + } var preview projectinstance.ActivationPreview _ = json.Unmarshal(recPreview.Body.Bytes(), &preview) diff --git a/internal/server/phase5_fault_injection_pg_test.go b/internal/server/phase5_fault_injection_pg_test.go index f9358e57..87632cb6 100644 --- a/internal/server/phase5_fault_injection_pg_test.go +++ b/internal/server/phase5_fault_injection_pg_test.go @@ -22,32 +22,12 @@ import ( "chat-agent-system/internal/workflowdef" ) -// setupPGProjectReadyToAdvance 创建并激活工作流、完成 intake 文档批准,返回可推进实例。 +// setupPGProjectReadyToAdvance 创建并激活官方 main 工作流、完成 intake 文档批准,返回可推进实例。 func setupPGProjectReadyToAdvance(t *testing.T, pool *pgxpool.Pool, handler http.Handler, srv *server.Server) (projectID, instanceID int64, creatorToken string, configRevision int) { t.Helper() ctx := context.Background() - wfSvc := workflowdef.NewService(pool, nil) - wfKey := "pg_p5_fault_" + time.Now().Format("150405.000000") - _, ver, err := wfSvc.CreateDefinition(ctx, workflowdef.CreateDefinitionInput{ - Key: wfKey, Name: "Phase5 Fault WF", - Definition: workflowdef.VersionDefinition{ - SchemaVersion: 1, - Stages: []workflowdef.StageDef{ - { - Key: "biz_intake", Name: "Intake", Position: 0, - DocTasks: []workflowdef.DocTaskDef{{ - Key: "intake", Required: true, Blocking: true, DefaultEnabled: true, - OutputDocType: "intake", OutputDocTitle: "Intake", - }}, - }, - {Key: "biz_feasibility", Name: "Feasibility", Position: 1, Optional: true}, - }, - }, - }, 1) - if err != nil { - t.Fatalf("create wf: %v", err) - } - _, _ = wfSvc.PublishVersion(ctx, ver.WorkflowID, ver.Version, 1) + wfKey := workflowdef.OfficialKeyMain + seedOfficialWorkflowManifest(t, pool, wfKey) creatorToken = loginTestUser(t, handler, "zhanggong", "123456") creatorID := fetchAuthUserID(t, handler, creatorToken) @@ -55,23 +35,27 @@ func setupPGProjectReadyToAdvance(t *testing.T, pool *pgxpool.Pool, handler http approverID := fetchAuthUserID(t, handler, loginTestUser(t, handler, "wangzong", "123456")) code := "p5-fault-" + time.Now().Format("150405.000000") - err = pool.QueryRow(ctx, ` + 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, 'Phase5 Fault Test', 'active', $2, 'planning', '', 1, 0) RETURNING id - `, code, creatorID).Scan(&projectID) + VALUES ($1, $2, 'active', $3, 'planning', '', 1, 0) RETURNING id + `, code, "Phase5 Fault Test "+code, creatorID).Scan(&projectID) if err != nil { t.Fatalf("insert project: %v", err) } - _, _ = pool.Exec(ctx, ` - INSERT INTO project_members (project_id, user_id, project_role, status, joined_at, updated_at) - VALUES ($1, $2, 'owner', 'active', now(), now()) ON CONFLICT DO NOTHING - `, projectID, creatorID) + ensureProjectMembers(t, pool, projectID, map[int64]string{ + creatorID: "owner", + reviewerID: "member", + approverID: "member", + }) t.Cleanup(func() { cleanupProject(t, pool, projectID) }) - reqDefault := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/api/v1/projects/%d/workflow-plan/default?workflow_key=%s&version=1", projectID, wfKey), nil) + reqDefault := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/api/v1/projects/%d/workflow-plan/default?workflow_key=%s&version=0", projectID, 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 { @@ -86,11 +70,17 @@ func setupPGProjectReadyToAdvance(t *testing.T, pool *pgxpool.Pool, handler http 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) + if recPreview.Code != http.StatusOK { + t.Fatalf("activation preview: %d %s", recPreview.Code, recPreview.Body.String()) + } var preview projectinstance.ActivationPreview _ = json.Unmarshal(recPreview.Body.Bytes(), &preview) @@ -102,6 +92,9 @@ func setupPGProjectReadyToAdvance(t *testing.T, pool *pgxpool.Pool, handler http 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 @@ -128,9 +121,9 @@ func TestPhase5_ConcurrentStageAdvance_PG(t *testing.T) { _, instanceID, creatorToken, configRevision := setupPGProjectReadyToAdvance(t, pool, handler, srv) advanceBody, _ := json.Marshal(projectinstance.AdvanceStageInput{ - StageKey: "biz_intake", ExpectedConfigRevision: configRevision, + StageKey: "V0.1_triage", ExpectedConfigRevision: configRevision, }) - url := fmt.Sprintf("/api/v1/workflow-instances/%d/stages/biz_intake/advance", instanceID) + url := fmt.Sprintf("/api/v1/workflow-instances/%d/stages/V0.1_triage/advance", instanceID) var wg sync.WaitGroup results := make([]int, 2) diff --git a/internal/server/task_run_resume_pg_test.go b/internal/server/task_run_resume_pg_test.go index 7e536c94..3ce5f879 100644 --- a/internal/server/task_run_resume_pg_test.go +++ b/internal/server/task_run_resume_pg_test.go @@ -19,6 +19,15 @@ import ( "chat-agent-system/internal/taskrun" ) +func pgResumeConfig(t *testing.T) *config.Config { + t.Helper() + cfg := configFromTestDatabaseURL(t) + cfg.DataDir = t.TempDir() + cfg.AgentBackend = "stub" + cfg.AllowInMemory = false + return cfg +} + func TestTaskRun_ResumeInterrupted_PG(t *testing.T) { pool := openPGPoolWithMigrations(t) if pool == nil { @@ -26,20 +35,14 @@ func TestTaskRun_ResumeInterrupted_PG(t *testing.T) { } defer pool.Close() - dataDir := t.TempDir() - cfg := &config.Config{ - Env: "dev", DataDir: dataDir, AgentBackend: "stub", AllowInMemory: false, - ContextDbHost: "127.0.0.1", ContextDbPort: 5433, ContextDbUser: "app", - ContextDbPassword: "dev_secret_db_pass", ContextDbName: "agent_system_dev", - AdminUsername: "admin", AdminPassword: "admin-secret-password-123", - } + cfg := pgResumeConfig(t) svc := bizdev.MustNewService(t, cfg) srv := server.NewServer(cfg, svc) handler := srv.Routes() token := loginTestUser(t, handler, "zhanggong", "123456") userID := fetchAuthUserID(t, handler, token) - workDir := filepath.Join(dataDir, "workspaces", "bizdev", "resume_test") + workDir := filepath.Join(cfg.DataDir, "workspaces", "bizdev", "resume_test") if err := os.MkdirAll(workDir, 0o755); err != nil { t.Fatal(err) } @@ -103,18 +106,12 @@ func TestTaskRun_ResumeRequiresConfirm_PG(t *testing.T) { } defer pool.Close() - dataDir := t.TempDir() - cfg := &config.Config{ - Env: "dev", DataDir: dataDir, AgentBackend: "stub", AllowInMemory: false, - ContextDbHost: "127.0.0.1", ContextDbPort: 5433, ContextDbUser: "app", - ContextDbPassword: "dev_secret_db_pass", ContextDbName: "agent_system_dev", - AdminUsername: "admin", AdminPassword: "admin-secret-password-123", - } + cfg := pgResumeConfig(t) srv := server.NewServer(cfg, bizdev.MustNewService(t, cfg)) handler := srv.Routes() token := loginTestUser(t, handler, "zhanggong", "123456") userID := fetchAuthUserID(t, handler, token) - workDir := filepath.Join(dataDir, "ws") + workDir := filepath.Join(cfg.DataDir, "ws") _ = os.MkdirAll(workDir, 0o755) runID := "run_no_confirm_" + time.Now().Format("150405.000000") @@ -144,17 +141,11 @@ func TestTaskRun_GetRunShowsResumable_PG(t *testing.T) { } defer pool.Close() - dataDir := t.TempDir() - cfg := &config.Config{ - Env: "dev", DataDir: dataDir, AgentBackend: "stub", AllowInMemory: false, - ContextDbHost: "127.0.0.1", ContextDbPort: 5433, ContextDbUser: "app", - ContextDbPassword: "dev_secret_db_pass", ContextDbName: "agent_system_dev", - AdminUsername: "admin", AdminPassword: "admin-secret-password-123", - } + cfg := pgResumeConfig(t) handler := server.NewServer(cfg, bizdev.MustNewService(t, cfg)).Routes() token := loginTestUser(t, handler, "zhanggong", "123456") userID := fetchAuthUserID(t, handler, token) - workDir := filepath.Join(dataDir, "workspaces", "bizdev", "resumable_flag") + workDir := filepath.Join(cfg.DataDir, "workspaces", "bizdev", "resumable_flag") _ = os.MkdirAll(workDir, 0o755) runID := "run_flag_" + time.Now().Format("150405.000000") diff --git a/internal/server/workflow_governance_pg_test.go b/internal/server/workflow_governance_pg_test.go index fce3b3d6..f6a4b340 100644 --- a/internal/server/workflow_governance_pg_test.go +++ b/internal/server/workflow_governance_pg_test.go @@ -134,8 +134,8 @@ func TestWorkflowGovernance_PG_ActivateApproveAdvanceChain(t *testing.T) { var err error 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 Phase5 Governance Test', 'active', $2, 'planning', '', 1, 0) RETURNING id - `, code, creatorID).Scan(&projectID) + VALUES ($1, $2, 'active', $3, 'planning', '', 1, 0) RETURNING id + `, code, "PG Phase5 Governance Test "+code, creatorID).Scan(&projectID) if err != nil { t.Fatalf("insert project: %v", err) }