update
This commit is contained in:
@@ -180,3 +180,144 @@ describe('fs-watcher updateSnapshot —— push 决策回归 (Round 13 fix fce2d
|
||||
});
|
||||
});
|
||||
|
||||
// audit R7a P1-19:fs-watcher 轮询改为「自适应间隔 + 不 drop」pending
|
||||
// 单测覆盖三件事:
|
||||
// 1. getPollInterval:lastChangeAt 在 ACTIVE_WINDOW 内 → 500ms,否则 5s
|
||||
// 2. markActivity 刷新 lastChangeAt → 立刻切到 active
|
||||
// 3. pending 语义:in-flight 期间触发的 tick 不丢,in-flight 完成后立即补一次
|
||||
describe('fs-watcher 自适应轮询 (audit R7a P1-19)', () => {
|
||||
it('getPollInterval:初始(lastChangeAt=0,距 now 远)→ QUIET 5000ms', () => {
|
||||
const w = makeWatcher();
|
||||
expect(w._test_getPollInterval()).toBe(5000);
|
||||
});
|
||||
|
||||
it('getPollInterval:markActivity 后立即 → ACTIVE 500ms', () => {
|
||||
const w = makeWatcher();
|
||||
w._test_markActivity();
|
||||
expect(w._test_getPollInterval()).toBe(500);
|
||||
});
|
||||
|
||||
it('getPollInterval:markActivity 后等到 ACTIVE_WINDOW 之外 → QUIET', async () => {
|
||||
const w = makeWatcher();
|
||||
w._test_markActivity();
|
||||
expect(w._test_getPollInterval()).toBe(500);
|
||||
// 模拟时间推进 POLL_ACTIVE_WINDOW_MS + 1:用 setTimeout 等真实时间。
|
||||
// 2100ms 等得起,测试只跑一次。备选用 vi.useFakeTimers + setSystemTime
|
||||
// 也行,但 fake timer + markActivity 内部 Date.now() 兼容性有边角问题。
|
||||
await new Promise((r) => setTimeout(r, 2100));
|
||||
expect(w._test_getPollInterval()).toBe(5000);
|
||||
});
|
||||
|
||||
it('pending 语义:in-flight 期间触发的 tick 不丢,in-flight 完后立即补', async () => {
|
||||
// 设计要点:pendingPoll 是**幂等**标志,不是队列。
|
||||
// - in-flight 期间任何 tick 都只把 pendingPoll 置 true(不重复累计)
|
||||
// - in-flight 完成后 finally 检查 pendingPoll:true → scheduleNextPoll(0)
|
||||
// - 紧跟的「补」是 1 次新 scanDir,不是 N 次
|
||||
// 所以「3 次 tick + resolve」→ scanCalls 期望 2(1 原 + 1 补),不是 3。
|
||||
//
|
||||
// 旧版(fixed 2s + pollInFlight drop)的语义是:3 次 tick 期间全部被 drop,
|
||||
// scanCalls 期望 1(且永远不会补)。这条测试是回归锚点:
|
||||
// 拿掉 pendingPoll 行为 → scanCalls 在 resolve 后仍为 1,测试失败。
|
||||
//
|
||||
// 时序坑:test 里 `await new Promise(r => setTimeout(r, 0))` 是先于
|
||||
// runPollTick finally 调度的;Node 同 delay 的 setTimeout 严格按调度顺序
|
||||
// 触发,test 的会先 fire,导致 scanCalls 还在 1 时 expect(2) 失败。
|
||||
// 修:用一个 polling 等待「scanCalls 达到 2」,让事件循环多跑几个 tick。
|
||||
let resolveScan;
|
||||
const scanCalls = [];
|
||||
const w = createFsWatcher({
|
||||
getMainWindow: () => null,
|
||||
resolveDataDir: () => '/data',
|
||||
scanDir: async () => {
|
||||
scanCalls.push(Date.now());
|
||||
// 慢:等我们手动 resolve
|
||||
await new Promise((r) => { resolveScan = r; });
|
||||
return { ok: true, entries: [] };
|
||||
},
|
||||
});
|
||||
// 跳过 fs.watch 订阅(测试不依赖真实目录事件)
|
||||
w._test_setWatchedDir('/data');
|
||||
// 第一次 tick:in-flight 启动,scanDir 进入 await
|
||||
w._test_runPollTick();
|
||||
// 第二次 tick:in-flight 还在,pendingPoll = true,scanDir 不再调用
|
||||
w._test_runPollTick();
|
||||
// 第三次 tick:同样 pendingPoll = true(幂等)
|
||||
w._test_runPollTick();
|
||||
// 期望 1 次 in-flight scanDir,pendingPoll 期间 0 次额外调用
|
||||
expect(scanCalls.length).toBe(1);
|
||||
// 解除 in-flight:第一次 scanDir 完成 → runPollTick finally 触发「补一次」
|
||||
resolveScan({ ok: true, entries: [] });
|
||||
// 等「补」的 scanDir 真的发起(scanCalls 长度变 2)。最坏情况:
|
||||
// in-flight 微任务 + emitFilesChanged 微任务 + setTimeout(0) 调度 +
|
||||
// setTimeout(0) 触发 + 新 runPollTick 进入 + 新 scanDir push —— 几十 ms 内。
|
||||
await new Promise((resolve) => {
|
||||
const start = Date.now();
|
||||
const tick = () => {
|
||||
if (scanCalls.length >= 2) return resolve();
|
||||
if (Date.now() - start > 200) return resolve(); // 兜底
|
||||
setTimeout(tick, 5);
|
||||
};
|
||||
tick();
|
||||
});
|
||||
expect(scanCalls.length).toBe(2);
|
||||
// 把「补」触发的第 2 次 scanDir 也 resolve 掉,不让它的 in-flight
|
||||
// 干扰其它测试
|
||||
if (resolveScan) resolveScan({ ok: true, entries: [] });
|
||||
// 关键:runPollTick 的 finally 走「无 pending → scheduleNextPoll(
|
||||
// getPollInterval())」会挂 5s 后的 timer。不显式清掉的话,timer 触发时
|
||||
// 又会调一次 scanDir(scanCalls 变 3),且新 scanDir 等到没人 resolve
|
||||
// 会一直挂着 —— watcher + scanCalls 闭包泄漏到进程结束,vitest 报告
|
||||
// "tests passed" 但实际上 open handle 没释放。pause 清 dirPollTimer +
|
||||
// pollPaused,让 watcher 真正失活。
|
||||
w._test_pauseDirPolling();
|
||||
});
|
||||
|
||||
// audit R7a P1-19 bugfix:pauseDirPolling 必须真正停止轮询。回归锚点:
|
||||
// - in-flight tick 还在 await scanDir 时调 pause
|
||||
// - scanDir 完成后 runPollTick finally 走 scheduleNextPoll(...)
|
||||
// - 旧版(无 pollPaused 闸)→ 仍挂 5s 后才退到的定时器
|
||||
// - 修后(有 pollPaused 闸)→ 不挂任何新 timer,轮询真停
|
||||
// 拿掉 pollPaused 检查 → _test_hasPollTimer() 在 resolveScan 后仍返回 true,
|
||||
// 本测试失败。仅查 scanCalls 长度不够 —— 5s 间隔下 50ms 等不出新 tick,
|
||||
// 必须直接断言 dirPollTimer 是否被挂。
|
||||
it('pause 期间 in-flight runPollTick finally 不 re-schedule', async () => {
|
||||
let resolveScan;
|
||||
const scanCalls = [];
|
||||
const w = createFsWatcher({
|
||||
getMainWindow: () => null,
|
||||
resolveDataDir: () => '/data',
|
||||
scanDir: async () => {
|
||||
scanCalls.push(Date.now());
|
||||
await new Promise((r) => { resolveScan = r; });
|
||||
return { ok: true, entries: [] };
|
||||
},
|
||||
});
|
||||
w._test_setWatchedDir('/data');
|
||||
// 启动一个 in-flight tick(scanDir 进入 await)→ runPollTick 被 setTimeout
|
||||
// 调度时 dirPollTimer 已被清,所以这一步后 hasPollTimer() = false
|
||||
w._test_runPollTick();
|
||||
expect(scanCalls.length).toBe(1);
|
||||
expect(w._test_hasPollTimer()).toBe(false);
|
||||
// 暂停:模拟窗口最小化 → pollPaused = true
|
||||
w._test_pauseDirPolling();
|
||||
// in-flight scanDir 完成 → runPollTick finally 执行:
|
||||
// 旧版:scheduleNextPoll(getPollInterval()) 挂 5s 后的 timer
|
||||
// 修后:pollPaused 短路,dirPollTimer 保持 null
|
||||
resolveScan({ ok: true, entries: [] });
|
||||
// 等 microtask + setTimeout(0) 都跑过。50ms 足够覆盖几轮事件循环。
|
||||
await new Promise((r) => setTimeout(r, 50));
|
||||
// 核心断言:pause 之后不应该有新的 poll timer 被挂上
|
||||
expect(w._test_hasPollTimer()).toBe(false);
|
||||
});
|
||||
|
||||
// 配对测试:resume 后必须能再跑轮询。验证 pollPaused 不会让恢复路径失活。
|
||||
it('resume 后轮询可重新启动', () => {
|
||||
const w = makeWatcher();
|
||||
w._test_setWatchedDir('/data');
|
||||
w._test_pauseDirPolling();
|
||||
// pause 后调 resume —— 不报错、watchedDir 还在即可(startDirPoll 在
|
||||
// makeWatcher 里没有真实 scanDir,所以不验证 emit 次数)。
|
||||
expect(() => w._test_resumeDirPolling()).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user