Files
Agent-System/internal/notification/service_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

129 lines
3.7 KiB
Go

package notification_test
import (
"context"
"os"
"strings"
"testing"
"time"
"github.com/jackc/pgx/v5/pgxpool"
"chat-agent-system/internal/db"
"chat-agent-system/internal/notification"
)
func openPool(t *testing.T) *pgxpool.Pool {
t.Helper()
// 隔离约定:仅提供专用测试库时运行,禁止默认回退到开发库。
if os.Getenv("TEST_DATABASE_URL") == "" {
t.Skip("TEST_DATABASE_URL not set; skipping notification PG test")
return nil
}
dsn := os.Getenv("TEST_DATABASE_URL")
if strings.Contains(dsn, "agent_system_dev") || strings.Contains(dsn, "agent_system_test") || strings.Contains(dsn, "agent_system_main") {
t.Fatal("TEST_DATABASE_URL must not point at a shared env database")
}
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)
}
return pool
}
func TestPG_EmitStageReadyDedupe(t *testing.T) {
pool := openPool(t)
defer pool.Close()
svc := notification.NewService(pool)
if !svc.Available() {
t.Fatal("notification service should be available after migration")
}
ctx := context.Background()
var projectID, instanceID, stageInstID int64
code := "notify-" + time.Now().Format("150405.000000")
err := pool.QueryRow(ctx, `
INSERT INTO projects (code, name, status, creator_user_id, lifecycle_status)
VALUES ($1, 'Notify Test ' || $2, 'active', 2, 'active') RETURNING id
`, code, 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)
})
var wfVersionID int64
err = pool.QueryRow(ctx, `SELECT id FROM workflow_versions ORDER BY id DESC LIMIT 1`).Scan(&wfVersionID)
if err != nil {
t.Skip("no workflow version for notification test")
}
err = pool.QueryRow(ctx, `
INSERT INTO project_workflow_instances (project_id, workflow_version_id, instance_no, status, config_revision, plan_hash, config_snapshot, created_by)
VALUES ($1, $2, 1, 'active', 3, 'h', '{}'::jsonb, 2) RETURNING id
`, projectID, wfVersionID).Scan(&instanceID)
if err != nil {
t.Fatalf("insert instance: %v", err)
}
err = pool.QueryRow(ctx, `
INSERT INTO project_stage_instances (workflow_instance_id, stage_key, position, enabled, status)
VALUES ($1, 'biz_intake', 0, true, 'active') RETURNING id
`, instanceID).Scan(&stageInstID)
if err != nil {
t.Fatalf("insert stage: %v", err)
}
payload := notification.StageReadyPayload{
ProjectID: projectID,
WorkflowInstanceID: instanceID,
StageInstanceID: stageInstID,
StageKey: "biz_intake",
StageName: "商务摸底",
ConfigRevision: 3,
RequiredDocCount: 1,
ApprovedDocCount: 1,
}
created, err := svc.EmitStageReadyToAdvance(ctx, 2, payload)
if err != nil {
t.Fatalf("emit: %v", err)
}
if !created {
t.Fatal("expected first emit to create notification")
}
again, err := svc.EmitStageReadyToAdvance(ctx, 2, payload)
if err != nil {
t.Fatalf("second emit: %v", err)
}
if again {
t.Fatal("expected dedupe to suppress second notification")
}
var count int
err = pool.QueryRow(ctx, `
SELECT COUNT(*) FROM notifications WHERE user_id = 2 AND project_id = $1 AND type = $2
`, projectID, notification.TypeStageReadyToAdvance).Scan(&count)
if err != nil {
t.Fatalf("count notifications: %v", err)
}
if count != 1 {
t.Fatalf("expected exactly one notification, got %d", count)
}
}