import { getLogLevel, setLogLevel, subscribeLogs, type LogEntry } from '@snowluma/common/logger'; import type { PacketSender, SendPacketResult } from '@snowluma/common/packet-sender'; import type { PacketInfo } from '@snowluma/common/protocol-types'; import { IdentityService } from '@snowluma/protocol/identity-service'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { BridgeManager } from '../src/bridge/manager'; const previousLogLevel = getLogLevel(); const OK_RESULT: SendPacketResult = { success: true, gotResponse: true, errorCode: 0, errorMessage: '', responseData: Buffer.alloc(0), }; function makeSender() { const sendPacket = vi.fn(async () => OK_RESULT); return { client: { sendPacket } satisfies PacketSender, sendPacket, }; } function runtimeTrace(entries: LogEntry[]): LogEntry[] { return entries.filter((entry) => entry.level === 'trace' && entry.scope === 'Bridge.Runtime'); } function testSends(sendPacket: ReturnType): unknown[][] { return sendPacket.mock.calls.filter(([cmd]) => String(cmd).startsWith('Test.')); } function ownershipSendTrace(entries: LogEntry[]): LogEntry[] { return runtimeTrace(entries).filter((entry) => { if (entry.message.startsWith('bridge_sender_fact ')) { return true; } if (entry.message.startsWith('bridge_send_')) { return entry.message.includes('serviceCmd="Test.'); } return false; }); } function packet(pid: number, uin: string): PacketInfo { return { pid, uin, serviceCmd: 'Test.Unhandled', seqId: 1, retCode: 0, fromClient: false, body: Buffer.alloc(0), }; } describe('BridgeManager PID ownership', () => { beforeEach(() => { vi.spyOn(IdentityService, 'openForUin').mockImplementation((uin) => IdentityService.memory(uin), ); }); afterEach(() => { setLogLevel(previousLogLevel); vi.restoreAllMocks(); }); it('traces binding, account replacement, detach, and session closure under semantic contexts', () => { const manager = new BridgeManager(); const entries: LogEntry[] = []; setLogLevel('trace'); const unsubscribe = subscribeLogs((entry) => entries.push(entry)); try { manager.onHookLogin(101, '10001', makeSender().client); manager.onHookLogin(202, '10001', makeSender().client); manager.onHookLogin(101, '20002', makeSender().client); manager.onPidDisconnected(202); manager.onPidDisconnected(101); manager.onPidDisconnected(101); const trace = runtimeTrace(entries).filter( (entry) => entry.message.startsWith('bridge_binding_') || entry.message.startsWith('bridge_detach_'), ); expect(trace.map((entry) => entry.message)).toEqual([ 'bridge_binding_start pid=101 uin="10001" source=login previousUin=null', 'bridge_binding_branch pid=101 uin="10001" branch=session_created', expect.stringMatching( /^bridge_binding_terminal pid=101 uin="10001" source=login outcome=completed reason=bound created=true activePid=101 elapsedMs=\d+$/, ), 'bridge_binding_start pid=202 uin="10001" source=login previousUin=null', expect.stringMatching( /^bridge_binding_terminal pid=202 uin="10001" source=login outcome=completed reason=bound created=false activePid=202 elapsedMs=\d+$/, ), 'bridge_binding_start pid=101 uin="20002" source=login previousUin="10001"', 'bridge_binding_branch pid=101 uin="20002" branch=account_rebind previousUin="10001"', 'bridge_binding_branch pid=101 uin="20002" branch=previous_session_retained previousUin="10001" activePid=202', 'bridge_binding_branch pid=101 uin="20002" branch=session_created', expect.stringMatching( /^bridge_binding_terminal pid=101 uin="20002" source=login outcome=completed reason=rebound created=true activePid=101 elapsedMs=\d+$/, ), 'bridge_detach_start pid=202 uin="10001"', 'bridge_detach_branch pid=202 uin="10001" branch=session_closed', expect.stringMatching( /^bridge_detach_terminal pid=202 uin="10001" outcome=completed reason=session_closed activePid=null elapsedMs=\d+$/, ), 'bridge_detach_start pid=101 uin="20002"', 'bridge_detach_branch pid=101 uin="20002" branch=session_closed', expect.stringMatching( /^bridge_detach_terminal pid=101 uin="20002" outcome=completed reason=session_closed activePid=null elapsedMs=\d+$/, ), ]); const starts = trace.filter((entry) => entry.message.includes('_start ')); const terminals = trace.filter((entry) => entry.message.includes('_terminal ')); expect(starts).toHaveLength(5); expect(terminals).toHaveLength(5); for (const start of starts) { expect(terminals.some((terminal) => terminal.req === start.req)).toBe(true); } } finally { unsubscribe(); } }); it('records ownership invariant failures as the unique detach terminal', () => { const manager = new BridgeManager(); const entries: LogEntry[] = []; setLogLevel('trace'); const unsubscribe = subscribeLogs((entry) => entries.push(entry)); try { manager.onHookLogin(101, '10001', makeSender().client); manager.getBridge('10001')!.detachPid(101); expect(() => manager.onPidDisconnected(101)).toThrowError( 'BridgeManager invariant violated: PID=101 is mapped to UIN=10001, but Bridge does not own the PID', ); const detach = runtimeTrace(entries).filter((entry) => entry.message.startsWith('bridge_detach_'), ); expect(detach.map((entry) => entry.message)).toEqual([ 'bridge_detach_start pid=101 uin="10001"', expect.stringMatching( /^bridge_detach_terminal pid=101 uin="10001" outcome=failed reason=invariant_violation error="BridgeManager invariant violated: PID=101 is mapped to UIN=10001, but Bridge does not own the PID" elapsedMs=\d+$/, ), ]); expect(detach[0]!.req).toBe(detach[1]!.req); } finally { unsubscribe(); } }); it('records complete incoming packet bytes under one top-level TRACE request', async () => { const manager = new BridgeManager(); const entries: LogEntry[] = []; const body = Buffer.from([ 0x00, 0xff, ...Array.from({ length: 160 }, (_, index) => (159 - index) & 0xff), ]); setLogLevel('trace'); const unsubscribe = subscribeLogs((entry) => entries.push(entry)); try { manager.onPacket({ pid: 101, uin: '10001', serviceCmd: 'Trace\nCommand', seqId: 987, retCode: -5, fromClient: true, body, }); await new Promise((resolve) => setImmediate(resolve)); const packetEntries = entries.filter( (entry) => entry.level === 'trace' && (entry.scope === 'Bridge.Packet' || entry.scope === 'Protocol.Packet'), ); expect(packetEntries).toHaveLength(3); expect(packetEntries.map((entry) => entry.req)).toEqual([ expect.any(Number), packetEntries[0]!.req, packetEntries[0]!.req, ]); expect(packetEntries[0]!.message).toBe( `packet_push serviceCmd="Trace\\nCommand" seqId=987 retCode=-5 fromClient=true pid=101 uin="10001" length=${body.length} body=${body.toString('hex')}`, ); expect(packetEntries[1]!.message).toBe( 'packet_branch serviceCmd="Trace\\nCommand" seqId=987 branch=parser_unregistered', ); expect(packetEntries[2]!.message).toMatch( /^packet_terminal serviceCmd="Trace\\nCommand" seqId=987 outcome=dropped reason=parser_unregistered events=0 dispatched=0 elapsedMs=\d+$/, ); for (const entry of packetEntries) { expect(entry.message).not.toContain('...'); } } finally { unsubscribe(); } }); it('records invalid UIN as a routed packet drop without creating a session', () => { const manager = new BridgeManager(); const entries: LogEntry[] = []; setLogLevel('trace'); const unsubscribe = subscribeLogs((entry) => entries.push(entry)); try { manager.onPacket({ ...packet(101, '0'), body: Buffer.from([0x00, 0xff]), }); expect(entries.filter((entry) => entry.scope === 'Bridge.Packet')).toEqual( expect.arrayContaining([ expect.objectContaining({ level: 'trace', req: expect.any(Number), message: 'packet_push serviceCmd="Test.Unhandled" seqId=1 retCode=0 fromClient=false pid=101 uin="0" length=2 body=00ff', }), expect.objectContaining({ level: 'trace', req: expect.any(Number), message: expect.stringMatching( /^packet_terminal serviceCmd="Test\.Unhandled" seqId=1 outcome=dropped reason=invalid_uin events=0 dispatched=0 elapsedMs=\d+$/, ), }), ]), ); expect(manager.getBridge('0')).toBeNull(); } finally { unsubscribe(); } }); it('assigns distinct request ids to independent incoming packets', () => { const manager = new BridgeManager(); const entries: LogEntry[] = []; setLogLevel('trace'); const unsubscribe = subscribeLogs((entry) => entries.push(entry)); try { manager.onPacket(packet(101, '0')); manager.onPacket(packet(202, '0')); const packetEntries = entries.filter( (entry) => entry.level === 'trace' && entry.scope === 'Bridge.Packet', ); expect(packetEntries).toHaveLength(4); expect(packetEntries[0]!.req).toEqual(expect.any(Number)); expect(packetEntries[1]!.req).toBe(packetEntries[0]!.req); expect(packetEntries[2]!.req).toEqual(expect.any(Number)); expect(packetEntries[3]!.req).toBe(packetEntries[2]!.req); expect(packetEntries[2]!.req).not.toBe(packetEntries[0]!.req); } finally { unsubscribe(); } }); it('does not render incoming packet bytes or allocate request IDs while TRACE is disabled', () => { const manager = new BridgeManager(); const entries: LogEntry[] = []; setLogLevel('debug'); const hex = vi.spyOn(Buffer.prototype, 'toString'); const unsubscribe = subscribeLogs((entry) => entries.push(entry)); try { manager.onPacket({ ...packet(0, '10001'), body: Buffer.from([0x00, 0xff]), }); expect(hex).not.toHaveBeenCalledWith('hex'); expect(entries.some((entry) => entry.req !== undefined)).toBe(false); } finally { unsubscribe(); hex.mockRestore(); } }); it('detaches a PID from its old UIN before starting its replacement session', () => { const manager = new BridgeManager(); const first = makeSender(); const second = makeSender(); const lifecycle: string[] = []; manager.subscribeSessions({ started: (uin) => lifecycle.push(`started:${uin}`) }); manager.subscribeSessions({ closed: (uin) => lifecycle.push(`closed:${uin}`) }); manager.onHookLogin(101, '10001', first.client); const oldBridge = manager.getBridge('10001')!; const dispose = vi.spyOn(oldBridge, 'dispose'); manager.onHookLogin(101, '20002', second.client); expect(lifecycle).toEqual(['started:10001', 'closed:10001', 'started:20002']); expect(manager.getBridge('10001')).toBeNull(); expect(oldBridge.hasPid(101)).toBe(false); expect(dispose).toHaveBeenCalledOnce(); expect(manager.getBridge('20002')!.hasPid(101)).toBe(true); }); it('keeps a multi-PID UIN alive and falls back to the remaining sender', async () => { const manager = new BridgeManager(); const first = makeSender(); const second = makeSender(); const closed = vi.fn(); manager.subscribeSessions({ closed: closed }); manager.onHookLogin(101, '10001', first.client); manager.onHookLogin(202, '10001', second.client); const bridge = manager.getBridge('10001')!; expect(bridge.activePid).toBe(202); await bridge.sendRawPacket('Test.BeforeDisconnect', new Uint8Array([1])); expect(testSends(second.sendPacket)).toHaveLength(1); expect(testSends(first.sendPacket)).toHaveLength(0); manager.onPidDisconnected(202); expect(manager.getBridge('10001')!).toBe(bridge); expect(bridge.activePid).toBe(101); expect(closed).not.toHaveBeenCalled(); await bridge.sendRawPacket('Test.AfterDisconnect', new Uint8Array([2])); expect(testSends(first.sendPacket)).toHaveLength(1); expect(testSends(second.sendPacket)).toHaveLength(1); manager.onPidDisconnected(101); expect(manager.getBridge('10001')).toBeNull(); expect(closed).toHaveBeenCalledOnce(); expect(closed).toHaveBeenCalledWith('10001', bridge, 101); await expect( bridge.sendRawPacket('Test.NoSender', new Uint8Array([3])), ).resolves.toMatchObject({ success: false, gotResponse: false, errorCode: -1, errorMessage: 'no packet sender attached', }); }); it('traces material sender selection, fallback, and no-sender drops without polling noise', async () => { const manager = new BridgeManager(); const first = makeSender(); const second = makeSender(); const entries: LogEntry[] = []; setLogLevel('trace'); const unsubscribe = subscribeLogs((entry) => entries.push(entry)); try { manager.onHookLogin(101, '10001', first.client); manager.onHookLogin(202, '10001', second.client); manager.onPacket(packet(202, '10001')); manager.onPidDisconnected(202); const bridge = manager.getBridge('10001')!; manager.onPidDisconnected(101); await bridge.sendRawPacket('Test.NoSender', new Uint8Array([0x00, 0xff])); const sender = ownershipSendTrace(entries); expect(sender.map((entry) => entry.message)).toEqual([ 'bridge_sender_fact event=selected uin="10001" pid=101 previousPid=null', 'bridge_sender_fact event=selected uin="10001" pid=202 previousPid=101', 'bridge_sender_fact event=fallback uin="10001" detachedPid=202 fallbackPid=101', 'bridge_send_start serviceCmd="Test.NoSender" uin="10001" activePid=null timeoutMs=15000 length=2 body=00ff', expect.stringMatching( /^bridge_send_terminal serviceCmd="Test\.NoSender" uin="10001" activePid=null outcome=dropped reason=no_sender length=2 body=00ff elapsedMs=\d+$/, ), ]); expect(sender[2]!.req).toBe( runtimeTrace(entries).find((entry) => entry.message.startsWith('bridge_detach_start pid=202 '), )!.req, ); expect(sender[3]!.req).toEqual(expect.any(Number)); expect(sender[4]!.req).toBe(sender[3]!.req); } finally { unsubscribe(); } }); it('reports an account healthy while any attached PID still receives packets', () => { const manager = new BridgeManager(); manager.onHookLogin(101, '10001', makeSender().client); manager.onHookLogin(202, '10001', makeSender().client); const bridge = manager.getBridge('10001')!; expect(bridge.receiveHealthy).toBe(true); manager.onPidReceiveHealthChanged(101, false); expect(bridge.receiveHealthy).toBe(true); manager.onPidReceiveHealthChanged(202, false); expect(bridge.receiveHealthy).toBe(false); manager.onPidReceiveHealthChanged(101, true); expect(bridge.receiveHealthy).toBe(true); }); it('prefers the most recently rebound live PID, then falls back by recency', async () => { const manager = new BridgeManager(); const first = makeSender(); const second = makeSender(); const third = makeSender(); const reboundFirst = makeSender(); const started = vi.fn(); manager.subscribeSessions({ started: started }); manager.onHookLogin(101, '10001', first.client); manager.onHookLogin(202, '10001', second.client); manager.onHookLogin(303, '10001', third.client); manager.onHookLogin(101, '10001', reboundFirst.client); const bridge = manager.getBridge('10001')!; expect(started).toHaveBeenCalledOnce(); expect(bridge.activePid).toBe(101); await bridge.sendRawPacket('Test.Rebound', new Uint8Array([1])); expect(testSends(reboundFirst.sendPacket)).toHaveLength(1); manager.onPidDisconnected(101); expect(bridge.activePid).toBe(303); await bridge.sendRawPacket('Test.Fallback', new Uint8Array([2])); expect(testSends(third.sendPacket)).toHaveLength(1); manager.onPidDisconnected(303); expect(bridge.activePid).toBe(202); await bridge.sendRawPacket('Test.SecondFallback', new Uint8Array([3])); expect(testSends(second.sendPacket)).toHaveLength(1); }); it('applies the same ownership transition when a packet reveals a new UIN', async () => { const manager = new BridgeManager(); const sender = makeSender(); const lifecycle: string[] = []; manager.subscribeSessions({ started: (uin) => lifecycle.push(`started:${uin}`) }); manager.subscribeSessions({ closed: (uin) => lifecycle.push(`closed:${uin}`) }); manager.onHookLogin(101, '10001', sender.client); manager.onPacket(packet(101, '20002')); expect(lifecycle).toEqual(['started:10001', 'closed:10001', 'started:20002']); expect(manager.getBridge('10001')).toBeNull(); const replacement = manager.getBridge('20002')!; expect(replacement.hasPid(101)).toBe(true); await replacement.sendRawPacket('Test.AfterPacketRebind', new Uint8Array([1])); expect(testSends(sender.sendPacket)).toHaveLength(1); }); it('keeps the old UIN alive when its active PID moves and a fallback remains', async () => { const manager = new BridgeManager(); const fallback = makeSender(); const moving = makeSender(); const rebound = makeSender(); const lifecycle: string[] = []; manager.subscribeSessions({ started: (uin) => lifecycle.push(`started:${uin}`) }); manager.subscribeSessions({ closed: (uin) => lifecycle.push(`closed:${uin}`) }); manager.onHookLogin(101, '10001', fallback.client); manager.onHookLogin(202, '10001', moving.client); const oldBridge = manager.getBridge('10001')!; expect(oldBridge.activePid).toBe(202); manager.onHookLogin(202, '20002', rebound.client); expect(lifecycle).toEqual(['started:10001', 'started:20002']); expect(manager.getBridge('10001')!).toBe(oldBridge); expect(oldBridge.hasPid(202)).toBe(false); expect(oldBridge.activePid).toBe(101); await oldBridge.sendRawPacket('Test.OldUinFallback', new Uint8Array([1])); expect(testSends(fallback.sendPacket)).toHaveLength(1); const newBridge = manager.getBridge('20002')!; expect(newBridge.activePid).toBe(202); await newBridge.sendRawPacket('Test.NewUinSender', new Uint8Array([2])); expect(testSends(rebound.sendPacket)).toHaveLength(1); manager.onPidDisconnected(202); expect(lifecycle).toEqual(['started:10001', 'started:20002', 'closed:20002']); manager.onPidDisconnected(101); expect(lifecycle).toEqual([ 'started:10001', 'started:20002', 'closed:20002', 'closed:10001', ]); }); it('does not emit another started edge when a PID joins an existing UIN', async () => { const manager = new BridgeManager(); const first = makeSender(); const second = makeSender(); const reboundFirst = makeSender(); const started = vi.fn(); const closed = vi.fn(); manager.subscribeSessions({ started: started }); manager.subscribeSessions({ closed: closed }); manager.onHookLogin(101, '10001', first.client); manager.onHookLogin(202, '20002', second.client); const targetBridge = manager.getBridge('20002')!; manager.onHookLogin(101, '20002', reboundFirst.client); expect(started).toHaveBeenCalledTimes(2); expect(closed).toHaveBeenCalledOnce(); expect(closed).toHaveBeenCalledWith('10001', expect.anything(), 101); expect(manager.getBridge('10001')).toBeNull(); expect(manager.getBridge('20002')!).toBe(targetBridge); expect(targetBridge.activePid).toBe(101); manager.onPidDisconnected(101); expect(targetBridge.activePid).toBe(202); await targetBridge.sendRawPacket('Test.ExistingTargetFallback', new Uint8Array([1])); expect(testSends(second.sendPacket)).toHaveLength(1); expect(closed).toHaveBeenCalledOnce(); manager.onPidDisconnected(202); expect(closed).toHaveBeenCalledTimes(2); }); it('emits exactly one started/closed pair per real lifecycle', () => { const manager = new BridgeManager(); const sender = makeSender(); const started = vi.fn(); const closed = vi.fn(); manager.subscribeSessions({ started: started }); manager.subscribeSessions({ closed: closed }); manager.onHookLogin(101, '10001', sender.client); manager.onHookLogin(101, '10001', sender.client); manager.onPacket(packet(101, '10001')); manager.onPacket(packet(101, '10001')); expect(started).toHaveBeenCalledOnce(); manager.onPidDisconnected(101); manager.onPidDisconnected(101); expect(closed).toHaveBeenCalledOnce(); manager.onHookLogin(101, '10001', sender.client); manager.onPidDisconnected(101); expect(started).toHaveBeenCalledTimes(2); expect(closed).toHaveBeenCalledTimes(2); }); it('fails fast with a routing terminal when Manager ownership has no sender', () => { const manager = new BridgeManager(); const sender = makeSender(); const entries: LogEntry[] = []; setLogLevel('trace'); const unsubscribe = subscribeLogs((entry) => entries.push(entry)); try { manager.onHookLogin(101, '10001', sender.client); // Remove the connection without updating the manager's ownership index. manager.getBridge('10001')!.detachPid(101); expect(() => manager.onPacket(packet(101, '10001'))).toThrowError( 'BridgeManager invariant violated: PID=101 has no sender in UIN=10001 session', ); const packetEntries = entries.filter( (entry) => entry.level === 'trace' && entry.scope === 'Bridge.Packet', ); expect(packetEntries).toHaveLength(2); expect(packetEntries[0]!.req).toEqual(expect.any(Number)); expect(packetEntries[1]).toMatchObject({ req: packetEntries[0]!.req, message: expect.stringMatching( /^packet_terminal serviceCmd="Test\.Unhandled" seqId=1 outcome=failed reason=routing_failed error="BridgeManager invariant violated: PID=101 has no sender in UIN=10001 session" events=0 dispatched=0 elapsedMs=\d+$/, ), }); expect(() => manager.onPidDisconnected(101)).toThrowError( 'BridgeManager invariant violated: PID=101 is mapped to UIN=10001, but Bridge does not own the PID', ); } finally { unsubscribe(); } }); });