This commit is contained in:
2026-10-16 16:34:15 +08:00
parent 7a3769670b
commit ff366a4daf
80 changed files with 9961 additions and 888 deletions

View File

@@ -0,0 +1,85 @@
// 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 仍有效
});
});