import { describe, it, expect } from 'vitest'; import { MockAutomationLifecycleEmitter } from '../../../mocks/MockAutomationLifecycleEmitter'; import { OverlaySyncService } from 'packages/application/services/OverlaySyncService'; import type { AutomationEvent } from 'packages/application/ports/IAutomationEventPublisher'; import type { OverlayAction } from 'packages/application/ports/IOverlaySyncPort'; type RendererOverlayState = | { status: 'idle' } | { status: 'starting'; actionId: string } | { status: 'in-progress'; actionId: string } | { status: 'completed'; actionId: string } | { status: 'failed'; actionId: string }; class RecordingPublisher { public events: AutomationEvent[] = []; async publish(event: AutomationEvent): Promise { this.events.push(event); } } function reduceEventsToRendererState(events: AutomationEvent[]): RendererOverlayState { let state: RendererOverlayState = { status: 'idle' }; for (const ev of events) { if (!ev.actionId) continue; switch (ev.type) { case 'modal-opened': case 'panel-attached': state = { status: 'starting', actionId: ev.actionId }; break; case 'action-started': state = { status: 'in-progress', actionId: ev.actionId }; break; case 'action-complete': state = { status: 'completed', actionId: ev.actionId }; break; case 'action-failed': case 'panel-missing': state = { status: 'failed', actionId: ev.actionId }; break; } } return state; } describe('renderer overlay lifecycle integration', () => { it('tracks starting → in-progress → completed lifecycle for a hosted action', async () => { const emitter = new MockAutomationLifecycleEmitter(); const publisher = new RecordingPublisher(); const svc = new OverlaySyncService({ lifecycleEmitter: emitter as any, publisher: publisher as any, logger: console as any, defaultTimeoutMs: 2_000, }); const action: OverlayAction = { id: 'hosted-session', label: 'Starting hosted session', }; const ackPromise = svc.startAction(action); expect(publisher.events[0]?.type).toBe('modal-opened'); expect(publisher.events[0]?.actionId).toBe('hosted-session'); await emitter.emit({ type: 'panel-attached', actionId: 'hosted-session', timestamp: Date.now(), payload: { selector: '#gridpilot-overlay' }, }); await emitter.emit({ type: 'action-started', actionId: 'hosted-session', timestamp: Date.now(), }); const ack = await ackPromise; expect(ack.id).toBe('hosted-session'); expect(ack.status).toBe('confirmed'); await publisher.publish({ type: 'panel-attached', actionId: 'hosted-session', timestamp: Date.now(), payload: { selector: '#gridpilot-overlay' }, } as AutomationEvent); await publisher.publish({ type: 'action-started', actionId: 'hosted-session', timestamp: Date.now(), } as AutomationEvent); await publisher.publish({ type: 'action-complete', actionId: 'hosted-session', timestamp: Date.now(), } as AutomationEvent); const rendererState = reduceEventsToRendererState(publisher.events); expect(rendererState.status).toBe('completed'); expect(rendererState.actionId).toBe('hosted-session'); }); it('ends in failed state when panel-missing is emitted', async () => { const emitter = new MockAutomationLifecycleEmitter(); const publisher = new RecordingPublisher(); const svc = new OverlaySyncService({ lifecycleEmitter: emitter as any, publisher: publisher as any, logger: console as any, defaultTimeoutMs: 200, }); const action: OverlayAction = { id: 'hosted-failure', label: 'Hosted session failing', }; void svc.startAction(action); await publisher.publish({ type: 'panel-attached', actionId: 'hosted-failure', timestamp: Date.now(), payload: { selector: '#gridpilot-overlay' }, } as AutomationEvent); await publisher.publish({ type: 'action-failed', actionId: 'hosted-failure', timestamp: Date.now(), payload: { reason: 'validation error' }, } as AutomationEvent); const rendererState = reduceEventsToRendererState(publisher.events); expect(rendererState.status).toBe('failed'); expect(rendererState.actionId).toBe('hosted-failure'); }); });