/** * test/desktop/notifications.test.ts — B2: computeNotifications batch diff. */ import { describe, test, expect } from 'vitest' import { computeNotifications } from '../../desktop/src/notifications.js' import type { NotifyOptions } from '../../desktop/src/notify-policy.js' import type { SessionStatusSnapshot } from '../../desktop/src/types.js' function snapshot(overrides: Partial = {}): SessionStatusSnapshot { return { sessionId: 'sess-a', claudeStatus: 'idle', pendingApproval: false, gate: null, title: undefined, ...overrides, } } const OPTS: NotifyOptions = { windowFocused: false, notifyOnApproval: true, notifyOnStatusChange: true, } describe('computeNotifications', () => { test('collects only the notifying decisions', () => { // Arrange: sess-a raises an approval (notify), sess-b is unchanged (silent). const prev = new Map([ ['sess-a', snapshot({ sessionId: 'sess-a', pendingApproval: false })], ['sess-b', snapshot({ sessionId: 'sess-b', claudeStatus: 'working' })], ]) const snapshots = [ snapshot({ sessionId: 'sess-a', pendingApproval: true, gate: 'tool' }), snapshot({ sessionId: 'sess-b', claudeStatus: 'working' }), ] // Act const { decisions } = computeNotifications(prev, snapshots, OPTS) // Assert expect(decisions).toHaveLength(1) expect(decisions[0].title).toBe('Approval needed') }) test('returns a new map keyed by sessionId', () => { const snapshots = [ snapshot({ sessionId: 'sess-a' }), snapshot({ sessionId: 'sess-b' }), ] const { next } = computeNotifications(new Map(), snapshots, OPTS) expect(next).toBeInstanceOf(Map) expect([...next.keys()].sort()).toEqual(['sess-a', 'sess-b']) expect(next.get('sess-a')).toEqual(snapshots[0]) }) test('does not mutate the previous map', () => { // Arrange const prev = new Map([ ['sess-a', snapshot({ sessionId: 'sess-a', claudeStatus: 'working' })], ]) const snapshots = [snapshot({ sessionId: 'sess-a', claudeStatus: 'waiting' })] // Act const { next } = computeNotifications(prev, snapshots, OPTS) // Assert: prev is untouched; next is a distinct object with the new value. expect(prev.size).toBe(1) expect(prev.get('sess-a')?.claudeStatus).toBe('working') expect(next).not.toBe(prev) expect(next.get('sess-a')?.claudeStatus).toBe('waiting') }) test('drops sessions that ended (absent from snapshots) from next', () => { // Arrange: sess-b existed before but is gone this round. const prev = new Map([ ['sess-a', snapshot({ sessionId: 'sess-a' })], ['sess-b', snapshot({ sessionId: 'sess-b' })], ]) const snapshots = [snapshot({ sessionId: 'sess-a' })] // Act const { next } = computeNotifications(prev, snapshots, OPTS) // Assert expect(next.has('sess-b')).toBe(false) expect(next.has('sess-a')).toBe(true) }) test('handles multiple notifying sessions', () => { const snapshots = [ snapshot({ sessionId: 'sess-a', pendingApproval: true, gate: 'plan' }), snapshot({ sessionId: 'sess-b', claudeStatus: 'stuck' }), ] const { decisions, next } = computeNotifications(new Map(), snapshots, OPTS) expect(decisions).toHaveLength(2) expect(next.size).toBe(2) }) test('returns no decisions for an empty snapshot list', () => { const prev = new Map([['sess-a', snapshot()]]) const { decisions, next } = computeNotifications(prev, [], OPTS) expect(decisions).toEqual([]) expect(next.size).toBe(0) }) })