package httpapi import ( "context" "encoding/json" "errors" "net/http" "net/http/httptest" "strings" "testing" "time" "knowledge-governance/internal/governance" ) // 生产闸口与整库状态契约的针对性测试:不做数据库集成,只验证 // "证据不齐就不建作业" 与 "三类版本不互相顶替" 这两条硬规则。 // 复用既有 fakeJobStore 提供 Store 的其余方法,只覆盖整库作业这几个。 type fakeLibraryStore struct { fakeJobStore grant *governance.LibraryReleaseAuthorization versions []governance.LibraryVersion jobs []governance.LibraryJob createN int lastJob governance.LibraryJob lastState governance.LibraryJobState // current 让用例表达"窗口列表里看不到、但精确查询能查到当前生效版本"这一现场形状; // currentErr 表达"读不到"(调用方必须 fail closed,不许当成没有生效版本)。 current *governance.LibraryVersion currentErr error // operations/lastOperation 记录"意图身份":重放与换键的差别在这里表达(F01)。 operations map[string]governance.LibraryOperation lastOperation governance.LibraryOperation } func (f *fakeLibraryStore) CreateLibraryJob(_ context.Context, job governance.LibraryJob, _ []string, _ time.Time) (governance.LibraryJob, bool, error) { f.createN++ f.lastJob = job return job, false, nil } // EnsureLibraryOperation 在 fake 里就是"同键同摘要 ⇒ 同一条意图":测试用例靠它表达 // "重放命中同意图 / 换键是新意图"(F01),不需要真实库。 func (f *fakeLibraryStore) EnsureLibraryOperation(_ context.Context, op governance.LibraryOperation) (governance.LibraryOperation, bool, error) { if f.operations == nil { f.operations = map[string]governance.LibraryOperation{} } if existing, ok := f.operations[op.OperationKey]; ok { if existing.RequestDigest != op.RequestDigest { return existing, false, governance.ErrLibraryOperationConflict } return existing, false, nil } f.operations[op.OperationKey] = op f.lastOperation = op return op, true, nil } func (f *fakeLibraryStore) LibraryJob(_ context.Context, id string) (governance.LibraryJob, error) { return governance.LibraryJob{ID: id}, nil } func (f *fakeLibraryStore) ListLibraryJobs(_ context.Context, scope string, _ int) ([]governance.LibraryJob, error) { result := make([]governance.LibraryJob, 0, len(f.jobs)) for _, job := range f.jobs { if job.Scope == scope { result = append(result, job) } } return result, nil } func (f *fakeLibraryStore) RecordLibraryVersion(_ context.Context, version governance.LibraryVersion) error { f.versions = append(f.versions, version) return nil } func (f *fakeLibraryStore) LibraryVersions(_ context.Context, scope string, _ int) ([]governance.LibraryVersion, error) { result := make([]governance.LibraryVersion, 0, len(f.versions)) for _, version := range f.versions { if version.Scope == scope { result = append(result, version) } } return result, nil } // 当前生效版本是精确查询:这里刻意只按 (scope, kind, 未顶替) 过滤,不模拟分页窗口, // 与真实 Store 的契约一致(窗口只用于展示列表)。 func (f *fakeLibraryStore) LibraryCurrentVersion(_ context.Context, scope string) (governance.LibraryVersion, bool, error) { if f.currentErr != nil { return governance.LibraryVersion{}, false, f.currentErr } if f.current != nil { return *f.current, true, nil } for _, version := range f.versions { if version.Scope == scope && version.Kind == governance.LibraryVersionProductionEffective && version.SupersededAt == nil { return version, true, nil } } return governance.LibraryVersion{}, false, nil } func (f *fakeLibraryStore) LibraryVersionOfSnapshot(_ context.Context, scope string, kind governance.LibraryVersionKind, snapshotID string) (governance.LibraryVersion, bool, error) { for _, version := range f.versions { if version.Scope == scope && version.Kind == kind && version.SnapshotID == snapshotID { return version, true, nil } } return governance.LibraryVersion{}, false, nil } func (f *fakeLibraryStore) UpdateLibraryStage(_ context.Context, _, _, _ string, _, _, _, _ *int64, _ string, _ time.Time) error { return nil } func (f *fakeLibraryStore) AdvanceLibraryJob(_ context.Context, _ string, state governance.LibraryJobState, _, _, _, _, _ string, _ time.Time) error { f.lastState = state return nil } func (f *fakeLibraryStore) CreateLibraryAuthorization(_ context.Context, item governance.LibraryReleaseAuthorization) error { f.grant = &item return nil } func (f *fakeLibraryStore) ActiveLibraryAuthorization(_ context.Context, scope, snapshotID, digest string, now time.Time) (governance.LibraryReleaseAuthorization, bool, error) { if f.grant == nil || f.grant.Scope != scope || f.grant.SnapshotID != snapshotID || f.grant.ParamsDigest != digest { return governance.LibraryReleaseAuthorization{}, false, nil } if !f.grant.ValidAt(now) { return governance.LibraryReleaseAuthorization{}, false, nil } return *f.grant, true, nil } func (f *fakeLibraryStore) LibraryAuthorizations(context.Context, string, int) ([]governance.LibraryReleaseAuthorization, error) { if f.grant == nil { return nil, nil } return []governance.LibraryReleaseAuthorization{*f.grant}, nil } func (f *fakeLibraryStore) RevokeLibraryAuthorization(context.Context, string, time.Time) error { return nil } func (f *fakeLibraryStore) AttachLibraryQueueJob(_ context.Context, _, _ string, _ time.Time) error { return nil } func (f *fakeLibraryStore) CancelLibraryJob(_ context.Context, _, _ string, _ time.Time) error { f.lastState = governance.LibraryJobCancelled return nil } // 续作走原作业:只有 failed/cancelled 才能回到 pending。 func (f *fakeLibraryStore) PrepareLibraryJobResume(_ context.Context, jobID string, _ time.Time) (governance.LibraryJob, bool, error) { for index, job := range f.jobs { if job.ID != jobID { continue } if job.State != governance.LibraryJobFailed && job.State != governance.LibraryJobCancelled { return job, false, nil } f.jobs[index].State = governance.LibraryJobPending return f.jobs[index], true, nil } return governance.LibraryJob{}, false, nil } func (f *fakeLibraryStore) ResetQueueJobForResume(_ context.Context, _ string, _ time.Time) error { return nil } func TestLibrarySyncGateRequiresEveryPieceOfEvidence(t *testing.T) { known := libraryKnownView{State: "known", Version: &libraryVersionView{ID: "v1"}} cases := []struct { name string config Config production libraryKnownView prebackup libraryKnownView target string allowed bool reason string }{ { name: "default installation is closed", config: Config{}, production: libraryKnownView{State: "unknown"}, prebackup: libraryKnownView{State: "unknown"}, target: "snap-1", allowed: false, reason: "生产写入闸口关闭", }, { name: "flags on but restore evidence missing", config: Config{ProductionReleaseRequestsEnabled: true, LibraryProductionDatabaseURL: "postgres://prod.invalid/db", LibraryProductionInternalKB: "kb-int"}, production: known, prebackup: known, target: "snap-1", allowed: false, reason: "没有已核验的生产恢复演练证据", }, { name: "capacity budget missing blocks even when everything else is present", config: Config{ProductionReleaseRequestsEnabled: true, ProductionBackupRestoreVerified: true, LibraryProductionDatabaseURL: "postgres://prod.invalid/db", LibraryProductionInternalKB: "kb-int"}, production: known, prebackup: known, target: "snap-1", allowed: false, reason: "容量预算未设置", }, { name: "everything present", config: Config{ProductionReleaseRequestsEnabled: true, ProductionBackupRestoreVerified: true, LibraryProductionDatabaseURL: "postgres://prod.invalid/db", LibraryProductionInternalKB: "kb-int", LibraryCapacityBudgetBytes: 1 << 40}, production: known, prebackup: known, target: "snap-1", allowed: true, }, } for _, testCase := range cases { t.Run(testCase.name, func(t *testing.T) { gate := librarySyncGateWithGrant(testCase.config, testCase.production, testCase.prebackup, testCase.target, true) if gate.SyncAllowed != testCase.allowed { t.Fatalf("sync_allowed = %v, want %v (%v)", gate.SyncAllowed, testCase.allowed, gate.Reasons) } if testCase.reason != "" { joined := strings.Join(gate.Reasons, "|") if !strings.Contains(joined, testCase.reason) { t.Fatalf("reasons %v missing %q", gate.Reasons, testCase.reason) } } }) } } // 闸口关闭时 POST /api/v1/library/syncs 必须直接拒绝,并且**不创建**作业: // 不能留下一个永远失败的空作业,也不能让按钮看起来接通了。 func TestLibrarySyncEnqueueRefusesWithoutCreatingJob(t *testing.T) { store := &fakeLibraryStore{} config := Config{Store: store} request := httptest.NewRequest(http.MethodPost, "/api/v1/library/syncs", strings.NewReader(`{"scope":"internal","target_snapshot_id":"snap-1"}`)) recorder := httptest.NewRecorder() librarySyncEnqueue(recorder, request, config) if recorder.Code != http.StatusServiceUnavailable { t.Fatalf("status = %d, want 503", recorder.Code) } if store.createN != 0 { t.Fatalf("created %d jobs while gate is closed", store.createN) } var body struct { Code string `json:"code"` Reasons []string `json:"reasons"` } if err := json.Unmarshal(recorder.Body.Bytes(), &body); err != nil { t.Fatalf("decode body: %v", err) } if body.Code != "library_sync_blocked" || len(body.Reasons) == 0 { t.Fatalf("unexpected refusal body: %s", recorder.Body.String()) } } // 预备份是作业内第一步生成的,不能拿"启动前还没有预备份"当门禁: // 否则预备份只能由作业生成、作业又要求预备份,永远启动不了。 // 但"上一次预备份失败"必须阻断重试。 func TestLibrarySyncEnqueueDoesNotRequirePrebackupBeforeStart(t *testing.T) { store := &fakeLibraryStore{} config := Config{Store: store, ProductionReleaseRequestsEnabled: true, ProductionBackupRestoreVerified: true, LibraryProductionDatabaseURL: "postgres://prod.invalid/db", LibraryProductionInternalKB: "kb-int", LibraryCapacityBudgetBytes: 1 << 40} gate := librarySyncGateWithGrant(config, libraryKnownView{State: "known"}, libraryKnownView{State: "unknown"}, "snap-1", true) if !gate.SyncAllowed { t.Fatalf("missing prebackup must not block the start: %v", gate.Reasons) } failed := librarySyncGateWithGrant(config, libraryKnownView{State: "known"}, libraryKnownView{State: "failed"}, "snap-1", true) if failed.SyncAllowed || !strings.Contains(strings.Join(failed.Reasons, "|"), "预备份失败") { t.Fatalf("failed prebackup must block: %v", failed.Reasons) } } // 首次发布:生产没有生效版本,但目标为空 = 合法空基线。 func TestLibrarySyncGateAllowsFirstPublishOnEmptyBaseline(t *testing.T) { config := Config{ProductionReleaseRequestsEnabled: true, ProductionBackupRestoreVerified: true, LibraryProductionDatabaseURL: "postgres://prod.invalid/db", LibraryProductionInternalKB: "kb-int", LibraryCapacityBudgetBytes: 1 << 40} empty := libraryKnownView{State: "unknown", EmptyBaseline: true} if gate := librarySyncGateWithGrant(config, empty, libraryKnownView{State: "unknown"}, "snap-1", true); !gate.SyncAllowed { t.Fatalf("empty production must be a legal first-publish baseline: %v", gate.Reasons) } // 非空但没有权威版本:仍然阻断。 if gate := librarySyncGateWithGrant(config, libraryKnownView{State: "unknown"}, libraryKnownView{State: "unknown"}, "snap-1", true); gate.SyncAllowed { t.Fatal("unknown non-empty production must block") } } // 上一次预备份失败时必须阻断重试(预备份本身是作业内生成的,不能当启动前门禁)。 func TestLibrarySyncEnqueueRequiresPrebackupForTarget(t *testing.T) { store := &fakeLibraryStore{versions: []governance.LibraryVersion{{ ID: "ver-prod-1", Scope: "internal", Kind: governance.LibraryVersionProductionEffective, SnapshotID: "prod-snap", VersionLabel: "prod-2026.10", CreatedAt: time.Now().UTC(), }, { ID: "ver-backup-1", Scope: "internal", Kind: governance.LibraryVersionProductionPrebackup, SnapshotID: "snap-1", CreatedAt: time.Now().UTC(), }}} config := Config{Store: store, ProductionReleaseRequestsEnabled: true, ProductionBackupRestoreVerified: true, LibraryProductionDatabaseURL: "postgres://prod.invalid/db", LibraryProductionInternalKB: "kb-int", LibraryCapacityBudgetBytes: 1 << 40} request := httptest.NewRequest(http.MethodPost, "/api/v1/library/syncs", strings.NewReader(`{"scope":"internal","target_snapshot_id":"snap-1"}`)) recorder := httptest.NewRecorder() librarySyncEnqueue(recorder, request, config) // 没有授权仍然 503,但原因里不能再出现"还没有生产预备份"这种死循环门禁。 if recorder.Code != http.StatusServiceUnavailable { t.Fatalf("status = %d, want 503", recorder.Code) } if strings.Contains(recorder.Body.String(), "还没有生产预备份") { t.Fatalf("start gate must not require a prebackup that only the job can create: %s", recorder.Body.String()) } } // 证据齐全 + 有效授权时才会真正建作业并入队;缺授权一律拒绝且不建作业。 func TestLibrarySyncEnqueueCreatesJobOnlyWithGrant(t *testing.T) { target := "snap-1" digest, err := librarySyncParamsDigest("internal", target) if err != nil { t.Fatal(err) } store := &fakeLibraryStore{versions: []governance.LibraryVersion{ {ID: "ver-prod", Scope: "internal", Kind: governance.LibraryVersionProductionEffective, SnapshotID: "prod-snap", VersionLabel: "prod-2026.10", CreatedAt: time.Now().UTC()}, {ID: "ver-backup", Scope: "internal", Kind: governance.LibraryVersionProductionPrebackup, SnapshotID: target, CreatedAt: time.Now().UTC()}, }} store.enqueueEchoRequestedID = true 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 + `"}` // 先没有授权 recorder := httptest.NewRecorder() librarySyncEnqueue(recorder, httptest.NewRequest(http.MethodPost, "/api/v1/library/syncs", strings.NewReader(body)), config) if recorder.Code != http.StatusServiceUnavailable { t.Fatalf("without grant status=%d body=%s", recorder.Code, recorder.Body.String()) } if store.createN != 0 { t.Fatal("must not create a job without a grant") } // 补上绑定同一目标与快照的授权 now := time.Now().UTC() store.grant = &governance.LibraryReleaseAuthorization{ID: "auth-1", Scope: "internal", SnapshotID: target, ParamsDigest: digest, AuthorizedBy: "admin", ExpiresAt: now.Add(30 * time.Minute), CreatedAt: now} recorder = httptest.NewRecorder() librarySyncEnqueue(recorder, httptest.NewRequest(http.MethodPost, "/api/v1/library/syncs", strings.NewReader(body)), config) if recorder.Code != http.StatusAccepted { t.Fatalf("with grant status=%d body=%s", recorder.Code, recorder.Body.String()) } if store.createN != 1 { t.Fatalf("created=%d, want 1", store.createN) } if store.enqueued.Type != "library.sync" { t.Fatalf("enqueued type=%q, want library.sync", store.enqueued.Type) } if !strings.Contains(string(store.enqueued.Payload), store.lastJob.ID) { t.Fatalf("queue payload must carry the library job id: %s", store.enqueued.Payload) } } // 真实现场(2026-10-29 16:59 的按版本恢复):恢复作业登记了"恢复前备份", // 但那份备份的内容就是**恢复前**的生产状态,永远不可能等于测试封存快照。 // 旧逻辑只认"与测试封存同快照的预备份",于是刚备份完就显示"尚未创建/无记录"。 func TestLibraryOverviewShowsLatestPrebackupWhenTargetDiffers(t *testing.T) { root := libraryFixtureRoot(t) sealed := sealSharedVersions(t, root, 1, 4)[0] now := time.Now().UTC() store := &fakeLibraryStore{versions: []governance.LibraryVersion{ {ID: "ver-effective", Scope: "internal", Kind: governance.LibraryVersionProductionEffective, SnapshotID: "prod-restored-snapshot", CreatedAt: now}, {ID: "ver-prebackup", Scope: "internal", Kind: governance.LibraryVersionProductionPrebackup, SnapshotID: "snap-before-restore", SourceJobID: "librestore-internal-1", CreatedAt: now.Add(-time.Second)}, {ID: "ver-sealed", Scope: "internal", Kind: governance.LibraryVersionTestSealed, SnapshotID: sealed, CreatedAt: now.Add(-2 * time.Hour)}, }} config := Config{Store: store, LibraryArchiveRoot: root, LibraryCapacityBudgetBytes: 1 << 30} request := httptest.NewRequest(http.MethodGet, "/api/v1/library/overview?scope=internal", nil) recorder := httptest.NewRecorder() libraryOverviewHandler(recorder, request, config) if recorder.Code != http.StatusOK { t.Fatalf("status = %d: %s", recorder.Code, recorder.Body.String()) } var body struct { Production libraryKnownView `json:"production"` Prebackup libraryKnownView `json:"prebackup"` Versions []libraryVersionRef `json:"versions"` TestSealed *librarySnapshotView `json:"test_sealed"` Gate struct { SyncAllowed bool `json:"sync_allowed"` Reasons []string `json:"reasons"` } `json:"gate"` } if err := json.Unmarshal(recorder.Body.Bytes(), &body); err != nil { t.Fatalf("decode overview: %v", err) } if body.TestSealed == nil || body.TestSealed.SnapshotID != sealed { t.Fatalf("test sealed candidate must be the registered one: %+v", body.TestSealed) } if body.Prebackup.State != "known" || body.Prebackup.Version == nil || body.Prebackup.Version.ID != "ver-prebackup" || body.Prebackup.Version.SourceJobID != "librestore-internal-1" { t.Fatalf("latest registered prebackup must be visible with its job: %+v", body.Prebackup) } if body.Prebackup.BoundToTarget { t.Fatal("a restore prebackup is not the publish target snapshot") } // 三类版本都要出现在轻量清单里,页面才能给每份归档写对类型。 if len(body.Versions) != 3 { t.Fatalf("versions = %+v", body.Versions) } for _, reason := range body.Gate.Reasons { if strings.Contains(reason, "预备份") { t.Fatalf("a registered prebackup must not be reported as missing: %q", reason) } } } // 三类版本必须分开读:production_effective 才是生产当前,测试封存不能顶替。 func TestLibraryOverviewKeepsVersionKindsSeparate(t *testing.T) { now := time.Now().UTC() store := &fakeLibraryStore{versions: []governance.LibraryVersion{ {ID: "ver-test", Scope: "internal", Kind: governance.LibraryVersionTestSealed, SnapshotID: "snap-1", CreatedAt: now}, {ID: "ver-prod", Scope: "internal", Kind: governance.LibraryVersionProductionEffective, SnapshotID: "prod-snap", VersionLabel: "prod-2026.10", CreatedAt: now}, {ID: "ver-backup-other", Scope: "internal", Kind: governance.LibraryVersionProductionPrebackup, SnapshotID: "other-snap", CreatedAt: now}, }} config := Config{Store: store, LibraryArchiveRoot: t.TempDir()} request := httptest.NewRequest(http.MethodGet, "/api/v1/library/overview?scope=internal", nil) recorder := httptest.NewRecorder() libraryOverviewHandler(recorder, request, config) if recorder.Code != http.StatusOK { t.Fatalf("status = %d: %s", recorder.Code, recorder.Body.String()) } var body struct { Production libraryKnownView `json:"production"` Prebackup libraryKnownView `json:"prebackup"` Versions []libraryVersionRef `json:"versions"` Diff struct { State string `json:"state"` } `json:"diff"` Gate struct { SyncAllowed bool `json:"sync_allowed"` } `json:"gate"` } if err := json.Unmarshal(recorder.Body.Bytes(), &body); err != nil { t.Fatalf("decode overview: %v", err) } if body.Production.State != "known" || body.Production.Version == nil || body.Production.Version.ID != "ver-prod" { t.Fatalf("production must be the effective version, got %+v", body.Production) } // 预备份按"最近一次生产写入前的备份"显示:归档不可用时没有可匹配的目标, // 但登记过的预备份必须仍然可见(否则真备份了却显示"尚未创建"), // 并且明确标出它并不是本次拟发布快照。 if body.Prebackup.State != "known" || body.Prebackup.Version == nil || body.Prebackup.Version.ID != "ver-backup-other" { t.Fatalf("registered prebackup must stay visible: %+v", body.Prebackup) } if body.Prebackup.BoundToTarget { t.Fatalf("prebackup must not claim to be the publish target without one: %+v", body.Prebackup) } if len(body.Versions) != 3 { t.Fatalf("overview must expose every registered version kind, got %+v", body.Versions) } if body.Diff.State != "unknown" { t.Fatalf("diff must be unknown until the comparison is wired, got %q", body.Diff.State) } if body.Gate.SyncAllowed { t.Fatal("gate must stay closed") } } // 现场形状:历史把生效版本挤出了展示窗口(LibraryVersions 返回空),但"当前生效版本" // 精确查询仍读得到它。同步闸口必须据此认为生产已有权威版本: // 读成"没有"会把一次不必要的覆盖放行,并把人推向"重新建基线"。 func TestLibrarySyncEnqueueUsesExactCurrentVersionBeyondWindow(t *testing.T) { target := "snap-1" digest, err := librarySyncParamsDigest("internal", target) if err != nil { t.Fatal(err) } now := time.Now().UTC() store := &fakeLibraryStore{ // 窗口:故意什么都不返回(模拟生效版本掉出"最近 N 条")。 versions: []governance.LibraryVersion{}, 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} 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 + `"}` recorder := httptest.NewRecorder() librarySyncEnqueue(recorder, httptest.NewRequest(http.MethodPost, "/api/v1/library/syncs", strings.NewReader(body)), config) if recorder.Code != http.StatusAccepted { t.Fatalf("status=%d body=%s(窗口外读到生效版本时闸口不该拒绝)", recorder.Code, recorder.Body.String()) } if store.createN != 1 { t.Fatalf("created=%d, want 1", store.createN) } } // 读不到当前生效版本时不许猜:fail closed(500)且不创建作业。 func TestLibrarySyncEnqueueFailsClosedWhenCurrentVersionUnreadable(t *testing.T) { target := "snap-1" digest, err := librarySyncParamsDigest("internal", target) if err != nil { t.Fatal(err) } now := time.Now().UTC() store := &fakeLibraryStore{currentErr: errors.New("version storage down")} store.grant = &governance.LibraryReleaseAuthorization{ID: "auth-1", Scope: "internal", SnapshotID: target, ParamsDigest: digest, AuthorizedBy: "admin", ExpiresAt: now.Add(30 * time.Minute), CreatedAt: now} config := Config{Store: store, ProductionReleaseRequestsEnabled: true, ProductionBackupRestoreVerified: true, LibraryProductionDatabaseURL: "postgres://prod.invalid/db", LibraryProductionInternalKB: "kb-int", LibraryCapacityBudgetBytes: 1 << 40} recorder := httptest.NewRecorder() librarySyncEnqueue(recorder, httptest.NewRequest(http.MethodPost, "/api/v1/library/syncs", strings.NewReader(`{"scope":"internal","target_snapshot_id":"`+target+`"}`)), config) if recorder.Code != http.StatusInternalServerError { t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String()) } if store.createN != 0 { t.Fatalf("created %d jobs while the current version was unreadable", store.createN) } }