Files
knowledge-governance/web/src/libraryOverviewFetch.test.ts
T
姚章浩 98294b9583
build / build (push) Failing after 24s
fix(library): 统一执行身份、终态收尾、准入与数据身份
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 集成、故障注入、部署。
2026-10-30 16:14:54 +08:00

54 lines
2.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
})
})