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 已声明可选跳过)。
193 lines
8.4 KiB
Go
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")
|
|
}
|
|
}
|