Files
knowledge-governance/internal/releasebundle/manifest_test.go
T
姚章浩 6440e3f679 feat(release): 发布目标知识库可选(源库 → 目标库映射)
- 迁移 000044:production_kb_bindings + release_item_executions.target_knowledge_base_id
- 系统设置新增「生产知识库绑定(对内/对外)」:从生产 WeKnora 实际列表选库,
  保存时真校验(不存在拒收;生产端不可达只提示并如实回报),仅管理员可改
- 执行计划按绑定解析目标库并冻结进 release_item_executions;空目标直接拒收;
  已发布过的条目保留原目标(重试/回滚回到同一处)
- DocumentBundle.RemapKnowledgeBase:knowledges/chunks/knowledge_tags/wiki_pages/
  wiki_folders 的 knowledge_base_id 与 ES 文档字段改写为目标库,重算表/文档摘要并重新封装
- 契约比对改为「声明的映射对」(KnowledgeBaseMapping),其余字段继续 fail-closed
- 契约检查器 / 目标身份 / 验证器全部走映射后的目标库;对象通道无需改写
- Compose 拆分:数据面接线 compose.release-sim.yaml + 两道门禁 compose.release-gates.yaml
- 测试:RemapKnowledgeBase 全通道/无操作/fail-closed、映射契约、绑定接口、RBAC、真实 PG 集成
2026-10-08 15:41:13 +08:00

168 lines
7.0 KiB
Go

