dcdc8d1ddd
全栈知识治理平台首次入库(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 干净
101 lines
6.0 KiB
Go
101 lines
6.0 KiB
Go
package postgres
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
)
|
|
|
|
// TestP2StableIdentityAndProvenMove freezes docs plan P2:
|
|
// identity_confidence is never downgraded from stable_id; the canonical
|
|
// compare_catalog_change_set function classifies a 1:1 content-hash relocation
|
|
// as proven move evidence, and never merges ambiguous copies.
|
|
func TestP2StableIdentityAndProvenMove(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)
|
|
}
|
|
hash := strings.Repeat("c", 64)
|
|
if _, err := pool.Exec(ctx, `
|
|
INSERT INTO source_connections(id,source_type,name,enabled) VALUES('conn-id','pp','ID PP',true);
|
|
INSERT INTO source_catalog_snapshots(id,source_connection_id,snapshot_sha256,source_label,node_count,folder_count,file_count,observed_at)
|
|
VALUES('snap-old','conn-id',repeat('1',64),'ID PP',2,1,1,'2026-09-11T00:00:00Z'),
|
|
('snap-new','conn-id',repeat('2',64),'ID PP',2,1,1,'2026-09-12T00:00:00Z'),
|
|
('snap-copy','conn-id',repeat('3',64),'ID PP',3,1,2,'2026-09-13T00:00:00Z');
|
|
INSERT INTO source_catalog_nodes(snapshot_id,node_id,parent_node_id,node_type,name,relative_path,source_locator_hash,size_bytes,direct_child_count,subtree_file_count,subtree_bytes,content_sha256)
|
|
VALUES
|
|
('snap-old','root',NULL,'folder','ID PP','',repeat('4',64),NULL,1,1,10,NULL),
|
|
('snap-old','f-old','root','file','手册.pdf','旧目录/手册.pdf',repeat('5',64),10,0,1,10,repeat('c',64)),
|
|
('snap-new','root',NULL,'folder','ID PP','',repeat('4',64),NULL,1,1,10,NULL),
|
|
('snap-new','f-new','root','file','手册改名.pdf','新目录/手册改名.pdf',repeat('6',64),10,0,1,10,repeat('c',64)),
|
|
('snap-copy','root',NULL,'folder','ID PP','',repeat('4',64),NULL,2,2,20,NULL),
|
|
('snap-copy','f-new','root','file','手册改名.pdf','新目录/手册改名.pdf',repeat('6',64),10,0,1,10,repeat('c',64)),
|
|
('snap-copy','f-copy','root','file','副本.pdf','新目录/副本.pdf',repeat('7',64),10,0,1,10,repeat('c',64));
|
|
INSERT INTO source_catalog_change_sets(id,source_connection_id,from_snapshot_id,to_snapshot_id,created_at)
|
|
VALUES('cs-move','conn-id','snap-old','snap-new',NOW()),
|
|
('cs-copy','conn-id','snap-new','snap-copy',NOW());
|
|
SELECT compare_catalog_change_set('cs-move');
|
|
SELECT compare_catalog_change_set('cs-copy');`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Proven move: the disappeared old locator is a delete carrying move evidence,
|
|
// and the reappearing new locator is an add carrying the same content hash.
|
|
var deleteReason, deleteOldHash string
|
|
if err := pool.QueryRow(ctx, `SELECT reason_code,COALESCE(old_content_sha256::text,'') FROM source_catalog_change_items WHERE change_set_id='cs-move' AND action='delete'`).Scan(&deleteReason, &deleteOldHash); err != nil || deleteReason != "content_hash_move" || deleteOldHash != hash {
|
|
t.Fatalf("cs-move delete=%q hash=%q err=%v", deleteReason, deleteOldHash, err)
|
|
}
|
|
var addReason, addNewHash string
|
|
if err := pool.QueryRow(ctx, `SELECT reason_code,COALESCE(new_content_sha256::text,'') FROM source_catalog_change_items WHERE change_set_id='cs-move' AND action='add'`).Scan(&addReason, &addNewHash); err != nil || addReason != "content_hash_move" || addNewHash != hash {
|
|
t.Fatalf("cs-move add=%q hash=%q err=%v", addReason, addNewHash, err)
|
|
}
|
|
// Counts were recalculated by the function.
|
|
var addCount, deleteCount int
|
|
if err := pool.QueryRow(ctx, `SELECT add_count,delete_count FROM source_catalog_change_sets WHERE id='cs-move'`).Scan(&addCount, &deleteCount); err != nil || addCount != 1 || deleteCount != 1 {
|
|
t.Fatalf("cs-move counts add=%d delete=%d err=%v", addCount, deleteCount, err)
|
|
}
|
|
|
|
// Ambiguous copy: hash appears twice on the new side → NOT a proven move;
|
|
// the copy is a plain add and the original stays untouched.
|
|
var copyReason string
|
|
if err := pool.QueryRow(ctx, `SELECT reason_code FROM source_catalog_change_items WHERE change_set_id='cs-copy' AND action='add' AND new_path LIKE '%副本%'`).Scan(©Reason); err != nil || copyReason != "locator" {
|
|
t.Fatalf("ambiguous copy reason=%q err=%v", copyReason, err)
|
|
}
|
|
var copyDeletes int
|
|
if err := pool.QueryRow(ctx, `SELECT COUNT(*) FROM source_catalog_change_items WHERE change_set_id='cs-copy' AND action='delete'`).Scan(©Deletes); err != nil || copyDeletes != 0 {
|
|
t.Fatalf("ambiguous copy must not delete the original: %d err=%v", copyDeletes, err)
|
|
}
|
|
|
|
// identity_confidence upsert: stable_id never downgrades; path can rise.
|
|
if _, err := pool.Exec(ctx, `
|
|
INSERT INTO source_connections(id,source_type,name,enabled) VALUES('conn-id2','pp','ID PP2',true) ON CONFLICT DO NOTHING;
|
|
INSERT INTO source_items(id,source_connection_id,stable_external_id,canonical_uri_hash,normalized_path,title,item_type,revision,visibility,visibility_origin,identity_confidence)
|
|
VALUES('item-stable','conn-id','ext-stable',repeat('f',64),'稳.pdf','稳.pdf','document','v1','internal','pp_public_tag','stable_id') ON CONFLICT DO NOTHING;`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := pool.Exec(ctx, `
|
|
INSERT INTO source_items(id,source_connection_id,stable_external_id,canonical_uri_hash,normalized_path,title,item_type,revision,visibility,visibility_origin,source_change,identity_confidence)
|
|
VALUES('item-stable','conn-id','ext-stable',repeat('f',64),'','稳.pdf','document','v1','internal','manual','added','content_hash')
|
|
ON CONFLICT (source_connection_id, stable_external_id) DO UPDATE
|
|
SET updated_at = source_items.updated_at,
|
|
identity_confidence = CASE WHEN source_items.identity_confidence='stable_id' THEN source_items.identity_confidence ELSE EXCLUDED.identity_confidence END;`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var confidence string
|
|
if err := pool.QueryRow(ctx, `SELECT identity_confidence FROM source_items WHERE id='item-stable'`).Scan(&confidence); err != nil || confidence != "stable_id" {
|
|
t.Fatalf("stable_id downgraded to %q err=%v", confidence, err)
|
|
}
|
|
}
|