issue#2 收官:按包测试库 + 生产同构审批夹具;短测全量必需门禁 #18

Merged
weichen merged 2 commits from feature/issue2-pg-tests-perpackage-db-2026-09-29 into dev 2026-09-29 10:11:43 +00:00
46 changed files with 335 additions and 154 deletions
Showing only changes of commit ee1e53b510 - Show all commits
+2 -6
View File
@@ -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
View File
Binary file not shown.
+1 -1
View File
@@ -100,4 +100,4 @@ func TestLoginMissingPasswordHash(t *testing.T) {
if err == nil || err.Error() != "用户名或密码错误" {
t.Fatalf("expected unified credential error, got %v", err)
}
}
}
+2 -6
View File
@@ -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)
+1 -1
View File
@@ -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 认证账户存储与会话存储组合。
+9 -9
View File
@@ -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 {
+4 -4
View File
@@ -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",
+2 -2
View File
@@ -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"`
}
-1
View File
@@ -787,4 +787,3 @@ func enrichMessageExportCapability(r *ChatMessageRecord) {
}
r.ExportCapability = evalCap
}
+9 -8
View File
@@ -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
View File
@@ -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
+6 -5
View File
@@ -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())
// 准备真实项目与用户
+5 -5
View File
@@ -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 采集选项。
+3 -3
View File
@@ -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{})
+9 -9
View File
@@ -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.
+1 -1
View File
@@ -6,8 +6,8 @@ import (
"fmt"
"log/slog"
"strings"
"unicode/utf8"
"time"
"unicode/utf8"
)
const (
+2 -2
View File
@@ -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.
+2 -8
View File
@@ -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
View File
@@ -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()
+26
View File
@@ -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
}
+37
View File
@@ -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
}
+1 -6
View File
@@ -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)
+8 -8
View File
@@ -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 同步文档任务状态并评估是否首次满足阶段推进条件。
+1 -1
View File
@@ -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{
+2 -2
View File
@@ -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"`
}
+1 -1
View File
@@ -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},
}},
}},
}
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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
}
+1 -1
View File
@@ -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 -1
View File
@@ -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)
+1 -1
View File
@@ -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 {
+5 -5
View File
@@ -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 存储管理器
+7 -5
View File
@@ -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
+4 -4
View File
@@ -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"`
}
+1 -6
View File
@@ -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)
+4 -7
View File
@@ -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)
}
-1
View File
@@ -1 +0,0 @@
Keeps //go:embed all:dist satisfiable before first npm run build.