0088004cd6
接管分类、基线判定、已知索引映射、同步闸口的创建判据与归档保留的保护标记, 原来都用 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 已声明可选跳过)。
148 lines
6.2 KiB
Go
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
|
|
}
|