Files
knowledge-governance/cmd/worker/library_writeset_test.go
T
姚章浩 98294b9583
build / build (push) Failing after 24s
fix(library): 统一执行身份、终态收尾、准入与数据身份
why:审计 19 项(12 P1 / 7 P2)来自同一批根因,逐点试错会把协议撕成十几份:
  · 意图与执行混为一谈(F01/F15):`library.sync:<scope>:<快照>` 是终身幂等键,
    A→B→A 的第三次发布拿到第一次的历史成功作业,什么都不做;
  · 版本行既是实体又是事件(F02/F09):主键只由 (kind,scope,快照) 决定、写入是
    upsert,第二个作业覆盖 `source_job_id`/`evidence`/`created_at`;
  · 接管没有代次(F13):两个 worker 都能认领同一 pending 作业,失租的旧执行还能
    写业务状态、释放别人的维护锁;
  · 早退与终态没有统一政策(F03/F04/F05/F06):半写窗口的前置检查失败会重新开门、
    服务起不来也照样开入口、journal 终态后收尾不可重建、恢复成功不回写 current;
  · 数据身份与准入各写一套(F07/F08/F14/F17):接管丢了复用语义、全库搬运不改
    location_hash、通用入口可造 library.* 作业、目标模型从没被问过;
  · 存储/容量/UI/CI 判据各自为政(F10/F11/F12/F16/F18/F19)。

what:按批准的一套协议集中实施(详见 handoffs/release-consolidated-implementation/
current-contract.md):
  · operation(一次明确意图)/ attempt(epoch 代次)/ 不可变版本事件 / current 指针;
    治理库迁移 76,只动治理库,不碰 WeKnora Schema/源码/镜像;
  · 所有业务状态、阶段、版本事件、维护锁写入带 epoch 守卫(失租代次写不进去);
    心跳失租立刻取消 handler;
  · 半写窗口早退保持关闭;服务未全部健康绝不开入口;journal 终态可从持久事实重建
    收尾;恢复成功登记 recovery 事件并推进 current;
  · worker 在动维护前核对业务行/kind/scope/队列关联;通用入口拒绝 library.*;
  · 全库搬运重算 location_hash 并与文档级导入共用实现;接管消费写集合的复用决定;
    发布前核目标实时模型契约、容量门禁前置;
  · 存储保护闭包改为全量依赖、类型只认显式登记、保护未知则不出可删候选;
  · 页面核 HTTP 状态与结构、恢复纳入轮询、Wiki 差异覆盖标题/摘要/父级。

how:`make verify` rc=0(16 项必需检查全绿,含 go test 全量与前端 485 例);
`go vet ./...` rc=0;`npx tsc -p tsconfig.json --noEmit` rc=0;逐项负例见各包测试
(F03/F04/F08/F12/F13/F14/F16/F18/F19 都有先红后绿记录)。
未执行:治理库迁移到任何已有库、真实 PG/ES/S3/WeKnora 集成、故障注入、部署。
2026-10-30 16:14:54 +08:00

