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:
committed by
weichen
co-authored by
Cursor
parent
47984f4863
commit
ad6e38d11a
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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{
|
||||
|
||||
Reference in New Issue
Block a user