Files
knowledge-governance/internal/httpapi/library_baseline_test.go
姚章浩 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

193 lines
8.4 KiB
Go

package httpapi
import (
"context"
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"knowledge-governance/internal/governance"
)
// 建立目标权威基线的入口契约:
// - 只有管理员能发起(它是生产侧维护动作);
// - 已有生效版本时拒绝(基线只解决首次发布);
// - 目标读不到时拒绝,且**不建作业、不登记版本**(不能用基线掩盖连接问题)。
func baselineRequest(body, role, username string) *http.Request {
request := httptest.NewRequest(http.MethodPost, "/api/v1/library/baselines", strings.NewReader(body))
request.Header.Set("Content-Type", "application/json")
ctx := context.WithValue(request.Context(), sessionKey{}, sessionIdentity{Username: username, Role: role})
return request.WithContext(ctx)
}
func decodeBody(t *testing.T, recorder *httptest.ResponseRecorder) map[string]any {
t.Helper()
var payload map[string]any
if err := json.Unmarshal(recorder.Body.Bytes(), &payload); err != nil {
t.Fatalf("response is not JSON: %s", recorder.Body.String())
}
return payload
}
func TestLibraryBaselineRequiresAdmin(t *testing.T) {
store := &fakeLibraryStore{}
recorder := httptest.NewRecorder()
libraryBaselineEnqueue(recorder, baselineRequest(`{"scope":"internal"}`, roleEngineer, "eng"), Config{Store: store})
if recorder.Code != http.StatusForbidden {
t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
}
if store.createN != 0 {
t.Fatal("non-admin must not create a baseline job")
}
}
func TestLibraryBaselineRefusesWhenEffectiveVersionExists(t *testing.T) {
store := &fakeLibraryStore{versions: []governance.LibraryVersion{{
ID: "libver-1", Scope: "internal", Kind: governance.LibraryVersionProductionEffective,
SnapshotID: "snap-effective", CreatedAt: time.Now().UTC(),
}}}
recorder := httptest.NewRecorder()
libraryBaselineEnqueue(recorder, baselineRequest(`{"scope":"internal"}`, roleAdmin, "admin"), Config{Store: store})
if recorder.Code != http.StatusConflict {
t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
}
if body := decodeBody(t, recorder); body["code"] != "library_baseline_exists" || body["snapshot_id"] != "snap-effective" {
t.Fatalf("body=%v", body)
}
if store.createN != 0 {
t.Fatal("existing effective version must not create another baseline job")
}
}
// 目标读不到(这里用"没配置目标库"复现):必须 503 并说明原因,
// 既不能建作业,也不能顺手写一条版本记录来"解决"未知状态。
func TestLibraryBaselineRefusesWhenTargetUnreadable(t *testing.T) {
store := &fakeLibraryStore{}
recorder := httptest.NewRecorder()
libraryBaselineEnqueue(recorder, baselineRequest(`{"scope":"external"}`, roleAdmin, "admin"), Config{Store: store})
if recorder.Code != http.StatusServiceUnavailable {
t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
}
body := decodeBody(t, recorder)
if body["code"] != "library_baseline_unprobeable" || !strings.Contains(func() string {
text, _ := body["error"].(string)
return text
}(), "无法读取目标整库状态") {
t.Fatalf("body=%v", body)
}
if store.createN != 0 {
t.Fatal("unreadable target must not create a baseline job")
}
if len(store.versions) != 0 {
t.Fatal("unreadable target must not record any version")
}
}
// 测试候选只认登记过 test_sealed 的那份快照:归档里同时存在更新的目标基线/
// 预备份快照时,绝不能把它们当成"拟发布的测试版本"。
func TestTestSealedCandidateComesFromRegistryNotNewestArchiveEntry(t *testing.T) {
sealed := librarySnapshotView{SnapshotID: "snap-sealed", CreatedAt: time.Date(2026, 10, 29, 8, 54, 0, 0, time.UTC)}
prebackup := librarySnapshotView{SnapshotID: "snap-prebackup", CreatedAt: time.Date(2026, 10, 29, 9, 30, 0, 0, time.UTC)}
byID := map[string]librarySnapshotView{sealed.SnapshotID: sealed, prebackup.SnapshotID: prebackup}
versions := []governance.LibraryVersion{
{Kind: governance.LibraryVersionProductionPrebackup, SnapshotID: "snap-prebackup"},
{Kind: governance.LibraryVersionTestSealed, SnapshotID: "snap-sealed"},
}
candidate, reason := testSealedOf(versions, byID)
if reason != "" {
t.Fatalf("reason=%q", reason)
}
if candidate == nil || candidate.SnapshotID != "snap-sealed" {
t.Fatalf("candidate=%v, want the registered test seal", candidate)
}
// 没有任何 test_sealed 登记:宁可说"还没有测试封存",也不拿别的快照顶替。
none, reason := testSealedOf([]governance.LibraryVersion{
{Kind: governance.LibraryVersionProductionPrebackup, SnapshotID: "snap-prebackup"},
}, byID)
if none != nil || !strings.Contains(reason, "还没有该目标的测试封存") {
t.Fatalf("candidate=%v reason=%q", none, reason)
}
// 登记过但它已经不在本机归档:说清是归档问题,不给候选。
missing, reason := testSealedOf([]governance.LibraryVersion{
{Kind: governance.LibraryVersionTestSealed, SnapshotID: "snap-gone"},
}, byID)
if missing != nil || !strings.Contains(reason, "不在本机归档里") {
t.Fatalf("candidate=%v reason=%q", missing, reason)
}
}
// 非空但无权威版本时,缺项必须指向真实入口,而不是一句"未知"。
func TestLibrarySyncGateNamesTheBaselineEntryForNonEmptyTarget(t *testing.T) {
gate := librarySyncGateWithGrant(Config{ProductionReleaseRequestsEnabled: true,
ProductionBackupRestoreVerified: true, LibraryProductionDatabaseURL: "postgres://prod.invalid/db",
LibraryProductionInternalKB: "kb-int", LibraryCapacityBudgetBytes: 1 << 30},
libraryKnownView{State: "unknown"}, libraryKnownView{State: "unknown"}, "snap-1", true)
if gate.SyncAllowed {
t.Fatal("unknown production version must block the sync")
}
joined := strings.Join(gate.Reasons, "|")
if !strings.Contains(joined, "目标非空且没有生效版本记录") || !strings.Contains(joined, "目标权威基线") {
t.Fatalf("reasons=%v", gate.Reasons)
}
}
// 维护能力是配置事实:配置齐全时必须说"已接线",缺项要列出缺哪个变量。
func TestLibraryMaintenanceCapabilityReportsWiringInsteadOfOneVagueSentence(t *testing.T) {
wired := libraryMaintenanceCapability(Config{
LibraryTargetInstance: "kgseal-app", LibraryTargetProject: "kgseal-weknora",
LibraryTargetWriters: []string{"app"}, LibraryTargetIngressURL: "http://kgseal-ingress:18084/health",
LibraryTargetIngressService: "ingress", LibraryTargetJournalRoot: "/var/lib/knowledge-governance/journal",
LibraryTargetDockerSocket: "/var/run/docker.sock",
})
if wired["configured"] != true {
t.Fatalf("capability=%v", wired)
}
if wired["instance"] != "kgseal-app" {
t.Fatalf("instance=%v", wired["instance"])
}
broken := libraryMaintenanceCapability(Config{LibraryTargetInstance: "kgseal-app"})
if broken["configured"] != false {
t.Fatalf("capability=%v", broken)
}
missing, _ := broken["missing"].([]string)
if len(missing) == 0 || !strings.Contains(strings.Join(missing, ","), "LIBRARY_TARGET_WRITERS") {
t.Fatalf("missing=%v", broken["missing"])
}
}
// 建基线的存在性判断同样不能靠展示窗口:生效版本可能在"最近 N 条"之外。
// 读不到(而不是"没有")时也必须 fail closed,不许当成可以建基线。
func TestLibraryBaselineUsesExactCurrentVersionBeyondWindow(t *testing.T) {
store := &fakeLibraryStore{current: &governance.LibraryVersion{ID: "libver-1", Scope: "internal",
Kind: governance.LibraryVersionProductionEffective, SnapshotID: "snap-effective",
CreatedAt: time.Now().UTC().Add(-96 * time.Hour)}}
recorder := httptest.NewRecorder()
libraryBaselineEnqueue(recorder, baselineRequest(`{"scope":"internal"}`, roleAdmin, "admin"), Config{Store: store})
if recorder.Code != http.StatusConflict {
t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
}
if body := decodeBody(t, recorder); body["code"] != "library_baseline_exists" || body["snapshot_id"] != "snap-effective" {
t.Fatalf("body=%v", body)
}
if store.createN != 0 {
t.Fatal("must not create a baseline job when an effective version exists")
}
failing := &fakeLibraryStore{currentErr: errors.New("version storage down")}
recorder = httptest.NewRecorder()
libraryBaselineEnqueue(recorder, baselineRequest(`{"scope":"internal"}`, roleAdmin, "admin"), Config{Store: failing})
if recorder.Code != http.StatusInternalServerError {
t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
}
if failing.createN != 0 {
t.Fatal("must not create a baseline job while the current version is unreadable")
}
}