diff --git a/desktop/windows/src/main/fileIndex/scanRules.test.ts b/desktop/windows/src/main/fileIndex/scanRules.test.ts index 76380ba6ad..abad454c38 100644 --- a/desktop/windows/src/main/fileIndex/scanRules.test.ts +++ b/desktop/windows/src/main/fileIndex/scanRules.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest' -import { shouldVisitDir, shouldIndexFile, MAX_DEPTH, MAX_FILE_SIZE } from './scanRules' +import { shouldVisitDir, shouldIndexFile, SKIP_DIRS, MAX_DEPTH, MAX_FILE_SIZE } from './scanRules' describe('shouldVisitDir', () => { it('skips noise directories', () => { @@ -7,6 +7,12 @@ describe('shouldVisitDir', () => { expect(shouldVisitDir('.git', 1)).toBe(false) expect(shouldVisitDir('__pycache__', 1)).toBe(false) expect(shouldVisitDir('.Trash', 1)).toBe(false) + expect(SKIP_DIRS.has('.Trash')).toBe(true) + }) + it('skips noise directories case-insensitively on Windows paths', () => { + expect(shouldVisitDir('Node_Modules', 1)).toBe(false) + expect(shouldVisitDir('.GIT', 1)).toBe(false) + expect(shouldVisitDir('__PYCACHE__', 1)).toBe(false) }) it('visits normal dirs within depth', () => { expect(shouldVisitDir('src', 1)).toBe(true) diff --git a/desktop/windows/src/main/fileIndex/scanRules.ts b/desktop/windows/src/main/fileIndex/scanRules.ts index 9b8f8d6810..eeaa4b247c 100644 --- a/desktop/windows/src/main/fileIndex/scanRules.ts +++ b/desktop/windows/src/main/fileIndex/scanRules.ts @@ -1,10 +1,11 @@ export const MAX_DEPTH = 3 export const MAX_FILE_SIZE = 500 * 1024 * 1024 // 500 MB, matching macOS export const SKIP_DIRS = new Set(['.Trash', 'node_modules', '.git', '__pycache__']) +const NORMALIZED_SKIP_DIRS = new Set([...SKIP_DIRS].map((name) => name.toLowerCase())) // True when a subdirectory at `depth` should be descended into. export function shouldVisitDir(name: string, depth: number): boolean { - return depth <= MAX_DEPTH && !SKIP_DIRS.has(name) + return depth <= MAX_DEPTH && !NORMALIZED_SKIP_DIRS.has(name.toLowerCase()) } // True when a file of `sizeBytes` should be recorded. diff --git a/desktop/windows/src/main/usage/userAssist.test.ts b/desktop/windows/src/main/usage/userAssist.test.ts index 1805d6026a..9938c922d8 100644 --- a/desktop/windows/src/main/usage/userAssist.test.ts +++ b/desktop/windows/src/main/usage/userAssist.test.ts @@ -10,9 +10,9 @@ function blob(opts: { len?: number }): Buffer { const b = Buffer.alloc(opts.len ?? 72) - if (b.length >= 8) b.writeInt32LE(opts.runCount ?? 0, 4) - if (b.length >= 12) b.writeInt32LE(opts.focusCount ?? 0, 8) - if (b.length >= 16) b.writeInt32LE(opts.focusMs ?? 0, 12) + if (b.length >= 8) b.writeUInt32LE(opts.runCount ?? 0, 4) + if (b.length >= 12) b.writeUInt32LE(opts.focusCount ?? 0, 8) + if (b.length >= 16) b.writeUInt32LE(opts.focusMs ?? 0, 12) if (b.length >= 68 && opts.lastUsedMs != null) { // ms epoch -> Windows FILETIME (100ns ticks since 1601-01-01) const ticks = (BigInt(opts.lastUsedMs) + 11644473600000n) * 10000n @@ -39,6 +39,15 @@ describe('parseUserAssistData', () => { expect(p!.focusCount).toBe(757) expect(p!.focusSeconds).toBe(43_147) // rounded }) + it('reads high-bit DWORD counters as unsigned values', () => { + const p = parseUserAssistData( + blob({ runCount: 3_000_000_000, focusCount: 4_000_000_000, focusMs: 3_000_000_000 }) + ) + expect(p).not.toBeNull() + expect(p!.runCount).toBe(3_000_000_000) + expect(p!.focusCount).toBe(4_000_000_000) + expect(p!.focusSeconds).toBe(3_000_000) + }) it('reads last-used from the FILETIME at offset 60', () => { const when = Date.UTC(2026, 5, 3, 12, 0, 0) const p = parseUserAssistData(blob({ focusMs: 1000, lastUsedMs: when })) @@ -63,7 +72,9 @@ describe('friendlyAppName', () => { expect(friendlyAppName('Telegram.TelegramDesktop')).toBe('TelegramDesktop') }) it('strips the package-family hash and !App suffix from packaged AUMIDs', () => { - expect(friendlyAppName('Microsoft.ZuneMusic_8wekyb3d8bbwe!Microsoft.ZuneMusic')).toBe('ZuneMusic') + expect(friendlyAppName('Microsoft.ZuneMusic_8wekyb3d8bbwe!Microsoft.ZuneMusic')).toBe( + 'ZuneMusic' + ) expect(friendlyAppName('5319275A.WhatsAppDesktop_cv1g1gvanyjgm!App')).toBe('WhatsAppDesktop') // Uses the package-name segment, not the !Activatable id. "SpotifyMusic" // still matches an indexed "Spotify" via rankApps' containment rule. @@ -74,11 +85,15 @@ describe('friendlyAppName', () => { }) it('uses the exe basename (without .exe) for full paths', () => { expect(friendlyAppName('C:\\Users\\me\\AppData\\Local\\Programs\\Warp\\Warp.exe')).toBe('Warp') - expect(friendlyAppName('C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe')).toBe('chrome') + expect(friendlyAppName('C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe')).toBe( + 'chrome' + ) }) it('ignores a leading KNOWNFOLDERID GUID segment in a path', () => { expect( - friendlyAppName('{1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}\\WindowsPowerShell\\v1.0\\powershell.exe') + friendlyAppName( + '{1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}\\WindowsPowerShell\\v1.0\\powershell.exe' + ) ).toBe('powershell') }) it('returns null for empty / GUID-only names', () => { @@ -91,9 +106,15 @@ describe('aggregateUserAssist', () => { it('decodes, drops control entries, and sums focus time per friendly name', () => { const raw = [ { name: rot13('UEME_CTLSESSION'), data: blob({ focusMs: 999_999 }) }, - { name: rot13('dev.warp.Warp'), data: blob({ focusMs: 60_000, runCount: 3, lastUsedMs: 100 }) }, + { + name: rot13('dev.warp.Warp'), + data: blob({ focusMs: 60_000, runCount: 3, lastUsedMs: 100 }) + }, // same friendly name via a full path -> merged - { name: rot13('C:\\x\\Warp\\Warp.exe'), data: blob({ focusMs: 30_000, runCount: 2, lastUsedMs: 200 }) }, + { + name: rot13('C:\\x\\Warp\\Warp.exe'), + data: blob({ focusMs: 30_000, runCount: 2, lastUsedMs: 200 }) + }, { name: rot13('Chrome'), data: blob({ focusMs: 120_000, lastUsedMs: 50 }) } ] const out = aggregateUserAssist(raw) diff --git a/desktop/windows/src/main/usage/userAssist.ts b/desktop/windows/src/main/usage/userAssist.ts index 0199980cb9..9a24333da4 100644 --- a/desktop/windows/src/main/usage/userAssist.ts +++ b/desktop/windows/src/main/usage/userAssist.ts @@ -50,15 +50,15 @@ export function rot13(s: string): string { // hold the focus-time field (control/sentinel entries can be tiny). export function parseUserAssistData(data: Buffer): ParsedUserAssist | null { if (data.length < OFF_FOCUS_MS + 4) return null - const focusMs = data.readInt32LE(OFF_FOCUS_MS) + const focusMs = data.readUInt32LE(OFF_FOCUS_MS) let lastUsed = 0 if (data.length >= OFF_LAST_USED_FILETIME + 8) { const ticks = data.readBigUInt64LE(OFF_LAST_USED_FILETIME) if (ticks > 0n) lastUsed = Number(ticks / 10_000n - FILETIME_UNIX_OFFSET_MS) } return { - runCount: data.readInt32LE(OFF_RUN_COUNT), - focusCount: data.readInt32LE(OFF_FOCUS_COUNT), + runCount: data.readUInt32LE(OFF_RUN_COUNT), + focusCount: data.readUInt32LE(OFF_FOCUS_COUNT), focusSeconds: Math.round(focusMs / 1000), lastUsed }