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,89 @@
// src/app.js onFilesChanged —— handler 并发闸(audit P1-4)
//
// 背景:原代码
// const myToken = ++fsHandlerToken; // 自增但还没 SET
// if (inFlightToken !== 0) { return; } // 检查还是 0
// inFlightToken = myToken; // 才 SET
// 两行间是 microtask 窗口:burst 50 events 时多个 callback 排在同一 microtask
// queue 里,每个都看到 inFlightToken === 0,全部过闸 → handler 并发跑
// (applyEntries 双跑、scanDir 双发、UI 闪烁)。
//
// 修复:抽 makeLock() 工厂 → { tryClaim, release, current }。tryClaim 是同步
// 「check-and-set」原子操作:if 锁空闲则 SET 并返新 token,否则返 null。
// 在 callback 入口 tryClaim → null 直接 return → 不再丢事件,统一存到
// pendingRescanRel(既有逻辑保留)。
//
// 测试策略:直接 import 工厂函数,模拟 burst 场景验证「N 次 tryClaim 只有
// 第一次成功」。
import { describe, it, expect } from 'vitest';
import { makeLock } from '../../src/fs-handler-lock.js';
describe('fs-handler lock(audit P1-4)', () => {
it('初始空闲 → tryClaim 返非零 token,current 反映该 token', () => {
const lock = makeLock();
const t = lock.tryClaim();
expect(t).toBeGreaterThan(0);
expect(lock.current()).toBe(t);
});
it('已占用时 tryClaim 返 null,current 不变', () => {
const lock = makeLock();
const t1 = lock.tryClaim();
const t2 = lock.tryClaim();
expect(t1).toBeGreaterThan(0);
expect(t2).toBe(null);
expect(lock.current()).toBe(t1);
});
it('release(token) → 锁重新空闲,tryClaim 能拿到新 token', () => {
const lock = makeLock();
const t1 = lock.tryClaim();
expect(lock.release(t1)).toBe(true);
expect(lock.current()).toBe(0);
const t2 = lock.tryClaim();
expect(t2).toBeGreaterThan(0);
expect(t2).not.toBe(t1);
});
it('release(非当前 token) → 返 false,锁不被错误释放', () => {
const lock = makeLock();
const t1 = lock.tryClaim();
expect(lock.release(99999)).toBe(false);
expect(lock.current()).toBe(t1);
});
it('burst 50 次 tryClaim → 只有 1 次成功', () => {
// 关键测试:模拟 microtask 批量场景
const lock = makeLock();
const results = Array.from({ length: 50 }, () => lock.tryClaim());
const successes = results.filter((r) => r !== null);
expect(successes).toHaveLength(1);
// 其余 49 次全部 null
expect(results.filter((r) => r === null)).toHaveLength(49);
});
it('release → tryClaim → 再 burst:交替可串行化多批事件', () => {
const lock = makeLock();
for (let batch = 0; batch < 5; batch++) {
const t = lock.tryClaim();
expect(t).not.toBe(null);
// 本批 10 次并发全应被拒
for (let i = 0; i < 10; i++) {
expect(lock.tryClaim()).toBe(null);
}
// 释放
expect(lock.release(t)).toBe(true);
}
});
it('token 自增:每次成功 tryClaim 返的 token 都不同', () => {
const lock = makeLock();
const t1 = lock.tryClaim();
lock.release(t1);
const t2 = lock.tryClaim();
lock.release(t2);
const t3 = lock.tryClaim();
expect(new Set([t1, t2, t3]).size).toBe(3);
});
});