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>
This commit is contained in:
weichen
2026-09-13 02:50:23 +00:00
committed by weichen
co-authored by Cursor
parent 47984f4863
commit ad6e38d11a
15 changed files with 415 additions and 47 deletions
+29
View File
@@ -0,0 +1,29 @@
package db
import (
"os"
"strings"
"testing"
)
const testDatabaseEnv = "TEST_DATABASE_URL"
// RequireTestDatabaseURL 集成测试必须显式提供隔离测试库,禁止默认连开发库。
func RequireTestDatabaseURL(t *testing.T) string {
t.Helper()
dsn := strings.TrimSpace(os.Getenv(testDatabaseEnv))
if dsn == "" {
t.Fatalf("%s must be set for PostgreSQL integration tests (refusing default dev database)", testDatabaseEnv)
}
return dsn
}
// SkipIfNoTestDatabaseURL 无隔离测试库时跳过(单元测试包内可选 PG 用例)。
func SkipIfNoTestDatabaseURL(t *testing.T) string {
t.Helper()
dsn := strings.TrimSpace(os.Getenv(testDatabaseEnv))
if dsn == "" {
t.Skipf("%s not set; skipping PostgreSQL integration test", testDatabaseEnv)
}
return dsn
}
@@ -0,0 +1,99 @@
package projectinstance_test
import (
"context"
"testing"
"time"
"chat-agent-system/internal/projectinstance"
"chat-agent-system/internal/projectworkflow"
"chat-agent-system/internal/workflowdef"
)
// TestPG_ActivateOfficialWorkflowRuntimeMetadata 回归激活链路:
// ActivateInstance.applyRuntimeMetadata 会遍历文档任务实例并逐条 UPDATE。
// 若在同一事务连接上 rows 未读完时就执行 Exec,pgx 会返回 "conn busy"。
// 本测试使用正式商务对接流程(3 阶段 7 任务)覆盖该路径。
func TestPG_ActivateOfficialWorkflowRuntimeMetadata(t *testing.T) {
pool := openPGPool(t)
defer pool.Close()
ctx := context.Background()
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)
}
pwStore := projectworkflow.NewStore(pool)
instStore := projectinstance.NewStore(pool)
svc := projectinstance.NewService(instStore, wfStore, pwStore, nil)
var projectID int64
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)
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, workflowdef.OfficialKeyMain, 0)
if err != nil {
t.Fatalf("default plan: %v", err)
}
for i := range plan.Stages {
for j := range plan.Stages[i].DocTasks {
plan.Stages[i].DocTasks[j].Duties = projectinstance.DocDuty{
AuthorUserID: 1, ReviewerUserID: 2, ApproverUserID: 3,
}
}
}
if _, err := svc.SaveWorkflowPlan(ctx, projectID, 1, true, projectinstance.SavePlanInput{
Plan: *plan, ExpectedPlanRevision: 0, ExpectedSelectionVersion: 1,
}); err != nil {
t.Fatalf("save plan: %v", err)
}
preview, err := svc.ActivationPreview(ctx, projectID, 1, true)
if err != nil {
t.Fatalf("preview: %v", err)
}
activated, err := svc.Activate(ctx, projectID, 1, true, projectinstance.ActivateInput{
ConfirmationToken: preview.ConfirmationToken,
ExpectedProjectVersion: preview.ExpectedProjectVersion,
ExpectedPlanRevision: preview.ExpectedPlanRevision,
})
if err != nil {
t.Fatalf("activate: %v", err)
}
if activated.Instance.Status != projectinstance.InstanceActive {
t.Fatalf("expected active instance, got %q", activated.Instance.Status)
}
// 运行时元数据应写回文档任务实例(回归 applyRuntimeMetadata 的逐条 UPDATE)。
var executorKey string
err = pool.QueryRow(ctx, `
SELECT dti.executor_key
FROM project_doc_task_instances dti
JOIN project_stage_instances si ON si.id = dti.stage_instance_id
WHERE si.workflow_instance_id = $1 AND dti.task_key = 'intake'
`, activated.Instance.ID).Scan(&executorKey)
if err != nil {
t.Fatalf("query runtime metadata: %v", err)
}
if executorKey != "bizdev.intake.v1" {
t.Fatalf("expected executor_key bizdev.intake.v1, got %q", executorKey)
}
}
+8 -5
View File
@@ -3,19 +3,21 @@ package projectinstance
import (
"context"
"testing"
"chat-agent-system/internal/workflowdef"
)
func TestHandleDocumentApproved_StageReady(t *testing.T) {
ctx := context.Background()
store := newMemStore()
_ = SeedProjectMeta(store, 1, LifecycleActive, "medical_device_development", 1, 1)
_ = SeedProjectMeta(store, 1, LifecycleActive, workflowdef.OfficialKeyMain, 1, 1)
svc := NewService(store, testResolver(), nil, &stubDocChecker{states: map[string]string{"intake": "approved"}})
plan := WorkflowPlan{
WorkflowDefinitionKey: "medical_device_development",
WorkflowDefinitionKey: workflowdef.OfficialKeyMain,
WorkflowVersion: 1,
Stages: []StagePlan{{
StageKey: "biz_intake", Enabled: true,
StageKey: "V0.1_triage", Enabled: true,
DocTasks: []DocTaskPlan{{
TaskKey: "intake", Enabled: true,
Duties: DocDuty{AuthorUserID: 2, ReviewerUserID: 3, ApproverUserID: 4},
@@ -26,7 +28,8 @@ func TestHandleDocumentApproved_StageReady(t *testing.T) {
ID: 1, ProjectID: 1, WorkflowVersionID: 10, Status: InstanceActive,
ConfigRevision: 1, PlanHash: "h", ConfigSnapshot: plan, InstanceNo: 1,
}
activated, err := store.ActivateInstance(ctx, 1, inst, "medical_device_development", 1, 2)
version := &workflowdef.Version{WorkflowKey: workflowdef.OfficialKeyMain, Version: 1}
activated, err := store.ActivateInstance(ctx, 1, inst, version, 1, 2)
if err != nil {
t.Fatalf("activate: %v", err)
}
@@ -38,7 +41,7 @@ func TestHandleDocumentApproved_StageReady(t *testing.T) {
if result == nil || !result.StageReady {
t.Fatalf("expected stage ready, got %+v", result)
}
if result.InstanceID != activated.ID || result.StageKey != "biz_intake" {
if result.InstanceID != activated.ID || result.StageKey != "V0.1_triage" {
t.Fatalf("unexpected result: %+v", result)
}
}
@@ -2,7 +2,6 @@ package projectinstance_test
import (
"context"
"os"
"testing"
"time"
@@ -18,10 +17,7 @@ import (
func openPGPool(t *testing.T) *pgxpool.Pool {
t.Helper()
dsn := os.Getenv("TEST_DATABASE_URL")
if dsn == "" {
dsn = "postgres://app:dev_secret_db_pass@127.0.0.1:5433/agent_system_dev?sslmode=disable"
}
dsn := db.SkipIfNoTestDatabaseURL(t)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
pool, err := pgxpool.New(ctx, dsn)
+35 -11
View File
@@ -40,32 +40,36 @@ func (s *stubVersionResolver) ListDefinitions(_ context.Context) ([]workflowdef.
}
func testResolver() *stubVersionResolver {
key := workflowdef.OfficialKeyMain
ver := &workflowdef.Version{
ID: 100, WorkflowID: 10, Version: 1, Status: workflowdef.StatusPublished,
WorkflowKey: "medical_device_development", WorkflowName: "医疗器械开发",
WorkflowKey: key, WorkflowName: workflowdef.OfficialWorkflowName(key),
Definition: workflowdef.VersionDefinition{
SchemaVersion: 2,
WorkflowKey: key,
Stages: []workflowdef.StageDef{{
Key: "biz_intake", Name: "商务 intake", Optional: false,
Key: "V0.1_triage", Name: "V0.1 需求摸底", Optional: false, Position: 10,
DocTasks: []workflowdef.DocTaskDef{{
Key: "intake", Required: true, Blocking: true, DefaultEnabled: true,
Key: "intake", Title: "用户初始需求收集表", CreatorConfigurable: true, BlockingWhenEnabled: true, DefaultEnabled: true,
OutputDocType: "intake", OutputDocTitle: "用户初始需求收集表",
}},
}},
},
}
return &stubVersionResolver{
defs: map[string]*workflowdef.Definition{
"medical_device_development": {ID: 10, Key: "medical_device_development", Name: "医疗器械开发", LatestPublishedVersion: 1},
key: {ID: 10, Key: key, Name: workflowdef.OfficialWorkflowName(key), LatestPublishedVersion: 1},
},
versions: map[string]*workflowdef.Version{"medical_device_development": ver},
versions: map[string]*workflowdef.Version{key: ver},
}
}
func validPlan() WorkflowPlan {
return WorkflowPlan{
WorkflowDefinitionKey: "medical_device_development",
WorkflowDefinitionKey: workflowdef.OfficialKeyMain,
WorkflowVersion: 1,
Stages: []StagePlan{{
StageKey: "biz_intake", Enabled: true,
StageKey: "V0.1_triage", Enabled: true,
DocTasks: []DocTaskPlan{{
TaskKey: "intake", Enabled: true,
Duties: DocDuty{AuthorUserID: 1, ReviewerUserID: 2, ApproverUserID: 3},
@@ -86,7 +90,7 @@ func TestService_PlanPreviewActivate(t *testing.T) {
if err != nil {
t.Fatalf("SaveWorkflowPlan: %v", err)
}
if settings.Plan == nil || settings.Plan.WorkflowDefinitionKey != "medical_device_development" {
if settings.Plan == nil || settings.Plan.WorkflowDefinitionKey != workflowdef.OfficialKeyMain {
t.Fatalf("expected saved plan, got %+v", settings.Plan)
}
@@ -99,14 +103,14 @@ func TestService_PlanPreviewActivate(t *testing.T) {
}
result, err := svc.Activate(ctx, 42, 1, true, ActivateInput{
ConfirmationToken: preview.ConfirmationToken,
ConfirmationToken: preview.ConfirmationToken,
ExpectedProjectVersion: preview.ExpectedProjectVersion,
ExpectedPlanRevision: preview.ExpectedPlanRevision,
})
if err != nil {
t.Fatalf("Activate: %v", err)
}
if result.LifecycleStatus != LifecycleActive || result.ActiveWorkflowKey != "medical_device_development" {
if result.LifecycleStatus != LifecycleActive || result.ActiveWorkflowKey != workflowdef.OfficialKeyMain {
t.Fatalf("unexpected activate result: %+v", result)
}
@@ -133,8 +137,28 @@ func TestService_BlockPlanEditWhenActive(t *testing.T) {
func TestService_SeparationOfDutyValidation(t *testing.T) {
bad := validPlan()
bad.Stages[0].DocTasks[0].Duties.ReviewerUserID = 1
res := validatePlan(bad, testResolver().versions["medical_device_development"])
res := validatePlan(bad, testResolver().versions[workflowdef.OfficialKeyMain])
if res.Valid {
t.Fatal("expected separation of duty violation")
}
}
func TestService_SavePlanReturnsValidationIssues(t *testing.T) {
ctx := context.Background()
store := newMemStore()
_ = SeedProjectMeta(store, 8, LifecyclePlanning, "", 1, 0)
svc := NewService(store, testResolver(), nil, nil)
bad := validPlan()
bad.Stages[0].DocTasks[0].Duties = DocDuty{}
_, err := svc.SaveWorkflowPlan(ctx, 8, 1, true, SavePlanInput{
Plan: bad, ExpectedPlanRevision: 0, ExpectedSelectionVersion: 1,
})
var validationErr *ValidationError
if !errors.As(err, &validationErr) {
t.Fatalf("expected ValidationError, got %v", err)
}
if len(validationErr.Issues) != 3 {
t.Fatalf("expected three missing-duty issues, got %+v", validationErr.Issues)
}
}
@@ -48,12 +48,12 @@ func TestResolveStageAccess_ActiveStageDutyOnly(t *testing.T) {
}
creatorAccess, _ := svc.ResolveStageAccess(ctx, 42, 1, true)
if !creatorAccess.StageKeyAllowed("biz_intake") {
if !creatorAccess.StageKeyAllowed("V0.1_triage") {
t.Fatal("creator should access active stage")
}
outsiderAccess, _ := svc.ResolveStageAccess(ctx, 42, 9, false)
if outsiderAccess.StageKeyAllowed("biz_intake") {
if outsiderAccess.StageKeyAllowed("V0.1_triage") {
t.Fatal("user without duty should not access active stage chat")
}
}
@@ -39,7 +39,7 @@ func TestService_AdvanceStageGate(t *testing.T) {
t.Fatalf("Activate: %v", err)
}
readiness, err := svc.GetStageReadiness(ctx, result.Instance.ID, "biz_intake", 1, true)
readiness, err := svc.GetStageReadiness(ctx, result.Instance.ID, "V0.1_triage", 1, true)
if err != nil {
t.Fatalf("GetStageReadiness: %v", err)
}
@@ -49,15 +49,15 @@ func TestService_AdvanceStageGate(t *testing.T) {
_ = settings
advanced, err := svc.AdvanceStage(ctx, result.Instance.ID, 1, true, AdvanceStageInput{
StageKey: "biz_intake", ExpectedConfigRevision: result.Instance.ConfigRevision,
StageKey: "V0.1_triage", ExpectedConfigRevision: result.Instance.ConfigRevision,
})
if err != nil {
t.Fatalf("AdvanceStage: %v", err)
}
if advanced.CompletedStage != "biz_intake" {
t.Fatalf("expected completed biz_intake, got %s", advanced.CompletedStage)
if advanced.CompletedStage != "V0.1_triage" {
t.Fatalf("expected completed V0.1_triage, got %s", advanced.CompletedStage)
}
if advanced.NextStageKey == "biz_intake" {
if advanced.NextStageKey == "V0.1_triage" {
t.Fatal("next stage should not be same as completed")
}
}
@@ -76,7 +76,7 @@ func TestService_AdvanceStageBlockedWithoutApproval(t *testing.T) {
})
_, err := svc.AdvanceStage(ctx, result.Instance.ID, 1, true, AdvanceStageInput{
StageKey: "biz_intake", ExpectedConfigRevision: result.Instance.ConfigRevision,
StageKey: "V0.1_triage", ExpectedConfigRevision: result.Instance.ConfigRevision,
})
if !errors.Is(err, ErrStageGateNotReady) {
t.Fatalf("expected ErrStageGateNotReady, got %v", err)
@@ -0,0 +1,67 @@
package projectinstance
import (
"testing"
"chat-agent-system/internal/workflowdef"
)
func TestValidateActivationPlan_EmptyWorkflowBlocked(t *testing.T) {
plan := WorkflowPlan{
WorkflowDefinitionKey: workflowdef.OfficialKeyProductManager,
WorkflowVersion: 2,
Stages: []StagePlan{{
StageKey: "requirements_baseline",
Enabled: true,
DocTasks: []DocTaskPlan{{
TaskKey: "system_reqs",
Enabled: false,
}},
}},
}
version := &workflowdef.Version{
Status: workflowdef.StatusPublished,
WorkflowKey: workflowdef.OfficialKeyProductManager,
Version: 2,
Definition: workflowdef.VersionDefinition{
SchemaVersion: 2,
Stages: []workflowdef.StageDef{{
Key: "requirements_baseline", Position: 10,
DocTasks: []workflowdef.DocTaskDef{{
Key: "system_reqs", CreatorConfigurable: true, DefaultEnabled: true,
OutputDocType: "system_reqs",
}},
}},
},
}
res := validatePlan(plan, version)
if !res.Valid {
t.Fatalf("save plan should allow all docs disabled: %+v", res.Issues)
}
act := validateActivationPlan(plan, version)
if act.Valid {
t.Fatal("activation should reject empty enabled doc set")
}
}
func TestBuildDefaultPlan_IncludesDisplayNames(t *testing.T) {
version := testResolver().versions[workflowdef.OfficialKeyMain]
plan := buildDefaultPlan(version)
if len(plan.Stages) != 1 || plan.Stages[0].StageName != "V0.1 需求摸底" {
t.Fatalf("expected stage display name, got %+v", plan.Stages)
}
if len(plan.Stages[0].DocTasks) != 1 || plan.Stages[0].DocTasks[0].TaskTitle != "用户初始需求收集表" {
t.Fatalf("expected task display title, got %+v", plan.Stages[0].DocTasks)
}
}
func TestApplyDefinitionLabels_OverridesClientText(t *testing.T) {
version := testResolver().versions[workflowdef.OfficialKeyMain]
plan := validPlan()
plan.Stages[0].StageName = "客户端伪造阶段"
plan.Stages[0].DocTasks[0].TaskTitle = "客户端伪造文档"
applyDefinitionLabels(&plan, version)
if plan.Stages[0].StageName != "V0.1 需求摸底" || plan.Stages[0].DocTasks[0].TaskTitle != "用户初始需求收集表" {
t.Fatalf("definition labels must win, got %+v", plan.Stages[0])
}
}
+33
View File
@@ -125,6 +125,39 @@ func TestProjectWorkflowService_CreateAndCreatorACL(t *testing.T) {
}
}
func TestProjectWorkflowService_ListAllProjects(t *testing.T) {
ctx := context.Background()
svc := NewService(NewStore(nil), NewDefaultCatalog())
creator := &auth.UserAccount{ID: 102, Username: "lisi", Name: "李主管"}
created, err := svc.CreateProject(ctx, creator, CreateProjectInput{Name: "管理员跨项目查看测试"})
if err != nil {
t.Fatalf("CreateProject failed: %v", err)
}
visible, err := svc.ListProjects(ctx, 999)
if err != nil {
t.Fatalf("ListProjects failed: %v", err)
}
if len(visible) != 0 {
t.Fatalf("unrelated user should not see projects, got %d", len(visible))
}
all, err := svc.ListAllProjects(ctx)
if err != nil {
t.Fatalf("ListAllProjects failed: %v", err)
}
found := false
for _, project := range all {
if project.ID == created.ID {
found = true
break
}
}
if !found {
t.Fatalf("ListAllProjects should include project %d", created.ID)
}
}
func TestProjectWorkflowService_LineageAndStale(t *testing.T) {
ctx := context.Background()
store := NewStore(nil)
+36
View File
@@ -25,6 +25,42 @@ func setupTestServer() (*httptest.Server, *auth.Service) {
return ts, srv.AuthService()
}
func TestAuthHandlers_PublicConfigUnauthenticated(t *testing.T) {
ts, authSvc := setupTestServer()
defer ts.Close()
_ = authSvc.UpdateConfig(t.Context(), auth.SystemAuthConfig{
AllowRegistration: true,
RequireApproval: true,
})
resp, err := http.Get(ts.URL + "/api/v1/auth/public-config")
if err != nil {
t.Fatalf("public-config request failed: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("Expected 200 for unauthenticated /auth/public-config, got %d", resp.StatusCode)
}
var cfg auth.SystemAuthConfig
if err := json.NewDecoder(resp.Body).Decode(&cfg); err != nil {
t.Fatalf("decode public-config: %v", err)
}
if !cfg.AllowRegistration || !cfg.RequireApproval {
t.Fatalf("unexpected public-config: %+v", cfg)
}
adminResp, err := http.Get(ts.URL + "/api/v1/admin/config")
if err != nil {
t.Fatalf("admin config request failed: %v", err)
}
defer adminResp.Body.Close()
if adminResp.StatusCode != http.StatusUnauthorized {
t.Fatalf("Expected 401 for unauthenticated /admin/config, got %d", adminResp.StatusCode)
}
}
func TestAuthHandlers_UnauthenticatedMeReturns401(t *testing.T) {
ts, _ := setupTestServer()
defer ts.Close()
+41
View File
@@ -9,8 +9,49 @@ import (
"chat-agent-system/internal/bizdev"
"chat-agent-system/internal/config"
"chat-agent-system/internal/projectworkflow"
)
func TestHandleListProjects_AdminCanSeeProjectsOwnedByOtherUsers(t *testing.T) {
cfg := &config.Config{DataDir: t.TempDir(), AgentBackend: "stub", AllowInMemory: true}
svc := bizdev.MustNewService(t, cfg)
srv := NewServer(cfg, svc)
ctx := t.Context()
creator, err := srv.AuthService().GetUserByID(ctx, 2)
if err != nil {
t.Fatalf("load creator: %v", err)
}
created, err := srv.pwSvc.CreateProject(ctx, creator, projectworkflow.CreateProjectInput{
Name: "非管理员创建的项目",
})
if err != nil {
t.Fatalf("create project: %v", err)
}
admin, err := srv.AuthService().GetUserByID(ctx, 1)
if err != nil {
t.Fatalf("load admin: %v", err)
}
req := httptest.NewRequest(http.MethodGet, "/api/v1/projects", nil)
req = req.WithContext(withPrincipal(req.Context(), admin))
rec := httptest.NewRecorder()
srv.handleListProjects(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
}
var projects []*projectworkflow.Project
if err := json.NewDecoder(rec.Body).Decode(&projects); err != nil {
t.Fatalf("decode projects: %v", err)
}
for _, project := range projects {
if project.ID == created.ID {
return
}
}
t.Fatalf("admin response should include project %d", created.ID)
}
func TestHandleRunProjectWorkflow_DecodesBodyWithoutContentLength(t *testing.T) {
cfg := &config.Config{
DataDir: t.TempDir(),
+8 -8
View File
@@ -207,18 +207,18 @@ func TestProjectWorkflow_TestManagerAndLineageStale(t *testing.T) {
require.Positive(t, proj.ID)
projectID := proj.ID
// 2. 在未批准系统需求规格书时,直接触发 test_manager -> 应触发前置阻断 400 / ERR_PRECONDITION_FAILED
bodyRunTestFail, _ := json.Marshal(map[string]any{
// 2. 无已批准 SRS 时,test_manager 仍可独立启动(PM/TE 解耦)
bodyRunTestNoSRS, _ := json.Marshal(map[string]any{
"task_key": "test_manager",
"project_name": proj.Name,
"backend": "stub",
})
reqRunTestFail := httptest.NewRequest(http.MethodPost, fmt.Sprintf("/api/v1/projects/%d/workflows/test_manager/run", projectID), bytes.NewReader(bodyRunTestFail))
withUserIDAuth(reqRunTestFail, loginTestUser(t, handler, "zhanggong", "123456"))
recRunTestFail := httptest.NewRecorder()
handler.ServeHTTP(recRunTestFail, reqRunTestFail)
assert.Equal(t, http.StatusBadRequest, recRunTestFail.Code)
assert.Contains(t, recRunTestFail.Body.String(), "前置条件未满足")
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)
@@ -7,7 +7,6 @@ import (
"fmt"
"net/http"
"net/http/httptest"
"os"
"testing"
"time"
@@ -25,10 +24,7 @@ import (
func openPGPoolWithMigrations(t *testing.T) *pgxpool.Pool {
t.Helper()
dsn := os.Getenv("TEST_DATABASE_URL")
if dsn == "" {
dsn = "postgres://app:dev_secret_db_pass@127.0.0.1:5433/agent_system_dev?sslmode=disable"
}
dsn := db.SkipIfNoTestDatabaseURL(t)
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
pool, err := pgxpool.New(ctx, dsn)
@@ -0,0 +1,43 @@
package workflowdef
import "testing"
func TestOfficialWorkflowKeys(t *testing.T) {
keys := OfficialWorkflowKeys()
if len(keys) != 3 {
t.Fatalf("expected 3 official keys, got %d", len(keys))
}
for _, k := range keys {
if !IsOfficialWorkflowKey(k) {
t.Fatalf("key %q not official", k)
}
}
}
func TestValidateDefinitionV2_OfficialDocCounts(t *testing.T) {
def, err := LoadManifestDefinition("main.v2.json")
if err != nil {
t.Fatal(err)
}
normalizeManifestDefinition(&def)
res := ValidateDefinitionV2(def)
if !res.Valid {
t.Fatalf("main v2 invalid: %+v", res.Issues)
}
}
func TestValidateDefinitionV2_RejectsUpstream(t *testing.T) {
def := VersionDefinition{
SchemaVersion: 2,
WorkflowKey: OfficialKeyTestManager,
RequiredUpstreamWorkflow: "product_manager",
Stages: []StageDef{{
Key: "s", Position: 10,
DocTasks: []DocTaskDef{{Key: "test_manager", OutputDocType: "test_manager", ExecutorKey: "squad.test_manager.v1"}},
}},
}
res := ValidateDefinitionV2(def)
if res.Valid {
t.Fatal("expected upstream rejection")
}
}
+6 -5
View File
@@ -2,21 +2,18 @@ package workflowdef_test
import (
"context"
"os"
"testing"
"time"
"github.com/jackc/pgx/v5/pgxpool"
"chat-agent-system/internal/db"
"chat-agent-system/internal/workflowdef"
)
func openPool(t *testing.T) *pgxpool.Pool {
t.Helper()
dsn := os.Getenv("TEST_DATABASE_URL")
if dsn == "" {
dsn = "postgres://app:dev_secret_db_pass@127.0.0.1:5433/agent_system_dev?sslmode=disable"
}
dsn := db.SkipIfNoTestDatabaseURL(t)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
pool, err := pgxpool.New(ctx, dsn)
@@ -43,6 +40,10 @@ func TestPublishVersion_ImmutableAfterPublish(t *testing.T) {
ctx := context.Background()
svc := workflowdef.NewService(pool, nil)
key := "test_wf_" + time.Now().Format("150405")
defer func() {
_, _ = pool.Exec(ctx, `DELETE FROM workflow_versions WHERE workflow_id IN (SELECT id FROM workflow_definitions WHERE def_key = $1)`, key)
_, _ = pool.Exec(ctx, `DELETE FROM workflow_definitions WHERE def_key = $1`, key)
}()
def, ver, err := svc.CreateDefinition(ctx, workflowdef.CreateDefinitionInput{
Key: key, Name: "测试工作流",
Definition: workflowdef.VersionDefinition{