import { describe, expect, it, vi } from 'vitest' import { useTransactionalFileWorker } from '../src/composables/useTransactionalFileWorker' type TestResponse = { type: 'parsed' id: number value: string } | { type: 'calculated' id: number value: string } | { type: 'error' id: number operation: 'parse' | 'calculate' error: string } class FakeWorker { onmessage: ((event: MessageEvent) => void) | null = null onerror: ((event: ErrorEvent) => void) | null = null onmessageerror: ((event: MessageEvent) => void) | null = null messages: Array> = [] terminated = false postMessage(message: Record) { this.messages.push(message) } terminate() { this.terminated = true } respond(response: TestResponse) { this.onmessage?.({ data: response } as MessageEvent) } fail() { this.onerror?.({ preventDefault: vi.fn() } as unknown as ErrorEvent) } } function file(name: string): File { return { name, size: 1 } as File } function createHarness() { const workers: FakeWorker[] = [] const parsed: string[] = [] const calculated: string[] = [] const parseErrors: string[] = [] const calculationErrors: string[] = [] const pendingWorkerErrors: boolean[] = [] const activeWorkerError = vi.fn() const manager = useTransactionalFileWorker, TestResponse>({ createWorker: () => { const worker = new FakeWorker() workers.push(worker) return worker as unknown as Worker }, onParsed: (response, selected) => parsed.push(`${selected.name}:${response.value}`), onCalculated: (response) => calculated.push(response.value), onParseError: (message) => parseErrors.push(message), onCalculationError: (message) => calculationErrors.push(message), onPendingWorkerError: (hasCurrentFile) => pendingWorkerErrors.push(hasCurrentFile), onActiveWorkerError: activeWorkerError, }) return { manager, workers, parsed, calculated, parseErrors, calculationErrors, pendingWorkerErrors, activeWorkerError, } } describe('transactional file Worker', () => { it('commits a parsed candidate and calculates with its Worker', () => { const harness = createHarness() expect(harness.manager.parseFile(file('first.hex'))).toBe(true) expect(harness.manager.parsing.value).toBe(true) expect(harness.workers[0].messages[0]).toMatchObject({ type: 'parse', id: 1 }) harness.workers[0].respond({ type: 'parsed', id: 1, value: 'ready' }) expect(harness.manager.parsing.value).toBe(false) expect(harness.parsed).toEqual(['first.hex:ready']) expect(harness.manager.calculate({ parameters: 'crc-32' })).toBe('started') expect(harness.manager.calculating.value).toBe(true) expect(harness.workers[0].messages[1]).toEqual({ type: 'calculate', id: 2, parameters: 'crc-32' }) harness.workers[0].respond({ type: 'calculated', id: 2, value: 'result' }) expect(harness.calculated).toEqual(['result']) expect(harness.manager.calculating.value).toBe(false) }) it('supports an explicit parse-request payload without changing default callers', () => { const workers: FakeWorker[] = [] const manager = useTransactionalFileWorker, TestResponse>({ createWorker: () => { const worker = new FakeWorker() workers.push(worker) return worker as unknown as Worker }, createParseRequest: (id, selected) => ({ type: 'parse', id, file: selected, mode: 'intel-hex' }), onParsed: vi.fn(), onCalculated: vi.fn(), onParseError: vi.fn(), onCalculationError: vi.fn(), onPendingWorkerError: vi.fn(), onActiveWorkerError: vi.fn(), }) const selected = file('firmware.hex') manager.parseFile(selected) expect(workers[0].messages[0]).toEqual({ type: 'parse', id: 1, file: selected, mode: 'intel-hex' }) }) it('preserves the active Worker when a replacement parse fails', () => { const harness = createHarness() harness.manager.parseFile(file('first.hex')) harness.workers[0].respond({ type: 'parsed', id: 1, value: 'first' }) harness.manager.parseFile(file('invalid.hex')) harness.workers[1].respond({ type: 'error', id: 2, operation: 'parse', error: 'bad checksum' }) expect(harness.parseErrors).toEqual(['bad checksum']) expect(harness.workers[0].terminated).toBe(false) expect(harness.workers[1].terminated).toBe(true) expect(harness.manager.calculate({})).toBe('started') }) it('atomically replaces the active Worker and ignores its stale result', () => { const harness = createHarness() harness.manager.parseFile(file('first.hex')) harness.workers[0].respond({ type: 'parsed', id: 1, value: 'first' }) harness.manager.calculate({}) harness.manager.parseFile(file('second.hex')) harness.workers[1].respond({ type: 'parsed', id: 3, value: 'second' }) expect(harness.workers[0].terminated).toBe(true) expect(harness.parsed).toEqual(['first.hex:first', 'second.hex:second']) expect(harness.manager.calculating.value).toBe(false) harness.workers[0].respond({ type: 'calculated', id: 2, value: 'stale' }) expect(harness.calculated).toEqual([]) }) it('distinguishes pending Worker failures with and without a current file', () => { const harness = createHarness() harness.manager.parseFile(file('first.hex')) harness.workers[0].fail() expect(harness.pendingWorkerErrors).toEqual([false]) harness.manager.parseFile(file('current.hex')) harness.workers[1].respond({ type: 'parsed', id: 2, value: 'current' }) harness.manager.parseFile(file('replacement.hex')) harness.workers[2].fail() expect(harness.pendingWorkerErrors).toEqual([false, true]) expect(harness.workers[1].terminated).toBe(false) }) it('terminates active and pending Workers and ignores late responses', () => { const harness = createHarness() harness.manager.parseFile(file('first.hex')) harness.workers[0].respond({ type: 'parsed', id: 1, value: 'first' }) harness.manager.parseFile(file('second.hex')) harness.manager.destroyWorkers() expect(harness.workers.every((worker) => worker.terminated)).toBe(true) harness.workers[1].respond({ type: 'parsed', id: 2, value: 'late' }) expect(harness.parsed).toEqual(['first.hex:first']) expect(harness.manager.parsing.value).toBe(false) expect(harness.manager.calculating.value).toBe(false) }) it('reports Worker construction failure without entering a busy state', () => { const pendingWorkerErrors: boolean[] = [] const manager = useTransactionalFileWorker, TestResponse>({ createWorker: () => { throw new Error('unsupported') }, onParsed: vi.fn(), onCalculated: vi.fn(), onParseError: vi.fn(), onCalculationError: vi.fn(), onPendingWorkerError: (hasCurrentFile) => pendingWorkerErrors.push(hasCurrentFile), onActiveWorkerError: vi.fn(), }) expect(manager.parseFile(file('first.hex'))).toBe(false) expect(pendingWorkerErrors).toEqual([false]) expect(manager.parsing.value).toBe(false) }) })