Files
knowledge-governance/cmd/worker/library_baseline_test.go
T
姚章浩 0088004cd6 fix(library): 当前生效版本改按精确查询
接管分类、基线判定、已知索引映射、同步闸口的创建判据与归档保留的保护标记,
原来都用 LibraryVersions(scope, 50/100) 这个分页窗口再在内存里筛。生效版本每
scope 只有一行、可能很久以前登记,历史一长就被挤出窗口 —— 于是"已生效"被读成
"没有生效":接管会把已经上线的目标重写一遍,基线入口会给已有权威版本的目标
再建一次基线。

现在统一走两条精确查询:LibraryCurrentVersion(kind='production_effective' AND
superseded_at IS NULL,每 scope 至多一行)与 LibraryVersionOfSnapshot(按快照
身份取回"已顶替"证据)。多行、自相矛盾、读不到一律 fail closed;同一含义只留
一份实现(knownIndexMapping 同步与恢复共用)。LibraryVersions 仍是 1..100 的
展示窗口,只用于列表。

验证:真实 PG 上先复现(窗口实现 ⇒ found=false),改后 7 条 store 契约用例与
worker 端到端判定用例全绿;本机 make verify 15 项 success(用例 1264 通过 /
0 失败 / 93 已声明可选跳过)。
2026-10-30 03:09:21 +08:00

148 lines
6.2 KiB
Go

