Files
knowledge-governance/internal/platform/postgres/scope_rules_integration_test.go
T
zheromiku dcdc8d1ddd feat: 知识治理平台(含已验证发布/回滚契约)
全栈知识治理平台首次入库(P1-P10 + 发布链路模拟验证闭环):

- 治理核心:源目录/变更集/人工审核/发布批次/发布执行(fail-closed 闸口)
- 测试通道:WeKnora 测试 KB 上传/轮询/验收;poll 完成时写入
  parser_generation/index_generation,修复 manual/seed 路径发布闸口永久拦截
- 发布适配器 v071(本地 source=测试角色 / 云端 target=生产角色 模拟验证通过):
  * ES 导出/导入 mapping 自适应(.keyword 子字段、可选 tenant_id)
  * OSS locator 支持嵌套 scheme;chunks.seq_id 由目标序列分配
  * bundle 归档同身份重试原子替换;verifier 按 bundle chunk_type 计数
  * 回滚 job payload 支持 prior_execution_id(strict 解码)
- 三通道发布+回滚演练实测:publish 4/4 verified,检索命中,
  rollback 4/4 rolled_back,目标端 PG is_enabled=false、ES is_enabled=false
- 前端 A·Data-console 风格全站统一,120 用例通过
- 门禁:go vet / go test 全包通过,gofmt 干净
2026-09-30 10:54:06 +08:00

138 lines
6.6 KiB
Go

