Files
Notes/tests/unit/cleanup-epoch.test.js
2026-10-16 16:34:15 +08:00

86 lines
3.4 KiB
JavaScript
Raw Permalink 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.
// main/ai.js cancelAll —— 顺序竞争 epoch 闸(audit P1-5)
//
// 背景:cancelAll 内部三步
// 1) for (controller of pending.values()) controller.abort();
// 2) pending.clear();
// 3) inFlightRequestIds.clear();
// 三步非原子。竞态:cancelAll 走到 step 1 刚 abort 完,runEdit 同步阶段
// 已经把「新 requestId + 新 controller」加进 pending + inFlightRequestIds →
// step 2/3 把这条新 entry 也清掉,runEdit 自以为还在跑,await 不到结果,
// 任何同 requestId 重试都找不到 controller。
//
// 修复:抽 makeCleanupEpoch() 工厂 → { snapshot, invalidate, current, isCurrent }。
// - cancelAll 入口 invalidate() → 序号 + 1(任何已 snapshot 的 in-flight 任务
// 之后 isCurrent() === false → 自行 bail)
// - runEdit 入口 snapshot() 拿当前序号;await 后 isCurrent() === false
// (说明本任务期间发生过 cancelAll)→ 不再 apply 任何副作用、直接 return
//
// 测试策略:纯函数工厂,无 fs / network / Electron 依赖。
import { describe, it, expect } from 'vitest';
import { makeCleanupEpoch } from '../../main/cleanup-epoch.js';
describe('cleanup epoch(audit P1-5)', () => {
it('初始:current = 0,snapshot(0) isCurrent 成立', () => {
const ep = makeCleanupEpoch();
expect(ep.current()).toBe(0);
expect(ep.snapshot()).toBe(0);
expect(ep.isCurrent(ep.snapshot())).toBe(true);
});
it('invalidate() → current 自增,isCurrent 旧 snapshot 返 false', () => {
const ep = makeCleanupEpoch();
const oldSnap = ep.snapshot();
ep.invalidate();
expect(ep.current()).toBeGreaterThan(oldSnap);
expect(ep.isCurrent(oldSnap)).toBe(false);
});
it('invalidate 之后再 snapshot 拿新序号,isCurrent 成立', () => {
const ep = makeCleanupEpoch();
ep.invalidate();
const newSnap = ep.snapshot();
expect(ep.isCurrent(newSnap)).toBe(true);
});
it('多次 invalidate 序号单调递增', () => {
const ep = makeCleanupEpoch();
const seqs = Array.from({ length: 5 }, () => {
const before = ep.current();
ep.invalidate();
return ep.current() - before;
});
// 每次都增 1
expect(seqs).toEqual([1, 1, 1, 1, 1]);
});
it('runEdit 模式:A 拿快照 → cancelAll → A 的 isCurrent 返 false', () => {
// 模拟 P1-5 竞态:A 在 cancelAll 之前 snapshot,cancelAll 之后 A 检查 → 失效
const ep = makeCleanupEpoch();
const aSnap = ep.snapshot();
// ... await 期间 ...
ep.invalidate(); // cancelAll 触发
expect(ep.isCurrent(aSnap)).toBe(false);
});
it('runEdit 模式:B 在 cancelAll 之后 snapshot → isCurrent 成立', () => {
// 新请求在 cancelAll 之后进入,snapshot 拿新序号 → isCurrent true
const ep = makeCleanupEpoch();
ep.invalidate();
const bSnap = ep.snapshot();
expect(ep.isCurrent(bSnap)).toBe(true);
});
it('cancelAll 期间 burst:B1 → cancelAll → B2 都拿自己的快照,互不影响', () => {
const ep = makeCleanupEpoch();
const a1 = ep.snapshot();
ep.invalidate();
const b1 = ep.snapshot();
ep.invalidate();
const b2 = ep.snapshot();
expect(ep.isCurrent(a1)).toBe(false); // A1 被 cancelAll 失效
expect(ep.isCurrent(b1)).toBe(false); // B1 也被第二次 cancelAll 失效
expect(ep.isCurrent(b2)).toBe(true); // B2 仍有效
});
});