Files
knowledge-governance/cmd/worker/library_writeset_integration_test.go
T
姚章浩 67743f2285
build / build (push) Successful in 1m27s
test(library): 真实PG验证超出历史窗口仍能取回写集合
fake 版本库不实现 Store 的 1..100 上限,正是它让"限 out of range"一路绿到线上。
这组用例跑真实 PostgreSQL:真实 Run → contentTrackedTarget → 真实
libraryStageRecorder.WriteSetPlanned → 真实 store,断言台账在第一笔写入之前可读、
原备份证据字段与 writeset_digest 原子共存,并把记录推到 250 条更新版本之后
(窗口里已看不到它)仍能按身份取回。

store 级契约用例覆盖:命中、真缺失 found=false、别的作业=冲突、同身份两行=拒绝挑选、
跨 scope/跨 kind 不命中、不完整身份拒绝。变异成"列最近 N 条再筛"时四条全红。
隔离 schema 逻辑收敛到 pgtest 一份,postgres 侧改为委托。
2026-10-30 02:32:26 +08:00

240 lines
9.9 KiB
Go

package main
import (
"context"
"encoding/json"
"fmt"
"os"
"testing"
"time"
"github.com/jackc/pgx/v5/pgxpool"
"knowledge-governance/internal/governance"
"knowledge-governance/internal/libraryrelease"
postgresstore "knowledge-governance/internal/platform/postgres"
"knowledge-governance/internal/platform/postgres/pgtest"
v071 "knowledge-governance/internal/releaseadapter/v071"
)
// 这一组用例跑在真实 PostgreSQL 上(TEST_DATABASE_URL),因为要证明的正是
// 治理库的契约:版本台账的身份查询不许依赖"最近 N 条",而且写集合锚点必须与
// 预备份原有证据共存。假的版本库不实现 1..100 的分页上限,正是它让真实现场
// 那个"library version limit out of range"一路绿到线上。
// ledgerTestTarget 是"只做预检 + 四个内容阶段"的目标:本用例要证明的是治理库侧的
// 台账契约,不是 v071 的生产写入;但预检的落盘(PersistWriteSet)是真实的,
// 因为接管/恢复就是按这份文件取回写集合。
type ledgerTestTarget struct {
root string
plan libraryrelease.WriteSetPlan
}
func (t *ledgerTestTarget) Current(context.Context) (libraryrelease.CurrentState, error) {
return libraryrelease.CurrentState{Counts: map[string]int64{"resources": 1}, Digests: map[string]string{"resources": "universe-digest"}}, nil
}
func (t *ledgerTestTarget) Apply(context.Context, libraryrelease.Stage, string) error { return nil }
func (t *ledgerTestTarget) Verify(context.Context, libraryrelease.Expectation) error { return nil }
func (t *ledgerTestTarget) Restore(context.Context, libraryrelease.BackupRef) error { return nil }
func (t *ledgerTestTarget) PlanWriteSet(_ context.Context, plan libraryrelease.WriteSetPlan) (libraryrelease.WriteSet, error) {
t.plan = plan
set := libraryrelease.WriteSet{Version: libraryrelease.WriteSetVersion, JobID: plan.JobID, Target: plan.Target,
TenantID: 10001, KnowledgeBaseID: "kb-ledger",
BackupSnapshotID: plan.BackupSnapshotID, TargetSnapshotID: plan.TargetSnapshotID, CreatedAt: time.Now().UTC(),
Rows: []libraryrelease.WriteSetRow{{Table: "resources", ID: "res-new"}},
Universe: plan.Universe}
if err := set.Seal(); err != nil {
return libraryrelease.WriteSet{}, err
}
if _, err := v071.PersistWriteSet(t.root, &set); err != nil {
return libraryrelease.WriteSet{}, err
}
return set, nil
}
// ledgerTestBackups 返回一份已经拍好的预备份(模拟"真实备份已完成"),
// WriteSet 留空:台账必须由真实预检算出来,不能由夹具直接塞进去。
type ledgerTestBackups struct {
ref libraryrelease.BackupRef
}
func (b ledgerTestBackups) Prepare(context.Context, string, libraryrelease.CurrentState) (libraryrelease.BackupRef, error) {
return b.ref, nil
}
func openLedgerStore(t *testing.T) (*postgresstore.Store, *pgxpool.Pool) {
t.Helper()
databaseURL := pgtest.IsolatedDatabaseURL(t, os.Getenv("TEST_DATABASE_URL"))
if databaseURL == "" {
t.Skip("TEST_DATABASE_URL is not set")
}
ctx := context.Background()
pool, err := pgxpool.New(ctx, databaseURL)
if err != nil {
t.Fatalf("connect: %v", err)
}
t.Cleanup(pool.Close)
if err := postgresstore.ApplyMigrations(ctx, pool); err != nil {
t.Fatalf("migrate: %v", err)
}
return postgresstore.NewStore(pool), pool
}
// seedVersionHistory 往同一 scope 里塞满比目标更新版本的历史记录:
// 目标记录被顶到任何"最近 N 条"窗口之外,只有按身份精确查询才能取回。
func seedVersionHistory(t *testing.T, ctx context.Context, store *postgresstore.Store, scope string, now time.Time, count int) {
t.Helper()
for i := 0; i < count; i++ {
snapshot := fmt.Sprintf("history-snap-%03d", i)
if err := store.RecordLibraryVersion(ctx, governance.LibraryVersion{
ID: governance.LibraryVersionID(governance.LibraryVersionProductionPrebackup, scope, snapshot),
Scope: scope, Kind: governance.LibraryVersionProductionPrebackup, SnapshotID: snapshot,
SourceJobID: fmt.Sprintf("history-job-%03d", i), CreatedBy: "worker",
Evidence: map[string]any{"backup_snapshot": snapshot, "backup_bytes": 1024},
CreatedAt: now.Add(-time.Duration(i) * time.Minute),
}); err != nil {
t.Fatalf("seed history version %d: %v", i, err)
}
}
}
func containsSnapshot(versions []governance.LibraryVersion, snapshotID string) bool {
for _, version := range versions {
if version.SnapshotID == snapshotID {
return true
}
}
return false
}
// TestLibraryRunAnchorsWriteSetAndKeepsLookingItUpBeyondHistoryWindow 是本问题的
// 端到端证据:真实 Run → contentTrackedTarget 包装 → 真实 libraryStageRecorder
// .WriteSetPlanned → 真实 store,并且历史长到把这条记录挤出"最近 N 条"窗口后,
// 登记/接管/恢复共用的精确查询仍然取得到它。
func TestLibraryRunAnchorsWriteSetAndKeepsLookingItUpBeyondHistoryWindow(t *testing.T) {
store, pool := openLedgerStore(t)
ctx := context.Background()
now := time.Now().UTC()
const (
scope = "internal"
jobID = "libsync-ledger-200"
snapshot = "backup-snap-ledger-200"
target = "target-snap-ledger-200"
params = "params-digest-ledger-200"
)
if _, _, err := store.CreateLibraryJob(ctx, governance.LibraryJob{ID: jobID, Scope: scope, Kind: governance.LibraryJobSync,
IdempotencyKey: "library.sync:internal:ledger-200", RequestDigest: params, Actor: "admin"},
governance.SyncStages(), now); err != nil {
t.Fatalf("create library job: %v", err)
}
root := t.TempDir()
inner := &ledgerTestTarget{root: root}
// 台账必须在第一笔写入之前就能按身份读回:图省事把登记挪到写之后,
// 崩溃时就没有"这次明确写了什么"可读。
anchorBeforeFirstWrite, checkedFirstWrite := false, false
wrapped := &contentTrackedTarget{inner: inner, onApply: func() {
if checkedFirstWrite {
return
}
checkedFirstWrite = true
if _, err := writeSetDigestOf(ctx, store, scope, jobID, snapshot); err == nil {
anchorBeforeFirstWrite = true
}
}}
recorder := libraryStageRecorder{store: store, jobID: jobID, scope: scope, mappingDigest: "mapping-live"}
outcome := libraryrelease.Run(ctx, libraryrelease.Request{JobID: jobID, Target: scope, SnapshotID: target,
ParamsDigest: params, Now: now, Grant: libraryrelease.Grant{Target: scope, SnapshotID: target,
ParamsDigest: params, ExpiresAt: now.Add(time.Hour), AuthorizedBy: "tester"}},
wrapped, ledgerTestBackups{ref: libraryrelease.BackupRef{ID: "backup-" + snapshot, SnapshotID: snapshot,
Bytes: 4096, Universe: map[string]libraryrelease.TableFingerprint{"resources": {Count: 1, Digest: "universe-digest"}}}},
recorder)
if outcome.State != "succeeded" {
t.Fatalf("台账登记不应让作业失败:state=%s error=%s", outcome.State, outcome.Error)
}
if inner.plan.JobID != jobID || inner.plan.BackupSnapshotID != snapshot {
t.Fatalf("包装层没有把预检落到内层目标:%+v", inner.plan)
}
// 登记必须发生在写第一笔之前。
if !anchorBeforeFirstWrite {
t.Fatal("第一笔写入时台账还读不回来:写集合登记没有发生在其之前")
}
digest, err := writeSetDigestOf(ctx, store, scope, jobID, snapshot)
if err != nil {
t.Fatalf("按作业+预备份取写集合摘要:%v", err)
}
if digest == "" {
t.Fatal("写集合摘要为空:登记没有落到版本证据里")
}
// 原证据字段必须与锚点共存(原子保留):整份替换会丢掉备份字节与索引映射,
// 而页面与恢复都在用它们。
set, err := loadWriteSetForJob(ctx, store, root, scope, jobID, snapshot)
if err != nil {
t.Fatalf("按作业+目标+预备份取回写集合:%v", err)
}
if set.Digest != digest {
t.Fatalf("取回的写集合摘要 = %s, want %s", set.Digest, digest)
}
var evidence []byte
if err := pool.QueryRow(ctx, `SELECT evidence FROM library_versions WHERE scope=$1 AND kind='production_prebackup'
AND source_job_id=$2 AND snapshot_id=$3`, scope, jobID, snapshot).Scan(&evidence); err != nil {
t.Fatalf("read evidence: %v", err)
}
assertEvidenceKeepsBackupFields(t, evidence, map[string]any{
"backup_bytes": float64(4096), "job": jobID, "index_mapping_sha256": "mapping-live", "writeset_digest": digest,
})
// 历史继续增长:把这条预备份记录推到 250 条更新记录之后,任何"最近 N 条"
// (真实 Store 上限 100)的窗口都看不到它——这正是现场"读不到现有版本证据"。
if _, err := pool.Exec(ctx, `UPDATE library_versions SET created_at=$2
WHERE scope=$1 AND kind='production_prebackup' AND source_job_id=$3 AND snapshot_id=$4`,
scope, now.Add(-48*time.Hour), jobID, snapshot); err != nil {
t.Fatalf("age prebackup version: %v", err)
}
seedVersionHistory(t, ctx, store, scope, now.Add(-time.Hour), 250)
window, err := store.LibraryVersions(ctx, scope, 100)
if err != nil {
t.Fatalf("list versions: %v", err)
}
if len(window) != 100 {
t.Fatalf("窗口条数 = %d, want 100", len(window))
}
if containsSnapshot(window, snapshot) {
t.Fatal("窗口里仍能看到目标记录:本用例无法证明精确查找(夹具失效)")
}
again, err := writeSetDigestOf(ctx, store, scope, jobID, snapshot)
if err != nil {
t.Fatalf("历史超过窗口后仍必须按身份取回摘要:%v", err)
}
if again != digest {
t.Fatalf("超出窗口后取回的摘要 = %s, want %s", again, digest)
}
if _, err := loadWriteSetForJob(ctx, store, root, scope, jobID, snapshot); err != nil {
t.Fatalf("历史超过窗口后接管/恢复仍必须取回写集合:%v", err)
}
}
func assertEvidenceKeepsBackupFields(t *testing.T, raw []byte, want map[string]any) {
t.Helper()
var evidence map[string]any
if err := json.Unmarshal(raw, &evidence); err != nil {
t.Fatalf("decode evidence: %v", err)
}
for key, value := range want {
got, ok := evidence[key]
if !ok {
t.Fatalf("证据字段 %s 丢失:%v", key, evidence)
}
if fmt.Sprint(got) != fmt.Sprint(value) {
t.Fatalf("证据字段 %s = %v, want %v", key, got, value)
}
}
}