diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6a5bcf3b..e245b2c3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,14 +42,10 @@ jobs: - name: arch-check (block cleared kinds) run: ./scripts/arch-check.sh --mode block - - name: go test (short, required since 2026-09-29) - # skip 清单 = issue #2 存量 PG 集成失败(2026-09-29 已修 10/15,余 5 点名跟踪); - # 新增失败不会被跳过,直接阻断。 + - name: go test (short, required) env: TEST_DATABASE_URL: postgres://app:ci_secret_db_pass@postgres:5432/agent_system_ci?sslmode=disable - run: > - go test -short -timeout 20m ./... - -skip 'TestCreateDocument_PG_ClientKeyIdempotentAndLedgerSurvivesNewService|TestPG_ActivationAndStageAdvance|TestE2E_WorkflowGovernance_CompleteAndReplan|TestPhase5_ConcurrentStageAdvance_PG|TestWorkflowGovernance_PG_ActivateApproveAdvanceChain' + run: go test -short -timeout 20m ./... - name: secrets scan (tracked files) run: | diff --git a/bizdev-cli b/bizdev-cli new file mode 100755 index 00000000..2372d75e Binary files /dev/null and b/bizdev-cli differ diff --git a/internal/auth/admin_reset_password_test.go b/internal/auth/admin_reset_password_test.go index b9f43369..1264b39b 100644 --- a/internal/auth/admin_reset_password_test.go +++ b/internal/auth/admin_reset_password_test.go @@ -100,4 +100,4 @@ func TestLoginMissingPasswordHash(t *testing.T) { if err == nil || err.Error() != "用户名或密码错误" { t.Fatalf("expected unified credential error, got %v", err) } -} \ No newline at end of file +} diff --git a/internal/auth/pg_store_test.go b/internal/auth/pg_store_test.go index 413e6d33..282388bc 100644 --- a/internal/auth/pg_store_test.go +++ b/internal/auth/pg_store_test.go @@ -1,9 +1,9 @@ package auth_test import ( + "chat-agent-system/internal/db" "context" "errors" - "os" "testing" "time" @@ -15,11 +15,7 @@ import ( func openTestPool(t *testing.T) *pgxpool.Pool { t.Helper() - dsn := os.Getenv("TEST_DATABASE_URL") - if dsn == "" { - t.Skip("TEST_DATABASE_URL must be set for PostgreSQL integration tests (refusing default dev database)") - return nil - } + dsn := db.SkipIfNoTestDatabaseURL(t) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() pool, err := pgxpool.New(ctx, dsn) diff --git a/internal/auth/store_factory.go b/internal/auth/store_factory.go index addd21d4..d96d0dcd 100644 --- a/internal/auth/store_factory.go +++ b/internal/auth/store_factory.go @@ -5,8 +5,8 @@ import ( "fmt" "strings" - "github.com/jackc/pgx/v5/pgxpool" "chat-agent-system/internal/config" + "github.com/jackc/pgx/v5/pgxpool" ) // Stores 认证账户存储与会话存储组合。 diff --git a/internal/ctxmgr/case_indexing.go b/internal/ctxmgr/case_indexing.go index 7aa2142c..611cb3af 100644 --- a/internal/ctxmgr/case_indexing.go +++ b/internal/ctxmgr/case_indexing.go @@ -66,15 +66,15 @@ func (m *ContextManager) IndexApprovedDocument(ctx context.Context, doc *docgov. regs := extractRegulations(sec.Content) meta := map[string]any{ - "task_key": doc.TaskKey, - "artifact_title": doc.Title, - "section_title": sec.Title, - "regulations": regs, - "author_name": doc.AuthorName, - "reviewer_name": doc.ReviewerName, - "approver_name": doc.ApproverName, - "stage": doc.Stage, - "approved_at": doc.ApprovedAt, + "task_key": doc.TaskKey, + "artifact_title": doc.Title, + "section_title": sec.Title, + "regulations": regs, + "author_name": doc.AuthorName, + "reviewer_name": doc.ReviewerName, + "approver_name": doc.ApproverName, + "stage": doc.Stage, + "approved_at": doc.ApprovedAt, } if m.pool != nil { diff --git a/internal/ctxmgr/chat_turn_sweep_test.go b/internal/ctxmgr/chat_turn_sweep_test.go index 99033185..71c8083a 100644 --- a/internal/ctxmgr/chat_turn_sweep_test.go +++ b/internal/ctxmgr/chat_turn_sweep_test.go @@ -17,10 +17,10 @@ func TestSweepExpiredProcessingTurns_Memory(t *testing.T) { future := time.Now().Add(5 * time.Minute) mgr.mu.Lock() mgr.turns[turnMemKey("sess-sweep", "turn-expired")] = &ChatTurnRecord{ - SessionID: "sess-sweep", - ClientTurnID: "turn-expired", - Status: TurnStatusProcessing, - LeaseExpiresAt: &past, + SessionID: "sess-sweep", + ClientTurnID: "turn-expired", + Status: TurnStatusProcessing, + LeaseExpiresAt: &past, } mgr.turns[turnMemKey("sess-sweep", "turn-fresh")] = &ChatTurnRecord{ SessionID: "sess-sweep", diff --git a/internal/ctxmgr/chat_unread.go b/internal/ctxmgr/chat_unread.go index b2d5499b..f9e79dde 100644 --- a/internal/ctxmgr/chat_unread.go +++ b/internal/ctxmgr/chat_unread.go @@ -12,8 +12,8 @@ import ( type ChatReadPosition struct { SessionID string `json:"session_id"` UserID int64 `json:"user_id"` - LastReadMessageID int64 `json:"last_read_message_id"` // 连续已读基线 - DismissedUnreadIDs []int64 `json:"dismissed_unread_ids"` // 离散跳读/倒序消费的消息 ID + LastReadMessageID int64 `json:"last_read_message_id"` // 连续已读基线 + DismissedUnreadIDs []int64 `json:"dismissed_unread_ids"` // 离散跳读/倒序消费的消息 ID UpdatedAt time.Time `json:"updated_at"` } diff --git a/internal/ctxmgr/chat_window.go b/internal/ctxmgr/chat_window.go index e735bd55..6cbaa481 100644 --- a/internal/ctxmgr/chat_window.go +++ b/internal/ctxmgr/chat_window.go @@ -787,4 +787,3 @@ func enrichMessageExportCapability(r *ChatMessageRecord) { } r.ExportCapability = evalCap } - diff --git a/internal/ctxmgr/chat_window_test.go b/internal/ctxmgr/chat_window_test.go index 26a58c4b..293acf19 100644 --- a/internal/ctxmgr/chat_window_test.go +++ b/internal/ctxmgr/chat_window_test.go @@ -1,6 +1,7 @@ package ctxmgr_test import ( + "chat-agent-system/internal/db" "context" "fmt" "os" @@ -241,11 +242,7 @@ func TestChatWindow_MemoryEquivalenceAndPagination(t *testing.T) { func TestChatWindow_DatabaseIntegration(t *testing.T) { dsn := os.Getenv("CONTEXT_DB_DSN") if dsn == "" { - dsn = os.Getenv("TEST_DATABASE_URL") - } - if dsn == "" { - t.Skip("CONTEXT_DB_DSN or TEST_DATABASE_URL must be set (refusing default dev database)") - return + dsn = db.SkipIfNoTestDatabaseURL(t) } ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() @@ -262,6 +259,10 @@ func TestChatWindow_DatabaseIntegration(t *testing.T) { return } + if err := db.RunMigrationsOnceForTest(ctx, pool, dsn); err != nil { + t.Fatalf("migration failed: %v", err) + } + mgr := ctxmgr.NewContextManager(pool, nil, ctxmgr.DefaultTimeDecayConfig()) sessID := fmt.Sprintf("sess_db_window_%d", time.Now().UnixNano()) @@ -348,9 +349,9 @@ func TestListChatCategories_MemoryMode(t *testing.T) { sessID := "sess_mem_cat_test" // 注入不同分类的消息 - _, _ = mgr.RecordUserMessageWithID(ctx, sessID, 1, "user", "阶段推进", "ADVANCE_STAGE", "", nil) // task - _, _ = mgr.RecordUserMessageWithID(ctx, sessID, 1, "user", "启动任务", "TRIGGER_TASK", "", nil) // task - _, _ = mgr.RecordUserMessageWithID(ctx, sessID, 1, "user", "合规咨询", "QUERY_REGULATIONS", "", nil) // consult + _, _ = mgr.RecordUserMessageWithID(ctx, sessID, 1, "user", "阶段推进", "ADVANCE_STAGE", "", nil) // task + _, _ = mgr.RecordUserMessageWithID(ctx, sessID, 1, "user", "启动任务", "TRIGGER_TASK", "", nil) // task + _, _ = mgr.RecordUserMessageWithID(ctx, sessID, 1, "user", "合规咨询", "QUERY_REGULATIONS", "", nil) // consult _, _ = mgr.RecordUserMessageWithID(ctx, sessID, 1, "user", "立项里程碑", "", "", map[string]any{"milestone": true}) // milestone stats, err := mgr.ListChatCategories(ctx, sessID) diff --git a/internal/ctxmgr/manager.go b/internal/ctxmgr/manager.go index 76f2363d..d41772e4 100644 --- a/internal/ctxmgr/manager.go +++ b/internal/ctxmgr/manager.go @@ -1,11 +1,11 @@ package ctxmgr import ( - "log/slog" "context" "encoding/json" "errors" "fmt" + "log/slog" "sync" "time" @@ -23,17 +23,17 @@ type ContextManager struct { embedder VectorEmbedder // 离线降级内存缓存 - mu sync.RWMutex - users map[int64]*UserPreference - projects map[int64]*ProjectCollectiveContext - messages map[string][]ChatMessageRecord // session_id -> messages - sessions map[string]*ChatSessionSummary // session_id -> summary - activities map[int64][]ActivityItem // project_id -> activities - memoryChunks []MemoryChunkItem // 语义记忆切片缓存 - summaries map[string]*ChatSummary // session_id -> rolling summary - digests map[string]map[string]*DailyDigest // session_id -> day_key -> DailyDigest - readPositions map[string]*ChatReadPosition // session_id:user_id -> read position - turns map[string]*ChatTurnRecord // sessionID|clientTurnID -> 幂等回合(仅内存降级) + mu sync.RWMutex + users map[int64]*UserPreference + projects map[int64]*ProjectCollectiveContext + messages map[string][]ChatMessageRecord // session_id -> messages + sessions map[string]*ChatSessionSummary // session_id -> summary + activities map[int64][]ActivityItem // project_id -> activities + memoryChunks []MemoryChunkItem // 语义记忆切片缓存 + summaries map[string]*ChatSummary // session_id -> rolling summary + digests map[string]map[string]*DailyDigest // session_id -> day_key -> DailyDigest + readPositions map[string]*ChatReadPosition // session_id:user_id -> read position + turns map[string]*ChatTurnRecord // sessionID|clientTurnID -> 幂等回合(仅内存降级) cache *MultiLevelCache // 多级统一缓存系统 (C1-C5) semanticSummarizer SemanticSummarizer // no-tool summarizer; nil → heuristic default shadowJobs []SemanticShadowJob // in-process durable fallback queue diff --git a/internal/ctxmgr/manager_test.go b/internal/ctxmgr/manager_test.go index 086a7a9a..0633e7c1 100644 --- a/internal/ctxmgr/manager_test.go +++ b/internal/ctxmgr/manager_test.go @@ -1,6 +1,7 @@ package ctxmgr import ( + "chat-agent-system/internal/db" "context" "os" "strings" @@ -130,11 +131,7 @@ func TestContextManager_TimeDecayActivities(t *testing.T) { func TestContextManager_DatabaseIntegration(t *testing.T) { dsn := os.Getenv("CONTEXT_DB_DSN") if dsn == "" { - dsn = os.Getenv("TEST_DATABASE_URL") - } - if dsn == "" { - t.Skip("CONTEXT_DB_DSN or TEST_DATABASE_URL must be set (refusing default dev database)") - return + dsn = db.SkipIfNoTestDatabaseURL(t) } ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) @@ -152,6 +149,10 @@ func TestContextManager_DatabaseIntegration(t *testing.T) { return } + if err := db.RunMigrationsOnceForTest(ctx, pool, dsn); err != nil { + t.Fatalf("migration failed: %v", err) + } + mgr := NewContextManager(pool, nil, DefaultTimeDecayConfig()) // 准备真实项目与用户 diff --git a/internal/ctxmgr/project_dialogue.go b/internal/ctxmgr/project_dialogue.go index 49d3b2d0..6b7d6ce1 100644 --- a/internal/ctxmgr/project_dialogue.go +++ b/internal/ctxmgr/project_dialogue.go @@ -13,11 +13,11 @@ const defaultProjectDialogueLimit = 120 // ProjectDialogueCollectResult 项目级对话采集结果(供上下文快照使用)。 type ProjectDialogueCollectResult struct { - Lines []string - ExcludedPrivate int - ExcludedNoAccess int - AllowedStageKey string - StageFilteredCount int + Lines []string + ExcludedPrivate int + ExcludedNoAccess int + AllowedStageKey string + StageFilteredCount int } // CollectProjectDialogueOptions 采集选项。 diff --git a/internal/ctxmgr/project_dialogue_test.go b/internal/ctxmgr/project_dialogue_test.go index 85f6518f..f4e14370 100644 --- a/internal/ctxmgr/project_dialogue_test.go +++ b/internal/ctxmgr/project_dialogue_test.go @@ -16,9 +16,9 @@ func TestCollectProjectDialogue_ExcludesPrivateAndIncludesShared(t *testing.T) { _, _ = mgr.RecordUserMessageWithID(ctx, "sess-501", 1, "user", "[private] 不应纳入", "", "", meta) _, _ = mgr.RecordUserMessageWithID(ctx, "sess-501", 1, "user", "项目需要支持多用户协作", "", "", meta) _, _ = mgr.RecordUserMessageWithID(ctx, "sess-501", 1, "assistant", "好的,我会整理需求要点。", "", "", map[string]any{ - "project_id": projectID, - "export_policy": string(replyexport.PolicyFull), - "content_kind": string(replyexport.KindDocumentText), + "project_id": projectID, + "export_policy": string(replyexport.PolicyFull), + "content_kind": string(replyexport.KindDocumentText), }) res, err := mgr.CollectProjectDialogue(ctx, projectID, CollectProjectDialogueOptions{}) diff --git a/internal/ctxmgr/semantic_summary.go b/internal/ctxmgr/semantic_summary.go index 4d7abe34..f08a50a1 100644 --- a/internal/ctxmgr/semantic_summary.go +++ b/internal/ctxmgr/semantic_summary.go @@ -62,15 +62,15 @@ type SemanticSummaryResult struct { // gray rollout a replaced report keeps the displaced deterministic narrative // so later deterministic regeneration merges clean material (rollback-safe). type SemanticShadowReport struct { - Status string `json:"status"` - Narrative string `json:"narrative,omitempty"` - DisplacedNarrative string `json:"displaced_narrative,omitempty"` - InputDigest string `json:"input_digest,omitempty"` - Model string `json:"model,omitempty"` - Protocol string `json:"protocol,omitempty"` - ToolPermissions string `json:"tool_permissions"` - FreshnessNote string `json:"freshness_note,omitempty"` - GeneratedAt time.Time `json:"generated_at"` + Status string `json:"status"` + Narrative string `json:"narrative,omitempty"` + DisplacedNarrative string `json:"displaced_narrative,omitempty"` + InputDigest string `json:"input_digest,omitempty"` + Model string `json:"model,omitempty"` + Protocol string `json:"protocol,omitempty"` + ToolPermissions string `json:"tool_permissions"` + FreshnessNote string `json:"freshness_note,omitempty"` + GeneratedAt time.Time `json:"generated_at"` } // SemanticSummarizer produces a narrative from minimized messages. diff --git a/internal/ctxmgr/shadow_task.go b/internal/ctxmgr/shadow_task.go index 4cb78fc4..37f4d38d 100644 --- a/internal/ctxmgr/shadow_task.go +++ b/internal/ctxmgr/shadow_task.go @@ -6,8 +6,8 @@ import ( "fmt" "log/slog" "strings" - "unicode/utf8" "time" + "unicode/utf8" ) const ( diff --git a/internal/ctxmgr/summary_flags.go b/internal/ctxmgr/summary_flags.go index 934d6055..21b460bc 100644 --- a/internal/ctxmgr/summary_flags.go +++ b/internal/ctxmgr/summary_flags.go @@ -26,8 +26,8 @@ const ( // SummaryFeatureFlags controls semantic summary rollout. REPLACE swaps the // served chat Narrative only for allowlisted sessions (fail-closed gray). type SummaryFeatureFlags struct { - Shadow bool - Replace bool + Shadow bool + Replace bool // ReplaceAllSessions is the "*" wildcard for ReplaceSessions: every // session (present and future) is eligible while the master Replace flag // stays on. An empty allowlist still replaces nothing. diff --git a/internal/db/migrate_test.go b/internal/db/migrate_test.go index e4c16a80..3fa50d2d 100644 --- a/internal/db/migrate_test.go +++ b/internal/db/migrate_test.go @@ -23,10 +23,7 @@ func TestMigrationGate_ExpectedFilesMatchAppliedVersion(t *testing.T) { t.Fatal("expected at least one migration file") } - dsn := os.Getenv("TEST_DATABASE_URL") - if dsn == "" { - t.Skip("TEST_DATABASE_URL not set; migration gate file count only") - } + dsn := SkipIfNoTestDatabaseURL(t) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() @@ -280,10 +277,7 @@ func rewriteDatabaseName(dsn, name string) string { } func TestRunMigrationsEmbeddedFallback(t *testing.T) { - dsn := os.Getenv("TEST_DATABASE_URL") - if dsn == "" { - t.Skip("TEST_DATABASE_URL not set; embedded fallback needs PG") - } + dsn := SkipIfNoTestDatabaseURL(t) ctx := context.Background() pool, err := pgxpool.New(ctx, dsn) if err != nil { diff --git a/internal/db/testdb.go b/internal/db/testdb.go index e0a8d35a..6328f711 100644 --- a/internal/db/testdb.go +++ b/internal/db/testdb.go @@ -2,7 +2,14 @@ package db import ( "context" + "crypto/sha256" + "database/sql" + "encoding/hex" + "fmt" + "net/url" "os" + "path/filepath" + "runtime" "strings" "sync" "testing" @@ -31,25 +38,118 @@ func RunMigrationsOnceForTest(ctx context.Context, pool *pgxpool.Pool, dsn strin const testDatabaseEnv = "TEST_DATABASE_URL" // RequireTestDatabaseURL 集成测试必须显式提供隔离测试库,禁止默认连开发库。 +// 返回按调用方包派生的独立数据库(_):包内共享、包间隔离, +// 消除 go test 并行包对同一库的迁移/种子互相干扰(2026-09-29 issue #2)。 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 + return derivedPackageDSN(t, 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 derivedPackageDSN(t, dsn) +} + +var ( + pkgDBMu sync.Mutex + pkgDBReady = map[string]string{} // 派生 DSN 缓存(进程内) +) + +func derivedPackageDSN(t *testing.T, baseDSN string) string { + t.Helper() + _, file, _, ok := runtime.Caller(2) // 跳过 helper 自身,取调用测试文件 + if !ok { + return baseDSN + } + pkgDir := filepath.ToSlash(filepath.Dir(file)) + // 仅取模块内相对路径段,避免绝对路径差异影响哈希 + if i := strings.Index(pkgDir, "internal/"); i >= 0 { + pkgDir = pkgDir[i:] + } else if i := strings.Index(pkgDir, "cmd/"); i >= 0 { + pkgDir = pkgDir[i:] + } + sum := sha256.Sum256([]byte(pkgDir)) + suffix := hex.EncodeToString(sum[:4]) + + u, err := url.Parse(baseDSN) + if err != nil || u.Path == "" { + return baseDSN // 非标准 DSN:退回原值,行为同旧版 + } + baseDB := strings.TrimPrefix(u.Path, "/") + if baseDB == "" { + return baseDSN + } + sanitized := strings.Map(func(r rune) rune { + switch { + case r >= 'a' && r <= 'z', r >= '0' && r <= '9': + return r + default: + return '_' + } + }, strings.ToLower(baseDB)) + name := fmt.Sprintf("%s_%s", sanitized, suffix) + if len(name) > 63 { + name = name[:63] + } + + u2 := *u + u2.Path = "/" + name + + pkgDBMu.Lock() + cached, done := pkgDBReady[name] + pkgDBMu.Unlock() + if done { + return cached + } + + if err := ensureTestDatabase(baseDSN, name); err != nil { + t.Fatalf("prepare per-package test database %s: %v", name, err) + } + dsn := u2.String() + pkgDBMu.Lock() + pkgDBReady[name] = dsn + pkgDBMu.Unlock() return dsn } +// ensureTestDatabase 在基库实例上幂等创建派生库(advisory lock 防并行包竞态)。 +func ensureTestDatabase(baseDSN, name string) error { + admindb, err := sql.Open("pgx", baseDSN) + if err != nil { + return err + } + defer admindb.Close() + var exists bool + if err := admindb.QueryRow(`SELECT EXISTS(SELECT 1 FROM pg_database WHERE datname=$1)`, name).Scan(&exists); err != nil { + return err + } + if exists { + return nil + } + if _, err := admindb.Exec(`SELECT pg_advisory_lock(hashtext($1))`, name); err != nil { + return err + } + defer admindb.Exec(`SELECT pg_advisory_unlock(hashtext($1))`, name) + if err := admindb.QueryRow(`SELECT EXISTS(SELECT 1 FROM pg_database WHERE datname=$1)`, name).Scan(&exists); err != nil { + return err + } + if exists { + return nil + } + _, err = admindb.Exec(fmt.Sprintf(`CREATE DATABASE %q`, name)) + return err +} + // RefuseSharedEnvDSN fails if the DSN targets a shared env database. func RefuseSharedEnvDSN(t *testing.T, dsn string) { t.Helper() diff --git a/internal/docgov/testsupport.go b/internal/docgov/testsupport.go new file mode 100644 index 00000000..1c4cbc90 --- /dev/null +++ b/internal/docgov/testsupport.go @@ -0,0 +1,26 @@ +package docgov + +import ( + "context" + "fmt" +) + +// FreezeHeadSnapshotForTest 把文档 head/review 快照指针冻结到 snapID +// (写本模块所有的 document_records),并清除引擎缓存使后续装载来自 PG。 +// +// 与 docrevision.SeedContentSnapshotForTest 配合:账本 seed 内容快照(docrevision +// 生产行为),治理侧冻结指针(本模块生产行为)。供集成测试构造合法批准链。 +func (e *GovernanceEngine) FreezeHeadSnapshotForTest(ctx context.Context, docID string, snapID int64) error { + if e == nil || e.pool == nil { + return fmt.Errorf("docgov: FreezeHeadSnapshotForTest requires a PG-backed engine") + } + if _, err := e.pool.Exec(ctx, + `UPDATE document_records SET review_snapshot_id=$1, head_snapshot_id=$1 WHERE doc_id=$2`, + snapID, docID); err != nil { + return fmt.Errorf("docgov: freeze head snapshot: %w", err) + } + e.memMu.Lock() + delete(e.memDocs, docID) + e.memMu.Unlock() + return nil +} diff --git a/internal/docrevision/testsupport.go b/internal/docrevision/testsupport.go new file mode 100644 index 00000000..6ec65441 --- /dev/null +++ b/internal/docrevision/testsupport.go @@ -0,0 +1,37 @@ +package docrevision + +import ( + "context" + "fmt" + "time" + + "github.com/jackc/pgx/v5/pgxpool" +) + +// SeedContentSnapshotForTest 以生产同构方式写入内容快照(本模块所有的 +// document_content_blobs + document_snapshots)并返回快照 ID。 +// +// 用途:集成测试构造合法的 送审→审核→批准 链——账本封快照正是本模块的 +// 生产行为。document_records 上的 head/review 指针回填归 docgov +//(GovernanceEngine.FreezeHeadSnapshotForTest),两者配合使用。 +func SeedContentSnapshotForTest(ctx context.Context, pool *pgxpool.Pool, docID string, actorID int64, content string) (int64, error) { + if pool == nil { + return 0, fmt.Errorf("docrevision: SeedContentSnapshotForTest requires a pool") + } + sha := fmt.Sprintf("sha_seed_%d", time.Now().UnixNano()) + var snapID int64 + err := pool.QueryRow(ctx, ` + WITH blob AS ( + INSERT INTO document_content_blobs (content_sha256, content_markdown, byte_size, created_at) + VALUES ($2, $3, $4, now()) + RETURNING content_sha256 + ) + INSERT INTO document_snapshots (doc_id, content_sha256, actor_user_id, actor_snapshot, source_kind, created_at) + SELECT $1::text, blob.content_sha256, $5, '[]'::jsonb, 'human', now() FROM blob + RETURNING id + `, docID, sha, content, len(content), actorID).Scan(&snapID) + if err != nil { + return 0, fmt.Errorf("docrevision: seed snapshot: %w", err) + } + return snapID, nil +} diff --git a/internal/notification/service_test.go b/internal/notification/service_test.go index 1cdd6fcd..455fd412 100644 --- a/internal/notification/service_test.go +++ b/internal/notification/service_test.go @@ -2,7 +2,6 @@ package notification_test import ( "context" - "os" "strings" "testing" "time" @@ -16,11 +15,7 @@ import ( 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") + dsn := db.SkipIfNoTestDatabaseURL(t) 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") } diff --git a/internal/projectinstance/access_governing_pg_test.go b/internal/projectinstance/access_governing_pg_test.go index eb122e42..a1e867bd 100644 --- a/internal/projectinstance/access_governing_pg_test.go +++ b/internal/projectinstance/access_governing_pg_test.go @@ -92,7 +92,7 @@ func TestPG_GoverningAccess_ActiveCreatorDutyOutsider(t *testing.T) { code := "pg-access-live-" + 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 Live', 'active', $2, 'planning', '', 1, 0) RETURNING id + VALUES ($1, 'Access Live ' || $1, 'active', $2, 'planning', '', 1, 0) RETURNING id `, code, creatorID).Scan(&projectID) if err != nil { t.Fatalf("insert project: %v", err) @@ -128,7 +128,7 @@ func TestPG_GoverningAccess_ActiveCreatorDutyOutsider(t *testing.T) { t.Fatalf("preview: %v", err) } if _, err := svc.Activate(ctx, projectID, creatorID, true, projectinstance.ActivateInput{ - ConfirmationToken: preview.ConfirmationToken, + ConfirmationToken: preview.ConfirmationToken, ExpectedProjectVersion: preview.ExpectedProjectVersion, ExpectedPlanRevision: preview.ExpectedPlanRevision, }); err != nil { diff --git a/internal/projectinstance/activation_official_pg_test.go b/internal/projectinstance/activation_official_pg_test.go index ba50efe6..9c3063c2 100644 --- a/internal/projectinstance/activation_official_pg_test.go +++ b/internal/projectinstance/activation_official_pg_test.go @@ -25,8 +25,8 @@ func TestPG_ActivateOfficialWorkflowRuntimeMetadata(t *testing.T) { t.Fatalf("load main.v2 manifest: %v", err) } if err := wfStore.UpsertPublishedSeed(ctx, workflowdef.CreateDefinitionInput{ - Key: workflowdef.OfficialKeyMain, - Name: "商务对接流程", + Key: workflowdef.OfficialKeyMain, + Name: "商务对接流程", Definition: def, }, 1); err != nil { t.Fatalf("seed official main workflow: %v", err) diff --git a/internal/projectinstance/approval.go b/internal/projectinstance/approval.go index d09000c3..e097c2ca 100644 --- a/internal/projectinstance/approval.go +++ b/internal/projectinstance/approval.go @@ -4,14 +4,14 @@ import "context" // DocumentApprovedResult 文档批准后阶段就绪评估结果。 type DocumentApprovedResult struct { - StageReady bool - InstanceID int64 - StageInstanceID int64 - StageKey string - StageName string - ConfigRevision int - RequiredDocCount int - ApprovedDocCount int + StageReady bool + InstanceID int64 + StageInstanceID int64 + StageKey string + StageName string + ConfigRevision int + RequiredDocCount int + ApprovedDocCount int } // HandleDocumentApproved 同步文档任务状态并评估是否首次满足阶段推进条件。 diff --git a/internal/projectinstance/approval_test.go b/internal/projectinstance/approval_test.go index 14dd1c43..e42c75d7 100644 --- a/internal/projectinstance/approval_test.go +++ b/internal/projectinstance/approval_test.go @@ -20,7 +20,7 @@ func TestHandleDocumentApproved_StageReady(t *testing.T) { StageKey: "V0.1_triage", Enabled: true, DocTasks: []DocTaskPlan{{ TaskKey: "intake", Enabled: true, - Duties: DocDuty{AuthorUserID: 2, ReviewerUserID: 3, ApproverUserID: 4}, + Duties: DocDuty{AuthorUserID: 2, ReviewerUserID: 3, ApproverUserID: 4}, }}, }}, } diff --git a/internal/projectinstance/duty_mode_test.go b/internal/projectinstance/duty_mode_test.go index bb000a66..7fa84ef1 100644 --- a/internal/projectinstance/duty_mode_test.go +++ b/internal/projectinstance/duty_mode_test.go @@ -187,4 +187,3 @@ func TestValidateAuthorOnlyDocTaskDuties(t *testing.T) { t.Fatalf("separated A/A/A: %+v", sep) } } - diff --git a/internal/projectinstance/pg_integration_test.go b/internal/projectinstance/pg_integration_test.go index c2063144..af989d54 100644 --- a/internal/projectinstance/pg_integration_test.go +++ b/internal/projectinstance/pg_integration_test.go @@ -9,6 +9,7 @@ import ( "chat-agent-system/internal/db" "chat-agent-system/internal/docgov" + "chat-agent-system/internal/docrevision" "chat-agent-system/internal/notification" "chat-agent-system/internal/projectinstance" "chat-agent-system/internal/projectworkflow" @@ -151,9 +152,22 @@ func TestPG_ActivationAndStageAdvance(t *testing.T) { t.Fatalf("create doc: %v", err) } _ = engine.MarkDocumentContentUpdated(ctx, docID, "hash_pg_p3", false) - _, _ = engine.SubmitForReview(ctx, docID, authorID, "author", "张") - _, _ = engine.ReviewApprove(ctx, docID, reviewerID, "reviewer", "李", "ok") - _, err = engine.ApproverApprove(ctx, docID, approverID, "approver", "王", "ok") + if _, err := engine.SubmitForReview(ctx, docID, authorID, "author", "张"); err != nil { + t.Fatalf("submit for review: %v", err) + } + snapID, serr := docrevision.SeedContentSnapshotForTest(ctx, pool, docID, authorID, "# pg p3 intake") + if serr != nil { + t.Fatalf("seed snapshot: %v", serr) + } + if ferr := engine.FreezeHeadSnapshotForTest(ctx, docID, snapID); ferr != nil { + t.Fatalf("freeze head snapshot: %v", ferr) + } + // 直写绕过了 docgov 引擎缓存;换新引擎使 approve 从 PG 装载快照。 + freshEngine := docgov.NewGovernanceEngine(pool) + if _, err := freshEngine.ReviewApprove(ctx, docID, reviewerID, "reviewer", "李", "ok"); err != nil { + t.Fatalf("review approve: %v", err) + } + _, err = freshEngine.ApproverApprove(ctx, docID, approverID, "approver", "王", "ok") if err != nil { t.Fatalf("approve doc: %v", err) } diff --git a/internal/projectinstance/stage_remediation_test.go b/internal/projectinstance/stage_remediation_test.go index c1a5d53b..431cbf41 100644 --- a/internal/projectinstance/stage_remediation_test.go +++ b/internal/projectinstance/stage_remediation_test.go @@ -69,7 +69,7 @@ func TestAdvanceStage_DoesNotChangeProjectOnNonLastStage(t *testing.T) { StageKey: "V0.5_feasibility", Enabled: true, DocTasks: []DocTaskPlan{{ TaskKey: "feasibility_reg", Enabled: true, - Duties: DocDuty{AuthorUserID: 1, ReviewerUserID: 2, ApproverUserID: 3}, + Duties: DocDuty{AuthorUserID: 1, ReviewerUserID: 2, ApproverUserID: 3}, }}, }) _, err := svc.SaveWorkflowPlan(ctx, 101, 1, true, SavePlanInput{ diff --git a/internal/projectinstance/terminate.go b/internal/projectinstance/terminate.go index 80db0d18..70c83cf8 100644 --- a/internal/projectinstance/terminate.go +++ b/internal/projectinstance/terminate.go @@ -20,8 +20,8 @@ type TerminateResult struct { // PendingDocSummary 终止预览中的待处理文档摘要。 type PendingDocSummary struct { - TaskKey string `json:"task_key"` - Status string `json:"status"` + TaskKey string `json:"task_key"` + Status string `json:"status"` StageKey string `json:"stage_key,omitempty"` } diff --git a/internal/projectinstance/testutil.go b/internal/projectinstance/testutil.go index 559d6e59..b37ae149 100644 --- a/internal/projectinstance/testutil.go +++ b/internal/projectinstance/testutil.go @@ -155,7 +155,7 @@ func validPlan() WorkflowPlan { StageKey: "V0.1_triage", Enabled: true, DocTasks: []DocTaskPlan{{ TaskKey: "intake", Enabled: true, - Duties: DocDuty{AuthorUserID: 1, ReviewerUserID: 2, ApproverUserID: 3}, + Duties: DocDuty{AuthorUserID: 1, ReviewerUserID: 2, ApproverUserID: 3}, }}, }}, } diff --git a/internal/server/chat_document_reader.go b/internal/server/chat_document_reader.go index eda843a9..4ac8420d 100644 --- a/internal/server/chat_document_reader.go +++ b/internal/server/chat_document_reader.go @@ -15,9 +15,9 @@ import ( "chat-agent-system/internal/docgov" "chat-agent-system/internal/intent" "chat-agent-system/internal/projectinstance" - "chat-agent-system/internal/textextract" "chat-agent-system/internal/storage" "chat-agent-system/internal/stream" + "chat-agent-system/internal/textextract" "chat-agent-system/internal/workflowscope" ) diff --git a/internal/server/e2e_workflow_governance_test.go b/internal/server/e2e_workflow_governance_test.go index 9dc522a5..de4b413b 100644 --- a/internal/server/e2e_workflow_governance_test.go +++ b/internal/server/e2e_workflow_governance_test.go @@ -2,6 +2,7 @@ package server_test import ( "bytes" + "chat-agent-system/internal/docrevision" "context" "encoding/json" "fmt" @@ -109,8 +110,19 @@ func TestE2E_WorkflowGovernance_CompleteAndReplan(t *testing.T) { t.Fatalf("create doc: %v", err) } _ = docEngine.MarkDocumentContentUpdated(ctx, docID, "hash_e2e_te", false) - _, _ = docEngine.SubmitForReview(ctx, docID, creatorID, "author", "Author") - _, _ = docEngine.ReviewApprove(ctx, docID, reviewerID, "reviewer", "Reviewer", "ok") + if _, err := docEngine.SubmitForReview(ctx, docID, creatorID, "author", "Author"); err != nil { + t.Fatalf("submit review: %v", err) + } + snapID, err := docrevision.SeedContentSnapshotForTest(ctx, pool, docID, creatorID, "# e2e governance") + if err != nil { + t.Fatalf("seed snapshot: %v", err) + } + if err := docEngine.FreezeHeadSnapshotForTest(ctx, docID, snapID); err != nil { + t.Fatalf("freeze head snapshot: %v", err) + } + if _, err := docEngine.ReviewApprove(ctx, docID, reviewerID, "reviewer", "Reviewer", "ok"); err != nil { + t.Fatalf("review approve: %v", err) + } _, err = docEngine.ApproverApprove(ctx, docID, approverID, "approver", "Approver", "ok") if err != nil { t.Fatalf("approve doc: %v", err) diff --git a/internal/server/handlers_context.go b/internal/server/handlers_context.go index d99f6dc2..7f07f402 100644 --- a/internal/server/handlers_context.go +++ b/internal/server/handlers_context.go @@ -48,7 +48,7 @@ func (s *Server) handleGetUserContext(w http.ResponseWriter, r *http.Request) { if err != nil || ownerID <= 0 || ownerID != userID { writeJSONResponse(w, http.StatusForbidden, map[string]string{ "error_code": "ERR_FORBIDDEN", - "message": "无权访问该会话上下文", + "message": "无权访问该会话上下文", }) return } diff --git a/internal/server/phase5_acceptance_pg_test.go b/internal/server/phase5_acceptance_pg_test.go index 86b8870b..bae5b058 100644 --- a/internal/server/phase5_acceptance_pg_test.go +++ b/internal/server/phase5_acceptance_pg_test.go @@ -58,7 +58,7 @@ func TestPhase5_AuthzNegativeMatrix_PG(t *testing.T) { creatorToken := loginTestUser(t, handler, "zhanggong", "123456") outsiderToken := loginTestUser(t, handler, "lizhuguan", "123456") - createBody, _ := json.Marshal(projectworkflow.CreateProjectInput{Name: "Phase5 Authz Matrix Test"}) + createBody, _ := json.Marshal(projectworkflow.CreateProjectInput{Name: fmt.Sprintf("Phase5 Authz Matrix Test %d", time.Now().UnixNano())}) reqCreate := httptest.NewRequest(http.MethodPost, "/api/v1/projects", bytes.NewReader(createBody)) withBearerAuth(reqCreate, creatorToken) recCreate := httptest.NewRecorder() diff --git a/internal/server/phase5_fault_injection_pg_test.go b/internal/server/phase5_fault_injection_pg_test.go index 196ace81..106037a0 100644 --- a/internal/server/phase5_fault_injection_pg_test.go +++ b/internal/server/phase5_fault_injection_pg_test.go @@ -2,6 +2,7 @@ package server_test import ( "bytes" + "chat-agent-system/internal/docrevision" "context" "encoding/json" "fmt" @@ -108,8 +109,19 @@ func setupPGProjectReadyToAdvance(t *testing.T, pool *pgxpool.Pool, handler http docEngine := bizdev.Snapshot(srv.BizdevService()).Gov _, _ = docEngine.CreateOrGetDocument(ctx, docID, projectID, "bd_intake", "Intake", "V0.1", "", creatorID, "A", reviewerID, "R", approverID, "P") _ = docEngine.MarkDocumentContentUpdated(ctx, docID, "hash_p5_fault", false) - _, _ = docEngine.SubmitForReview(ctx, docID, creatorID, "author", "Author") - _, _ = docEngine.ReviewApprove(ctx, docID, reviewerID, "reviewer", "Reviewer", "ok") + if _, serr := docEngine.SubmitForReview(ctx, docID, creatorID, "author", "Author"); serr != nil { + t.Fatalf("submit review: %v", serr) + } + snapID, serr := docrevision.SeedContentSnapshotForTest(ctx, pool, docID, creatorID, "# phase5") + if serr != nil { + t.Fatalf("seed snapshot: %v", serr) + } + if ferr := docEngine.FreezeHeadSnapshotForTest(ctx, docID, snapID); ferr != nil { + t.Fatalf("freeze head snapshot: %v", ferr) + } + if _, serr := docEngine.ReviewApprove(ctx, docID, reviewerID, "reviewer", "Reviewer", "ok"); serr != nil { + t.Fatalf("review approve: %v", serr) + } _, _ = docEngine.ApproverApprove(ctx, docID, approverID, "approver", "Approver", "ok") instSvc := projectinstance.NewService(projectinstance.NewStore(pool), nil, nil, nil) if _, err = instSvc.HandleDocumentApproved(ctx, projectID, "bd_intake"); err != nil { diff --git a/internal/server/project_create_pg_test.go b/internal/server/project_create_pg_test.go index 8d8f1204..055a39f8 100644 --- a/internal/server/project_create_pg_test.go +++ b/internal/server/project_create_pg_test.go @@ -3,9 +3,11 @@ package server_test import ( "bytes" "encoding/json" + "fmt" "net/http" "net/http/httptest" "testing" + "time" "unicode/utf8" "github.com/jackc/pgx/v5/pgxpool" @@ -24,7 +26,7 @@ func TestProjectCreate_PG_ChineseNameViaHTTP(t *testing.T) { token := loginTestUser(t, handler, "zhanggong", "123456") body, _ := json.Marshal(projectworkflow.CreateProjectInput{ - Name: "PG Phase5 治理链中文名称测试", + Name: fmt.Sprintf("PG Phase5 治理链中文名称测试 %d", time.Now().UnixNano()), DeviceType: "超声手术设备", IntendedUse: "软组织切除", TargetMarket: "NMPA 三类", diff --git a/internal/server/project_members_authz_test.go b/internal/server/project_members_authz_test.go index 687df461..493d4171 100644 --- a/internal/server/project_members_authz_test.go +++ b/internal/server/project_members_authz_test.go @@ -1,10 +1,10 @@ package server_test import ( + "chat-agent-system/internal/db" "context" "net/http" "net/http/httptest" - "os" "testing" "time" @@ -17,11 +17,7 @@ import ( func openPGPool(t *testing.T) *pgxpool.Pool { t.Helper() - dsn := os.Getenv("TEST_DATABASE_URL") - if dsn == "" { - t.Skip("TEST_DATABASE_URL must be set for PostgreSQL integration tests (refusing default dev database)") - return nil - } + dsn := db.SkipIfNoTestDatabaseURL(t) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() pool, err := pgxpool.New(ctx, dsn) diff --git a/internal/server/task_run_authz_pg_test.go b/internal/server/task_run_authz_pg_test.go index df4bf484..3b3d0571 100644 --- a/internal/server/task_run_authz_pg_test.go +++ b/internal/server/task_run_authz_pg_test.go @@ -25,7 +25,7 @@ func TestTaskRunAuthz_OutsiderForbidden_PG(t *testing.T) { creatorToken := loginTestUser(t, handler, "zhanggong", "123456") outsiderToken := loginTestUser(t, handler, "lizhuguan", "123456") - createBody, _ := json.Marshal(projectworkflow.CreateProjectInput{Name: "Task Run Authz Test"}) + createBody, _ := json.Marshal(projectworkflow.CreateProjectInput{Name: fmt.Sprintf("Task Run Authz Test %d", time.Now().UnixNano())}) reqCreate := httptest.NewRequest(http.MethodPost, "/api/v1/projects", bytes.NewReader(createBody)) withBearerAuth(reqCreate, creatorToken) recCreate := httptest.NewRecorder() diff --git a/internal/server/workflow_governance_pg_test.go b/internal/server/workflow_governance_pg_test.go index ed4ef34f..51913136 100644 --- a/internal/server/workflow_governance_pg_test.go +++ b/internal/server/workflow_governance_pg_test.go @@ -2,6 +2,7 @@ package server_test import ( "bytes" + "chat-agent-system/internal/docrevision" "context" "encoding/json" "fmt" @@ -226,6 +227,13 @@ func TestWorkflowGovernance_PG_ActivateApproveAdvanceChain(t *testing.T) { } _ = docEngine.MarkDocumentContentUpdated(ctx, docID, "hash_pg_p5", false) _, _ = docEngine.SubmitForReview(ctx, docID, creatorID, "author", "Author") + snapID, serr := docrevision.SeedContentSnapshotForTest(ctx, pool, docID, creatorID, "# governance pg") + if serr != nil { + t.Fatalf("seed snapshot: %v", serr) + } + if ferr := docEngine.FreezeHeadSnapshotForTest(ctx, docID, snapID); ferr != nil { + t.Fatalf("freeze head snapshot: %v", ferr) + } _, _ = docEngine.ReviewApprove(ctx, docID, reviewerID, "reviewer", "Reviewer", "ok") _, err = docEngine.ApproverApprove(ctx, docID, approverID, "approver", "Approver", "ok") if err != nil { diff --git a/internal/storage/cas.go b/internal/storage/cas.go index e9e6460c..62b0417a 100644 --- a/internal/storage/cas.go +++ b/internal/storage/cas.go @@ -30,12 +30,12 @@ type CAS struct { embedder Embedder // 离线/降级内存索引 (当数据库未连接时) - mu sync.RWMutex - memoryBlobs map[string]*BlobInfo - memoryRefs map[int64]*FileReference + mu sync.RWMutex + memoryBlobs map[string]*BlobInfo + memoryRefs map[int64]*FileReference memoryChunks []*MemoryChunk - nextRefID int64 - nextChunkID int64 + nextRefID int64 + nextChunkID int64 } // NewCAS 创建 CAS 存储管理器 diff --git a/internal/storage/cas_db_test.go b/internal/storage/cas_db_test.go index a3d2bd9b..186333b5 100644 --- a/internal/storage/cas_db_test.go +++ b/internal/storage/cas_db_test.go @@ -1,6 +1,7 @@ package storage import ( + "chat-agent-system/internal/db" "context" "os" "testing" @@ -11,12 +12,9 @@ import ( func TestCAS_DatabaseIntegration(t *testing.T) { // 隔离优先:TEST_DATABASE_URL 指向专用测试库;未提供时跳过,禁止默认连开发库。 - dsn := os.Getenv("TEST_DATABASE_URL") + dsn := db.SkipIfNoTestDatabaseURL(t) if dsn == "" { - dsn = os.Getenv("CONTEXT_DB_DSN") - } - if dsn == "" { - t.Skip("TEST_DATABASE_URL/CONTEXT_DB_DSN not set (refusing default dev database); skipping CAS DB integration test") + t.Skip("TEST_DATABASE_URL not set; skipping CAS DB integration test") return } @@ -30,6 +28,10 @@ func TestCAS_DatabaseIntegration(t *testing.T) { } defer pool.Close() + if err := db.RunMigrationsOnceForTest(ctx, pool, dsn); err != nil { + t.Fatalf("migration failed: %v", err) + } + if err := pool.Ping(ctx); err != nil { t.Skipf("context db ping failed (%v), skipping DB integration test", err) return diff --git a/internal/storage/types.go b/internal/storage/types.go index 1a8fa1e1..f2dce324 100644 --- a/internal/storage/types.go +++ b/internal/storage/types.go @@ -70,8 +70,8 @@ type MemorySearchOptions struct { // MemoryChunkHit 记忆检索命中结果 type MemoryChunkHit struct { - Chunk *MemoryChunk `json:"chunk"` - RawScore float64 `json:"raw_score"` - TimeDecay float64 `json:"time_decay"` - FinalScore float64 `json:"final_score"` + Chunk *MemoryChunk `json:"chunk"` + RawScore float64 `json:"raw_score"` + TimeDecay float64 `json:"time_decay"` + FinalScore float64 `json:"final_score"` } diff --git a/internal/taskrun/store_lease_pg_test.go b/internal/taskrun/store_lease_pg_test.go index 1e103297..55c5dffd 100644 --- a/internal/taskrun/store_lease_pg_test.go +++ b/internal/taskrun/store_lease_pg_test.go @@ -2,7 +2,6 @@ package taskrun_test import ( "context" - "os" "testing" "time" @@ -14,11 +13,7 @@ import ( func openLeaseTestPool(t *testing.T) *pgxpool.Pool { t.Helper() - dsn := os.Getenv("TEST_DATABASE_URL") - if dsn == "" { - t.Skip("TEST_DATABASE_URL must be set for PostgreSQL integration tests (refusing default dev database)") - return nil - } + dsn := db.SkipIfNoTestDatabaseURL(t) ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) defer cancel() pool, err := pgxpool.New(ctx, dsn) diff --git a/internal/workspace/registry_pg_test.go b/internal/workspace/registry_pg_test.go index 7ce94fe5..9cebc47c 100644 --- a/internal/workspace/registry_pg_test.go +++ b/internal/workspace/registry_pg_test.go @@ -1,9 +1,9 @@ package workspace import ( + "chat-agent-system/internal/db" "context" "fmt" - "os" "testing" "time" @@ -12,14 +12,11 @@ import ( ) func TestPGRegistryRoundTrip(t *testing.T) { - url := os.Getenv("TEST_DATABASE_URL") - if url == "" { - t.Skip("set TEST_DATABASE_URL to run the workspace registry postgres test") - } + dsn := db.SkipIfNoTestDatabaseURL(t) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() schema := fmt.Sprintf("agentx_reg_%d", time.Now().UnixNano()) - boot, err := pgxpool.New(ctx, url) + boot, err := pgxpool.New(ctx, dsn) if err != nil { t.Fatal(err) } @@ -32,7 +29,7 @@ func TestPGRegistryRoundTrip(t *testing.T) { defer ccancel() _, _ = boot.Exec(cctx, "DROP SCHEMA "+schema+" CASCADE") }) - cfg, err := pgxpool.ParseConfig(url) + cfg, err := pgxpool.ParseConfig(dsn) if err != nil { t.Fatal(err) }