fe6e218d9c
build / build (push) Successful in 1m17s
候选归档 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 档案)。
132 lines
4.6 KiB
Go
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)
|
|
}
|
|
}
|