Dev Build / cleanup (push) Waiting to run
Dev Build / typecheck (push) Waiting to run
Publish MCP to npm / Build, test, and publish MCP (push) Waiting to run
Publish SDK to npm / Build, test, and publish SDK (push) Waiting to run
Sync action catalog to docs / sync (push) Waiting to run
Publish UI to npm / Build and publish UI (push) Waiting to run
Build and Release / preflight (push) Waiting to run
126 lines
4.7 KiB
TypeScript
126 lines
4.7 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest';
|
|
import { StateBus } from '../src/webui/state-bus';
|
|
import { createStateWiring } from '../src/webui/state-wiring';
|
|
import { BridgeManager } from '../src/bridge/manager';
|
|
|
|
describe('createStateWiring', () => {
|
|
it('keeps a replacement wiring independent of its disposed predecessor', async () => {
|
|
const old = new BridgeManager();
|
|
const current = new BridgeManager();
|
|
const listener = vi.fn();
|
|
const previousWiring = createStateWiring(old);
|
|
previousWiring.bus.subscribe(listener);
|
|
previousWiring.dispose();
|
|
const wiring = createStateWiring(current);
|
|
wiring.bus.subscribe(listener);
|
|
|
|
old.onHookLogin(4100, '12345', {} as never);
|
|
expect(listener).not.toHaveBeenCalled();
|
|
current.onHookLogin(4200, '23456', {} as never);
|
|
expect(listener.mock.calls).toEqual([['qq-list'], ['connections']]);
|
|
|
|
wiring.dispose();
|
|
await old.dispose();
|
|
await current.dispose();
|
|
});
|
|
|
|
it('unsubscribes on disposal before another account starts', async () => {
|
|
const bm = new BridgeManager();
|
|
const subscribe = vi.spyOn(bm, 'subscribeSessions');
|
|
const wiring = createStateWiring(bm);
|
|
const publish = vi.spyOn(wiring.bus, 'publish');
|
|
wiring.dispose();
|
|
wiring.dispose();
|
|
|
|
bm.onHookLogin(4300, '34567', {} as never);
|
|
expect(publish).not.toHaveBeenCalled();
|
|
expect(subscribe).toHaveBeenCalledTimes(1);
|
|
await bm.dispose();
|
|
});
|
|
|
|
it('releases its session subscription when disposed', () => {
|
|
const unsubscribe = vi.fn();
|
|
const source = { subscribeSessions: vi.fn(() => unsubscribe) };
|
|
const wiring = createStateWiring(source);
|
|
|
|
wiring.dispose();
|
|
|
|
expect(source.subscribeSessions).toHaveBeenCalledOnce();
|
|
expect(unsubscribe).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it('provides a fresh StateBus and an onSessionsChanged that publishes "processes"', () => {
|
|
const wiring = createStateWiring(new BridgeManager());
|
|
expect(wiring.bus).toBeInstanceOf(StateBus);
|
|
const sub = vi.fn();
|
|
wiring.bus.subscribe(sub);
|
|
wiring.onSessionsChanged();
|
|
expect(sub).toHaveBeenCalledExactlyOnceWith('processes');
|
|
});
|
|
|
|
it('binds a BridgeManager so session-started publishes both "qq-list" and "connections"', () => {
|
|
const bm = new BridgeManager();
|
|
const wiring = createStateWiring(bm);
|
|
const seen: string[] = [];
|
|
wiring.bus.subscribe((r) => seen.push(r));
|
|
|
|
// Real onHookLogin path — fires the session-started listener internally
|
|
// exactly once (per uin).
|
|
bm.onHookLogin(4242, '12345', {} as never);
|
|
expect(seen).toEqual(['qq-list', 'connections']);
|
|
});
|
|
|
|
it('publishes "qq-list" + "connections" on session-closed', () => {
|
|
const bm = new BridgeManager();
|
|
const wiring = createStateWiring(bm);
|
|
bm.onHookLogin(4242, '12345', {} as never);
|
|
const sub = vi.fn();
|
|
wiring.bus.subscribe(sub);
|
|
|
|
bm.onPidDisconnected(4242);
|
|
|
|
// Order is qq-list first (account list shrinks) then connections
|
|
// (adapter status list shrinks). Two distinct resource invalidations.
|
|
expect(sub).toHaveBeenCalledTimes(2);
|
|
expect(sub).toHaveBeenNthCalledWith(1, 'qq-list');
|
|
expect(sub).toHaveBeenNthCalledWith(2, 'connections');
|
|
});
|
|
|
|
it('dispose() latches so later Hook/Bridge edges publish nothing', () => {
|
|
const bm = new BridgeManager();
|
|
const wiring = createStateWiring(bm);
|
|
const sub = vi.fn();
|
|
wiring.bus.subscribe(sub);
|
|
|
|
wiring.dispose();
|
|
|
|
// onSessionsChanged becomes a no-op.
|
|
wiring.onSessionsChanged();
|
|
expect(sub).not.toHaveBeenCalled();
|
|
|
|
// BridgeManager listener fires nothing into a disposed bus.
|
|
bm.onHookLogin(4242, '12345', {} as never);
|
|
expect(sub).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('survives a BridgeManager listener throw — the OTHER listeners on the same BridgeManager keep working', () => {
|
|
const bm = new BridgeManager();
|
|
const wiring = createStateWiring(bm);
|
|
|
|
// A peer subscriber (e.g. OneBotManager) added AFTER state-wiring throws.
|
|
const peer = vi.fn(() => {
|
|
throw new Error('peer down');
|
|
});
|
|
bm.subscribeSessions({ started: peer });
|
|
|
|
const sub = vi.fn();
|
|
wiring.bus.subscribe(sub);
|
|
|
|
expect(() => bm.onHookLogin(4242, '12345', {} as never)).not.toThrow();
|
|
// The state-wiring listener still ran (BridgeManager isolates exceptions
|
|
// per listener).
|
|
expect(sub).toHaveBeenCalledWith('qq-list');
|
|
expect(sub).toHaveBeenCalledWith('connections');
|
|
});
|
|
});
|