c5e033dd0e
build / build (push) Failing after 15s
- 同作业号重跑读持久日志+已封存清单按首次请求重放(source_read=false, writers_stopped=false),换范围/绑定库或清单校验失败即拒绝 - 归档用量按跨版本去重统计内容字节,清单/未引用 blob/实际分配单列, 不再用各版本 unique 之和冒充归档占用 - 冻结窗口内复核来源契约,不一致即中止;拒绝组外同镜像运行容器 - 补齐整库只读接口的 OpenAPI 契约 - 按用户确认取消 A06:文档/页面/接口不再把效果测试列为封版门槛
132 lines
4.5 KiB
Go
132 lines
4.5 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(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("")
|
|
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)
|
|
}
|
|
}
|