feat: 0.9.0 忽略规则、增量缓存跨会话持久化、快照基线、字符级预算自适应与性能基准E2E - 忽略规则(gitignore简化子集:目录整树剪枝不枚举不计数、扩展名通配与名字匹配、随偏好持久化)、扫描增量缓存落盘userData跨会话自动注入(LRU 8对、防御性解析、渲染端零改动)、快照基线(保存/加载JSON对照六类状态变迁统计与条目标注、对照随重扫刷新、报告附快照口径)、字符级变更组预算随输入规模自适应(下限与固定值一致小输入零变化、大输入按总组流半数放宽上限100万封顶)、大文件性能基准E2E(5万行<15s与300KB GBK<10s耗时断言)+新功能E2E、文档与版本号同步

This commit is contained in:
2026-08-18 22:58:00 +08:00
parent fa13160848
commit 35fa81df9d
26 changed files with 2525 additions and 550 deletions
+113
View File
@@ -581,3 +581,116 @@ describe('scanFolders - 增量重扫(previous 缓存)', () => {
}
})
})
describe('scanFolders - 忽略规则', () => {
it('目录规则整树剪枝:子树不枚举、不计数、不出现在结果与缓存', async () => {
const l = await mkdtemp(join(tmpdir(), 'difflens-ign1-l-'))
const r = await mkdtemp(join(tmpdir(), 'difflens-ign1-r-'))
try {
await writeFile(join(l, 'keep.txt'), 'a')
await writeFile(join(r, 'keep.txt'), 'a')
// 深层子树:node_modules/pkg/dist/x.js(全部应被剪枝)
for (const d of [l, r]) {
await mkdir(join(d, 'node_modules', 'pkg', 'dist'), { recursive: true })
await writeFile(join(d, 'node_modules', 'pkg', 'dist', 'x.js'), 'x')
await writeFile(join(d, 'node_modules', 'pkg', 'y.js'), 'y')
}
const res = await scanFolders(l, r, { ignore: ['node_modules/'] })
expect(res.entries.map((e) => e.rel)).toEqual(['keep.txt'])
expect(res.total).toBe(1)
expect(res.truncated).toBe(false)
expect(res.cache.map((c) => c.rel)).toEqual(['keep.txt'])
} finally {
await rm(l, { recursive: true, force: true })
await rm(r, { recursive: true, force: true })
}
})
it('扩展名通配与名字匹配跳过文件(大小写不敏感)', async () => {
const l = await mkdtemp(join(tmpdir(), 'difflens-ign2-l-'))
const r = await mkdtemp(join(tmpdir(), 'difflens-ign2-r-'))
try {
for (const d of [l, r]) {
await writeFile(join(d, 'keep.txt'), 'a')
await writeFile(join(d, 'debug.LOG'), 'x')
await writeFile(join(d, 'temp'), 't')
await writeFile(join(d, 'other.txt'), 'o')
}
const res = await scanFolders(l, r, { ignore: ['*.log', 'temp'] })
expect(res.entries.map((e) => e.rel).sort()).toEqual(['keep.txt', 'other.txt'])
expect(res.total).toBe(2)
} finally {
await rm(l, { recursive: true, force: true })
await rm(r, { recursive: true, force: true })
}
})
it('被忽略文件不占用 maxFiles:原本截断的场景忽略后完整扫描', async () => {
const l = await mkdtemp(join(tmpdir(), 'difflens-ign3-l-'))
const r = await mkdtemp(join(tmpdir(), 'difflens-ign3-r-'))
try {
for (let i = 0; i < 5; i++) {
await writeFile(join(l, `f${i}.txt`), String(i))
await writeFile(join(r, `f${i}.txt`), String(i))
}
for (const d of [l, r]) {
await mkdir(join(d, 'dist'), { recursive: true })
for (let i = 0; i < 5; i++) await writeFile(join(d, 'dist', `b${i}.txt`), 'b')
}
// 无忽略 + maxFiles=6:截断
const noIgnore = await scanFolders(l, r, { maxFiles: 6 })
expect(noIgnore.truncated).toBe(true)
// 忽略 dist/ 后仅 5 个文件:完整扫描
const res = await scanFolders(l, r, { maxFiles: 6, ignore: ['dist/'] })
expect(res.truncated).toBe(false)
expect(res.total).toBe(5)
expect(res.entries).toHaveLength(5)
} finally {
await rm(l, { recursive: true, force: true })
await rm(r, { recursive: true, force: true })
}
})
it('忽略规则变化后重扫:缓存未命中的重新纳入条目走全量判定', async () => {
const l = await mkdtemp(join(tmpdir(), 'difflens-ign4-l-'))
const r = await mkdtemp(join(tmpdir(), 'difflens-ign4-r-'))
try {
await writeFile(join(l, 'a.txt'), 'a')
await writeFile(join(r, 'a.txt'), 'a')
await mkdir(join(l, 'dist'), { recursive: true })
await mkdir(join(r, 'dist'), { recursive: true })
await writeFile(join(l, 'dist/x.txt'), 'x')
await writeFile(join(r, 'dist/x.txt'), 'x')
// 第一次:忽略 dist/
const first = await scanFolders(l, r, { ignore: ['dist/'] })
expect(first.entries.map((e) => e.rel)).toEqual(['a.txt'])
// 第二次:去掉忽略规则 + 携带缓存 → dist/x.txt 未命中缓存,全量判定 same
const second = await scanFolders(l, r, { previous: first.cache })
expect(byRel(second.entries, 'a.txt')?.status).toBe('same')
expect(byRel(second.entries, 'dist/x.txt')?.status).toBe('same')
expect(second.cache.map((c) => c.rel).sort()).toEqual(['a.txt', 'dist/x.txt'])
} finally {
await rm(l, { recursive: true, force: true })
await rm(r, { recursive: true, force: true })
}
})
it('空规则数组与全非法规则不产生任何剪枝(行为与无规则一致)', async () => {
const l = await mkdtemp(join(tmpdir(), 'difflens-ign5-l-'))
const r = await mkdtemp(join(tmpdir(), 'difflens-ign5-r-'))
try {
for (const d of [l, r]) {
await writeFile(join(d, 'a.txt'), 'a')
await mkdir(join(d, 'node_modules'), { recursive: true })
await writeFile(join(d, 'node_modules', 'x.js'), 'x')
}
const empty = await scanFolders(l, r, { ignore: [] })
expect(empty.entries).toHaveLength(2)
const invalid = await scanFolders(l, r, { ignore: ['# note', 'a/b', '**'] })
expect(invalid.entries).toHaveLength(2)
} finally {
await rm(l, { recursive: true, force: true })
await rm(r, { recursive: true, force: true })
}
})
})