98294b9583
build / build (push) Failing after 24s
why:审计 19 项(12 P1 / 7 P2)来自同一批根因,逐点试错会把协议撕成十几份:
· 意图与执行混为一谈(F01/F15):`library.sync:<scope>:<快照>` 是终身幂等键,
A→B→A 的第三次发布拿到第一次的历史成功作业,什么都不做;
· 版本行既是实体又是事件(F02/F09):主键只由 (kind,scope,快照) 决定、写入是
upsert,第二个作业覆盖 `source_job_id`/`evidence`/`created_at`;
· 接管没有代次(F13):两个 worker 都能认领同一 pending 作业,失租的旧执行还能
写业务状态、释放别人的维护锁;
· 早退与终态没有统一政策(F03/F04/F05/F06):半写窗口的前置检查失败会重新开门、
服务起不来也照样开入口、journal 终态后收尾不可重建、恢复成功不回写 current;
· 数据身份与准入各写一套(F07/F08/F14/F17):接管丢了复用语义、全库搬运不改
location_hash、通用入口可造 library.* 作业、目标模型从没被问过;
· 存储/容量/UI/CI 判据各自为政(F10/F11/F12/F16/F18/F19)。
what:按批准的一套协议集中实施(详见 handoffs/release-consolidated-implementation/
current-contract.md):
· operation(一次明确意图)/ attempt(epoch 代次)/ 不可变版本事件 / current 指针;
治理库迁移 76,只动治理库,不碰 WeKnora Schema/源码/镜像;
· 所有业务状态、阶段、版本事件、维护锁写入带 epoch 守卫(失租代次写不进去);
心跳失租立刻取消 handler;
· 半写窗口早退保持关闭;服务未全部健康绝不开入口;journal 终态可从持久事实重建
收尾;恢复成功登记 recovery 事件并推进 current;
· worker 在动维护前核对业务行/kind/scope/队列关联;通用入口拒绝 library.*;
· 全库搬运重算 location_hash 并与文档级导入共用实现;接管消费写集合的复用决定;
发布前核目标实时模型契约、容量门禁前置;
· 存储保护闭包改为全量依赖、类型只认显式登记、保护未知则不出可删候选;
· 页面核 HTTP 状态与结构、恢复纳入轮询、Wiki 差异覆盖标题/摘要/父级。
how:`make verify` rc=0(16 项必需检查全绿,含 go test 全量与前端 485 例);
`go vet ./...` rc=0;`npx tsc -p tsconfig.json --noEmit` rc=0;逐项负例见各包测试
(F03/F04/F08/F12/F13/F14/F16/F18/F19 都有先红后绿记录)。
未执行:治理库迁移到任何已有库、真实 PG/ES/S3/WeKnora 集成、故障注入、部署。
54 lines
2.1 KiB
TypeScript
54 lines
2.1 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||
import { fetchLibraryOverview } from './librarySnapshots'
|
||
|
||
// F16:overview 的非 2xx 响应曾被当成正常状态,页面随后在 `overview.gate.sync_allowed`
|
||
// 上抛 TypeError(白屏)。这里把三种真实失败形状固定下来:401/503 与畸形 JSON。
|
||
const originalFetch = globalThis.fetch
|
||
|
||
afterEach(() => {
|
||
globalThis.fetch = originalFetch
|
||
vi.restoreAllMocks()
|
||
})
|
||
|
||
describe('fetchLibraryOverview(HTTP 状态与结构)', () => {
|
||
it('401 的 {error} 不许被当成发布状态', async () => {
|
||
globalThis.fetch = vi.fn(
|
||
async () => new Response(JSON.stringify({ error: 'unauthenticated' }), { status: 401 })
|
||
) as unknown as typeof fetch
|
||
await expect(fetchLibraryOverview('internal')).rejects.toThrow(/HTTP 401/)
|
||
})
|
||
|
||
it('503 的闸口拒绝同样必须报错', async () => {
|
||
globalThis.fetch = vi.fn(
|
||
async () => new Response(JSON.stringify({ available: false, reason: '存储不可用' }), { status: 503 })
|
||
) as unknown as typeof fetch
|
||
await expect(fetchLibraryOverview('internal')).rejects.toThrow(/HTTP 503/)
|
||
})
|
||
|
||
it('200 但缺 gate/jobs 的结构不符也要报错', async () => {
|
||
globalThis.fetch = vi.fn(
|
||
async () => new Response(JSON.stringify({ scope: 'internal' }), { status: 200 })
|
||
) as unknown as typeof fetch
|
||
await expect(fetchLibraryOverview('internal')).rejects.toThrow(/结构不符合预期/)
|
||
})
|
||
|
||
it('结构正确的 200 正常返回(避免把正常请求也拦掉)', async () => {
|
||
const payload = {
|
||
scope: 'internal',
|
||
production: { state: 'unknown' },
|
||
prebackup: { state: 'unknown' },
|
||
diff: { state: 'unknown', added: [], updated: [], removed: [] },
|
||
jobs: [],
|
||
gate: { sync_allowed: false, reasons: [] },
|
||
authorization: { state: 'none' },
|
||
capture_available: false,
|
||
now: new Date().toISOString(),
|
||
}
|
||
globalThis.fetch = vi.fn(
|
||
async () => new Response(JSON.stringify(payload), { status: 200 })
|
||
) as unknown as typeof fetch
|
||
const value = await fetchLibraryOverview('internal')
|
||
expect(value?.gate.sync_allowed).toBe(false)
|
||
})
|
||
})
|