Files
knowledge-governance/internal/releaseadapter/v071/document_target_test.go
T
姚章浩 ea6e56b304 feat(release): 文档发布会带上知识库 wiki 快照并支持撤权剥离
发布通道原来只有三条:PostgreSQL 关系行、Elasticsearch 分片、OSS 对象。
WeKnora 的 wiki 图谱是 wiki_pages 一张纯 PG 表(in_links/out_links 存 slug),
所以它不在任何通道里——撤权后生产 wiki 会一直留着已撤权文档的页面与引用。

本次新增第 5 条通道:库级 wiki 快照,挂在现有文档发布包里,检索与 wiki
同一个 release 单元(同一次执行、同一次校验、同一次回滚):

- 导出:ExportWiki 按 knowledge_base_id 全量读 wiki_pages/wiki_folders;
  收敛门禁(库内无解析中/合成中文档)不满足直接失败并给中文原因;
  未启用 wiki 且无 wiki 行的库不参与同步(生产 wiki 保持原样)。
- 撤权:item.Action == "delete" 时在导出端剥离该文档的 wiki 贡献
  (source_refs/chunk_refs 去条目、无引用页面整页不带、摘死链、清 parent_slug、
  无页面引用的空文件夹及其祖先一并去掉),使“源端仍在只停止发布”和
  “源端删除”两种情况收敛到同一结果。
- 导入:库级 replace-by-kb(同一事务内 DELETE by kb 后灌入快照,文件夹先于页面),
  新增 ReplaceWiki 供撤权路径使用(文档行只 disable,wiki 整体替换)。
- 校验:VerifyWiki 用生产 WeKnora wiki API 比对页面 slug 集合,撤权时额外断言
  生产 wiki 不再引用被撤权文档。

驱动与验收:新增 cmd/release-channel-e2e(复用发布 worker 真实代码路径,
支持 publish/withdraw/rollback),scripts/wiki_e2e_test5.sh、
scripts/wiki_e2e_newkb.sh 两套端到端脚本;证据见
docs/WIKI_CO_TRANSPORT_ACCEPTANCE.md。
2026-09-30 10:54:07 +08:00

182 lines
6.5 KiB
Go