package releasebundle
import (
"encoding/json"
"strings"
"testing"
"time"
)
func TestSealAndValidateV071Manifest(t *testing.T) {
manifest := Manifest{
FormatVersion: "weknora-release-bundle/v1",
ExecutionID: "rx-safe", PublicationID: "tp-safe", SourceItemVersionID: "sv-safe",
SourceRevision: "source-v1", ParseRevision: "parser-v1@index-v1", ReleaseRevision: "release-v1",
CreatedAt: time.Date(2026, 9, 6, 8, 0, 0, 0, time.UTC),
Contract: Contract{
WeKnoraVersion: "v0.7.1", MigrationVersion: 74, TenantID: 10000,
KnowledgeBaseID: "kb-safe", KnowledgeID: "knowledge-safe",
RetrieverEngine: "elasticsearch_v8", EmbeddingModel: "bge-m3", EmbeddingDimension: 1024,
ElasticsearchMappingSHA256: strings.Repeat("a", 64),
},
Tables: []TableDigest{
{Name: "chunks", RowCount: 2, SHA256: strings.Repeat("b", 64)},
{Name: "knowledge_tags", RowCount: 1, SHA256: strings.Repeat("f", 64)},
{Name: "knowledges", RowCount: 1, SHA256: strings.Repeat("c", 64)},
},
Search: SearchDigest{DocumentCount: 2, SHA256: strings.Repeat("d", 64)},
Assets: AssetDigest{ObjectCount: 1, TotalBytes: 42, SHA256: strings.Repeat("e", 64)},
}
sealed, err := Seal(manifest)
if err != nil {
t.Fatal(err)
}
if len(sealed.BundleSHA256) != 64 {
t.Fatalf("bundle sha=%q", sealed.BundleSHA256)
}
target := TargetContract{
WeKnoraVersion: "v0.7.1", MigrationVersion: 74, TenantID: 10000,
KnowledgeBaseID: "kb-safe", RetrieverEngine: "elasticsearch_v8",
EmbeddingModel: "bge-m3", EmbeddingDimension: 1024,
ElasticsearchMappingSHA256: strings.Repeat("a", 64),
}
if err := Validate(sealed, target, sameIDMapping()); err != nil {
t.Fatal(err)
}
encoded, err := json.Marshal(sealed)
if err != nil {
t.Fatal(err)
}
for _, forbidden := range []string{"endpoint", "password", "secret", "api_key", "physical_path", "database_url"} {
if strings.Contains(strings.ToLower(string(encoded)), forbidden) {
t.Fatalf("manifest leaked forbidden field %q", forbidden)
}
}
}
func TestSealCanonicalizesTableOrder(t *testing.T) {
base := validManifest()
first, err := Seal(base)
if err != nil {
t.Fatal(err)
}
base.Tables[0], base.Tables[1] = base.Tables[1], base.Tables[0]
second, err := Seal(base)
if err != nil {
t.Fatal(err)
}
if first.BundleSHA256 != second.BundleSHA256 {
t.Fatalf("digest differs: %s vs %s", first.BundleSHA256, second.BundleSHA256)
}
}
func TestValidateRejectsContractDriftAndUnknownTables(t *testing.T) {
manifest, err := Seal(validManifest())
if err != nil {
t.Fatal(err)
}
target := validTarget()
tests := []struct {
name string
mutate func(*Manifest, *TargetContract)
}{
{name: "version", mutate: func(_ *Manifest, target *TargetContract) { target.WeKnoraVersion = "v0.7.2" }},
{name: "migration", mutate: func(_ *Manifest, target *TargetContract) { target.MigrationVersion = 73 }},
{name: "kb", mutate: func(_ *Manifest, target *TargetContract) { target.KnowledgeBaseID = "other" }},
{name: "embedding", mutate: func(_ *Manifest, target *TargetContract) { target.EmbeddingDimension = 768 }},
{name: "mapping", mutate: func(_ *Manifest, target *TargetContract) { target.ElasticsearchMappingSHA256 = strings.Repeat("f", 64) }},
{name: "unknown table", mutate: func(manifest *Manifest, _ *TargetContract) {
manifest.Tables = append(manifest.Tables, TableDigest{Name: "users", RowCount: 1, SHA256: strings.Repeat("1", 64)})
}},
{name: "tampered bundle", mutate: func(manifest *Manifest, _ *TargetContract) { manifest.Search.DocumentCount++ }},
{name: "missing parse revision", mutate: func(manifest *Manifest, _ *TargetContract) { manifest.ParseRevision = "" }},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
candidate, contract := manifest, target
candidate.Tables = append([]TableDigest(nil), manifest.Tables...)
tt.mutate(&candidate, &contract)
if err := Validate(candidate, contract, sameIDMapping()); err == nil {
t.Fatal("Validate() error=nil")
}
})
}
}
func validManifest() Manifest {
return Manifest{
FormatVersion: "weknora-release-bundle/v1", ExecutionID: "rx-safe", PublicationID: "tp-safe",
SourceItemVersionID: "sv-safe", SourceRevision: "source-v1", ParseRevision: "parser-v1@index-v1", ReleaseRevision: "release-v1",
CreatedAt: time.Date(2026, 9, 6, 8, 0, 0, 0, time.UTC),
Contract: Contract{WeKnoraVersion: "v0.7.1", MigrationVersion: 74, TenantID: 10000, KnowledgeBaseID: "kb-safe", KnowledgeID: "knowledge-safe", RetrieverEngine: "elasticsearch_v8", EmbeddingModel: "bge-m3", EmbeddingDimension: 1024, ElasticsearchMappingSHA256: strings.Repeat("a", 64)},
Tables: []TableDigest{{Name: "knowledges", RowCount: 1, SHA256: strings.Repeat("b", 64)}, {Name: "chunks", RowCount: 1, SHA256: strings.Repeat("c", 64)}},
Search: SearchDigest{DocumentCount: 1, SHA256: strings.Repeat("d", 64)}, Assets: AssetDigest{SHA256: strings.Repeat("e", 64)},
}
}
func validTarget() TargetContract {
return TargetContract{WeKnoraVersion: "v0.7.1", MigrationVersion: 74, TenantID: 10000, KnowledgeBaseID: "kb-safe", RetrieverEngine: "elasticsearch_v8", EmbeddingModel: "bge-m3", EmbeddingDimension: 1024, ElasticsearchMappingSHA256: strings.Repeat("a", 64)}
}
// sameIDMapping is the historical semantics: no declared target, so the
// production base must carry the tested base's id.
func sameIDMapping() KnowledgeBaseMapping {
return KnowledgeBaseMapping{SourceKnowledgeBaseID: "kb-safe"}
}
// A declared 源库 → 目标库 mapping lets the bundle land in a DIFFERENT base,
// which is the whole point of the production binding setting.
func TestValidateAcceptsDeclaredCrossBaseMapping(t *testing.T) {
manifest, err := Seal(validManifest())
if err != nil {
t.Fatal(err)
}
target := validTarget()
target.KnowledgeBaseID = "prod-kb"
mapping := KnowledgeBaseMapping{SourceKnowledgeBaseID: "kb-safe", TargetKnowledgeBaseID: "prod-kb"}
if err := Validate(manifest, target, mapping); err != nil {
t.Fatalf("declared cross-base release must validate: %v", err)
}
}
// An undeclared, stale or self-inconsistent mapping must fail closed: neither
// "target drifted somewhere else" nor "mapping points at the source" passes.
func TestValidateRejectsUndeclaredOrDriftedMapping(t *testing.T) {
manifest, err := Seal(validManifest())
if err != nil {
t.Fatal(err)
}
tests := []struct {
name string
mapping KnowledgeBaseMapping
target string
}{
{name: "mapping source does not match bundle", mapping: KnowledgeBaseMapping{SourceKnowledgeBaseID: "other"}, target: "other"},
{name: "target differs from declared mapping", mapping: KnowledgeBaseMapping{SourceKnowledgeBaseID: "kb-safe", TargetKnowledgeBaseID: "prod-kb"}, target: "another-kb"},
{name: "mapping target differs while target keeps source id", mapping: KnowledgeBaseMapping{SourceKnowledgeBaseID: "kb-safe", TargetKnowledgeBaseID: "prod-kb"}, target: "kb-safe"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
target := validTarget()
target.KnowledgeBaseID = tt.target
if err := Validate(manifest, target, tt.mapping); err == nil {
t.Fatal("Validate() error=nil")
}
})
}
}