/** * Unit tests for PhyHubClient.resolveDeviceSpaceId (TECH-1394) — the * spaceId lookup behind space-scoped hardwareId minting. What matters: the * cached device status short-circuits when it already carries a spaceId, * the direct request bypasses getDeviceStatus's 10s cache window (a * started-but-unanswered status request must not fail a healthy boot-time * mint), the timeout clears and rejects with an actionable message, and a * status without a spaceId gets its own distinct error. * * PhyHubClient's constructor wires sockets, so the stub is built from the * prototype with only the fields resolveDeviceSpaceId touches. */ import { describe, expect, it, jest } from 'bun:test'; import { PhyHubClient } from '..'; const buildClientStub = (lastDeviceStatusResponse?: { spaceId?: string }) => { const client: any = Object.create(PhyHubClient.prototype); client.lastDeviceStatusResponse = lastDeviceStatusResponse; client.assureSocketConnection = jest.fn().mockResolvedValue(undefined); client.armRequestErrorListener = jest.fn().mockReturnValue(() => undefined); client.EVENTS = { GET_DEVICE_STATUS: 'getDeviceStatus' }; client.emit = jest.fn(); return client; }; describe('PhyHubClient.resolveDeviceSpaceId', () => { it('serves the cached device status without emitting when it carries a spaceId', async () => { const client = buildClientStub({ spaceId: '68a1f0abcdef1234567890ab' }); await expect(client.resolveDeviceSpaceId()).resolves.toBe('68a1f0abcdef1234567890ab'); expect(client.emit).not.toHaveBeenCalled(); }); it('asks directly when the cache slot is empty — bypassing the getDeviceStatus 10s window', async () => { const client = buildClientStub(undefined); client.emit = jest.fn((_event: string, callback: (response: any) => void) => { callback({ spaceId: '68a1f0abcdef1234567890ab' }); }); await expect(client.resolveDeviceSpaceId()).resolves.toBe('68a1f0abcdef1234567890ab'); expect(client.emit).toHaveBeenCalledWith('getDeviceStatus', expect.any(Function)); }); it('rejects with a distinct error when the device status response has no spaceId', async () => { const client = buildClientStub(undefined); client.emit = jest.fn((_event: string, callback: (response: any) => void) => { callback({ deviceId: 'device-1' }); }); await expect(client.resolveDeviceSpaceId()).rejects.toThrow('did not include a spaceId'); }); it('rejects on timeout when nothing answers, naming the wait', async () => { const client = buildClientStub(undefined); await expect(client.resolveDeviceSpaceId(20)).rejects.toThrow('gave no answer within 20ms'); }); it('detaches the socket-error listener on every settle path', async () => { const detachErrorListener = jest.fn(); const client = buildClientStub(undefined); client.armRequestErrorListener = jest.fn().mockReturnValue(detachErrorListener); client.emit = jest.fn((_event: string, callback: (response: any) => void) => { callback({ spaceId: '68a1f0abcdef1234567890ab' }); }); await client.resolveDeviceSpaceId(); expect(detachErrorListener).toHaveBeenCalled(); }); });