import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; import { afterEach, describe, expect, it, vi } from 'vitest'; vi.mock('electron', () => ({ Notification: Object.assign( vi.fn().mockImplementation(() => ({ show: vi.fn(), on: vi.fn(), })), { isSupported: vi.fn().mockReturnValue(false) } ), BrowserWindow: vi.fn(), })); function createConfigManagerStub() { return { getConfig: vi.fn().mockReturnValue({ notifications: { enabled: true, soundEnabled: false, snoozedUntil: null, ignoredRegex: [], ignoredRepositories: [], }, }), clearSnooze: vi.fn(), }; } describe('NotificationManager storage migration', () => { let tempHome: string | null = null; afterEach(() => { if (tempHome) { fs.rmSync(tempHome, { recursive: true, force: true }); tempHome = null; } vi.unstubAllEnvs(); vi.resetModules(); }); function useTempHome(): string { tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'notification-migration-home-')); vi.stubEnv('HOME', tempHome); return tempHome; } function makeStoredNotification(id: string) { return { id, title: id, message: 'Copied', timestamp: new Date().toISOString(), type: 'error', isRead: false, createdAt: Date.now(), }; } it('copies legacy notification history to the new Agent Teams filename', async () => { const home = useTempHome(); const legacyPath = path.join(home, '.claude', 'claude-devtools-notifications.json'); const currentPath = path.join(home, '.claude', 'agent-teams-notifications.json'); const legacyNotifications = [makeStoredNotification('legacy-notification')]; fs.mkdirSync(path.dirname(legacyPath), { recursive: true }); fs.writeFileSync(legacyPath, JSON.stringify(legacyNotifications), 'utf8'); const { NotificationManager } = await import('../../../../src/main/services/infrastructure/NotificationManager'); const manager = new NotificationManager(createConfigManagerStub() as never); await manager.initialize(); const result = await manager.getNotifications({ limit: 10 }); expect(result.notifications.map((notification) => notification.id)).toEqual([ 'legacy-notification', ]); expect(JSON.parse(fs.readFileSync(currentPath, 'utf8'))).toEqual(legacyNotifications); expect(fs.existsSync(legacyPath)).toBe(true); }); it('keeps existing Agent Teams notification history when legacy history also exists', async () => { const home = useTempHome(); const legacyPath = path.join(home, '.claude', 'claude-devtools-notifications.json'); const currentPath = path.join(home, '.claude', 'agent-teams-notifications.json'); const currentNotifications = [makeStoredNotification('current-notification')]; fs.mkdirSync(path.dirname(currentPath), { recursive: true }); fs.writeFileSync( legacyPath, JSON.stringify([{ ...currentNotifications[0], id: 'legacy-notification' }]), 'utf8' ); fs.writeFileSync(currentPath, JSON.stringify(currentNotifications), 'utf8'); const { NotificationManager } = await import('../../../../src/main/services/infrastructure/NotificationManager'); const manager = new NotificationManager(createConfigManagerStub() as never); await manager.initialize(); const result = await manager.getNotifications({ limit: 10 }); expect(result.notifications.map((notification) => notification.id)).toEqual([ 'current-notification', ]); expect(JSON.parse(fs.readFileSync(currentPath, 'utf8'))).toEqual(currentNotifications); }); it('copies pre-devtools notification history when newer legacy history is absent', async () => { const home = useTempHome(); const legacyPath = path.join(home, '.claude', 'claude-code-context-notifications.json'); const currentPath = path.join(home, '.claude', 'agent-teams-notifications.json'); const legacyNotifications = [makeStoredNotification('pre-devtools-notification')]; fs.mkdirSync(path.dirname(legacyPath), { recursive: true }); fs.writeFileSync(legacyPath, JSON.stringify(legacyNotifications), 'utf8'); const { NotificationManager } = await import('../../../../src/main/services/infrastructure/NotificationManager'); const manager = new NotificationManager(createConfigManagerStub() as never); await manager.initialize(); const result = await manager.getNotifications({ limit: 10 }); expect(result.notifications.map((notification) => notification.id)).toEqual([ 'pre-devtools-notification', ]); expect(JSON.parse(fs.readFileSync(currentPath, 'utf8'))).toEqual(legacyNotifications); expect(fs.existsSync(legacyPath)).toBe(true); }); it('prefers valid older notification history over an invalid newer legacy file', async () => { const home = useTempHome(); const invalidNewerLegacyPath = path.join(home, '.claude', 'claude-devtools-notifications.json'); const validOlderLegacyPath = path.join( home, '.claude', 'claude-code-context-notifications.json' ); const currentPath = path.join(home, '.claude', 'agent-teams-notifications.json'); const legacyNotifications = [makeStoredNotification('older-valid-notification')]; fs.mkdirSync(path.dirname(invalidNewerLegacyPath), { recursive: true }); fs.writeFileSync(invalidNewerLegacyPath, '', 'utf8'); fs.writeFileSync(validOlderLegacyPath, JSON.stringify(legacyNotifications), 'utf8'); const { NotificationManager } = await import('../../../../src/main/services/infrastructure/NotificationManager'); const manager = new NotificationManager(createConfigManagerStub() as never); await manager.initialize(); const result = await manager.getNotifications({ limit: 10 }); expect(result.notifications.map((notification) => notification.id)).toEqual([ 'older-valid-notification', ]); expect(JSON.parse(fs.readFileSync(currentPath, 'utf8'))).toEqual(legacyNotifications); }); it('recovers and compacts a notification history file with concatenated JSON', async () => { const home = useTempHome(); const currentPath = path.join(home, '.claude', 'agent-teams-notifications.json'); const currentNotifications = [makeStoredNotification('current-valid-notification')]; const trailingNotifications = [makeStoredNotification('trailing-garbage-notification')]; fs.mkdirSync(path.dirname(currentPath), { recursive: true }); fs.writeFileSync( currentPath, `${JSON.stringify(currentNotifications, null, 2)}\n${JSON.stringify(trailingNotifications)}`, 'utf8' ); const { NotificationManager } = await import('../../../../src/main/services/infrastructure/NotificationManager'); const manager = new NotificationManager(createConfigManagerStub() as never); await manager.initialize(); const result = await manager.getNotifications({ limit: 10 }); expect(result.notifications.map((notification) => notification.id)).toEqual([ 'current-valid-notification', ]); await vi.waitFor(() => { expect(JSON.parse(fs.readFileSync(currentPath, 'utf8'))).toEqual(currentNotifications); }); }); it('keeps notification history valid after rapid consecutive saves', async () => { const home = useTempHome(); const currentPath = path.join(home, '.claude', 'agent-teams-notifications.json'); const { NotificationManager } = await import('../../../../src/main/services/infrastructure/NotificationManager'); const manager = new NotificationManager(createConfigManagerStub() as never); await manager.initialize(); await Promise.all( Array.from({ length: 20 }, (_, index) => manager.addTeamNotification({ teamEventType: 'user_inbox', teamName: 'team-a', teamDisplayName: 'Team A', from: 'alice', summary: `Message ${index}`, body: `Message ${index}`, dedupeKey: `rapid-save-${index}`, suppressToast: true, }) ) ); await vi.waitFor(() => { const parsed = JSON.parse(fs.readFileSync(currentPath, 'utf8')) as Array<{ id: string }>; expect(parsed).toHaveLength(20); }); }); });