5defe238e0
build / build (push) Successful in 1m34s
真实事故:源 KB(10000/0c84a098) 同步到目标 KB(10001/e64e82b5) 时,归档行被原样 插入目标库,落在来源租户/知识库;资源阶段随即报"生产缺少资源 ... 的存储路径" (资源查询按目标租户过滤,看不见刚写进去的行)。回滚只按目标范围回放,于是页面 报"已恢复到预备份",目标却多出 4 文件/38 分块/21 资源/4 绑定。 改: - 写入与校验共用 retargetRow:tenant_id / knowledge_base_id / 资源定位符(后端、 provider、桶、租户前缀) / storage_backend_id / provider / embedding_model_id 统一映射到目标范围;行里的来源身份对不上就失败,宁可不写。 - Verify 增加"本次写集合仍以来源身份残留"判定;Restore 增加整库指纹比对(预备份 时刻 vs 恢复后)与孤儿资源清理,回滚后用 Verify 复核,恢复工具退出 0 不再等于 恢复完成。 - 新增 cmd/libraryresidue:按归档身份精确、可备份、可复核地清理已知的范围外残留。 验证:make verify 全绿;真实 PG+ES 用例复现"源目标 KB 不同 + 目标非空 + 资源阶段 失败",断言回滚后无来源范围残留、其它租户行未动、恢复后内容=预备份;正向用例断言 行落在目标范围且资源定位符指向目标存储。
102 lines
4.7 KiB
Go
102 lines
4.7 KiB
Go
package v071
|
|
|
|
import (
|
|
"encoding/json"
|
|
"testing"
|
|
)
|
|
|
|
// 同步把字节写进目标桶,但资源行是从快照原样搬的:定位符里写着来源桶时,
|
|
// 目标实例之后取图/附件会指向测试环境的桶(跨环境引用)或直接取不到。
|
|
// 只换桶、不换键,两种定位符形状都要能改。
|
|
func TestRewriteLocatorBucketKeepsKeyAndSwapsBucket(t *testing.T) {
|
|
for _, testCase := range []struct{ locator, want string }{
|
|
{"storage://45064e4f-434c-4752-a3ae-5fb1e2f54a7d/minio://kgmaint-1028a/10001/k/ab.md",
|
|
"storage://45064e4f-434c-4752-a3ae-5fb1e2f54a7d/minio://kgseal-1029a/10001/k/ab.md"},
|
|
{"oss://source-bucket/10/k/ab.png", "oss://target-bucket/10/k/ab.png"},
|
|
} {
|
|
bucket := "kgseal-1029a"
|
|
if testCase.locator[:3] == "oss" {
|
|
bucket = "target-bucket"
|
|
}
|
|
got, ok := rewriteLocatorBucket(testCase.locator, bucket)
|
|
if !ok || got != testCase.want {
|
|
t.Fatalf("rewrite(%q)=%q ok=%v want %q", testCase.locator, got, ok, testCase.want)
|
|
}
|
|
}
|
|
// 解析不出来就报错,不猜:宁可失败也不能写一行指向别处的资源。
|
|
for _, bad := range []string{"", "storage://x/", "storage://backend/minio://bucket", "oss://bucket/"} {
|
|
if _, ok := rewriteLocatorBucket(bad, "target"); ok {
|
|
t.Fatalf("非法定位符被接受了:%q", bad)
|
|
}
|
|
}
|
|
if _, ok := rewriteLocatorBucket("oss://bucket/key", ""); ok {
|
|
t.Fatal("没有目标桶时不该改写")
|
|
}
|
|
// 裸对象键里没有桶(落盘用配置桶),原样返回而不是失败。
|
|
if got, ok := rewriteLocatorBucket("weknora/10000/k/ab.docx", "target"); !ok || got != "weknora/10000/k/ab.docx" {
|
|
t.Fatalf("裸键被改写了:%q ok=%v", got, ok)
|
|
}
|
|
}
|
|
|
|
// 只换桶不够:storage://<id> 是来源实例 storage_backends 的主键。目标实例没有这条行时,
|
|
// 原版应用会以 storage backend not found 落到默认服务并拒绝整条路径(真跑 500 code1007),
|
|
// 所以后端 id 必须一起换成本次写入解析到的目标 id。
|
|
func TestRewriteLocatorSwapsStorageBackendWithBucket(t *testing.T) {
|
|
got, ok := rewriteLocatorTarget("storage://source-backend/minio://kgmaint-1028a/10001/k/ab.md",
|
|
"target-backend", "kgseal-1029a")
|
|
if !ok || got != "storage://target-backend/minio://kgseal-1029a/10001/k/ab.md" {
|
|
t.Fatalf("rewrite=%q ok=%v", got, ok)
|
|
}
|
|
// 没有解析到后端时不猜:保留原 id(调用方在没有目标桶时不会走到这里)。
|
|
got, ok = rewriteLocatorTarget("storage://source-backend/minio://b/k", "", "b2")
|
|
if !ok || got != "storage://source-backend/minio://b2/k" {
|
|
t.Fatalf("rewrite without backend=%q ok=%v", got, ok)
|
|
}
|
|
if _, ok := rewriteLocatorTarget("storage://x/minio://b/k", "../evil", "b2"); ok {
|
|
t.Fatal("非法后端 id 被接受了")
|
|
}
|
|
}
|
|
|
|
// 行变换用解析到的目标后端与目标桶:写入与校验共用这一份,摘要才不会两边打架。
|
|
func TestResourceRowTransformRewritesBackendAndBucket(t *testing.T) {
|
|
target := &ProductionLibraryTarget{config: ProductionLibraryConfig{ObjectsBucket: "kgseal-1029a"},
|
|
storageBackend: "target-backend", snapshotTenant: 10001, snapshotKB: "kb",
|
|
scope: ProductionScope{TenantID: 10001, KnowledgeBaseID: "kb"}}
|
|
row := map[string]json.RawMessage{
|
|
"tenant_id": json.RawMessage(`10001`),
|
|
"physical_path": json.RawMessage(`"storage://source-backend/minio://kgmaint-1028a/10001/k/ab.md"`)}
|
|
if err := target.rowTransform("resources")(row); err != nil {
|
|
t.Fatalf("row transform failed: %v", err)
|
|
}
|
|
var got string
|
|
if err := json.Unmarshal(row["physical_path"], &got); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got != "storage://target-backend/minio://kgseal-1029a/10001/k/ab.md" {
|
|
t.Fatalf("transform=%q", got)
|
|
}
|
|
}
|
|
|
|
// 存储后端 id 在 resources 表里还有独立一列:原版应用按它找文件服务。
|
|
// 只改定位符不改这一列,目标仍是 storage backend not found(真跑 500)。
|
|
func TestResourceRowTransformRewritesBackendColumn(t *testing.T) {
|
|
target := &ProductionLibraryTarget{config: ProductionLibraryConfig{ObjectsBucket: "kgseal-1029a"},
|
|
storageBackend: "target-backend", snapshotTenant: 10001, snapshotKB: "kb",
|
|
scope: ProductionScope{TenantID: 10001, KnowledgeBaseID: "kb"}}
|
|
row := map[string]json.RawMessage{
|
|
"tenant_id": json.RawMessage(`10001`),
|
|
"physical_path": json.RawMessage(`"storage://source-backend/minio://kgmaint-1028a/1/k/ab.md"`),
|
|
"storage_backend_id": json.RawMessage(`"source-backend"`),
|
|
}
|
|
if err := target.rowTransform("resources")(row); err != nil {
|
|
t.Fatalf("row transform failed: %v", err)
|
|
}
|
|
var backend string
|
|
if err := json.Unmarshal(row["storage_backend_id"], &backend); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if backend != "target-backend" {
|
|
t.Fatalf("storage_backend_id=%q", backend)
|
|
}
|
|
}
|