8a68ed1819
父审查确认四项是原 finding 未闭环,本包按真实调用链修完(先 RED 再修)。 R4a 正常流程必现阻断:API 先入 handoff 队列作业并关联,交接却只接受"未关联", 每次正常发布都被自己的所有权检查挡住。新增 governance.LibraryHandoffQueueJobID (jobs.id 为 varchar(36),原实现拼后缀超长,插入即失败);EnqueueAndAttachLibraryJob 增加"声明的前驱队列作业号",只允许当前无人持有或正是该 handoff 时替换,任意替换拒绝。 测试从 API 真实前驱(已关联)开始,覆盖正常交接、错误前驱拒绝、交接后重放、 以及 release 与 handoff 两条队列行都存在。 R4b 迟到 handoff 污染已交接作业:失败出口(含 Push 错误)改为所有权 CAS (FailLibraryHandoffIfOwner),只在业务作业仍由本次 handoff 持有时写 state/stage/recovery; 已交接/已终态零修改。所有权判定改为只看 library_jobs.queue_job_id(阶段行是否存在 与所有权无关,原实现会误判)。阶段写入也带所有权。用测试屏障确定性地复现"失败出口 到来时所有权已切给 release",验证 release running 与 succeeded 两种状态下零修改。 R2 回传游标每轮重置:改为跨轮保留(单轮仍 20 页预算),读到不足一页才回到起点; 新增 pullCursor 与跨轮覆盖测试(2100 条引用、每轮 5 页仍全部覆盖,不重复不漏取)。 R3 原产地误判:封存成功后会再登记 production_effective(同一快照),原实现把 sealed+target 判成冲突,正常再发布/历史恢复被误拒。现在原产地只由捕获事件决定 (test_sealed=源 / production_prebackup=目标),effective/recovery_verified 只是引用; 二者冲突或都没有捕获事件仍拒绝。测试覆盖正常成功链、真冲突、无捕获事件三种。 验证:隔离 fixture PostgreSQL 实跑 cmd/worker 全包通过(含上述四条真实调用链); libraryhandoff/librarysnapshot/httpapi/governance/platform-postgres/libraryctl 全绿; 完整 ci_checks 见提交时日志。未部署、未推送、未构建镜像;真实跨主机 SSH 链路仍未验证。
53 lines
1.8 KiB
Go
53 lines
1.8 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"encoding/json"
|
|
"testing"
|
|
|
|
"knowledge-governance/internal/governance"
|
|
)
|
|
|
|
// 搬运接线时,发布作业必须"先搬运":队列类型换成 handoff,释放作业的 ID 仍是基础 ID。
|
|
func TestLibraryQueueRequestGatesReleaseOnHandoff(t *testing.T) {
|
|
jobID := "libsync-internal-1"
|
|
snapshot := "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
|
|
|
|
id, queueType, payload, err := libraryQueueRequest(true, jobID, "internal", snapshot, librarySyncJobType)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if queueType != libraryHandoffJobType {
|
|
t.Fatalf("queue type = %q, want %q", queueType, libraryHandoffJobType)
|
|
}
|
|
if id != governance.LibraryHandoffQueueJobID(jobID) {
|
|
t.Fatalf("handoff queue id = %q", id)
|
|
}
|
|
// jobs.id 是 varchar(36):交接用的队列作业号必须能真正插进去(曾因拼后缀超长而必失败)。
|
|
if len(id) > 36 {
|
|
t.Fatalf("handoff queue id 长度 %d 超过 jobs.id 上限 36", len(id))
|
|
}
|
|
var decoded map[string]string
|
|
if err := json.Unmarshal(payload, &decoded); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if decoded["next_type"] != librarySyncJobType || decoded["target_snapshot_id"] != snapshot {
|
|
t.Fatalf("handoff payload = %#v", decoded)
|
|
}
|
|
|
|
// 未接线:行为与今天完全一致(直接投递发布作业,ID 是基础 ID)。
|
|
id, queueType, payload, err = libraryQueueRequest(false, jobID, "internal", snapshot, libraryRestoreJobType)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if queueType != libraryRestoreJobType || id != governance.QueueJobIDFor(jobID) {
|
|
t.Fatalf("unconfigured queue request = %q %q", queueType, id)
|
|
}
|
|
decoded = map[string]string{}
|
|
if err := json.Unmarshal(payload, &decoded); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, present := decoded["next_type"]; present {
|
|
t.Fatal("release payload must not carry next_type when handoff is off")
|
|
}
|
|
}
|