import { __resetRecentProjectsClientCacheForTests, getRecentProjectsClientSnapshot, loadRecentProjectsWithClientCache, } from '@features/recent-projects/renderer/utils/recentProjectsClientCache'; import { afterEach, describe, expect, it, vi } from 'vitest'; import type { DashboardRecentProject, DashboardRecentProjectsPayload, } from '@features/recent-projects/contracts'; const project = (id: string): DashboardRecentProject => ({ id, name: id, primaryPath: `/tmp/${id}`, associatedPaths: [`/tmp/${id}`], mostRecentActivity: Date.parse('2026-04-14T12:00:00.000Z'), providerIds: ['anthropic'], source: 'claude', openTarget: { type: 'synthetic-path', path: `/tmp/${id}`, }, }); const payload = ( id: string, overrides: Partial = {} ): DashboardRecentProjectsPayload => ({ projects: [project(id)], degraded: false, ...overrides, }); const LOCAL_CACHE_KEY = 'local'; const SSH_CACHE_KEY = 'ssh-dev'; function deferred(): { promise: Promise; resolve: (value: T) => void; } { let resolve!: (value: T) => void; const promise = new Promise((promiseResolve) => { resolve = promiseResolve; }); return { promise, resolve }; } describe('recentProjectsClientCache', () => { afterEach(() => { __resetRecentProjectsClientCacheForTests(); vi.useRealTimers(); vi.restoreAllMocks(); }); it('returns cached projects while the client cache is fresh', async () => { const loader = vi.fn().mockResolvedValue(payload('alpha')); await expect(loadRecentProjectsWithClientCache(LOCAL_CACHE_KEY, loader)).resolves.toEqual( payload('alpha') ); await expect(loadRecentProjectsWithClientCache(LOCAL_CACHE_KEY, loader)).resolves.toEqual( payload('alpha') ); expect(loader).toHaveBeenCalledTimes(1); expect(getRecentProjectsClientSnapshot(LOCAL_CACHE_KEY)?.payload).toEqual(payload('alpha')); }); it('revalidates stale cache without dropping the previous snapshot', async () => { vi.useFakeTimers(); vi.setSystemTime(new Date('2026-04-14T12:00:00.000Z')); const loader = vi .fn<() => Promise>() .mockResolvedValueOnce(payload('alpha')) .mockResolvedValueOnce(payload('beta')); await loadRecentProjectsWithClientCache(LOCAL_CACHE_KEY, loader); vi.setSystemTime(new Date('2026-04-14T12:00:16.000Z')); expect(getRecentProjectsClientSnapshot(LOCAL_CACHE_KEY)).toMatchObject({ payload: payload('alpha'), isStale: true, }); await expect( loadRecentProjectsWithClientCache(LOCAL_CACHE_KEY, loader, { force: true }) ).resolves.toEqual(payload('beta')); expect(loader).toHaveBeenCalledTimes(2); expect(getRecentProjectsClientSnapshot(LOCAL_CACHE_KEY)).toMatchObject({ payload: payload('beta'), isStale: false, }); }); it('deduplicates concurrent client refreshes', async () => { const resolveLoaderRef: { current: ((payload: DashboardRecentProjectsPayload) => void) | null; } = { current: null, }; const loader = vi.fn( () => new Promise((resolve) => { resolveLoaderRef.current = resolve; }) ); const first = loadRecentProjectsWithClientCache(LOCAL_CACHE_KEY, loader, { force: true }); const second = loadRecentProjectsWithClientCache(LOCAL_CACHE_KEY, loader, { force: true }); expect(loader).toHaveBeenCalledTimes(1); resolveLoaderRef.current?.(payload('alpha')); await expect(first).resolves.toEqual(payload('alpha')); await expect(second).resolves.toEqual(payload('alpha')); }); it('keeps degraded payload snapshots fresh long enough to avoid hot retry loops', async () => { vi.useFakeTimers(); vi.setSystemTime(new Date('2026-04-14T12:00:00.000Z')); const loader = vi .fn<() => Promise>() .mockResolvedValueOnce(payload('alpha', { degraded: true })); await expect(loadRecentProjectsWithClientCache(LOCAL_CACHE_KEY, loader)).resolves.toEqual( payload('alpha', { degraded: true }) ); vi.setSystemTime(new Date('2026-04-14T12:00:01.000Z')); expect(getRecentProjectsClientSnapshot(LOCAL_CACHE_KEY)).toMatchObject({ payload: payload('alpha', { degraded: true }), isStale: false, }); vi.setSystemTime(new Date('2026-04-14T12:00:20.000Z')); expect(getRecentProjectsClientSnapshot(LOCAL_CACHE_KEY)).toMatchObject({ payload: payload('alpha', { degraded: true }), isStale: false, }); vi.setSystemTime(new Date('2026-04-14T12:00:31.000Z')); expect(getRecentProjectsClientSnapshot(LOCAL_CACHE_KEY)).toMatchObject({ payload: payload('alpha', { degraded: true }), isStale: true, }); }); it('normalizes legacy array responses from the loader during mixed-version dev reloads', async () => { const loader = vi .fn<() => Promise>() .mockResolvedValue([project('alpha')]); await expect(loadRecentProjectsWithClientCache(LOCAL_CACHE_KEY, loader)).resolves.toEqual( payload('alpha') ); expect(getRecentProjectsClientSnapshot(LOCAL_CACHE_KEY)?.payload).toEqual(payload('alpha')); }); it('does not serve a cached payload across active context keys', async () => { const loader = vi .fn<() => Promise>() .mockResolvedValueOnce(payload('local-alpha')) .mockResolvedValueOnce(payload('ssh-beta')); await expect(loadRecentProjectsWithClientCache(LOCAL_CACHE_KEY, loader)).resolves.toEqual( payload('local-alpha') ); expect(getRecentProjectsClientSnapshot(SSH_CACHE_KEY)).toBeNull(); await expect(loadRecentProjectsWithClientCache(SSH_CACHE_KEY, loader)).resolves.toEqual( payload('ssh-beta') ); expect(loader).toHaveBeenCalledTimes(2); expect(getRecentProjectsClientSnapshot(LOCAL_CACHE_KEY)).toBeNull(); expect(getRecentProjectsClientSnapshot(SSH_CACHE_KEY)?.payload).toEqual(payload('ssh-beta')); }); it('does not reuse or cache an in-flight payload for a different context key', async () => { const localRequest = deferred(); const sshRequest = deferred(); const loader = vi .fn<() => Promise>() .mockReturnValueOnce(localRequest.promise) .mockReturnValueOnce(sshRequest.promise); const localLoad = loadRecentProjectsWithClientCache(LOCAL_CACHE_KEY, loader, { force: true }); const sshLoad = loadRecentProjectsWithClientCache(SSH_CACHE_KEY, loader, { force: true }); expect(loader).toHaveBeenCalledTimes(2); sshRequest.resolve(payload('ssh-beta')); await expect(sshLoad).resolves.toEqual(payload('ssh-beta')); expect(getRecentProjectsClientSnapshot(SSH_CACHE_KEY)?.payload).toEqual(payload('ssh-beta')); localRequest.resolve(payload('local-alpha')); await expect(localLoad).resolves.toEqual(payload('local-alpha')); expect(getRecentProjectsClientSnapshot(LOCAL_CACHE_KEY)).toBeNull(); expect(getRecentProjectsClientSnapshot(SSH_CACHE_KEY)?.payload).toEqual(payload('ssh-beta')); }); });