Merge pull request 'issue#2 收官:按包测试库 + 生产同构审批夹具;短测全量必需门禁' (#18) from feature/issue2-pg-tests-perpackage-db-2026-09-29 into dev
This commit was merged in pull request #18.
This commit is contained in:
@@ -42,14 +42,10 @@ jobs:
|
|||||||
- name: arch-check (block cleared kinds)
|
- name: arch-check (block cleared kinds)
|
||||||
run: ./scripts/arch-check.sh --mode block
|
run: ./scripts/arch-check.sh --mode block
|
||||||
|
|
||||||
- name: go test (short, required since 2026-09-29)
|
- name: go test (short, required)
|
||||||
# skip 清单 = issue #2 存量 PG 集成失败(2026-09-29 已修 10/15,余 5 点名跟踪);
|
|
||||||
# 新增失败不会被跳过,直接阻断。
|
|
||||||
env:
|
env:
|
||||||
TEST_DATABASE_URL: postgres://app:ci_secret_db_pass@postgres:5432/agent_system_ci?sslmode=disable
|
TEST_DATABASE_URL: postgres://app:ci_secret_db_pass@postgres:5432/agent_system_ci?sslmode=disable
|
||||||
run: >
|
run: go test -short -timeout 20m ./...
|
||||||
go test -short -timeout 20m ./...
|
|
||||||
-skip 'TestCreateDocument_PG_ClientKeyIdempotentAndLedgerSurvivesNewService|TestPG_ActivationAndStageAdvance|TestE2E_WorkflowGovernance_CompleteAndReplan|TestPhase5_ConcurrentStageAdvance_PG|TestWorkflowGovernance_PG_ActivateApproveAdvanceChain'
|
|
||||||
|
|
||||||
- name: secrets scan (tracked files)
|
- name: secrets scan (tracked files)
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
Executable
BIN
Binary file not shown.
@@ -100,4 +100,4 @@ func TestLoginMissingPasswordHash(t *testing.T) {
|
|||||||
if err == nil || err.Error() != "用户名或密码错误" {
|
if err == nil || err.Error() != "用户名或密码错误" {
|
||||||
t.Fatalf("expected unified credential error, got %v", err)
|
t.Fatalf("expected unified credential error, got %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
package auth_test
|
package auth_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"chat-agent-system/internal/db"
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"os"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -15,11 +15,7 @@ import (
|
|||||||
|
|
||||||
func openTestPool(t *testing.T) *pgxpool.Pool {
|
func openTestPool(t *testing.T) *pgxpool.Pool {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
dsn := os.Getenv("TEST_DATABASE_URL")
|
dsn := db.SkipIfNoTestDatabaseURL(t)
|
||||||
if dsn == "" {
|
|
||||||
t.Skip("TEST_DATABASE_URL must be set for PostgreSQL integration tests (refusing default dev database)")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
pool, err := pgxpool.New(ctx, dsn)
|
pool, err := pgxpool.New(ctx, dsn)
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5/pgxpool"
|
|
||||||
"chat-agent-system/internal/config"
|
"chat-agent-system/internal/config"
|
||||||
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Stores 认证账户存储与会话存储组合。
|
// Stores 认证账户存储与会话存储组合。
|
||||||
|
|||||||
@@ -66,15 +66,15 @@ func (m *ContextManager) IndexApprovedDocument(ctx context.Context, doc *docgov.
|
|||||||
regs := extractRegulations(sec.Content)
|
regs := extractRegulations(sec.Content)
|
||||||
|
|
||||||
meta := map[string]any{
|
meta := map[string]any{
|
||||||
"task_key": doc.TaskKey,
|
"task_key": doc.TaskKey,
|
||||||
"artifact_title": doc.Title,
|
"artifact_title": doc.Title,
|
||||||
"section_title": sec.Title,
|
"section_title": sec.Title,
|
||||||
"regulations": regs,
|
"regulations": regs,
|
||||||
"author_name": doc.AuthorName,
|
"author_name": doc.AuthorName,
|
||||||
"reviewer_name": doc.ReviewerName,
|
"reviewer_name": doc.ReviewerName,
|
||||||
"approver_name": doc.ApproverName,
|
"approver_name": doc.ApproverName,
|
||||||
"stage": doc.Stage,
|
"stage": doc.Stage,
|
||||||
"approved_at": doc.ApprovedAt,
|
"approved_at": doc.ApprovedAt,
|
||||||
}
|
}
|
||||||
|
|
||||||
if m.pool != nil {
|
if m.pool != nil {
|
||||||
|
|||||||
@@ -17,10 +17,10 @@ func TestSweepExpiredProcessingTurns_Memory(t *testing.T) {
|
|||||||
future := time.Now().Add(5 * time.Minute)
|
future := time.Now().Add(5 * time.Minute)
|
||||||
mgr.mu.Lock()
|
mgr.mu.Lock()
|
||||||
mgr.turns[turnMemKey("sess-sweep", "turn-expired")] = &ChatTurnRecord{
|
mgr.turns[turnMemKey("sess-sweep", "turn-expired")] = &ChatTurnRecord{
|
||||||
SessionID: "sess-sweep",
|
SessionID: "sess-sweep",
|
||||||
ClientTurnID: "turn-expired",
|
ClientTurnID: "turn-expired",
|
||||||
Status: TurnStatusProcessing,
|
Status: TurnStatusProcessing,
|
||||||
LeaseExpiresAt: &past,
|
LeaseExpiresAt: &past,
|
||||||
}
|
}
|
||||||
mgr.turns[turnMemKey("sess-sweep", "turn-fresh")] = &ChatTurnRecord{
|
mgr.turns[turnMemKey("sess-sweep", "turn-fresh")] = &ChatTurnRecord{
|
||||||
SessionID: "sess-sweep",
|
SessionID: "sess-sweep",
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ import (
|
|||||||
type ChatReadPosition struct {
|
type ChatReadPosition struct {
|
||||||
SessionID string `json:"session_id"`
|
SessionID string `json:"session_id"`
|
||||||
UserID int64 `json:"user_id"`
|
UserID int64 `json:"user_id"`
|
||||||
LastReadMessageID int64 `json:"last_read_message_id"` // 连续已读基线
|
LastReadMessageID int64 `json:"last_read_message_id"` // 连续已读基线
|
||||||
DismissedUnreadIDs []int64 `json:"dismissed_unread_ids"` // 离散跳读/倒序消费的消息 ID
|
DismissedUnreadIDs []int64 `json:"dismissed_unread_ids"` // 离散跳读/倒序消费的消息 ID
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -787,4 +787,3 @@ func enrichMessageExportCapability(r *ChatMessageRecord) {
|
|||||||
}
|
}
|
||||||
r.ExportCapability = evalCap
|
r.ExportCapability = evalCap
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package ctxmgr_test
|
package ctxmgr_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"chat-agent-system/internal/db"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
@@ -241,11 +242,7 @@ func TestChatWindow_MemoryEquivalenceAndPagination(t *testing.T) {
|
|||||||
func TestChatWindow_DatabaseIntegration(t *testing.T) {
|
func TestChatWindow_DatabaseIntegration(t *testing.T) {
|
||||||
dsn := os.Getenv("CONTEXT_DB_DSN")
|
dsn := os.Getenv("CONTEXT_DB_DSN")
|
||||||
if dsn == "" {
|
if dsn == "" {
|
||||||
dsn = os.Getenv("TEST_DATABASE_URL")
|
dsn = db.SkipIfNoTestDatabaseURL(t)
|
||||||
}
|
|
||||||
if dsn == "" {
|
|
||||||
t.Skip("CONTEXT_DB_DSN or TEST_DATABASE_URL must be set (refusing default dev database)")
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
@@ -262,6 +259,10 @@ func TestChatWindow_DatabaseIntegration(t *testing.T) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := db.RunMigrationsOnceForTest(ctx, pool, dsn); err != nil {
|
||||||
|
t.Fatalf("migration failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
mgr := ctxmgr.NewContextManager(pool, nil, ctxmgr.DefaultTimeDecayConfig())
|
mgr := ctxmgr.NewContextManager(pool, nil, ctxmgr.DefaultTimeDecayConfig())
|
||||||
sessID := fmt.Sprintf("sess_db_window_%d", time.Now().UnixNano())
|
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"
|
sessID := "sess_mem_cat_test"
|
||||||
|
|
||||||
// 注入不同分类的消息
|
// 注入不同分类的消息
|
||||||
_, _ = mgr.RecordUserMessageWithID(ctx, sessID, 1, "user", "阶段推进", "ADVANCE_STAGE", "", nil) // task
|
_, _ = 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", "启动任务", "TRIGGER_TASK", "", nil) // task
|
||||||
_, _ = mgr.RecordUserMessageWithID(ctx, sessID, 1, "user", "合规咨询", "QUERY_REGULATIONS", "", nil) // consult
|
_, _ = mgr.RecordUserMessageWithID(ctx, sessID, 1, "user", "合规咨询", "QUERY_REGULATIONS", "", nil) // consult
|
||||||
_, _ = mgr.RecordUserMessageWithID(ctx, sessID, 1, "user", "立项里程碑", "", "", map[string]any{"milestone": true}) // milestone
|
_, _ = mgr.RecordUserMessageWithID(ctx, sessID, 1, "user", "立项里程碑", "", "", map[string]any{"milestone": true}) // milestone
|
||||||
|
|
||||||
stats, err := mgr.ListChatCategories(ctx, sessID)
|
stats, err := mgr.ListChatCategories(ctx, sessID)
|
||||||
|
|||||||
+12
-12
@@ -1,11 +1,11 @@
|
|||||||
package ctxmgr
|
package ctxmgr
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log/slog"
|
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -23,17 +23,17 @@ type ContextManager struct {
|
|||||||
embedder VectorEmbedder
|
embedder VectorEmbedder
|
||||||
|
|
||||||
// 离线降级内存缓存
|
// 离线降级内存缓存
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
users map[int64]*UserPreference
|
users map[int64]*UserPreference
|
||||||
projects map[int64]*ProjectCollectiveContext
|
projects map[int64]*ProjectCollectiveContext
|
||||||
messages map[string][]ChatMessageRecord // session_id -> messages
|
messages map[string][]ChatMessageRecord // session_id -> messages
|
||||||
sessions map[string]*ChatSessionSummary // session_id -> summary
|
sessions map[string]*ChatSessionSummary // session_id -> summary
|
||||||
activities map[int64][]ActivityItem // project_id -> activities
|
activities map[int64][]ActivityItem // project_id -> activities
|
||||||
memoryChunks []MemoryChunkItem // 语义记忆切片缓存
|
memoryChunks []MemoryChunkItem // 语义记忆切片缓存
|
||||||
summaries map[string]*ChatSummary // session_id -> rolling summary
|
summaries map[string]*ChatSummary // session_id -> rolling summary
|
||||||
digests map[string]map[string]*DailyDigest // session_id -> day_key -> DailyDigest
|
digests map[string]map[string]*DailyDigest // session_id -> day_key -> DailyDigest
|
||||||
readPositions map[string]*ChatReadPosition // session_id:user_id -> read position
|
readPositions map[string]*ChatReadPosition // session_id:user_id -> read position
|
||||||
turns map[string]*ChatTurnRecord // sessionID|clientTurnID -> 幂等回合(仅内存降级)
|
turns map[string]*ChatTurnRecord // sessionID|clientTurnID -> 幂等回合(仅内存降级)
|
||||||
cache *MultiLevelCache // 多级统一缓存系统 (C1-C5)
|
cache *MultiLevelCache // 多级统一缓存系统 (C1-C5)
|
||||||
semanticSummarizer SemanticSummarizer // no-tool summarizer; nil → heuristic default
|
semanticSummarizer SemanticSummarizer // no-tool summarizer; nil → heuristic default
|
||||||
shadowJobs []SemanticShadowJob // in-process durable fallback queue
|
shadowJobs []SemanticShadowJob // in-process durable fallback queue
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package ctxmgr
|
package ctxmgr
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"chat-agent-system/internal/db"
|
||||||
"context"
|
"context"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -130,11 +131,7 @@ func TestContextManager_TimeDecayActivities(t *testing.T) {
|
|||||||
func TestContextManager_DatabaseIntegration(t *testing.T) {
|
func TestContextManager_DatabaseIntegration(t *testing.T) {
|
||||||
dsn := os.Getenv("CONTEXT_DB_DSN")
|
dsn := os.Getenv("CONTEXT_DB_DSN")
|
||||||
if dsn == "" {
|
if dsn == "" {
|
||||||
dsn = os.Getenv("TEST_DATABASE_URL")
|
dsn = db.SkipIfNoTestDatabaseURL(t)
|
||||||
}
|
|
||||||
if dsn == "" {
|
|
||||||
t.Skip("CONTEXT_DB_DSN or TEST_DATABASE_URL must be set (refusing default dev database)")
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||||
@@ -152,6 +149,10 @@ func TestContextManager_DatabaseIntegration(t *testing.T) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := db.RunMigrationsOnceForTest(ctx, pool, dsn); err != nil {
|
||||||
|
t.Fatalf("migration failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
mgr := NewContextManager(pool, nil, DefaultTimeDecayConfig())
|
mgr := NewContextManager(pool, nil, DefaultTimeDecayConfig())
|
||||||
|
|
||||||
// 准备真实项目与用户
|
// 准备真实项目与用户
|
||||||
|
|||||||
@@ -13,11 +13,11 @@ const defaultProjectDialogueLimit = 120
|
|||||||
|
|
||||||
// ProjectDialogueCollectResult 项目级对话采集结果(供上下文快照使用)。
|
// ProjectDialogueCollectResult 项目级对话采集结果(供上下文快照使用)。
|
||||||
type ProjectDialogueCollectResult struct {
|
type ProjectDialogueCollectResult struct {
|
||||||
Lines []string
|
Lines []string
|
||||||
ExcludedPrivate int
|
ExcludedPrivate int
|
||||||
ExcludedNoAccess int
|
ExcludedNoAccess int
|
||||||
AllowedStageKey string
|
AllowedStageKey string
|
||||||
StageFilteredCount int
|
StageFilteredCount int
|
||||||
}
|
}
|
||||||
|
|
||||||
// CollectProjectDialogueOptions 采集选项。
|
// 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", "[private] 不应纳入", "", "", meta)
|
||||||
_, _ = mgr.RecordUserMessageWithID(ctx, "sess-501", 1, "user", "项目需要支持多用户协作", "", "", meta)
|
_, _ = mgr.RecordUserMessageWithID(ctx, "sess-501", 1, "user", "项目需要支持多用户协作", "", "", meta)
|
||||||
_, _ = mgr.RecordUserMessageWithID(ctx, "sess-501", 1, "assistant", "好的,我会整理需求要点。", "", "", map[string]any{
|
_, _ = mgr.RecordUserMessageWithID(ctx, "sess-501", 1, "assistant", "好的,我会整理需求要点。", "", "", map[string]any{
|
||||||
"project_id": projectID,
|
"project_id": projectID,
|
||||||
"export_policy": string(replyexport.PolicyFull),
|
"export_policy": string(replyexport.PolicyFull),
|
||||||
"content_kind": string(replyexport.KindDocumentText),
|
"content_kind": string(replyexport.KindDocumentText),
|
||||||
})
|
})
|
||||||
|
|
||||||
res, err := mgr.CollectProjectDialogue(ctx, projectID, CollectProjectDialogueOptions{})
|
res, err := mgr.CollectProjectDialogue(ctx, projectID, CollectProjectDialogueOptions{})
|
||||||
|
|||||||
@@ -62,15 +62,15 @@ type SemanticSummaryResult struct {
|
|||||||
// gray rollout a replaced report keeps the displaced deterministic narrative
|
// gray rollout a replaced report keeps the displaced deterministic narrative
|
||||||
// so later deterministic regeneration merges clean material (rollback-safe).
|
// so later deterministic regeneration merges clean material (rollback-safe).
|
||||||
type SemanticShadowReport struct {
|
type SemanticShadowReport struct {
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Narrative string `json:"narrative,omitempty"`
|
Narrative string `json:"narrative,omitempty"`
|
||||||
DisplacedNarrative string `json:"displaced_narrative,omitempty"`
|
DisplacedNarrative string `json:"displaced_narrative,omitempty"`
|
||||||
InputDigest string `json:"input_digest,omitempty"`
|
InputDigest string `json:"input_digest,omitempty"`
|
||||||
Model string `json:"model,omitempty"`
|
Model string `json:"model,omitempty"`
|
||||||
Protocol string `json:"protocol,omitempty"`
|
Protocol string `json:"protocol,omitempty"`
|
||||||
ToolPermissions string `json:"tool_permissions"`
|
ToolPermissions string `json:"tool_permissions"`
|
||||||
FreshnessNote string `json:"freshness_note,omitempty"`
|
FreshnessNote string `json:"freshness_note,omitempty"`
|
||||||
GeneratedAt time.Time `json:"generated_at"`
|
GeneratedAt time.Time `json:"generated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// SemanticSummarizer produces a narrative from minimized messages.
|
// SemanticSummarizer produces a narrative from minimized messages.
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"strings"
|
"strings"
|
||||||
"unicode/utf8"
|
|
||||||
"time"
|
"time"
|
||||||
|
"unicode/utf8"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
@@ -26,8 +26,8 @@ const (
|
|||||||
// SummaryFeatureFlags controls semantic summary rollout. REPLACE swaps the
|
// SummaryFeatureFlags controls semantic summary rollout. REPLACE swaps the
|
||||||
// served chat Narrative only for allowlisted sessions (fail-closed gray).
|
// served chat Narrative only for allowlisted sessions (fail-closed gray).
|
||||||
type SummaryFeatureFlags struct {
|
type SummaryFeatureFlags struct {
|
||||||
Shadow bool
|
Shadow bool
|
||||||
Replace bool
|
Replace bool
|
||||||
// ReplaceAllSessions is the "*" wildcard for ReplaceSessions: every
|
// ReplaceAllSessions is the "*" wildcard for ReplaceSessions: every
|
||||||
// session (present and future) is eligible while the master Replace flag
|
// session (present and future) is eligible while the master Replace flag
|
||||||
// stays on. An empty allowlist still replaces nothing.
|
// 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")
|
t.Fatal("expected at least one migration file")
|
||||||
}
|
}
|
||||||
|
|
||||||
dsn := os.Getenv("TEST_DATABASE_URL")
|
dsn := SkipIfNoTestDatabaseURL(t)
|
||||||
if dsn == "" {
|
|
||||||
t.Skip("TEST_DATABASE_URL not set; migration gate file count only")
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
@@ -280,10 +277,7 @@ func rewriteDatabaseName(dsn, name string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestRunMigrationsEmbeddedFallback(t *testing.T) {
|
func TestRunMigrationsEmbeddedFallback(t *testing.T) {
|
||||||
dsn := os.Getenv("TEST_DATABASE_URL")
|
dsn := SkipIfNoTestDatabaseURL(t)
|
||||||
if dsn == "" {
|
|
||||||
t.Skip("TEST_DATABASE_URL not set; embedded fallback needs PG")
|
|
||||||
}
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
pool, err := pgxpool.New(ctx, dsn)
|
pool, err := pgxpool.New(ctx, dsn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
+101
-1
@@ -2,7 +2,14 @@ package db
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/sha256"
|
||||||
|
"database/sql"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -31,25 +38,118 @@ func RunMigrationsOnceForTest(ctx context.Context, pool *pgxpool.Pool, dsn strin
|
|||||||
const testDatabaseEnv = "TEST_DATABASE_URL"
|
const testDatabaseEnv = "TEST_DATABASE_URL"
|
||||||
|
|
||||||
// RequireTestDatabaseURL 集成测试必须显式提供隔离测试库,禁止默认连开发库。
|
// RequireTestDatabaseURL 集成测试必须显式提供隔离测试库,禁止默认连开发库。
|
||||||
|
// 返回按调用方包派生的独立数据库(<base>_<pkgHash>):包内共享、包间隔离,
|
||||||
|
// 消除 go test 并行包对同一库的迁移/种子互相干扰(2026-09-29 issue #2)。
|
||||||
func RequireTestDatabaseURL(t *testing.T) string {
|
func RequireTestDatabaseURL(t *testing.T) string {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
dsn := strings.TrimSpace(os.Getenv(testDatabaseEnv))
|
dsn := strings.TrimSpace(os.Getenv(testDatabaseEnv))
|
||||||
if dsn == "" {
|
if dsn == "" {
|
||||||
t.Fatalf("%s must be set for PostgreSQL integration tests (refusing default dev database)", testDatabaseEnv)
|
t.Fatalf("%s must be set for PostgreSQL integration tests (refusing default dev database)", testDatabaseEnv)
|
||||||
}
|
}
|
||||||
return dsn
|
return derivedPackageDSN(t, dsn)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SkipIfNoTestDatabaseURL 无隔离测试库时跳过(单元测试包内可选 PG 用例)。
|
// SkipIfNoTestDatabaseURL 无隔离测试库时跳过(单元测试包内可选 PG 用例)。
|
||||||
|
// 同样返回按包派生库。
|
||||||
func SkipIfNoTestDatabaseURL(t *testing.T) string {
|
func SkipIfNoTestDatabaseURL(t *testing.T) string {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
dsn := strings.TrimSpace(os.Getenv(testDatabaseEnv))
|
dsn := strings.TrimSpace(os.Getenv(testDatabaseEnv))
|
||||||
if dsn == "" {
|
if dsn == "" {
|
||||||
t.Skipf("%s not set; skipping PostgreSQL integration test", testDatabaseEnv)
|
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
|
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.
|
// RefuseSharedEnvDSN fails if the DSN targets a shared env database.
|
||||||
func RefuseSharedEnvDSN(t *testing.T, dsn string) {
|
func RefuseSharedEnvDSN(t *testing.T, dsn string) {
|
||||||
t.Helper()
|
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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"os"
|
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -16,11 +15,7 @@ import (
|
|||||||
func openPool(t *testing.T) *pgxpool.Pool {
|
func openPool(t *testing.T) *pgxpool.Pool {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
// 隔离约定:仅提供专用测试库时运行,禁止默认回退到开发库。
|
// 隔离约定:仅提供专用测试库时运行,禁止默认回退到开发库。
|
||||||
if os.Getenv("TEST_DATABASE_URL") == "" {
|
dsn := db.SkipIfNoTestDatabaseURL(t)
|
||||||
t.Skip("TEST_DATABASE_URL not set; skipping notification PG test")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
dsn := os.Getenv("TEST_DATABASE_URL")
|
|
||||||
if strings.Contains(dsn, "agent_system_dev") || strings.Contains(dsn, "agent_system_test") || strings.Contains(dsn, "agent_system_main") {
|
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")
|
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")
|
code := "pg-access-live-" + time.Now().Format("150405.000")
|
||||||
err = pool.QueryRow(ctx, `
|
err = pool.QueryRow(ctx, `
|
||||||
INSERT INTO projects (code, name, status, creator_user_id, lifecycle_status, active_workflow_key, workflow_selection_version, workflow_plan_revision)
|
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)
|
`, code, creatorID).Scan(&projectID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("insert project: %v", err)
|
t.Fatalf("insert project: %v", err)
|
||||||
@@ -128,7 +128,7 @@ func TestPG_GoverningAccess_ActiveCreatorDutyOutsider(t *testing.T) {
|
|||||||
t.Fatalf("preview: %v", err)
|
t.Fatalf("preview: %v", err)
|
||||||
}
|
}
|
||||||
if _, err := svc.Activate(ctx, projectID, creatorID, true, projectinstance.ActivateInput{
|
if _, err := svc.Activate(ctx, projectID, creatorID, true, projectinstance.ActivateInput{
|
||||||
ConfirmationToken: preview.ConfirmationToken,
|
ConfirmationToken: preview.ConfirmationToken,
|
||||||
ExpectedProjectVersion: preview.ExpectedProjectVersion,
|
ExpectedProjectVersion: preview.ExpectedProjectVersion,
|
||||||
ExpectedPlanRevision: preview.ExpectedPlanRevision,
|
ExpectedPlanRevision: preview.ExpectedPlanRevision,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ func TestPG_ActivateOfficialWorkflowRuntimeMetadata(t *testing.T) {
|
|||||||
t.Fatalf("load main.v2 manifest: %v", err)
|
t.Fatalf("load main.v2 manifest: %v", err)
|
||||||
}
|
}
|
||||||
if err := wfStore.UpsertPublishedSeed(ctx, workflowdef.CreateDefinitionInput{
|
if err := wfStore.UpsertPublishedSeed(ctx, workflowdef.CreateDefinitionInput{
|
||||||
Key: workflowdef.OfficialKeyMain,
|
Key: workflowdef.OfficialKeyMain,
|
||||||
Name: "商务对接流程",
|
Name: "商务对接流程",
|
||||||
Definition: def,
|
Definition: def,
|
||||||
}, 1); err != nil {
|
}, 1); err != nil {
|
||||||
t.Fatalf("seed official main workflow: %v", err)
|
t.Fatalf("seed official main workflow: %v", err)
|
||||||
|
|||||||
@@ -4,14 +4,14 @@ import "context"
|
|||||||
|
|
||||||
// DocumentApprovedResult 文档批准后阶段就绪评估结果。
|
// DocumentApprovedResult 文档批准后阶段就绪评估结果。
|
||||||
type DocumentApprovedResult struct {
|
type DocumentApprovedResult struct {
|
||||||
StageReady bool
|
StageReady bool
|
||||||
InstanceID int64
|
InstanceID int64
|
||||||
StageInstanceID int64
|
StageInstanceID int64
|
||||||
StageKey string
|
StageKey string
|
||||||
StageName string
|
StageName string
|
||||||
ConfigRevision int
|
ConfigRevision int
|
||||||
RequiredDocCount int
|
RequiredDocCount int
|
||||||
ApprovedDocCount int
|
ApprovedDocCount int
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleDocumentApproved 同步文档任务状态并评估是否首次满足阶段推进条件。
|
// HandleDocumentApproved 同步文档任务状态并评估是否首次满足阶段推进条件。
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ func TestHandleDocumentApproved_StageReady(t *testing.T) {
|
|||||||
StageKey: "V0.1_triage", Enabled: true,
|
StageKey: "V0.1_triage", Enabled: true,
|
||||||
DocTasks: []DocTaskPlan{{
|
DocTasks: []DocTaskPlan{{
|
||||||
TaskKey: "intake", Enabled: true,
|
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)
|
t.Fatalf("separated A/A/A: %+v", sep)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
|
|
||||||
"chat-agent-system/internal/db"
|
"chat-agent-system/internal/db"
|
||||||
"chat-agent-system/internal/docgov"
|
"chat-agent-system/internal/docgov"
|
||||||
|
"chat-agent-system/internal/docrevision"
|
||||||
"chat-agent-system/internal/notification"
|
"chat-agent-system/internal/notification"
|
||||||
"chat-agent-system/internal/projectinstance"
|
"chat-agent-system/internal/projectinstance"
|
||||||
"chat-agent-system/internal/projectworkflow"
|
"chat-agent-system/internal/projectworkflow"
|
||||||
@@ -151,9 +152,22 @@ func TestPG_ActivationAndStageAdvance(t *testing.T) {
|
|||||||
t.Fatalf("create doc: %v", err)
|
t.Fatalf("create doc: %v", err)
|
||||||
}
|
}
|
||||||
_ = engine.MarkDocumentContentUpdated(ctx, docID, "hash_pg_p3", false)
|
_ = engine.MarkDocumentContentUpdated(ctx, docID, "hash_pg_p3", false)
|
||||||
_, _ = engine.SubmitForReview(ctx, docID, authorID, "author", "张")
|
if _, err := engine.SubmitForReview(ctx, docID, authorID, "author", "张"); err != nil {
|
||||||
_, _ = engine.ReviewApprove(ctx, docID, reviewerID, "reviewer", "李", "ok")
|
t.Fatalf("submit for review: %v", err)
|
||||||
_, err = engine.ApproverApprove(ctx, docID, approverID, "approver", "王", "ok")
|
}
|
||||||
|
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 {
|
if err != nil {
|
||||||
t.Fatalf("approve doc: %v", err)
|
t.Fatalf("approve doc: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ func TestAdvanceStage_DoesNotChangeProjectOnNonLastStage(t *testing.T) {
|
|||||||
StageKey: "V0.5_feasibility", Enabled: true,
|
StageKey: "V0.5_feasibility", Enabled: true,
|
||||||
DocTasks: []DocTaskPlan{{
|
DocTasks: []DocTaskPlan{{
|
||||||
TaskKey: "feasibility_reg", Enabled: true,
|
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{
|
_, err := svc.SaveWorkflowPlan(ctx, 101, 1, true, SavePlanInput{
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ type TerminateResult struct {
|
|||||||
|
|
||||||
// PendingDocSummary 终止预览中的待处理文档摘要。
|
// PendingDocSummary 终止预览中的待处理文档摘要。
|
||||||
type PendingDocSummary struct {
|
type PendingDocSummary struct {
|
||||||
TaskKey string `json:"task_key"`
|
TaskKey string `json:"task_key"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
StageKey string `json:"stage_key,omitempty"`
|
StageKey string `json:"stage_key,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ func validPlan() WorkflowPlan {
|
|||||||
StageKey: "V0.1_triage", Enabled: true,
|
StageKey: "V0.1_triage", Enabled: true,
|
||||||
DocTasks: []DocTaskPlan{{
|
DocTasks: []DocTaskPlan{{
|
||||||
TaskKey: "intake", Enabled: true,
|
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/docgov"
|
||||||
"chat-agent-system/internal/intent"
|
"chat-agent-system/internal/intent"
|
||||||
"chat-agent-system/internal/projectinstance"
|
"chat-agent-system/internal/projectinstance"
|
||||||
"chat-agent-system/internal/textextract"
|
|
||||||
"chat-agent-system/internal/storage"
|
"chat-agent-system/internal/storage"
|
||||||
"chat-agent-system/internal/stream"
|
"chat-agent-system/internal/stream"
|
||||||
|
"chat-agent-system/internal/textextract"
|
||||||
"chat-agent-system/internal/workflowscope"
|
"chat-agent-system/internal/workflowscope"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package server_test
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"chat-agent-system/internal/docrevision"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -109,8 +110,19 @@ func TestE2E_WorkflowGovernance_CompleteAndReplan(t *testing.T) {
|
|||||||
t.Fatalf("create doc: %v", err)
|
t.Fatalf("create doc: %v", err)
|
||||||
}
|
}
|
||||||
_ = docEngine.MarkDocumentContentUpdated(ctx, docID, "hash_e2e_te", false)
|
_ = docEngine.MarkDocumentContentUpdated(ctx, docID, "hash_e2e_te", false)
|
||||||
_, _ = docEngine.SubmitForReview(ctx, docID, creatorID, "author", "Author")
|
if _, err := docEngine.SubmitForReview(ctx, docID, creatorID, "author", "Author"); err != nil {
|
||||||
_, _ = docEngine.ReviewApprove(ctx, docID, reviewerID, "reviewer", "Reviewer", "ok")
|
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")
|
_, err = docEngine.ApproverApprove(ctx, docID, approverID, "approver", "Approver", "ok")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("approve doc: %v", err)
|
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 {
|
if err != nil || ownerID <= 0 || ownerID != userID {
|
||||||
writeJSONResponse(w, http.StatusForbidden, map[string]string{
|
writeJSONResponse(w, http.StatusForbidden, map[string]string{
|
||||||
"error_code": "ERR_FORBIDDEN",
|
"error_code": "ERR_FORBIDDEN",
|
||||||
"message": "无权访问该会话上下文",
|
"message": "无权访问该会话上下文",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ func TestPhase5_AuthzNegativeMatrix_PG(t *testing.T) {
|
|||||||
creatorToken := loginTestUser(t, handler, "zhanggong", "123456")
|
creatorToken := loginTestUser(t, handler, "zhanggong", "123456")
|
||||||
outsiderToken := loginTestUser(t, handler, "lizhuguan", "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))
|
reqCreate := httptest.NewRequest(http.MethodPost, "/api/v1/projects", bytes.NewReader(createBody))
|
||||||
withBearerAuth(reqCreate, creatorToken)
|
withBearerAuth(reqCreate, creatorToken)
|
||||||
recCreate := httptest.NewRecorder()
|
recCreate := httptest.NewRecorder()
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package server_test
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"chat-agent-system/internal/docrevision"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -108,8 +109,19 @@ func setupPGProjectReadyToAdvance(t *testing.T, pool *pgxpool.Pool, handler http
|
|||||||
docEngine := bizdev.Snapshot(srv.BizdevService()).Gov
|
docEngine := bizdev.Snapshot(srv.BizdevService()).Gov
|
||||||
_, _ = docEngine.CreateOrGetDocument(ctx, docID, projectID, "bd_intake", "Intake", "V0.1", "", creatorID, "A", reviewerID, "R", approverID, "P")
|
_, _ = 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.MarkDocumentContentUpdated(ctx, docID, "hash_p5_fault", false)
|
||||||
_, _ = docEngine.SubmitForReview(ctx, docID, creatorID, "author", "Author")
|
if _, serr := docEngine.SubmitForReview(ctx, docID, creatorID, "author", "Author"); serr != nil {
|
||||||
_, _ = docEngine.ReviewApprove(ctx, docID, reviewerID, "reviewer", "Reviewer", "ok")
|
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")
|
_, _ = docEngine.ApproverApprove(ctx, docID, approverID, "approver", "Approver", "ok")
|
||||||
instSvc := projectinstance.NewService(projectinstance.NewStore(pool), nil, nil, nil)
|
instSvc := projectinstance.NewService(projectinstance.NewStore(pool), nil, nil, nil)
|
||||||
if _, err = instSvc.HandleDocumentApproved(ctx, projectID, "bd_intake"); err != nil {
|
if _, err = instSvc.HandleDocumentApproved(ctx, projectID, "bd_intake"); err != nil {
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ package server_test
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5/pgxpool"
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
@@ -24,7 +26,7 @@ func TestProjectCreate_PG_ChineseNameViaHTTP(t *testing.T) {
|
|||||||
token := loginTestUser(t, handler, "zhanggong", "123456")
|
token := loginTestUser(t, handler, "zhanggong", "123456")
|
||||||
|
|
||||||
body, _ := json.Marshal(projectworkflow.CreateProjectInput{
|
body, _ := json.Marshal(projectworkflow.CreateProjectInput{
|
||||||
Name: "PG Phase5 治理链中文名称测试",
|
Name: fmt.Sprintf("PG Phase5 治理链中文名称测试 %d", time.Now().UnixNano()),
|
||||||
DeviceType: "超声手术设备",
|
DeviceType: "超声手术设备",
|
||||||
IntendedUse: "软组织切除",
|
IntendedUse: "软组织切除",
|
||||||
TargetMarket: "NMPA 三类",
|
TargetMarket: "NMPA 三类",
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
package server_test
|
package server_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"chat-agent-system/internal/db"
|
||||||
"context"
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"os"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -17,11 +17,7 @@ import (
|
|||||||
|
|
||||||
func openPGPool(t *testing.T) *pgxpool.Pool {
|
func openPGPool(t *testing.T) *pgxpool.Pool {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
dsn := os.Getenv("TEST_DATABASE_URL")
|
dsn := db.SkipIfNoTestDatabaseURL(t)
|
||||||
if dsn == "" {
|
|
||||||
t.Skip("TEST_DATABASE_URL must be set for PostgreSQL integration tests (refusing default dev database)")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
pool, err := pgxpool.New(ctx, dsn)
|
pool, err := pgxpool.New(ctx, dsn)
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ func TestTaskRunAuthz_OutsiderForbidden_PG(t *testing.T) {
|
|||||||
creatorToken := loginTestUser(t, handler, "zhanggong", "123456")
|
creatorToken := loginTestUser(t, handler, "zhanggong", "123456")
|
||||||
outsiderToken := loginTestUser(t, handler, "lizhuguan", "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))
|
reqCreate := httptest.NewRequest(http.MethodPost, "/api/v1/projects", bytes.NewReader(createBody))
|
||||||
withBearerAuth(reqCreate, creatorToken)
|
withBearerAuth(reqCreate, creatorToken)
|
||||||
recCreate := httptest.NewRecorder()
|
recCreate := httptest.NewRecorder()
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package server_test
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"chat-agent-system/internal/docrevision"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -226,6 +227,13 @@ func TestWorkflowGovernance_PG_ActivateApproveAdvanceChain(t *testing.T) {
|
|||||||
}
|
}
|
||||||
_ = docEngine.MarkDocumentContentUpdated(ctx, docID, "hash_pg_p5", false)
|
_ = docEngine.MarkDocumentContentUpdated(ctx, docID, "hash_pg_p5", false)
|
||||||
_, _ = docEngine.SubmitForReview(ctx, docID, creatorID, "author", "Author")
|
_, _ = 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")
|
_, _ = docEngine.ReviewApprove(ctx, docID, reviewerID, "reviewer", "Reviewer", "ok")
|
||||||
_, err = docEngine.ApproverApprove(ctx, docID, approverID, "approver", "Approver", "ok")
|
_, err = docEngine.ApproverApprove(ctx, docID, approverID, "approver", "Approver", "ok")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -30,12 +30,12 @@ type CAS struct {
|
|||||||
embedder Embedder
|
embedder Embedder
|
||||||
|
|
||||||
// 离线/降级内存索引 (当数据库未连接时)
|
// 离线/降级内存索引 (当数据库未连接时)
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
memoryBlobs map[string]*BlobInfo
|
memoryBlobs map[string]*BlobInfo
|
||||||
memoryRefs map[int64]*FileReference
|
memoryRefs map[int64]*FileReference
|
||||||
memoryChunks []*MemoryChunk
|
memoryChunks []*MemoryChunk
|
||||||
nextRefID int64
|
nextRefID int64
|
||||||
nextChunkID int64
|
nextChunkID int64
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewCAS 创建 CAS 存储管理器
|
// NewCAS 创建 CAS 存储管理器
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package storage
|
package storage
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"chat-agent-system/internal/db"
|
||||||
"context"
|
"context"
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -11,12 +12,9 @@ import (
|
|||||||
|
|
||||||
func TestCAS_DatabaseIntegration(t *testing.T) {
|
func TestCAS_DatabaseIntegration(t *testing.T) {
|
||||||
// 隔离优先:TEST_DATABASE_URL 指向专用测试库;未提供时跳过,禁止默认连开发库。
|
// 隔离优先:TEST_DATABASE_URL 指向专用测试库;未提供时跳过,禁止默认连开发库。
|
||||||
dsn := os.Getenv("TEST_DATABASE_URL")
|
dsn := db.SkipIfNoTestDatabaseURL(t)
|
||||||
if dsn == "" {
|
if dsn == "" {
|
||||||
dsn = os.Getenv("CONTEXT_DB_DSN")
|
t.Skip("TEST_DATABASE_URL not set; skipping CAS DB integration test")
|
||||||
}
|
|
||||||
if dsn == "" {
|
|
||||||
t.Skip("TEST_DATABASE_URL/CONTEXT_DB_DSN not set (refusing default dev database); skipping CAS DB integration test")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,6 +28,10 @@ func TestCAS_DatabaseIntegration(t *testing.T) {
|
|||||||
}
|
}
|
||||||
defer pool.Close()
|
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 {
|
if err := pool.Ping(ctx); err != nil {
|
||||||
t.Skipf("context db ping failed (%v), skipping DB integration test", err)
|
t.Skipf("context db ping failed (%v), skipping DB integration test", err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -70,8 +70,8 @@ type MemorySearchOptions struct {
|
|||||||
|
|
||||||
// MemoryChunkHit 记忆检索命中结果
|
// MemoryChunkHit 记忆检索命中结果
|
||||||
type MemoryChunkHit struct {
|
type MemoryChunkHit struct {
|
||||||
Chunk *MemoryChunk `json:"chunk"`
|
Chunk *MemoryChunk `json:"chunk"`
|
||||||
RawScore float64 `json:"raw_score"`
|
RawScore float64 `json:"raw_score"`
|
||||||
TimeDecay float64 `json:"time_decay"`
|
TimeDecay float64 `json:"time_decay"`
|
||||||
FinalScore float64 `json:"final_score"`
|
FinalScore float64 `json:"final_score"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package taskrun_test
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"os"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -14,11 +13,7 @@ import (
|
|||||||
|
|
||||||
func openLeaseTestPool(t *testing.T) *pgxpool.Pool {
|
func openLeaseTestPool(t *testing.T) *pgxpool.Pool {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
dsn := os.Getenv("TEST_DATABASE_URL")
|
dsn := db.SkipIfNoTestDatabaseURL(t)
|
||||||
if dsn == "" {
|
|
||||||
t.Skip("TEST_DATABASE_URL must be set for PostgreSQL integration tests (refusing default dev database)")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
pool, err := pgxpool.New(ctx, dsn)
|
pool, err := pgxpool.New(ctx, dsn)
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
package workspace
|
package workspace
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"chat-agent-system/internal/db"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -12,14 +12,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestPGRegistryRoundTrip(t *testing.T) {
|
func TestPGRegistryRoundTrip(t *testing.T) {
|
||||||
url := os.Getenv("TEST_DATABASE_URL")
|
dsn := db.SkipIfNoTestDatabaseURL(t)
|
||||||
if url == "" {
|
|
||||||
t.Skip("set TEST_DATABASE_URL to run the workspace registry postgres test")
|
|
||||||
}
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
schema := fmt.Sprintf("agentx_reg_%d", time.Now().UnixNano())
|
schema := fmt.Sprintf("agentx_reg_%d", time.Now().UnixNano())
|
||||||
boot, err := pgxpool.New(ctx, url)
|
boot, err := pgxpool.New(ctx, dsn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -32,7 +29,7 @@ func TestPGRegistryRoundTrip(t *testing.T) {
|
|||||||
defer ccancel()
|
defer ccancel()
|
||||||
_, _ = boot.Exec(cctx, "DROP SCHEMA "+schema+" CASCADE")
|
_, _ = boot.Exec(cctx, "DROP SCHEMA "+schema+" CASCADE")
|
||||||
})
|
})
|
||||||
cfg, err := pgxpool.ParseConfig(url)
|
cfg, err := pgxpool.ParseConfig(dsn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user