|
| 1 | +import * as assert from 'assert'; |
| 2 | +import * as os from 'os'; |
| 3 | +import * as path from 'path'; |
| 4 | +import * as vscode from 'vscode'; |
| 5 | +import * as J from '../..'; |
| 6 | +import { ScanEntries } from '../../provider/features/scan-entries'; |
| 7 | +import { SCOPE_DEFAULT } from '../../ext'; |
| 8 | +import { JournalPageType, ScopeDirectory } from '../../model'; |
| 9 | +import { TestLogger } from '../test-logger'; |
| 10 | + |
| 11 | +async function seedEntry(base: string, year: number, month: number, day: number, content = '# Entry\n'): Promise<void> { |
| 12 | + const yy = String(year).padStart(4, '0'); |
| 13 | + const mm = String(month).padStart(2, '0'); |
| 14 | + const dd = String(day).padStart(2, '0'); |
| 15 | + const dir = vscode.Uri.file(path.join(base, yy, mm)); |
| 16 | + await vscode.workspace.fs.createDirectory(dir); |
| 17 | + const file = vscode.Uri.file(path.join(base, yy, mm, `${dd}.md`)); |
| 18 | + await vscode.workspace.fs.writeFile(file, new TextEncoder().encode(content)); |
| 19 | +} |
| 20 | + |
| 21 | +suite('Issue #187 — ScanEntries cache short-circuit and invalidation', () => { |
| 22 | + let originalBase: string | undefined; |
| 23 | + let tmpBase: string; |
| 24 | + let ctrl: J.Util.Ctrl; |
| 25 | + let scanner: ScanEntries; |
| 26 | + let walkCount: number; |
| 27 | + let originalWalkDir: any; |
| 28 | + |
| 29 | + setup(async () => { |
| 30 | + const config = vscode.workspace.getConfiguration('journal'); |
| 31 | + originalBase = config.get<string>('base'); |
| 32 | + |
| 33 | + tmpBase = path.join(os.tmpdir(), `issue187-base-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`); |
| 34 | + await vscode.workspace.fs.createDirectory(vscode.Uri.file(tmpBase)); |
| 35 | + await config.update('base', tmpBase, vscode.ConfigurationTarget.Workspace); |
| 36 | + |
| 37 | + await seedEntry(tmpBase, 2025, 3, 5); |
| 38 | + await seedEntry(tmpBase, 2025, 3, 8); |
| 39 | + await seedEntry(tmpBase, 2025, 4, 1); |
| 40 | + |
| 41 | + const refreshed = vscode.workspace.getConfiguration('journal'); |
| 42 | + ctrl = new J.Util.Ctrl(refreshed); |
| 43 | + ctrl.logger = new TestLogger(false); |
| 44 | + scanner = new ScanEntries(ctrl); |
| 45 | + |
| 46 | + walkCount = 0; |
| 47 | + originalWalkDir = (ScanEntries.prototype as any).walkDir; |
| 48 | + (ScanEntries.prototype as any).walkDir = async function (dir: string, threshold: number, callback: Function): Promise<void> { |
| 49 | + if (typeof dir === 'string' && dir.startsWith(tmpBase)) { |
| 50 | + walkCount++; |
| 51 | + } |
| 52 | + return originalWalkDir.call(this, dir, threshold, callback); |
| 53 | + }; |
| 54 | + }); |
| 55 | + |
| 56 | + teardown(async () => { |
| 57 | + (ScanEntries.prototype as any).walkDir = originalWalkDir; |
| 58 | + const config = vscode.workspace.getConfiguration('journal'); |
| 59 | + await config.update('base', originalBase, vscode.ConfigurationTarget.Workspace); |
| 60 | + try { await vscode.workspace.fs.delete(vscode.Uri.file(tmpBase), { recursive: true }); } catch { /* ignore */ } |
| 61 | + }); |
| 62 | + |
| 63 | + async function runScan(): Promise<void> { |
| 64 | + const directories = new Set<ScopeDirectory>([{ path: tmpBase, scope: SCOPE_DEFAULT }]); |
| 65 | + let resolveDone: () => void = () => { /* set below */ }; |
| 66 | + const done = new Promise<void>(resolve => { resolveDone = resolve; }); |
| 67 | + |
| 68 | + let pendingDirs = 0; |
| 69 | + let walkStarted = false; |
| 70 | + |
| 71 | + const callback = (_entries: any[], _picker: any, _type: any) => { |
| 72 | + // first callback signals at least one walk pass completed |
| 73 | + if (!walkStarted) { |
| 74 | + walkStarted = true; |
| 75 | + } |
| 76 | + }; |
| 77 | + |
| 78 | + await scanner.getPreviouslyAccessedFiles(Date.now() - 1000 * 60 * 60 * 24 * 365, callback as any, null, JournalPageType.entry, directories); |
| 79 | + // scanDirectory is fire-and-forget inside getPreviouslyAccessedFiles; let the microtask queue drain |
| 80 | + await new Promise(resolve => setTimeout(resolve, 100)); |
| 81 | + // silence unused warnings |
| 82 | + void pendingDirs; void resolveDone; void done; |
| 83 | + } |
| 84 | + |
| 85 | + test('first scan walks the filesystem', async () => { |
| 86 | + await runScan(); |
| 87 | + assert.ok(walkCount > 0, `expected ScanEntries.walkDir to run on first scan, got ${walkCount}`); |
| 88 | + }); |
| 89 | + |
| 90 | + test('second scan with populated cache does NOT walk the filesystem', async () => { |
| 91 | + await runScan(); |
| 92 | + const firstCount = walkCount; |
| 93 | + assert.ok(firstCount > 0, 'precondition: first scan must have walked the FS'); |
| 94 | + |
| 95 | + walkCount = 0; |
| 96 | + await runScan(); |
| 97 | + |
| 98 | + assert.strictEqual(walkCount, 0, `expected zero walkDir calls on cached scan, got ${walkCount}`); |
| 99 | + }); |
| 100 | + |
| 101 | + test('clearCache() restores dirty state — next scan walks again', async () => { |
| 102 | + await runScan(); |
| 103 | + assert.ok(walkCount > 0); |
| 104 | + |
| 105 | + scanner.clearCache(); |
| 106 | + |
| 107 | + walkCount = 0; |
| 108 | + await runScan(); |
| 109 | + |
| 110 | + assert.ok(walkCount > 0, `expected fresh walk after clearCache, got ${walkCount}`); |
| 111 | + }); |
| 112 | +}); |
0 commit comments