67743f2285
build / build (push) Successful in 1m27s
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 侧改为委托。
240 lines
9.9 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|