f5e0f8d20f
build / build (push) Successful in 1m7s
将可维护总表与可替换2.0镜像分离,稳定来源关联和原始事件键保留; 源端更新/删除不覆盖主表或人工复核。首次收录保留原评估审计, 主表输入/复核独立持久化;历史原标注不冒充新AI或冻结证据。 历史导入绑定源哈希、逐行批准清单与真实恢复备份;三字段精确去重, 批次及原字段留档,重复执行不覆盖后续修改。仅操作本地治理库。 验证:完整make verify退出0;镜像与区域两组隔离PG race退出0; 真实恢复数据隔离导入12522、跳过792、暂缓1,总表15050; 重放保留模拟修改,结构down拒绝丢历史;真实浏览器第41周551条完整, 历史原标注和独立主表复核读回通过,移动布局及JS异常0。
88 lines
3.4 KiB
Go
88 lines
3.4 KiB
Go
package qaopsmirror
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"knowledge-governance/internal/dobotaiops"
|
|
pg "knowledge-governance/internal/platform/postgres"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestMasterTableKeepsLocalEditsAcrossSourceRefreshAndDeletion(t *testing.T) {
|
|
p := mirrorFixture(t)
|
|
ctx := context.Background()
|
|
s := NewStore(p, "fixture-master")
|
|
queue := pg.NewStore(p)
|
|
entry := fixtureEntry("master-source", "source original answer")
|
|
var wire map[string]any
|
|
if e := json.Unmarshal(entry.Payload, &wire); e != nil {
|
|
t.Fatal(e)
|
|
}
|
|
wire["asked_at"] = "2026-10-06T08:00:00+08:00"
|
|
wire["asker"] = "fixture asker"
|
|
entry.Payload, _ = json.Marshal(wire)
|
|
keeper := fixtureEntry("other-outside-window", "unrelated retained row")
|
|
source := &fixtureSource{rows: []dobotaiops.MirrorRecord{entry, keeper}}
|
|
job := claimMirrorJob(t, s, queue, "master-first")
|
|
if e := s.HandleSync(ctx, *job, func(context.Context) (Source, error) { return source, nil }); e != nil {
|
|
t.Fatal(e)
|
|
}
|
|
start, _ := time.Parse(time.RFC3339, "2026-10-05T00:00:00+08:00")
|
|
q := dobotaiops.RecordQuery{Window: dobotaiops.Window{TenantKey: s.tenant, Start: start, End: start.AddDate(0, 0, 7)}, Limit: 100}
|
|
if err := queue.Complete(ctx, job.ID, job.LeaseOwner, time.Now()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
page, e := s.ListMasterRecords(ctx, q)
|
|
if e != nil || page.Total != 1 || len(page.Items) != 1 {
|
|
t.Fatal("new source record was not admitted into the durable master", e)
|
|
}
|
|
if _, e = p.Exec(ctx, `UPDATE qa_ops_master_records SET payload=jsonb_set(payload,'{answer}','"local edited answer"') WHERE tenant_key=$1 AND id='master-source'`, s.tenant); e != nil {
|
|
t.Fatal(e)
|
|
}
|
|
wire["answer"] = "changed at source"
|
|
entry.Payload, _ = json.Marshal(wire)
|
|
entry.Key.Fingerprint = stringsForMasterFixture("b")
|
|
source.rows = []dobotaiops.MirrorRecord{entry, keeper}
|
|
job = claimMirrorJob(t, s, queue, "master-source-update")
|
|
if e = s.HandleSync(ctx, *job, func(context.Context) (Source, error) { return source, nil }); e != nil {
|
|
t.Fatal(e)
|
|
}
|
|
if err := queue.Complete(ctx, job.ID, job.LeaseOwner, time.Now()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
page, e = s.ListMasterRecords(ctx, q)
|
|
if e != nil || page.Items[0].Answer != "local edited answer" {
|
|
t.Fatal("source refresh overwrote master edits", e)
|
|
}
|
|
source.rows = []dobotaiops.MirrorRecord{keeper}
|
|
job = claimMirrorJob(t, s, queue, "master-source-delete")
|
|
if e = s.HandleSync(ctx, *job, func(context.Context) (Source, error) { return source, nil }); e != nil {
|
|
t.Fatal(e)
|
|
}
|
|
if err := queue.Complete(ctx, job.ID, job.LeaseOwner, time.Now()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
page, e = s.ListMasterRecords(ctx, q)
|
|
if e != nil || page.Total != 1 || page.Items[0].Answer != "local edited answer" {
|
|
t.Fatal("source deletion removed local master content", e)
|
|
}
|
|
if _, e = p.Exec(ctx, `UPDATE qa_ops_mirror_state SET ready=false,blocked=true WHERE tenant_key=$1`, s.tenant); e != nil {
|
|
t.Fatal(e)
|
|
}
|
|
page, e = s.ListMasterRecords(ctx, q)
|
|
if e != nil || page.Total != 1 {
|
|
t.Fatal("source availability blocked independent master data", e)
|
|
}
|
|
if _, e = p.Exec(ctx, `UPDATE qa_ops_master_records SET payload=jsonb_set(payload,'{id}','"relabelled-id"') WHERE tenant_key=$1 AND id='master-source'`, s.tenant); e == nil {
|
|
t.Fatal("master payload ID was allowed to diverge from the stable master ID")
|
|
}
|
|
}
|
|
func stringsForMasterFixture(s string) string {
|
|
out := ""
|
|
for i := 0; i < 64; i++ {
|
|
out += s
|
|
}
|
|
return out
|
|
}
|