From c8e3ce5268813511999865b8e173b6293d15aa1a Mon Sep 17 00:00:00 2001 From: Dave Date: Mon, 27 Jul 2026 10:36:40 +0200 Subject: [PATCH] test(cleaner): cover exclusions surviving a directory-level delete Descending the recency check closed a second gap worth pinning down. The old top-level scan only tested the entries it listed, so a directory could be offered as one item and then recursively deleted along with an excluded file buried inside it. An excluded descendant now blocks the collapse, so only its settled siblings are offered. --- src/main/services/file-utils.recency.test.ts | 27 +++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/main/services/file-utils.recency.test.ts b/src/main/services/file-utils.recency.test.ts index 96ee3087..183b8787 100644 --- a/src/main/services/file-utils.recency.test.ts +++ b/src/main/services/file-utils.recency.test.ts @@ -3,8 +3,10 @@ import { mkdtempSync, mkdirSync, rmSync, writeFileSync, utimesSync } from 'fs' import { tmpdir } from 'os' import { join } from 'path' +const state = vi.hoisted(() => ({ exclusions: [] as string[] })) + vi.mock('./settings-store', () => ({ - getSettings: () => ({ cleaner: { secureDelete: false, skipRecentMinutes: 60 }, exclusions: [] }), + getSettings: () => ({ cleaner: { secureDelete: false, skipRecentMinutes: 60 }, exclusions: state.exclusions }), })) vi.mock('./scan-cache', () => ({ getCachedItems: () => [] })) @@ -41,6 +43,7 @@ function paths(result: { items: Array<{ path: string }> }): string[] { beforeEach(() => { testDir = mkdtempSync(join(tmpdir(), 'kudu-recency-')) + state.exclusions = [] }) afterEach(() => { @@ -133,4 +136,26 @@ describe('scanDirectory with deepRecencyCheck', () => { expect(result.items).toHaveLength(0) expect(result.itemCount).toBe(0) }) + + // The top-level scan only ever tested the entries it listed, so a directory + // could be offered whole and then recursively deleted along with an excluded + // file buried inside it. Descending closes that off too. + it('never offers a directory holding an excluded descendant', async () => { + file('js/settled', 180, 100) + file('js/keep-me.txt', 180, 7) + state.exclusions = [join(testDir, 'js', 'keep-me.txt')] + + const result = await scanDirectory(testDir, 'browser', 'Test', DEEP) + expect(paths(result)).toEqual([join(testDir, 'js', 'settled')]) + expect(result.totalSize).toBe(100) + }) + + it('honours an *.ext exclusion below the top level', async () => { + file('js/settled', 180, 100) + file('js/nested/notes.log', 180, 7) + state.exclusions = ['*.log'] + + const result = await scanDirectory(testDir, 'browser', 'Test', DEEP) + expect(paths(result)).toEqual([join(testDir, 'js', 'settled')]) + }) })