import { describe, expect, it, vi } from 'vitest'; import type { RobotHotspotAdapter, RobotHotspotAdapterCandidate } from '@electron/robot-hotspot/adapter'; import { RobotHotspotError, createRobotHotspotModule } from '@electron/robot-hotspot'; import { MacosWorkerController, requestMacosWorker } from '@electron/robot-hotspot/macos'; import type { MacosWorkerLike } from '@electron/robot-hotspot/macos'; function candidate(ssid: string, signalPercent: number, overrides: Partial = {}): RobotHotspotAdapterCandidate { return { ssid, signalPercent, connected: false, open: true, nativeKey: ssid, ...overrides }; } function fakeAdapter(hotspots: RobotHotspotAdapterCandidate[]): RobotHotspotAdapter { return { scan: vi.fn(async () => hotspots), connect: vi.fn(async () => {}), currentSsid: vi.fn(async () => hotspots[0]?.ssid ?? null), clear: vi.fn(), }; } async function expectCode(promise: Promise, code: string): Promise { await expect(promise).rejects.toMatchObject({ name: 'RobotHotspotError', code }); } describe('Robot hotspot module', () => { it('terminates a hanging macOS native worker when the request is aborted', async () => { const worker = { postMessage: vi.fn(), once: vi.fn(function (this: MacosWorkerLike) { return this; }), off: vi.fn(function (this: MacosWorkerLike) { return this; }), terminate: vi.fn(async () => 1), } as unknown as MacosWorkerLike; const controller = new AbortController(); const request = requestMacosWorker(worker, { id: 1, operation: 'scan' }, controller.signal); const timeout = new RobotHotspotError('scan_failed'); controller.abort(timeout); await expect(request).rejects.toBe(timeout); expect(worker.terminate).toHaveBeenCalledOnce(); }); it('does not create or post to a replacement macOS worker until prior termination settles', async () => { let finishTermination!: (code: number) => void; const firstTermination = new Promise((resolve) => { finishTermination = resolve; }); const createFakeWorker = (termination: Promise): MacosWorkerLike => ({ postMessage: vi.fn(), once: vi.fn(function (this: MacosWorkerLike) { return this; }), off: vi.fn(function (this: MacosWorkerLike) { return this; }), terminate: vi.fn(() => termination), }); const firstWorker = createFakeWorker(firstTermination); const secondWorker = createFakeWorker(Promise.resolve(1)); const factory = vi.fn() .mockReturnValueOnce(firstWorker) .mockReturnValueOnce(secondWorker); const controller = new MacosWorkerController(factory); const firstAbort = new AbortController(); const firstRequest = controller.request({ operation: 'scan' }, firstAbort.signal); await vi.waitFor(() => expect(firstWorker.postMessage).toHaveBeenCalledOnce()); firstAbort.abort(new RobotHotspotError('scan_failed')); await expect(firstRequest).rejects.toMatchObject({ code: 'scan_failed' }); const secondAbort = new AbortController(); const secondRequest = controller.request({ operation: 'scan' }, secondAbort.signal); await Promise.resolve(); expect(factory).toHaveBeenCalledTimes(1); expect(secondWorker.postMessage).not.toHaveBeenCalled(); finishTermination(1); await vi.waitFor(() => expect(secondWorker.postMessage).toHaveBeenCalledOnce()); expect(factory).toHaveBeenCalledTimes(2); secondAbort.abort(new RobotHotspotError('scan_failed')); await expect(secondRequest).rejects.toMatchObject({ code: 'scan_failed' }); expect(firstWorker.terminate).toHaveBeenCalledOnce(); }); it('does not continue to permission or native work when aborted behind the termination barrier', async () => { let finishTermination!: (code: number) => void; const firstTermination = new Promise((resolve) => { finishTermination = resolve; }); const firstWorker = { postMessage: vi.fn(), once: vi.fn(function (this: MacosWorkerLike) { return this; }), off: vi.fn(function (this: MacosWorkerLike) { return this; }), terminate: vi.fn(() => firstTermination), } as unknown as MacosWorkerLike; const replacementWorker = { postMessage: vi.fn(), once: vi.fn(function (this: MacosWorkerLike) { return this; }), off: vi.fn(function (this: MacosWorkerLike) { return this; }), terminate: vi.fn(async () => 1), } as unknown as MacosWorkerLike; const factory = vi.fn() .mockReturnValueOnce(firstWorker) .mockReturnValueOnce(replacementWorker); const controller = new MacosWorkerController(factory); const firstAbort = new AbortController(); const firstRequest = controller.request({ operation: 'scan' }, firstAbort.signal); await vi.waitFor(() => expect(firstWorker.postMessage).toHaveBeenCalledOnce()); firstAbort.abort(new RobotHotspotError('scan_failed')); await expect(firstRequest).rejects.toMatchObject({ code: 'scan_failed' }); const permissionOrNativeContinuation = vi.fn(); const blockedAbort = new AbortController(); const blockedFlow = (async () => { await controller.ready(blockedAbort.signal); permissionOrNativeContinuation(); await controller.request({ operation: 'scan' }, blockedAbort.signal); })(); blockedAbort.abort(new RobotHotspotError('scan_failed')); finishTermination(1); await expect(blockedFlow).rejects.toMatchObject({ code: 'scan_failed' }); expect(permissionOrNativeContinuation).not.toHaveBeenCalled(); expect(factory).toHaveBeenCalledTimes(1); expect(replacementWorker.postMessage).not.toHaveBeenCalled(); }); it.runIf(process.platform === 'win32')('loads the Windows WLAN adapter definitions', async () => { const { createWindowsRobotHotspotAdapter } = await import('@electron/robot-hotspot/windows'); await expect(createWindowsRobotHotspotAdapter()).resolves.toMatchObject({ scan: expect.any(Function), connect: expect.any(Function), currentSsid: expect.any(Function), }); }); it('projects only open printable Xiaozhi SSIDs and merges duplicates using the strongest signal', async () => { const adapter = fakeAdapter([ candidate('Other-1', 100), candidate('Xiaozhi-secured', 90, { open: false }), candidate('Xiaozhi-bad\nname', 90), candidate('Xiaozhi-bidi\u202eoverride', 90), candidate('Xiaozhi-zero\u200bwidth', 90), candidate('Xiaozhi-line\u2028separator', 90), candidate('Xiaozhi-lone\ud800surrogate', 90), candidate(`Xiaozhi-${'界'.repeat(9)}`, 80), candidate('Xiaozhi-device', 20), candidate('Xiaozhi-device', 81, { connected: true }), ]); const module = createRobotHotspotModule('windows', adapter, { createId: () => 'opaque-id' }); await expect(module.scan()).resolves.toEqual({ platform: 'windows', hotspots: [{ candidateId: 'opaque-id', ssid: 'Xiaozhi-device', signalPercent: 81, connected: true }], }); }); it('bounds the projected candidate list', async () => { const adapter = fakeAdapter(Array.from( { length: 70 }, (_, index) => candidate(`Xiaozhi-${String(index).padStart(2, '0')}`, 70 - index), )); let id = 0; const module = createRobotHotspotModule('windows', adapter, { createId: () => `id-${id += 1}` }); const result = await module.scan(); expect(result.hotspots).toHaveLength(64); expect(result.hotspots.at(0)?.ssid).toBe('Xiaozhi-00'); expect(result.hotspots.at(-1)?.ssid).toBe('Xiaozhi-63'); }); it('expires candidates after the short snapshot TTL and when clear is called', async () => { let now = 10; const adapter = fakeAdapter([candidate('Xiaozhi-one', 50)]); const module = createRobotHotspotModule('macos', adapter, { now: () => now, candidateTtlMs: 100, createId: () => 'id-1' }); await module.scan(); now = 110; await expectCode(module.connect('id-1'), 'candidate_expired'); now = 10; await module.scan(); module.clear(); await expectCode(module.connect('id-1'), 'candidate_expired'); }); it('rejects overlapping native work as busy', async () => { let finishConnect!: () => void; const adapter = fakeAdapter([candidate('Xiaozhi-one', 50)]); adapter.connect = vi.fn(() => new Promise((resolve) => { finishConnect = resolve; })); adapter.currentSsid = vi.fn(async () => 'Xiaozhi-one'); const module = createRobotHotspotModule('windows', adapter, { createId: () => 'id-1' }); await module.scan(); const connecting = module.connect('id-1'); await Promise.resolve(); await expectCode(module.scan(), 'busy'); finishConnect(); await expect(connecting).resolves.toMatchObject({ connected: true, ssid: 'Xiaozhi-one' }); }); it('does not report success until the exact current SSID is verified', async () => { const adapter = fakeAdapter([candidate('Xiaozhi-target', 50)]); adapter.currentSsid = vi.fn() .mockResolvedValueOnce('Xiaozhi-other') .mockResolvedValueOnce(null) .mockResolvedValueOnce('Xiaozhi-target'); const module = createRobotHotspotModule('windows', adapter, { createId: () => 'id-1', verificationIntervalMs: 1, connectTimeoutMs: 100, }); await module.scan(); await expect(module.connect('id-1')).resolves.toEqual({ connected: true, candidateId: 'id-1', ssid: 'Xiaozhi-target' }); expect(adapter.currentSsid).toHaveBeenCalledTimes(3); }); it('keeps stable errors free of native diagnostics', async () => { const adapter = fakeAdapter([]); adapter.scan = vi.fn(async () => { throw new Error('native secret diagnostic'); }); const module = createRobotHotspotModule('windows', adapter); const error = await module.scan().catch((caught: unknown) => caught); expect(error).toEqual(new RobotHotspotError('scan_failed')); expect(String(error)).not.toContain('native secret diagnostic'); }); });