package main
import (
"context"
"errors"
"strings"
"testing"
"time"
"knowledge-governance/internal/governance"
)
// 崩溃续作的版本判定:本作业+同一快照 = 只收尾;别人的生效版本 = 拒绝;都没有 = 正常建基线。
// "已顶替"的旧版本由 Store 的精确查询过滤掉(LibraryCurrentVersion 只返回未顶替的那一行),
// 所以这里只需要一个事实:当前生效的是谁。
func TestClassifyBaselineVersions(t *testing.T) {
const jobA, jobB, snap = "libbase-internal-1", "libbase-internal-2", "snap-abc"
effective := func(sourceJob, snapshot string) governance.LibraryVersion {
return governance.LibraryVersion{
Scope: "internal", Kind: governance.LibraryVersionProductionEffective,
SnapshotID: snapshot, SourceJobID: sourceJob, CreatedAt: time.Now().UTC(),
}
}
if state := classifyBaselineVersions(governance.LibraryVersion{}, false, jobA, snap); state.Own || state.Foreign != nil {
t.Fatalf("没有生效版本必须既不是 own 也不是 foreign: %+v", state)
}
if state := classifyBaselineVersions(effective(jobA, snap), true, jobA, snap); !state.Own {
t.Fatalf("same job + same snapshot must be recognized as own: %+v", state)
}
// 快照不同就不是"我登记的那份",不能当成可收尾。
if state := classifyBaselineVersions(effective(jobA, "snap-other"), true, jobA, snap); state.Own || state.Foreign == nil {
t.Fatalf("different snapshot of same job must not be own: %+v", state)
}
// 别人的生效版本:即使快照相同也必须拒绝。
foreign := classifyBaselineVersions(effective(jobB, snap), true, jobA, snap)
if foreign.Own || foreign.Foreign == nil || foreign.Foreign.SourceJobID != jobB {
t.Fatalf("foreign effective version must be reported: %+v", foreign)
}
// 作业行里还没有持久化快照(persistedSnapshot 为空)时,自己登记的版本也算 foreign:
// 没有绑定身份的证据不能被当成"我可以只收尾"。
if state := classifyBaselineVersions(effective(jobA, snap), true, jobA, ""); state.Own || state.Foreign == nil {
t.Fatalf("no persisted snapshot must not be own: %+v", state)
}
}
// 版本已登记 + 健康:走成功边界,释放两把占用并把作业标成功。
func TestSettleBaselineWindowFinishesWhenRecorded(t *testing.T) {
store := &fakeFinishStore{}
if err := settleBaselineWindow(context.Background(), store, fakeHealth{}, "job-1", "internal", "kgseal-app",
"snap-abc", true, "succeeded", nil); err != nil {
t.Fatalf("settle: %v", err)
}
if !containsCall(store.calls, "advance_succeeded") {
t.Fatalf("job must be marked succeeded: %v", store.calls)
}
if containsCall(store.states, "needs_restore") {
t.Fatalf("success path must not hold the target in maintenance: %v", store.states)
}
if !containsCall(store.calls, "scope_release") || !containsCall(store.calls, "instance_release") {
t.Fatalf("both locks must be released: %v", store.calls)
}
}
// 版本已登记但目标服务/入口没恢复:不能报成功,必须保持维护并标 needs_restore。
func TestSettleBaselineWindowHoldsWhenHealthFails(t *testing.T) {
store := &fakeFinishStore{}
err := settleBaselineWindow(context.Background(), store, fakeHealth{err: errors.New("kgseal-ingress 未就绪")},
"job-1", "internal", "kgseal-app", "snap-abc", true, "succeeded", nil)
if err == nil {
t.Fatal("unhealthy target must not report success")
}
if containsCall(store.calls, "advance_succeeded") {
t.Fatalf("must not mark succeeded while target is unhealthy: %v", store.calls)
}
if countCall(store.states, "needs_restore") != 2 {
t.Fatalf("must hold both records with needs_restore: %v", store.states)
}
}
// 没有登记、窗口仍是 held:只能保持维护(服务确实没回来),绝不能释放占用。
func TestSettleBaselineWindowKeepsHeldWindowInMaintenance(t *testing.T) {
store := &fakeFinishStore{}
err := settleBaselineWindow(context.Background(), store, fakeHealth{}, "job-1", "internal", "kgseal-app",
"snap-abc", false, "held", errors.New("停写入者后后台仍有任务"))
if err == nil || !strings.Contains(err.Error(), "保持维护") {
t.Fatalf("err=%v", err)
}
if containsCall(store.calls, "scope_release") || containsCall(store.calls, "instance_release") {
t.Fatalf("held window must not release locks: %v", store.calls)
}
if countCall(store.states, "needs_restore") != 2 {
t.Fatalf("held window must be recorded as needs_restore: %v", store.states)
}
if !containsCall(store.calls, "advance_failed") {
t.Fatalf("job must fail: %v", store.calls)
}
}
// 没有登记、但窗口已按原状态开放:状态必须跟着变成"已释放",
// 不能留下"显示维护中、实际入口已开"。
func TestSettleBaselineWindowReleasesWhenWindowRestored(t *testing.T) {
store := &fakeFinishStore{}
err := settleBaselineWindow(context.Background(), store, fakeHealth{}, "job-1", "internal", "kgseal-app",
"snap-abc", false, "failed", errors.New("捕获目标整库失败"))
if err == nil || !strings.Contains(err.Error(), "未建立基线") {
t.Fatalf("err=%v", err)
}
if !containsCall(store.calls, "scope_release") || !containsCall(store.calls, "instance_release") {
t.Fatalf("restored window must release both locks: %v", store.calls)
}
if containsCall(store.states, "needs_restore") {
t.Fatalf("released window must not be marked as needs_restore: %v", store.states)
}
if !containsCall(store.calls, "advance_failed") {
t.Fatalf("job must fail: %v", store.calls)
}
}
// 释放失败时不能在作业上写"无需恢复":那会让界面与实际占用分叉。
func TestSettleBaselineWindowReportsReleaseFailure(t *testing.T) {
store := &fakeFinishStore{failOn: "scope_release"}
err := settleBaselineWindow(context.Background(), store, fakeHealth{}, "job-1", "internal", "kgseal-app",
"snap-abc", false, "failed", nil)
if err == nil || !strings.Contains(err.Error(), "维护占用释放失败") {
t.Fatalf("err=%v", err)
}
}
func countCall(calls []string, want string) int {
count := 0
for _, call := range calls {
if call == want {
count++
}
}
return count
}
func containsCall(calls []string, want string) bool {
for _, call := range calls {
if call == want {
return true
}
}
return false
}