afe9affc2a
build / build (push) Successful in 1m27s
为什么:远端 CI 的格式门在推送后判红(run 258,job build,format=failure), 本地 `make verify` 那次跑的是"拉取前"的文件集,漏掉了随后新增/改写的三个文件。 失败项:internal/httpapi/library_intent_test.go、internal/jobs/lease_loss_test.go、 internal/platform/postgres/library_protection.go。 怎么验:`gofmt -l internal/ cmd/` 输出为空;`python3 scripts/check_format.py` rc=0 (Go:✅,前端:✅)。只改空白,不改逻辑。
126 lines
6.2 KiB
Go
126 lines
6.2 KiB
Go
package httpapi
|
||
|
||
import (
|
||
"net/http"
|
||
"net/http/httptest"
|
||
"strings"
|
||
"testing"
|
||
"time"
|
||
|
||
"knowledge-governance/internal/governance"
|
||
)
|
||
|
||
// F01:同一个目标快照的**新意图**必须真的执行,不能被"这个快照曾经成功过"吞掉。
|
||
//
|
||
// 现场形状:同步 B 成功 → 恢复 A → 再次同步 B。第三次请求是一次合法意图,
|
||
// 旧实现返回第一次的成功作业(202, 不再并入队),用户要的状态没实现。
|
||
//
|
||
// 判据:换 operation_key ⇒ 新 operation(ID 不同、作业幂等键不同);
|
||
// 同一个 operation_key ⇒ 同一 operation(重放)。
|
||
func TestLibrarySyncNewIntentReExecutesInsteadOfReusingHistory(t *testing.T) {
|
||
target := "snap-1"
|
||
digest, err := librarySyncParamsDigest("internal", target)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
now := time.Now().UTC()
|
||
newStore := func() *fakeLibraryStore {
|
||
store := &fakeLibraryStore{
|
||
current: &governance.LibraryVersion{ID: "ver-prod", Scope: "internal",
|
||
Kind: governance.LibraryVersionProductionEffective, SnapshotID: "prod-snap",
|
||
VersionLabel: "prod-2026.10", CreatedAt: now.Add(-72 * time.Hour)},
|
||
}
|
||
store.enqueueEchoRequestedID = true
|
||
store.grant = &governance.LibraryReleaseAuthorization{ID: "auth-1", Scope: "internal", SnapshotID: target,
|
||
ParamsDigest: digest, AuthorizedBy: "admin", ExpiresAt: now.Add(30 * time.Minute), CreatedAt: now}
|
||
return store
|
||
}
|
||
config := func(store *fakeLibraryStore) Config {
|
||
return Config{Store: store, ProductionReleaseRequestsEnabled: true, ProductionBackupRestoreVerified: true,
|
||
LibraryProductionDatabaseURL: "postgres://prod.invalid/db", LibraryProductionInternalKB: "kb-int",
|
||
LibraryCapacityBudgetBytes: 1 << 40}
|
||
}
|
||
post := func(store *fakeLibraryStore, body string) *httptest.ResponseRecorder {
|
||
recorder := httptest.NewRecorder()
|
||
librarySyncEnqueue(recorder, httptest.NewRequest(http.MethodPost, "/api/v1/library/syncs",
|
||
strings.NewReader(body)), config(store))
|
||
return recorder
|
||
}
|
||
withKey := `{"scope":"internal","target_snapshot_id":"` + target + `","operation_key":"%s"}`
|
||
|
||
// 第一次意图:建作业。
|
||
first := newStore()
|
||
if recorder := post(first, strings.Replace(withKey, "%s", "intent-1", 1)); recorder.Code != http.StatusAccepted {
|
||
t.Fatalf("首次发布会话 status=%d body=%s", recorder.Code, recorder.Body.String())
|
||
}
|
||
firstOperation := first.lastOperation.ID
|
||
if firstOperation == "" || first.lastJob.OperationID != firstOperation {
|
||
t.Fatalf("作业必须挂在意图上:job.operation=%q intent=%q", first.lastJob.OperationID, firstOperation)
|
||
}
|
||
|
||
// 同一次意图重放(双击/超时重发):同一个 operation,不产生第二条意图。
|
||
replay := newStore()
|
||
if recorder := post(replay, strings.Replace(withKey, "%s", "intent-1", 1)); recorder.Code != http.StatusAccepted {
|
||
t.Fatalf("重放 status=%d body=%s", recorder.Code, recorder.Body.String())
|
||
}
|
||
if replay.lastOperation.ID != firstOperation {
|
||
t.Fatalf("同一意图键得到了不同 operation:%q vs %q", replay.lastOperation.ID, firstOperation)
|
||
}
|
||
|
||
// 新意图(同一快照、不同键):必须是新 operation ⇒ 新作业幂等键 ⇒ 真的执行。
|
||
second := newStore()
|
||
if recorder := post(second, strings.Replace(withKey, "%s", "intent-2", 1)); recorder.Code != http.StatusAccepted {
|
||
t.Fatalf("新意图 status=%d body=%s", recorder.Code, recorder.Body.String())
|
||
}
|
||
if second.lastOperation.ID == firstOperation {
|
||
t.Fatal("同一快照的新意图必须得到新 operation:否则历史成功会吞掉本次请求(F01)")
|
||
}
|
||
if second.lastJob.IdempotencyKey == first.lastJob.IdempotencyKey {
|
||
t.Fatal("新意图的作业幂等键与历史作业相同:CreateLibraryJob 会返回旧作业,什么也不执行")
|
||
}
|
||
if second.createN != 1 {
|
||
t.Fatalf("新意图必须建作业并执行,created=%d", second.createN)
|
||
}
|
||
}
|
||
|
||
// 同一个意图键配上不同快照 = 冲突,不能悄悄共享谱系。
|
||
func TestLibrarySyncIntentKeyReusedWithAnotherSnapshotConflicts(t *testing.T) {
|
||
target := "snap-1"
|
||
digest, _ := librarySyncParamsDigest("internal", target)
|
||
now := time.Now().UTC()
|
||
store := &fakeLibraryStore{current: &governance.LibraryVersion{ID: "ver-prod", Scope: "internal",
|
||
Kind: governance.LibraryVersionProductionEffective, SnapshotID: "prod-snap", CreatedAt: now.Add(-time.Hour)}}
|
||
store.grant = &governance.LibraryReleaseAuthorization{ID: "auth-1", Scope: "internal", SnapshotID: target,
|
||
ParamsDigest: digest, AuthorizedBy: "admin", ExpiresAt: now.Add(time.Hour), CreatedAt: now}
|
||
// 预先登记一条"同键但不同快照"的意图。
|
||
if _, _, err := store.EnsureLibraryOperation(nil, governance.LibraryOperation{
|
||
ID: "libop-x", Scope: "internal", Kind: governance.LibraryJobSync, TargetSnapshotID: "other-snap",
|
||
RequestDigest: "different", OperationKey: target + "#intent-1", CreatedAt: now}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
config := Config{Store: store, ProductionReleaseRequestsEnabled: true, ProductionBackupRestoreVerified: true,
|
||
LibraryProductionDatabaseURL: "postgres://prod.invalid/db", LibraryProductionInternalKB: "kb-int",
|
||
LibraryCapacityBudgetBytes: 1 << 40}
|
||
body := `{"scope":"internal","target_snapshot_id":"` + target + `","operation_key":"intent-1"}`
|
||
recorder := httptest.NewRecorder()
|
||
librarySyncEnqueue(recorder, httptest.NewRequest(http.MethodPost, "/api/v1/library/syncs", strings.NewReader(body)), config)
|
||
if recorder.Code != http.StatusConflict {
|
||
t.Fatalf("同键不同内容必须 409,status=%d body=%s", recorder.Code, recorder.Body.String())
|
||
}
|
||
}
|
||
|
||
// F14:通用 queue 入口不许创建整库保留类型——那里的角色检查替代不了发布准入。
|
||
func TestCreateJobRejectsReservedLibraryTypes(t *testing.T) {
|
||
store := &fakeJobStore{}
|
||
config := Config{Store: store}
|
||
body := `{"type":"library.baseline","payload":{"scope":"internal"},"idempotency_key":"k-1"}`
|
||
recorder := httptest.NewRecorder()
|
||
createJob(recorder, httptest.NewRequest(http.MethodPost, "/api/v1/jobs", strings.NewReader(body)), config)
|
||
if recorder.Code != http.StatusBadRequest {
|
||
t.Fatalf("保留类型必须被通用入口拒绝:status=%d body=%s", recorder.Code, recorder.Body.String())
|
||
}
|
||
if !strings.Contains(recorder.Body.String(), "发布管理专用入口") {
|
||
t.Fatalf("拒绝原因必须指向专用入口:%s", recorder.Body.String())
|
||
}
|
||
}
|