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) } }