Files
Literature-Management/tests/unit/renderer/lib/bootstrap-single-flight.test.ts
2026-10-21 14:42:59 +08:00

140 lines
5.3 KiB
TypeScript

// tests/unit/renderer/lib/bootstrap-single-flight.test.ts
//
// 守卫(r206,2026-10-20):bootstrap 的**单飞**(single-flight)契约。
//
// 起因(r85 recon):main.tsx 后台 IIFE 第一行发 bootstrap,App.tsx passive
// effect 又「幂等兜底」地看 usePathsStore.loaded 决定要不要再发 —— 但
// React passive effect flush(<16ms)永远早于 bootstrap IPC 往返(50-200ms),
// loaded 此刻必然还是 false,所以**每次冷启都并发两次 bootstrap IPC**
// (dev StrictMode 三次)。main.tsx 里那句「loaded 守卫保证不会跑 legacy
// fallback」是**不生效的假守卫**。
//
// 契约(本文件逐条钉):
// - 并发调用命中同一 promise ⇒ 只发**一次** IPC;
// - 成功结果(**含 ok:false 的合法回复**)被缓存 ⇒ 不重发 —— 与
// main.tsx「失败后置 loaded=true」的「本轮启动尝试过」语义一致;
// - rejection(桥未就绪 / IPC 抛)清缓存 ⇒ 瞬时 race 之后重调可再试
// (这才是原注释「幂等兜底」想表达却没做到的行为);
// - 桥缺失(api.bootstrap 不是函数)转成 **rejection** 而不是同步 throw
// —— 符合 r83 契约(调用点带同线 typeof guard,Proxy 属性读安全)。
//
// 隔离手段:bootstrap.ts 的 inflight 是**模块级**变量 ⇒ 每例
// vi.resetModules() + 动态 import 拿全新模块实例。
import { afterEach, describe, expect, it, vi } from 'vitest';
import type { BootstrapPayload, IpcResult } from '@shared/types';
const OK_PAYLOAD = {
settings: {} as BootstrapPayload['settings'],
paths: {} as BootstrapPayload['paths'],
} as BootstrapPayload;
async function freshBootstrap() {
vi.resetModules();
return await import('@renderer/lib/bootstrap');
}
function setBridge(value: unknown): void {
(window as unknown as { api?: unknown }).api = value;
}
afterEach(() => {
setBridge(undefined);
vi.restoreAllMocks();
});
describe('bootstrap 单飞(r206)', () => {
it('并发调用只发一次 IPC', async () => {
const mod = await freshBootstrap();
const bootstrap = vi.fn().mockResolvedValue({ ok: true, data: OK_PAYLOAD } as IpcResult<BootstrapPayload>);
setBridge({ bootstrap });
const [a, b] = await Promise.all([mod.bootstrapOnce(), mod.bootstrapOnce()]);
expect(bootstrap).toHaveBeenCalledTimes(1);
expect(a).toBe(b); // 同一个 promise ⇒ 同一个结果对象
});
it('三个并发调用仍只发一次', async () => {
const mod = await freshBootstrap();
const bootstrap = vi.fn().mockResolvedValue({ ok: true, data: OK_PAYLOAD } as IpcResult<BootstrapPayload>);
setBridge({ bootstrap });
await Promise.all([mod.bootstrapOnce(), mod.bootstrapOnce(), mod.bootstrapOnce()]);
expect(bootstrap).toHaveBeenCalledTimes(1);
});
it('成功结果被缓存:后续调用不再发 IPC', async () => {
const mod = await freshBootstrap();
const bootstrap = vi.fn().mockResolvedValue({ ok: true, data: OK_PAYLOAD } as IpcResult<BootstrapPayload>);
setBridge({ bootstrap });
await mod.bootstrapOnce();
await mod.bootstrapOnce();
expect(bootstrap).toHaveBeenCalledTimes(1);
});
it('ok:false 的合法回复也缓存(不重发)', async () => {
const mod = await freshBootstrap();
const bootstrap = vi
.fn()
.mockResolvedValue({ ok: false, code: 'INTERNAL', message: 'x' } as IpcResult<BootstrapPayload>);
setBridge({ bootstrap });
const first = await mod.bootstrapOnce();
const second = await mod.bootstrapOnce();
expect(bootstrap).toHaveBeenCalledTimes(1);
expect(first).toBe(second);
expect(first.ok).toBe(false);
});
it('rejection 清缓存:之后重调能再试(幂等兜底真的兜住了)', async () => {
const mod = await freshBootstrap();
const bootstrap = vi
.fn()
.mockRejectedValueOnce(new Error('bridge race'))
.mockResolvedValueOnce({ ok: true, data: OK_PAYLOAD } as IpcResult<BootstrapPayload>);
setBridge({ bootstrap });
await expect(mod.bootstrapOnce()).rejects.toThrow('bridge race');
const retry = await mod.bootstrapOnce();
expect(bootstrap).toHaveBeenCalledTimes(2);
expect(retry.ok).toBe(true);
});
it('桥缺失 ⇒ 返回 rejected promise(不是同步 throw)', async () => {
const mod = await freshBootstrap();
setBridge(undefined);
// 同步 throw 会让调用点的 try/catch 之外代码炸掉;契约要求是 rejection。
let promise: Promise<IpcResult<BootstrapPayload>> | null = null;
expect(() => {
promise = mod.bootstrapOnce();
}).not.toThrow();
await expect(promise!).rejects.toThrow(/bridge missing/);
});
it('桥在但方法不是函数 ⇒ 同样转成 rejection', async () => {
const mod = await freshBootstrap();
setBridge({ bootstrap: 'not-a-function' });
await expect(mod.bootstrapOnce()).rejects.toThrow(/bridge missing/);
});
it('单飞是**跨调用**的:第二个 awaiter 拿到的还是第一次的结果', async () => {
const mod = await freshBootstrap();
const bootstrap = vi.fn().mockResolvedValue({ ok: true, data: OK_PAYLOAD } as IpcResult<BootstrapPayload>);
setBridge({ bootstrap });
const p1 = mod.bootstrapOnce();
const p2 = mod.bootstrapOnce();
expect(p1).toBe(p2); // 连 promise 引用都相同
await p1;
await p2;
expect(bootstrap).toHaveBeenCalledTimes(1);
});
});