feat: 0.10.0 快照内容级对照、忽略规则高级 glob 与扫描进度反馈 - 快照v2可选携带差异条目双侧内容(folder:read-batch批量读取IPC受控并发、单文件2MB与总量32MB双限超限整体降级为纯状态快照保存永不失败、对照模式双击进入快照基线vs当前文件的内容对比完整复用单文件能力、v1旧快照兼容加载、contents解析防御绝不部分加载、保存期间读取遮罩)、忽略规则匹配器重写支持多段路径/前导/锚定/整段**跨段/段内*通配/!否定规则(最后匹配优先与gitignore一致、父目录剪枝后子项无法反否定、无效规则UI实时提示不再静默丢弃、纯逻辑迁src/shared两进程共用源码)、扫描进度推送(首个push型IPC、scanId复用扫描会话序号旧扫描迟到进度天然丢弃、遮罩显示已比对N/M)、新增33单测与4条E2E(596单测+28E2E全过覆盖率98.16/93.28/93.69/98.16),文档四件套同步
This commit is contained in:
+105
-1
@@ -675,6 +675,65 @@ describe('scanFolders - 忽略规则', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('多段路径从根锚定剪枝:仅命中该路径(其他分支同名目录不受影响)', async () => {
|
||||
const l = await mkdtemp(join(tmpdir(), 'difflens-ign6-l-'))
|
||||
const r = await mkdtemp(join(tmpdir(), 'difflens-ign6-r-'))
|
||||
try {
|
||||
for (const d of [l, r]) {
|
||||
await mkdir(join(d, 'src', 'temp'), { recursive: true })
|
||||
await mkdir(join(d, 'lib', 'temp'), { recursive: true })
|
||||
await writeFile(join(d, 'src', 'temp', 'a.txt'), 'a')
|
||||
await writeFile(join(d, 'lib', 'temp', 'b.txt'), 'b')
|
||||
await writeFile(join(d, 'src', 'keep.txt'), 'k')
|
||||
}
|
||||
const res = await scanFolders(l, r, { ignore: ['src/temp'] })
|
||||
// src/temp 剪枝;lib/temp 同名目录在其他分支不受影响;被忽略文件不计入 total
|
||||
expect(res.entries.map((e) => e.rel)).toEqual(['lib/temp/b.txt', 'src/keep.txt'])
|
||||
expect(res.total).toBe(2)
|
||||
} finally {
|
||||
await rm(l, { recursive: true, force: true })
|
||||
await rm(r, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
it('** 跨段剪枝与否定救回(父目录未被剪枝时子路径可被反否定)', async () => {
|
||||
const l = await mkdtemp(join(tmpdir(), 'difflens-ign7-l-'))
|
||||
const r = await mkdtemp(join(tmpdir(), 'difflens-ign7-r-'))
|
||||
try {
|
||||
for (const d of [l, r]) {
|
||||
await mkdir(join(d, 'build', 'a', 'temp'), { recursive: true })
|
||||
await mkdir(join(d, 'build', 'special', 'temp'), { recursive: true })
|
||||
await writeFile(join(d, 'build', 'a', 'temp', 'd1.txt'), 'x')
|
||||
await writeFile(join(d, 'build', 'special', 'temp', 'd2.txt'), 'y')
|
||||
await writeFile(join(d, 'keep.txt'), 'k')
|
||||
}
|
||||
// build/a/temp 剪枝;build/special/temp 被否定救回(build 与 special 未被剪枝,BFS 正常下钻)
|
||||
const res = await scanFolders(l, r, { ignore: ['build/**/temp/', '!build/special/temp/'] })
|
||||
expect(res.entries.map((e) => e.rel)).toEqual(['build/special/temp/d2.txt', 'keep.txt'])
|
||||
} finally {
|
||||
await rm(l, { recursive: true, force: true })
|
||||
await rm(r, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
it('父目录被剪枝后否定规则救不回子项(gitignore 同款语义)', async () => {
|
||||
const l = await mkdtemp(join(tmpdir(), 'difflens-ign8-l-'))
|
||||
const r = await mkdtemp(join(tmpdir(), 'difflens-ign8-r-'))
|
||||
try {
|
||||
for (const d of [l, r]) {
|
||||
await mkdir(join(d, 'node_modules', 'pkg'), { recursive: true })
|
||||
await writeFile(join(d, 'node_modules', 'pkg', 'keep.js'), 'k')
|
||||
await writeFile(join(d, 'app.js'), 'a')
|
||||
}
|
||||
// node_modules/ 目录命中即整树剪枝,!node_modules/pkg/keep.js 无法救回
|
||||
const res = await scanFolders(l, r, { ignore: ['node_modules/', '!node_modules/pkg/keep.js'] })
|
||||
expect(res.entries.map((e) => e.rel)).toEqual(['app.js'])
|
||||
} 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-'))
|
||||
@@ -686,7 +745,8 @@ describe('scanFolders - 忽略规则', () => {
|
||||
}
|
||||
const empty = await scanFolders(l, r, { ignore: [] })
|
||||
expect(empty.entries).toHaveLength(2)
|
||||
const invalid = await scanFolders(l, r, { ignore: ['# note', 'a/b', '**'] })
|
||||
// 全部为非法规则(空段 / ! 后为空 / 段内 **):不产生任何剪枝
|
||||
const invalid = await scanFolders(l, r, { ignore: ['# note', 'a//b', '!', 'a**b'] })
|
||||
expect(invalid.entries).toHaveLength(2)
|
||||
} finally {
|
||||
await rm(l, { recursive: true, force: true })
|
||||
@@ -694,3 +754,47 @@ describe('scanFolders - 忽略规则', () => {
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('scanFolders - 比对进度回调(onProgress)', () => {
|
||||
it('需读内容的条目按批推进进度:done 单调递增至 total', async () => {
|
||||
const l = await mkdtemp(join(tmpdir(), 'difflens-prog-l-'))
|
||||
const r = await mkdtemp(join(tmpdir(), 'difflens-prog-r-'))
|
||||
try {
|
||||
// 40 个大小一致条目(需读内容比对)+ 2 个单侧(无 IO)→ tasks = 40,3 个批次
|
||||
for (let i = 0; i < 40; i++) {
|
||||
await writeFile(join(l, `f${String(i).padStart(2, '0')}.txt`), `c${i}`)
|
||||
await writeFile(join(r, `f${String(i).padStart(2, '0')}.txt`), `c${i}`)
|
||||
}
|
||||
await writeFile(join(l, 'only-l.txt'), 'L')
|
||||
const events: Array<{ done: number; total: number }> = []
|
||||
const res = await scanFolders(l, r, {
|
||||
onProgress: (done, total) => events.push({ done, total })
|
||||
})
|
||||
expect(res.entries).toHaveLength(41)
|
||||
expect(events.length).toBeGreaterThan(0)
|
||||
// total 恒为需读内容的条目总数;done 单调递增且最终到达 total
|
||||
expect(events.every((e) => e.total === 40)).toBe(true)
|
||||
for (let i = 1; i < events.length; i++) {
|
||||
expect(events[i].done).toBeGreaterThan(events[i - 1].done)
|
||||
}
|
||||
expect(events[events.length - 1].done).toBe(40)
|
||||
} finally {
|
||||
await rm(l, { recursive: true, force: true })
|
||||
await rm(r, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
it('全部条目零 IO(缓存命中/单侧缺失)时不触发进度回调', async () => {
|
||||
const l = await mkdtemp(join(tmpdir(), 'difflens-prog2-l-'))
|
||||
const r = await mkdtemp(join(tmpdir(), 'difflens-prog2-r-'))
|
||||
try {
|
||||
await writeFile(join(l, 'only-l.txt'), 'L')
|
||||
const events: Array<{ done: number; total: number }> = []
|
||||
await scanFolders(l, r, { onProgress: (done, total) => events.push({ done, total }) })
|
||||
expect(events).toHaveLength(0)
|
||||
} finally {
|
||||
await rm(l, { recursive: true, force: true })
|
||||
await rm(r, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user