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)
}
diff --git a/web/dist/.gitkeep b/web/dist/.gitkeep
deleted file mode 100644
index da53b4ed..00000000
--- a/web/dist/.gitkeep
+++ /dev/null
@@ -1 +0,0 @@
-Keeps //go:embed all:dist satisfiable before first npm run build.