package v071
import (
"context"
"os"
"reflect"
"testing"
"knowledge-governance/internal/releasebundle"
releaseworkflow "knowledge-governance/internal/workflows/release"
)
func TestDocumentTargetCoordinatesPrepareActivateVerifyAndRollback(t *testing.T) {
root := t.TempDir()
_ = os.Chmod(root, 0o700)
archive, _ := NewBundleArchive(root, 16*1024*1024)
current := testDocumentBundle(t)
prior := cloneBundleWithRevision(t, current, "prod-v0")
if err := archive.Save(prior); err != nil {
t.Fatal(err)
}
calls := []string{}
target, err := NewDocumentTarget(DocumentTargetConfig{Archive: archive, Contract: &stubContractInspector{contract: targetContractForBundle(current)}, Postgres: &stubPostgresTarget{calls: &calls}, Search: &stubSearchTarget{calls: &calls}, Objects: &stubObjectTarget{calls: &calls}, Verifier: &stubProductionVerifier{calls: &calls}})
if err != nil {
t.Fatal(err)
}
item := releasePlanForArchive()
item.PreviousProductionRevision = "prod-v0"
change, err := target.Prepare(context.Background(), item, current)
if err != nil {
t.Fatal(err)
}
if change.PreviousRevision != "prod-v0" || change.ProductionRevision != "review-v1" {
t.Fatalf("change=%#v", change)
}
if err := target.Activate(context.Background(), item, change); err != nil {
t.Fatal(err)
}
if err := target.Verify(context.Background(), item, change); err != nil {
t.Fatal(err)
}
if err := target.Compensate(context.Background(), item, change); err != nil {
t.Fatal(err)
}
if _, err := target.Restore(context.Background(), item, "prod-v0"); err != nil {
t.Fatal(err)
}
if err := target.VerifyRestored(context.Background(), item, "prod-v0"); err != nil {
t.Fatal(err)
}
want := []string{"object-stage:review-v1", "object-stage:review-v1", "object-promote:review-v1", "postgres-replace", "search-replace", "verify:review-v1", "verify-wiki:review-v1", "object-stage:prod-v0", "object-promote:prod-v0", "postgres-replace", "search-replace", "object-stage:prod-v0", "object-promote:prod-v0", "postgres-replace", "search-replace", "verify:prod-v0", "verify-wiki:prod-v0"}
if !reflect.DeepEqual(calls, want) {
t.Fatalf("calls=%v\nwant=%v", calls, want)
}
}
func TestDocumentTargetCompensatesFirstReleaseByDisableWithoutDelete(t *testing.T) {
root := t.TempDir()
_ = os.Chmod(root, 0o700)
archive, _ := NewBundleArchive(root, 16*1024*1024)
bundle := testDocumentBundle(t)
calls := []string{}
target, _ := NewDocumentTarget(DocumentTargetConfig{Archive: archive, Contract: &stubContractInspector{contract: targetContractForBundle(bundle)}, Postgres: &stubPostgresTarget{calls: &calls}, Search: &stubSearchTarget{calls: &calls}, Objects: &stubObjectTarget{calls: &calls}, Verifier: &stubProductionVerifier{calls: &calls}})
item := releasePlanForArchive()
change, err := target.Prepare(context.Background(), item, bundle)
if err != nil {
t.Fatal(err)
}
if change.PreviousRevision != "absent" {
t.Fatalf("change=%#v", change)
}
if err := target.Compensate(context.Background(), item, change); err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(calls, []string{"object-stage:review-v1", "postgres-disable", "search-disable", "verify-disabled"}) {
t.Fatalf("calls=%v", calls)
}
}
type stubContractInspector struct {
contract releasebundle.TargetContract
err error
}
func (s *stubContractInspector) Contract(context.Context, releaseworkflow.PlanItem) (releasebundle.TargetContract, error) {
return s.contract, s.err
}
type stubPostgresTarget struct {
calls *[]string
err error
}
func (s *stubPostgresTarget) Replace(_ context.Context, _ PostgresIdentity, _ PostgresSnapshot) error {
*s.calls = append(*s.calls, "postgres-replace")
return s.err
}
func (s *stubPostgresTarget) Disable(context.Context, PostgresIdentity) error {
*s.calls = append(*s.calls, "postgres-disable")
return s.err
}
func (s *stubPostgresTarget) ReplaceWiki(_ context.Context, _ PostgresIdentity, _ PostgresSnapshot) error {
*s.calls = append(*s.calls, "postgres-replace-wiki")
return s.err
}
type stubSearchTarget struct {
calls *[]string
err error
}
func (s *stubSearchTarget) Replace(_ context.Context, _ SearchIdentity, _ SearchSnapshot) (SearchSnapshot, error) {
*s.calls = append(*s.calls, "search-replace")
return SearchSnapshot{}, s.err
}
func (s *stubSearchTarget) Disable(context.Context, SearchIdentity) error {
*s.calls = append(*s.calls, "search-disable")
return s.err
}
type stubObjectTarget struct {
calls *[]string
err error
}
func (s *stubObjectTarget) Stage(_ context.Context, revision string, _ ObjectSnapshot) (ObjectStage, error) {
*s.calls = append(*s.calls, "object-stage:"+revision)
return ObjectStage{ReleaseRevision: revision}, s.err
}
func (s *stubObjectTarget) Promote(_ context.Context, stage ObjectStage) error {
*s.calls = append(*s.calls, "object-promote:"+stage.ReleaseRevision)
return s.err
}
type stubProductionVerifier struct {
calls *[]string
err error
}
func (s *stubProductionVerifier) Verify(_ context.Context, _ releaseworkflow.PlanItem, bundle *DocumentBundle) error {
*s.calls = append(*s.calls, "verify:"+bundle.Manifest.ReleaseRevision)
return s.err
}
func (s *stubProductionVerifier) VerifyDisabled(context.Context, releaseworkflow.PlanItem) error {
*s.calls = append(*s.calls, "verify-disabled")
return s.err
}
func (s *stubProductionVerifier) VerifyWiki(_ context.Context, _ releaseworkflow.PlanItem, bundle *DocumentBundle) error {
*s.calls = append(*s.calls, "verify-wiki:"+bundle.Manifest.ReleaseRevision)
return s.err
}
func cloneBundleWithRevision(t *testing.T, bundle *DocumentBundle, revision string) *DocumentBundle {
t.Helper()
clone := *bundle
clone.Manifest = bundle.Manifest
clone.Manifest.ReleaseRevision = revision
clone.Manifest.BundleSHA256 = ""
sealed, err := releasebundle.Seal(clone.Manifest)
if err != nil {
t.Fatal(err)
}
clone.Manifest = sealed
return &clone
}
func targetContractForBundle(bundle *DocumentBundle) releasebundle.TargetContract {
c := bundle.Manifest.Contract
return releasebundle.TargetContract{WeKnoraVersion: c.WeKnoraVersion, MigrationVersion: c.MigrationVersion, TenantID: c.TenantID, KnowledgeBaseID: c.KnowledgeBaseID, RetrieverEngine: c.RetrieverEngine, EmbeddingModel: c.EmbeddingModel, EmbeddingDimension: c.EmbeddingDimension, ElasticsearchMappingSHA256: c.ElasticsearchMappingSHA256}
}
func revisionFromPostgres(snapshot PostgresSnapshot) string {
if snapshot.Table("chunks").Rows[0][0] == '{' {
return "review-v1"
}
return "unknown"
}
func revisionFromSearch(snapshot SearchSnapshot) string {
if len(snapshot.Documents) > 0 {
return "review-v1"
}
return "unknown"
}