|
| 1 | +// @vitest-environment happy-dom |
| 2 | +import { describe, it, expect, beforeEach, vi } from 'vitest'; |
| 3 | +import { |
| 4 | + ensureSlashCommandsScan, |
| 5 | + getSlashCommandsCached, |
| 6 | + _resetSlashCommandCacheForTests, |
| 7 | +} from '../../../../src/renderer/utils/slash-command-cache'; |
| 8 | +import type { SlashCommandEntry } from '../../../../src/shared/types'; |
| 9 | + |
| 10 | +interface MockGlobal { |
| 11 | + mcode: { |
| 12 | + slashCommands: { scan: ReturnType<typeof vi.fn> }; |
| 13 | + }; |
| 14 | +} |
| 15 | + |
| 16 | +function setScanResult(value: SlashCommandEntry[] | Promise<SlashCommandEntry[]>): ReturnType<typeof vi.fn> { |
| 17 | + const scan = vi.fn(() => (value instanceof Promise ? value : Promise.resolve(value))); |
| 18 | + (window as unknown as MockGlobal).mcode = { |
| 19 | + slashCommands: { scan }, |
| 20 | + }; |
| 21 | + return scan; |
| 22 | +} |
| 23 | + |
| 24 | +function entry(name: string): SlashCommandEntry { |
| 25 | + return { name, description: '', source: 'user' }; |
| 26 | +} |
| 27 | + |
| 28 | +describe('slash-command-cache', () => { |
| 29 | + beforeEach(() => { |
| 30 | + _resetSlashCommandCacheForTests(); |
| 31 | + }); |
| 32 | + |
| 33 | + it('returns undefined from getSlashCommandsCached before any scan', () => { |
| 34 | + setScanResult([]); |
| 35 | + expect(getSlashCommandsCached('claude', '/repo')).toBeUndefined(); |
| 36 | + }); |
| 37 | + |
| 38 | + it('caches the scan result and lowercases names', async () => { |
| 39 | + setScanResult([entry('Foo'), entry('BAR')]); |
| 40 | + const set = await ensureSlashCommandsScan('claude', '/repo'); |
| 41 | + expect(set).toEqual(new Set(['foo', 'bar'])); |
| 42 | + expect(getSlashCommandsCached('claude', '/repo')).toBe(set); |
| 43 | + }); |
| 44 | + |
| 45 | + it('dedupes concurrent scans for the same key into a single IPC call', async () => { |
| 46 | + let resolveScan: (cmds: SlashCommandEntry[]) => void = () => {}; |
| 47 | + const pending = new Promise<SlashCommandEntry[]>((res) => { resolveScan = res; }); |
| 48 | + const scan = setScanResult(pending); |
| 49 | + |
| 50 | + const p1 = ensureSlashCommandsScan('claude', '/repo'); |
| 51 | + const p2 = ensureSlashCommandsScan('claude', '/repo'); |
| 52 | + expect(scan).toHaveBeenCalledTimes(1); |
| 53 | + |
| 54 | + resolveScan([entry('alpha')]); |
| 55 | + const [s1, s2] = await Promise.all([p1, p2]); |
| 56 | + expect(s1).toBe(s2); |
| 57 | + expect(s1).toEqual(new Set(['alpha'])); |
| 58 | + }); |
| 59 | + |
| 60 | + it('keys cache by sessionType and cwd', async () => { |
| 61 | + const scan = setScanResult([entry('hi')]); |
| 62 | + await ensureSlashCommandsScan('claude', '/a'); |
| 63 | + await ensureSlashCommandsScan('claude', '/b'); |
| 64 | + await ensureSlashCommandsScan('codex', '/a'); |
| 65 | + expect(scan).toHaveBeenCalledTimes(3); |
| 66 | + |
| 67 | + // Subsequent calls hit the cache. |
| 68 | + await ensureSlashCommandsScan('claude', '/a'); |
| 69 | + expect(scan).toHaveBeenCalledTimes(3); |
| 70 | + }); |
| 71 | + |
| 72 | + it('does not cache errors so a later attempt can retry', async () => { |
| 73 | + const scan = vi.fn() |
| 74 | + .mockRejectedValueOnce(new Error('boom')) |
| 75 | + .mockResolvedValueOnce([entry('ok')]); |
| 76 | + (window as unknown as MockGlobal).mcode = { |
| 77 | + slashCommands: { scan }, |
| 78 | + }; |
| 79 | + |
| 80 | + const first = await ensureSlashCommandsScan('claude', '/repo'); |
| 81 | + expect(first).toEqual(new Set()); |
| 82 | + expect(getSlashCommandsCached('claude', '/repo')).toBeUndefined(); |
| 83 | + |
| 84 | + const second = await ensureSlashCommandsScan('claude', '/repo'); |
| 85 | + expect(second).toEqual(new Set(['ok'])); |
| 86 | + expect(scan).toHaveBeenCalledTimes(2); |
| 87 | + }); |
| 88 | +}); |
0 commit comments