Files
knowledge-governance/internal/httpapi/library_snapshots_test.go
T
姚章浩 fe6e218d9c
build / build (push) Successful in 1m17s
fix(library): 整库只读接口改清单级读取,修掉真实超时
候选归档 79 版 / 56MB blob 上,overview、storage、snapshots 三个只读接口
都在 10s 请求期限后返回 503。分段计时定位到唯一瓶颈是按版本重读全部 blob:
Open 0ms、LoadManifest×79 11~22ms、ContentUsage 15~25ms、
Usage(逐版 Load 验字节)13.75~14.11s;单版 Load 261~290ms。

- Usage 拆成 Usage(逐版复核字节,同步/恢复与运维 CLI 用)与
  UsageFromManifests(同一份去重实现,只做清单级校验),列举路径改用后者;
- library_storage.go 每版两次 Load、snapshots 每版一次 Load 改为一次清单读取;
- wiki/文档差异只读所需部件,被读部件仍由 OpenPart 复核摘要;
- LoadManifest 与 Load 共用同一套清单判据(严格解码 + 重新编码校验);
- 列举响应带 verification=manifest / verified=false,详情整版复核后才是 content,
  避免把"清单可读"读成"完整性已验证";
- libraryWithDeadline 去掉超时后再写已结束响应的 goroutine 写法
  (候选环境实测 http: superfluous response.WriteHeader call)。

回归:多版本共享大 blob 时枚举不读 blob 字节(篡改后枚举仍成功),
而 Load/OpenPart/Usage 仍拒绝坏 blob;坏清单两条路径都整条报错。
make verify 15 项全绿(local 档案)。
2026-10-28 21:52:14 +08:00

132 lines
4.6 KiB
Go

package httpapi
import (
"bytes"
"fmt"
"os"
"path/filepath"
"strings"
"testing"
"time"
"knowledge-governance/internal/librarysnapshot"
)
// The 整库版本 page reads a sealed archive through this exact function. A
// synthetic archive keeps the assertion about what the page shows independent of
// whether a real capture has run on this host.
func TestReadLibrarySnapshotsReportsSealedTotals(t *testing.T) {
root, err := os.MkdirTemp(os.TempDir(), "kglib-")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(root)
if err = os.Chmod(root, 0700); err != nil {
t.Fatal(err)
}
archive, err := librarysnapshot.Open(root, 1024*1024)
if err != nil {
t.Fatal(err)
}
// The archive re-derives row counts from the stored bytes, so the bodies must
// carry exactly the declared number of rows.
rows := func(count int) string {
if count == 0 {
return "[]"
}
items := make([]string, count)
for i := range items {
items[i] = fmt.Sprintf(`{"id":"00000000-0000-4000-8000-%012d"}`, i)
}
return "[" + strings.Join(items, ",") + "]"
}
parts := []struct {
kind, name string
count int64
body string
}{
{"database", "knowledges", 4, rows(4)},
{"database", "chunks", 38, rows(38)},
{"database", "resources", 21, rows(21)},
{"database", "resource_bindings", 4, rows(4)},
{"database", "knowledge_tags", 0, rows(0)},
{"database", "knowledge_tag_relations", 0, rows(0)},
{"index", "chunks", 122, rows(122)},
{"wiki", "wiki_pages", 31, rows(31)},
{"wiki", "wiki_folders", 21, rows(21)},
{"wiki", "wiki_page_issues", 0, rows(0)},
{"wiki", "wiki_log_entries", 11, rows(11)},
}
// Every resource row must have its own archived object: the archive refuses a
// snapshot whose resource dependencies are incomplete.
for i := 0; i < 21; i++ {
parts = append(parts, struct {
kind, name string
count int64
body string
}{"resource", fmt.Sprintf("00000000-0000-4000-8000-%012d", i), 1, fmt.Sprintf("object-%d", i)})
}
manifest := librarysnapshot.Manifest{ValidationPolicy: librarysnapshot.ArtifactAcceptancePolicy,
Format: librarysnapshot.Format, Scope: "internal", TenantID: 10000,
KnowledgeBaseID: "0c84a098-d3ed-4b82-a165-e6de9691d6a8", CreatedAt: time.Now().UTC(),
Compatibility: librarysnapshot.Compatibility{WeKnoraVersion: "v0.7.1", SchemaVersion: 74,
RetrieverEngine: "elasticsearch_v8", EmbeddingModel: "text-embedding-v4", EmbeddingDimension: 1024,
IndexMappingSHA256: "137cde2965f3b56521f5fb722b4e0617f46d0e509e571b217fe01e090d5e4d01"}}
for _, part := range parts {
stored, err := archive.Put(bytes.NewReader([]byte(part.body)))
if err != nil {
t.Fatal(err)
}
stored.Kind, stored.Name, stored.Count = part.kind, part.name, part.count
manifest.Parts = append(manifest.Parts, stored)
}
snapshotID, err := archive.Seal(manifest)
if err != nil {
t.Fatal(err)
}
if err = archive.Close(); err != nil {
t.Fatal(err)
}
list, err := readLibrarySnapshots(t.Context(), root)
if err != nil {
t.Fatal(err)
}
if !list.Available || len(list.Snapshots) != 1 || list.Snapshots[0].SnapshotID != snapshotID {
t.Fatalf("unexpected list %+v", list)
}
snapshot := list.Snapshots[0]
want := map[string]int64{"documents": 4, "chunks": 38, "index_documents": 122, "resources": 21, "wiki_pages": 31}
for key, value := range want {
if snapshot.Totals[key] != value {
t.Fatalf("total %s = %d, want %d", key, snapshot.Totals[key], value)
}
}
// The archive-wide content figure deduplicates blobs across versions, so it
// can be smaller than a version's logical sum but never larger.
if snapshot.LogicalBytes <= 0 || list.ContentBytes <= 0 || list.ContentBytes > snapshot.LogicalBytes {
t.Fatalf("bytes not reported: logical=%d content=%d", snapshot.LogicalBytes, list.ContentBytes)
}
if list.BlobAllocatedBytes < list.BlobFileBytes || list.ManifestFileBytes <= 0 {
t.Fatalf("filesystem facts not separated: %+v", list)
}
if snapshot.Compatibility.EmbeddingDimension != 1024 || snapshot.ValidationPolicy != librarysnapshot.ArtifactAcceptancePolicy {
t.Fatalf("contract not reported: %+v", snapshot)
}
if list.CaptureNote == "" {
t.Fatal("page must explain who runs the capture")
}
if _, err = os.Stat(filepath.Join(root, "versions", snapshotID+".json")); err != nil {
t.Fatalf("sealed manifest missing: %v", err)
}
}
func TestReadLibrarySnapshotsWithoutArchiveIsExplicitlyUnwired(t *testing.T) {
list, err := readLibrarySnapshots(t.Context(), "")
if err != nil {
t.Fatal(err)
}
if list.Available || list.Reason == "" || len(list.Snapshots) != 0 {
t.Fatalf("empty configuration must be reported as unwired, got %+v", list)
}
}