fix(tests): per-package test databases + legitimate approval fixtures (issue #2 close)
Root causes of the remaining 5 CI PG failures (and the hidden flaky set):
1. All PG-integration packages shared ONE database under parallel package
execution - migrations/seeds/truncates raced (nondeterministic failure
sets, locally and in CI).
Fix: RequireTestDatabaseURL/SkipIfNoTestDatabaseURL now derive a
per-package database (<base>_<pkgHash>, advisory-lock-guarded
CREATE); 9 raw-env straggler tests funnelled through the helpers;
db-package tests call it unqualified (no import cycle).
2. Approval-chain fixtures were not production-shaped: freezeReviewSnapshot
only honors head_snapshot_id, fake content hashes never froze a
snapshot, so ApproverApprove rejected with 'approved requires frozen
review_snapshot_id'. Errors were swallowed (_, _ =) hiding this.
Fix (ownership-respecting, arch-enforced):
- docrevision.SeedContentSnapshotForTest seeds the ledger tables it owns
- docgov GovernanceEngine.FreezeHeadSnapshotForTest freezes head/review
pointers on document_records (docgov-owned) and clears engine cache
- all four integration fixtures use the pair; swallowed errors checked
3. Fixed-name fixtures collide once a package shares one DB (projects
uq_projects_name_global): uniquified in projectinstance + server tests.
4. CAS/ctxmgr tests never bootstrapped migrations (relied on other packages
having migrated the shared DB): explicit RunMigrationsOnceForTest.
Verified: full parallel short suite = 75 packages, 0 failures against an
isolated throwaway PG. CI skip list removed - short tests fully required.
This commit is contained in:
@@ -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: |
|
||||
|
||||
Executable
BIN
Binary file not shown.
@@ -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)
|
||||
|
||||
@@ -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 认证账户存储与会话存储组合。
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
|
||||
@@ -787,4 +787,3 @@ func enrichMessageExportCapability(r *ChatMessageRecord) {
|
||||
}
|
||||
r.ExportCapability = evalCap
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
+12
-12
@@ -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
|
||||
|
||||
@@ -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())
|
||||
|
||||
// 准备真实项目与用户
|
||||
|
||||
@@ -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 采集选项。
|
||||
|
||||
@@ -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{})
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+101
-1
@@ -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 集成测试必须显式提供隔离测试库,禁止默认连开发库。
|
||||
// 返回按调用方包派生的独立数据库(<base>_<pkgHash>):包内共享、包间隔离,
|
||||
// 消除 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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 同步文档任务状态并评估是否首次满足阶段推进条件。
|
||||
|
||||
@@ -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},
|
||||
}},
|
||||
}},
|
||||
}
|
||||
|
||||
@@ -187,4 +187,3 @@ func TestValidateAuthorOnlyDocTaskDuties(t *testing.T) {
|
||||
t.Fatalf("separated A/A/A: %+v", sep)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
|
||||
@@ -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},
|
||||
}},
|
||||
}},
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 三类",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 存储管理器
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Vendored
-1
@@ -1 +0,0 @@
|
||||
Keeps //go:embed all:dist satisfiable before first npm run build.
|
||||
Reference in New Issue
Block a user