package postgres
import (
"context"
"errors"
"os"
"testing"
"time"
"github.com/jackc/pgx/v5/pgxpool"
"knowledge-governance/internal/governance"
)
func TestScopeRulesResolveItemOverrideThenNearestDirectoryThenSourceDefault(t *testing.T) {
databaseURL := isolatedIntegrationDatabaseURL(t, os.Getenv("TEST_DATABASE_URL"))
if databaseURL == "" {
t.Skip("TEST_DATABASE_URL is not set")
}
ctx := context.Background()
pool, err := pgxpool.New(ctx, databaseURL)
if err != nil {
t.Fatal(err)
}
defer pool.Close()
if err := ApplyMigrations(ctx, pool); err != nil {
t.Fatal(err)
}
_, err = pool.Exec(ctx, `
INSERT INTO source_connections (id, source_type, name, enabled) VALUES ('conn-scope', 'pp', 'Scope PP', true);
INSERT INTO source_items (id, source_connection_id, stable_external_id, canonical_uri_hash, normalized_path, title, item_type, revision, visibility, visibility_origin)
VALUES
('item-a', 'conn-scope', 'a', repeat('a',64), '产品/说明书', 'A.pdf', 'document', 'v1', 'external', 'pp_public_tag'),
('item-b', 'conn-scope', 'b', repeat('b',64), '产品/说明书', 'B.pdf', 'document', 'v1', 'external', 'pp_public_tag');
INSERT INTO source_item_versions (id, source_item_id, revision, content_state)
VALUES ('version-a','item-a','v1','over_limit'), ('version-b','item-b','v1','over_limit');
INSERT INTO test_publications (id,source_item_version_id,test_kb_id,knowledge_id,ingest_state,processing_state,retrieval_state,updated_at)
VALUES ('publication-a','version-a','test-kb','knowledge-a','uploaded','completed','unpublished',NOW());
INSERT INTO acceptance_results (id,publication_id,run_id,check_type,state,policy_version,evidence_summary,observed_at)
VALUES
('accept-a-1','publication-a','run-a','knowledge_exists','passed','v1','{}',NOW()),
('accept-a-2','publication-a','run-a','chunks_present','passed','v1','{}',NOW()),
('accept-a-3','publication-a','run-a','retrieval_hit','manual_required','v1','{}',NOW());`)
if err != nil {
t.Fatal(err)
}
store := NewStore(pool)
now := time.Date(2026, 9, 8, 1, 0, 0, 0, time.UTC)
if _, err = store.MutateScopeRule(ctx, governance.ScopeRuleMutation{TargetType: governance.ScopeTargetDirectory, ConnectionID: "conn-scope", DirectoryPath: "产品", Visibility: governance.VisibilityInternal, OperatorRef: "engineer-1", Reason: "内部产品资料", Now: now}); err != nil {
t.Fatal(err)
}
reviews, err := store.ListReviewCandidates(ctx, governance.ReviewCandidateFilter{Limit: 10})
if err != nil || len(reviews.Items) != 1 || reviews.Items[0].CurrentVisibility != governance.VisibilityInternal {
t.Fatalf("review candidate effective scope=%#v err=%v", reviews, err)
}
if _, err = store.MutateScopeRule(ctx, governance.ScopeRuleMutation{TargetType: governance.ScopeTargetItem, SourceItemID: "item-b", Visibility: governance.VisibilityExternal, OperatorRef: "engineer-1", Reason: "该文件已公开", Now: now.Add(time.Minute)}); err != nil {
t.Fatal(err)
}
page, err := store.ListSourceAssets(ctx, governance.SourceAssetFilter{Limit: 10})
if err != nil {
t.Fatal(err)
}
got := map[string]governance.SourceAsset{}
for _, item := range page.Items {
got[item.ID] = item
}
if got["item-a"].Visibility != governance.VisibilityInternal || got["item-a"].VisibilityOrigin != "directory_rule" || got["item-a"].ScopeRulePath != "产品" {
t.Fatalf("directory effective scope = %#v", got["item-a"])
}
if got["item-b"].Visibility != governance.VisibilityExternal || got["item-b"].VisibilityOrigin != "file_override" {
t.Fatalf("item override = %#v", got["item-b"])
}
rules, err := store.ListScopeRules(ctx, "conn-scope")
if err != nil || len(rules) != 2 {
t.Fatalf("rules=%#v err=%v", rules, err)
}
if _, err = store.MutateScopeRule(ctx, governance.ScopeRuleMutation{TargetType: governance.ScopeTargetItem, SourceItemID: "item-b", Clear: true, OperatorRef: "engineer-1", Reason: "恢复目录设置", Now: now.Add(2 * time.Minute)}); err != nil {
t.Fatal(err)
}
page, _ = store.ListSourceAssets(ctx, governance.SourceAssetFilter{Limit: 10})
for _, item := range page.Items {
if item.ID == "item-b" && (item.Visibility != governance.VisibilityInternal || item.VisibilityOrigin != "directory_rule") {
t.Fatalf("cleared item = %#v", item)
}
}
if _, err = store.MutateScopeRule(ctx, governance.ScopeRuleMutation{TargetType: governance.ScopeTargetDirectory, ConnectionID: "conn-scope", DirectoryPath: "产品", Clear: true, OperatorRef: "engineer-1", Reason: "恢复来源设置", Now: now.Add(3 * time.Minute)}); err != nil {
t.Fatal(err)
}
page, _ = store.ListSourceAssets(ctx, governance.SourceAssetFilter{Limit: 10})
for _, item := range page.Items {
if item.Visibility != governance.VisibilityExternal || item.VisibilityOrigin != "pp_public_tag" {
t.Fatalf("source fallback = %#v", item)
}
}
var events int
if err = pool.QueryRow(ctx, `SELECT COUNT(*) FROM scope_change_events`).Scan(&events); err != nil || events != 4 {
t.Fatalf("events=%d err=%v", events, err)
}
}
func TestMutateScopeRuleWithOperationReplaysOnceAndRejectsConflict(t *testing.T) {
databaseURL := isolatedIntegrationDatabaseURL(t, os.Getenv("TEST_DATABASE_URL"))
if databaseURL == "" {
t.Skip("TEST_DATABASE_URL is not set")
}
ctx := context.Background()
pool, err := pgxpool.New(ctx, databaseURL)
if err != nil {
t.Fatal(err)
}
defer pool.Close()
if err := ApplyMigrations(ctx, pool); err != nil {
t.Fatal(err)
}
if _, err := pool.Exec(ctx, `INSERT INTO source_connections(id,source_type,name,enabled) VALUES('conn-scope-op','pp','Scope operation',true)`); err != nil {
t.Fatal(err)
}
store := NewStore(pool)
request := governance.ScopeRuleMutation{TargetType: governance.ScopeTargetDirectory, ConnectionID: "conn-scope-op", DirectoryPath: "产品", Visibility: governance.VisibilityInternal, OperatorRef: "engineer-1", Reason: "内部资料", Now: time.Date(2026, 9, 11, 9, 0, 0, 0, time.UTC)}
first, replayed, err := store.MutateScopeRuleWithOperation(ctx, request, "op-scope-1")
if err != nil || replayed || first.Visibility != governance.VisibilityInternal {
t.Fatalf("first=%#v replayed=%v err=%v", first, replayed, err)
}
second, replayed, err := store.MutateScopeRuleWithOperation(ctx, request, "op-scope-1")
if err != nil || !replayed || second != first {
t.Fatalf("second=%#v replayed=%v err=%v", second, replayed, err)
}
conflict := request
conflict.Reason = "不同的决定"
if _, _, err := store.MutateScopeRuleWithOperation(ctx, conflict, "op-scope-1"); !errors.Is(err, ErrOperationRequestConflict) {
t.Fatalf("conflict err=%v", err)
}
var events int
if err := pool.QueryRow(ctx, `SELECT COUNT(*) FROM scope_change_events`).Scan(&events); err != nil || events != 1 {
t.Fatalf("events=%d err=%v", events, err)
}
}