390 lines
18 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import (
"context"
"errors"
"os"
"path/filepath"
"strings"
"testing"
"time"
"knowledge-governance/internal/governance"
"knowledge-governance/internal/libraryrelease"
v071 "knowledge-governance/internal/releaseadapter/v071"
)
// 这里验证的是"治理侧的锚点":摘要真的写进版本证据、能按作业+目标+预备份取回,
// 并且任何一步对不上都返回错误(调用方据此不写目标 / 保持维护)。
// 归档文件的读写在 v071 的写集合用例里用真实文件验证。
type fakeVersionStore struct {
versions []governance.LibraryVersion
recorded *governance.LibraryVersion
err error
}
// LibraryVersionByRef 按 scope+kind+作业+快照四个条件精确命中:
// 真实 store 已不再提供"列最近 N 条再筛"这条路,夹具也不能假装还能那样查。
func (f *fakeVersionStore) LibraryVersionByRef(_ context.Context, scope string, kind governance.LibraryVersionKind,
sourceJobID, snapshotID string) (governance.LibraryVersion, bool, error) {
if f.err != nil {
return governance.LibraryVersion{}, false, f.err
}
for _, version := range f.versions {
if version.Scope != "" && version.Scope != scope {
continue
}
if version.Kind == kind && version.SnapshotID == snapshotID && version.SourceJobID == sourceJobID {
return version, true, nil
}
}
return governance.LibraryVersion{}, false, nil
}
func (f *fakeVersionStore) RecordLibraryVersion(_ context.Context, version governance.LibraryVersion) error {
f.recorded = &version
return nil
}
func sealedLedger(t *testing.T, digest string) libraryrelease.WriteSet {
t.Helper()
set := libraryrelease.WriteSet{Version: libraryrelease.WriteSetVersion, JobID: "job-1", Target: "internal",
TenantID: 10001, KnowledgeBaseID: "kb", BackupSnapshotID: "backup-snap",
TargetSnapshotID: "target-snap", CreatedAt: time.Now().UTC(),
Rows: []libraryrelease.WriteSetRow{{Table: "resources", ID: "res-new"}},
Universe: map[string]libraryrelease.TableFingerprint{"resources": {Count: 2, Digest: "d"}}}
if err := set.Seal(); err != nil {
t.Fatal(err)
}
return set
}
func TestWriteSetEvidenceKeepsBackupEvidenceAndAddsAnchor(t *testing.T) {
set := sealedLedger(t, "")
evidence := writeSetEvidence(map[string]any{"backup_bytes": int64(4096), "index_mapping_sha256": "old"}, set, "mapping-1")
if evidence["backup_bytes"] != int64(4096) {
t.Fatalf("原预备份证据必须保留:%v", evidence)
}
if evidence["writeset_digest"] != set.Digest {
t.Fatalf("writeset_digest = %v, want %s", evidence["writeset_digest"], set.Digest)
}
if evidence["writeset_universe_digest"] != set.UniverseDigest() || evidence["writeset_universe_digest"] == "" {
t.Fatalf("整实例证据摘要必须登记:%v", evidence)
}
if evidence["index_mapping_sha256"] != "mapping-1" {
t.Fatalf("当前索引映射必须覆盖旧值:%v", evidence)
}
if evidence["writeset_job"] != nil && evidence["job"] != "job-1" {
t.Fatalf("写集合必须绑定作业:%v", evidence)
}
}
func TestWriteSetDigestLookupRequiresJobTargetAndSnapshotMatch(t *testing.T) {
store := &fakeVersionStore{versions: []governance.LibraryVersion{
{ID: "v1", Scope: "internal", Kind: governance.LibraryVersionProductionPrebackup, SnapshotID: "backup-snap",
SourceJobID: "job-1", Evidence: map[string]any{"writeset_digest": "digest-1"}},
{ID: "v2", Scope: "internal", Kind: governance.LibraryVersionProductionPrebackup, SnapshotID: "backup-snap",
SourceJobID: "job-2", Evidence: map[string]any{"writeset_digest": "digest-2"}},
}}
digest, err := writeSetDigestOf(context.Background(), store, "internal", "job-1", "backup-snap")
if err != nil || digest != "digest-1" {
t.Fatalf("digest = %q, err = %v", digest, err)
}
// 别的作业的版本不能被拿来用。
if _, err := writeSetDigestOf(context.Background(), store, "internal", "job-3", "backup-snap"); err == nil {
t.Fatal("找不到本作业的版本时必须失败")
}
// 有版本但没登记摘要 → 失败(不许当成"没有写集合"降级)。
missing := &fakeVersionStore{versions: []governance.LibraryVersion{{Kind: governance.LibraryVersionProductionPrebackup,
SnapshotID: "backup-snap", SourceJobID: "job-1", Evidence: map[string]any{}}}}
if _, err := writeSetDigestOf(context.Background(), missing, "internal", "job-1", "backup-snap"); err == nil {
t.Fatal("没有写集合摘要时必须失败")
}
// 读不到版本 → 失败。
if _, err := writeSetDigestOf(context.Background(), &fakeVersionStore{err: errors.New("db down")},
"internal", "job-1", "backup-snap"); err == nil {
t.Fatal("版本查询失败必须向上传递")
}
}
func TestLoadWriteSetForJobChecksBinding(t *testing.T) {
root := t.TempDir()
set := sealedLedger(t, "")
if _, err := v071.PersistWriteSet(root, &set); err != nil {
t.Fatal(err)
}
store := &fakeVersionStore{versions: []governance.LibraryVersion{{Kind: governance.LibraryVersionProductionPrebackup,
SnapshotID: "backup-snap", SourceJobID: "job-1", Evidence: map[string]any{"writeset_digest": set.Digest}}}}
loaded, err := loadWriteSetForJob(context.Background(), store, root, "internal", "job-1", "backup-snap")
if err != nil {
t.Fatalf("load: %v", err)
}
if loaded.Digest != set.Digest {
t.Fatalf("digest = %s", loaded.Digest)
}
// 目标不一致 → 拒绝(同一份记录不能被拿去恢复另一个目标)。
if _, err := loadWriteSetForJob(context.Background(), store, root, "external", "job-1", "backup-snap"); err == nil {
t.Fatal("目标不一致时必须失败")
}
// 作业不一致 → 拒绝。
if _, err := loadWriteSetForJob(context.Background(), store, root, "internal", "job-2", "backup-snap"); err == nil {
t.Fatal("作业不一致时必须失败")
}
// 记录文件被删 → 拒绝。
if err := os.Chmod(filepath.Join(root, "writesets", set.Digest+".json"), 0600); err != nil {
t.Fatal(err)
}
if err := os.Remove(filepath.Join(root, "writesets", set.Digest+".json")); err != nil {
t.Fatal(err)
}
if _, err := loadWriteSetForJob(context.Background(), store, root, "internal", "job-1", "backup-snap"); err == nil {
t.Fatal("写集合文件缺失时必须失败")
}
}
// 接管用的预备份来源:没有写集合就不许继续(返回错误,而不是给一个空引用)。
func TestLedgerBackupStoreRefusesWithoutWriteSet(t *testing.T) {
store := ledgerBackupStore{ref: libraryrelease.BackupRef{ID: "backup-1", SnapshotID: "backup-snap"}}
if _, err := store.Prepare(context.Background(), "target-snap", libraryrelease.CurrentState{}); err == nil {
t.Fatal("没有写集合时接管必须失败")
}
set := sealedLedger(t, "")
store.write = &set
// 复用不重做备份:真实字节只能来自首次登记的证据,缺了必须失败(见下一个用例)。
store.ref.Bytes = 4096
ref, err := store.Prepare(context.Background(), "target-snap", libraryrelease.CurrentState{})
if err != nil {
t.Fatalf("prepare: %v", err)
}
if ref.WriteSet == nil || ref.WriteSet.Digest != set.Digest {
t.Fatalf("接管必须带上持久化的写集合:%+v", ref)
}
}
// 包装层必须把"写集合预检"这件事透出去:orchestrator 用的是类型断言,
// 少一个方法就等于静默跳过台账(真实后果:真作业没有 writeset_digest、也没有
// writesets/ 文件,崩溃接管无台账可读)。
type plainTarget struct{ applied int }
func (t *plainTarget) Current(context.Context) (libraryrelease.CurrentState, error) {
return libraryrelease.CurrentState{}, nil
}
func (t *plainTarget) Apply(context.Context, libraryrelease.Stage, string) error {
t.applied++
return nil
}
func (t *plainTarget) Verify(context.Context, libraryrelease.Expectation) error { return nil }
func (t *plainTarget) Restore(context.Context, libraryrelease.BackupRef) error { return nil }
type planningTarget struct {
plainTarget
planned int
last libraryrelease.WriteSetPlan
}
func (t *planningTarget) PlanWriteSet(_ context.Context, plan libraryrelease.WriteSetPlan) (libraryrelease.WriteSet, error) {
t.planned++
t.last = plan
return libraryrelease.WriteSet{Version: libraryrelease.WriteSetVersion, JobID: "job-1", Target: "internal"}, nil
}
func TestContentTrackedTargetForwardsWriteSetPlanning(t *testing.T) {
inner := &planningTarget{}
wrapped := &contentTrackedTarget{inner: inner, onApply: func() {}}
planner, ok := libraryrelease.Target(wrapped).(libraryrelease.WriteSetPlanner)
if !ok {
t.Fatal("包装后的目标必须仍然实现 WriteSetPlanner,否则写集合会被静默跳过")
}
if _, err := planner.PlanWriteSet(context.Background(), libraryrelease.WriteSetPlan{JobID: "job-1", Target: "internal",
PriorSnapshots: []libraryrelease.PriorSnapshot{{JobID: "job-0", SnapshotID: "snap-0", Kind: libraryrelease.PriorSnapshotSync}}}); err != nil {
t.Fatalf("plan: %v", err)
}
if inner.planned != 1 {
t.Fatalf("预检必须落到内层目标,planned=%d", inner.planned)
}
// 来源证据也必须透传:丢掉它就会让"恢复保留下来的资源"永远无法证明可复用。
if len(inner.last.PriorSnapshots) != 1 || inner.last.PriorSnapshots[0].SnapshotID != "snap-0" {
t.Fatalf("PriorSyncs 没有透传到内层:%+v", inner.last)
}
// 内层不支持预检 ⇒ 硬错误,不许"没有台账也照写"。
strict := &contentTrackedTarget{inner: &plainTarget{}, onApply: func() {}}
if _, err := strict.PlanWriteSet(context.Background(), libraryrelease.WriteSetPlan{}); err == nil {
t.Fatal("内层不支持写集合预检时必须报错")
} else if !strings.Contains(err.Error(), "写集合") {
t.Fatalf("错误必须点名写集合:%v", err)
}
}
// 编排路径上的同一个断言:Run 是按 target.(WriteSetPlanner) 决定要不要预检的,
// 所以包装体必须在**真实调用链里**也把预检带上(只测直接调用内层会漏掉这一层)。
type orchestratorTarget struct {
planned int
applied []libraryrelease.Stage
}
func (t *orchestratorTarget) Current(context.Context) (libraryrelease.CurrentState, error) {
return libraryrelease.CurrentState{}, nil
}
func (t *orchestratorTarget) Apply(_ context.Context, stage libraryrelease.Stage, _ string) error {
t.applied = append(t.applied, stage)
return nil
}
func (t *orchestratorTarget) Verify(context.Context, libraryrelease.Expectation) error { return nil }
func (t *orchestratorTarget) Restore(context.Context, libraryrelease.BackupRef) error { return nil }
func (t *orchestratorTarget) PlanWriteSet(_ context.Context, plan libraryrelease.WriteSetPlan) (libraryrelease.WriteSet, error) {
t.planned++
set := libraryrelease.WriteSet{Version: libraryrelease.WriteSetVersion, JobID: plan.JobID, Target: plan.Target,
TenantID: 10001, KnowledgeBaseID: "kb", BackupSnapshotID: plan.BackupSnapshotID,
TargetSnapshotID: plan.TargetSnapshotID, CreatedAt: time.Now().UTC(),
Universe: plan.Universe,
Rows: []libraryrelease.WriteSetRow{{Table: "resources", ID: "res-1"}}}
if err := set.Seal(); err != nil {
return libraryrelease.WriteSet{}, err
}
return set, nil
}
type stubBackupStore struct{}
func (stubBackupStore) Prepare(context.Context, string, libraryrelease.CurrentState) (libraryrelease.BackupRef, error) {
return libraryrelease.BackupRef{ID: "backup-1", SnapshotID: "backup-snap",
Universe: map[string]libraryrelease.TableFingerprint{"resources": {Count: 1, Digest: "abc"}}}, nil
}
func TestRunPlansWriteSetThroughContentTrackedWrapper(t *testing.T) {
inner := &orchestratorTarget{}
tracked := &contentTrackedTarget{inner: inner, onApply: func() {}}
now := time.Now().UTC()
request := libraryrelease.Request{JobID: "job-1", Target: "internal", SnapshotID: "snap-1",
ParamsDigest: "digest-1", Now: now,
Grant: libraryrelease.Grant{Target: "internal", SnapshotID: "snap-1", ParamsDigest: "digest-1",
ExpiresAt: now.Add(time.Hour), AuthorizedBy: "acceptance"}}
outcome := libraryrelease.Run(context.Background(), request, tracked, stubBackupStore{}, nil)
if outcome.State != "succeeded" {
t.Fatalf("outcome = %+v", outcome)
}
if inner.planned != 1 {
t.Fatalf("编排路径没有经过包装层触发写集合预检,planned=%d", inner.planned)
}
if outcome.Backup.WriteSet == nil || outcome.Backup.WriteSet.Digest == "" {
t.Fatalf("预检结果必须带回给编排层(恢复要按它清理):%+v", outcome.Backup)
}
if len(inner.applied) != len(libraryrelease.Stages()) {
t.Fatalf("四类写入必须都执行,applied=%v", inner.applied)
}
}
// 复用登记必须把"这一轮"的字段合并进已持久化的证据,而不是换一份新的。
//
// 现场(2026-10-30 演练):BackupPrepared 在接管复用时新造证据,把首次规划写入的
// writeset_* 整段抹掉;verify_effective 的摘要从 fddd98cf… 变成空,第二次中断
// 再接管就读不回写集合。这里把"抹掉"这件事在纯函数层面钉死。
func TestPrebackupEvidenceKeepsWriteSetAnchorOnReuse(t *testing.T) {
existing := map[string]any{"backup_snapshot": "backup-snap", "job": "job-1",
"backup_bytes": float64(4096), "writeset_digest": "digest-1",
"writeset_universe_digest": "universe-1", "writeset_rows": float64(3),
"index_mapping_sha256": "mapping-old"}
evidence, err := prebackupEvidence(existing, libraryrelease.BackupRef{ID: "backup-backup-snap",
SnapshotID: "backup-snap", Bytes: 4096, Reused: true}, "job-1", "mapping-live")
if err != nil {
t.Fatalf("复用合并:%v", err)
}
for key, want := range map[string]any{"writeset_digest": "digest-1", "writeset_universe_digest": "universe-1",
"writeset_rows": float64(3), "backup_bytes": int64(4096), "backup_snapshot": "backup-snap",
"job": "job-1", "backup_reused": true, "index_mapping_sha256": "mapping-live"} {
if got := evidence[key]; got != want {
t.Fatalf("复用后 %s = %v, want %v(整份替换会丢写集合锚点)", key, got, want)
}
}
// 复用但没有 bytes(历史缺陷的调用形状):字节必须从已登记的证据里补回来。
restored, err := prebackupEvidence(existing, libraryrelease.BackupRef{SnapshotID: "backup-snap", Reused: true},
"job-1", "")
if err != nil {
t.Fatalf("复用缺字节时应从证据补齐:%v", err)
}
if restored["backup_bytes"] != int64(4096) {
t.Fatalf("补回来的字节 = %v, want 4096", restored["backup_bytes"])
}
// 负例:身份不符一律失败,不做"看起来对就合"。
foreignJob := map[string]any{"job": "job-2", "writeset_digest": "digest-other"}
if _, err := prebackupEvidence(foreignJob, libraryrelease.BackupRef{SnapshotID: "backup-snap",
Bytes: 4096, Reused: true}, "job-1", ""); err == nil {
t.Fatal("证据属于别的作业时必须失败(否则会把别人的写集合带进来)")
}
foreignSnapshot := map[string]any{"job": "job-1", "backup_snapshot": "backup-other"}
if _, err := prebackupEvidence(foreignSnapshot, libraryrelease.BackupRef{SnapshotID: "backup-snap",
Bytes: 4096, Reused: true}, "job-1", ""); err == nil {
t.Fatal("证据绑定别的预备份时必须失败")
}
// 负例:两份不同的字节数不能被当成同一份备份。
if _, err := prebackupEvidence(existing, libraryrelease.BackupRef{SnapshotID: "backup-snap",
Bytes: 2048, Reused: true}, "job-1", ""); err == nil {
t.Fatal("复用字节数与已登记不一致时必须失败")
}
// 负例:复用却拿不出任何可信字节 → 拒绝(不许写 0)。
if _, err := prebackupEvidence(map[string]any{"job": "job-1"}, libraryrelease.BackupRef{SnapshotID: "backup-snap",
Reused: true}, "job-1", ""); err == nil {
t.Fatal("复用缺少可信字节数时必须失败")
}
}
// 写集合只能落定一次:第二次预检摘要不同 ⇒ 台账不再描述"写了什么" ⇒ 失败。
func TestAnchorWriteSetRefusesConflictingDigestAndForeignEvidence(t *testing.T) {
set := sealedLedger(t, "")
recorded := func(evidence map[string]any) *fakeVersionStore {
return &fakeVersionStore{versions: []governance.LibraryVersion{{Kind: governance.LibraryVersionProductionPrebackup,
SnapshotID: "backup-snap", SourceJobID: "job-1", Evidence: evidence}}}
}
anchor := func(store libraryVersionLedger) error {
return anchorWriteSet(context.Background(), store, "", "internal", "job-1", "m", set)
}
if err := anchor(recorded(map[string]any{"job": "job-2"})); err == nil {
t.Fatal("版本证据属于别的作业时必须失败")
}
if err := anchor(recorded(map[string]any{"writeset_digest": "another-digest"})); err == nil {
t.Fatal("已登记不同写集合摘要时必须失败")
}
// 正例对照:同一摘要重复登记是幂等的。
ok := recorded(map[string]any{"writeset_digest": set.Digest})
if err := anchor(ok); err != nil {
t.Fatalf("同一份写集合重复登记应当幂等:%v", err)
}
if ok.recorded == nil || ok.recorded.Evidence["writeset_digest"] != set.Digest {
t.Fatalf("登记没有落到证据上:%+v", ok.recorded)
}
}
// 复用没有真实字节时不许继续:0 字节基线在生效版本证据里与"空备份"无法区分。
func TestLedgerBackupStoreRefusesZeroBytes(t *testing.T) {
set := sealedLedger(t, "")
store := ledgerBackupStore{ref: libraryrelease.BackupRef{ID: "backup-1", SnapshotID: "backup-snap"}, write: &set}
if _, err := store.Prepare(context.Background(), "target-snap", libraryrelease.CurrentState{}); err == nil {
t.Fatal("缺少真实字节数时接管必须失败")
}
store.ref.Bytes = 4096
ref, err := store.Prepare(context.Background(), "target-snap", libraryrelease.CurrentState{})
if err != nil {
t.Fatalf("prepare: %v", err)
}
if ref.Bytes != 4096 || !ref.Reused {
t.Fatalf("复用预备份必须带真实字节与 Reused:%+v", ref)
}
}
// 生效版本证据必须带真实字节;没有字节时不许写 0 冒充。
func TestEffectiveEvidenceCarriesRealBytes(t *testing.T) {
withBytes := effectiveEvidence(libraryrelease.BackupRef{SnapshotID: "backup-snap", Bytes: 4096}, "restore", "m")
if withBytes["backup_bytes"] != int64(4096) || withBytes["backup_snapshot"] != "backup-snap" ||
withBytes["action"] != "restore" || withBytes["index_mapping_sha256"] != "m" {
t.Fatalf("生效版本证据 = %v", withBytes)
}
without := effectiveEvidence(libraryrelease.BackupRef{SnapshotID: "backup-snap"}, "restore", "")
if _, ok := without["backup_bytes"]; ok {
t.Fatalf("未知字节数不许写成 0:%v", without)
}
}