86 lines
3.4 KiB
JavaScript
86 lines
3.4 KiB
JavaScript
// 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 仍有效
|
||
});
|
||
});
|