SnowLuma v2.4.2 source snapshot
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

This commit is contained in:
TanTan committed 2026-11-13 09:02:00 +08:00
commit fa9c73363d
1367 files changed
+355735

No files matched your search

@@ -0,0 +1,185 @@
import { setImmediate as nextTurn } from 'node:timers/promises';
import { IdentityService } from '@snowluma/protocol/identity-service';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import type { PacketInfo } from '@snowluma/common/protocol-types';
import { BridgeManager } from '../src/bridge/manager';
function deferred() {
let resolve!: () => void;
let reject!: (error: Error) => void;
const promise = new Promise<void>((yes, no) => {
resolve = yes;
reject = no;
});
return { promise, resolve, reject };
}
describe('BridgeManager shutdown', () => {
let manager: BridgeManager;
const sender = { sendPacket: vi.fn() } as never;
beforeEach(() => {
vi.spyOn(IdentityService, 'openForUin').mockImplementation((uin, _root, beforeOpen) => {
void beforeOpen?.catch(() => undefined);
return IdentityService.memory(uin);
});
manager = new BridgeManager();
});
afterEach(async () => {
await manager.dispose().catch(() => undefined);
vi.restoreAllMocks();
});
it('drains every detached session for one account and reports each failure once', async () => {
const previous = deferred();
const latest = deferred();
const firstError = new Error('previous close failed');
const lastError = new Error('latest close failed');
manager.onHookLogin(101, '10001', sender);
vi.spyOn(manager.getBridge('10001')!.identity, 'close').mockReturnValue(previous.promise);
manager.onPidDisconnected(101);
previous.reject(firstError);
await nextTurn();
manager.onHookLogin(202, '10001', sender);
vi.spyOn(manager.getBridge('10001')!.identity, 'close').mockReturnValue(latest.promise);
manager.onPidDisconnected(202);
let settled = false;
const result = manager
.dispose()
.catch((error: AggregateError) => error)
.then((error) => {
settled = true;
return error;
});
try {
await nextTurn();
expect(settled).toBe(false);
} finally {
latest.reject(lastError);
}
const error = await result;
expect(error).toBeInstanceOf(AggregateError);
expect((error as AggregateError).errors).toEqual([firstError, lastError]);
});
it('includes the session whose close observer requested shutdown', async () => {
const cleanup = deferred();
let shutdown!: Promise<void>;
let settled = false;
manager.onHookLogin(101, '10001', sender);
vi.spyOn(manager.getBridge('10001')!.identity, 'close').mockReturnValue(cleanup.promise);
manager.subscribeSessions({
closed: () => {
shutdown = manager.dispose().then(() => {
settled = true;
});
},
});
manager.onPidDisconnected(101);
try {
await nextTurn();
expect(settled).toBe(false);
} finally {
cleanup.resolve();
await shutdown;
}
});
it('waits for every previous close before opening the account again', async () => {
const first = deferred();
const second = deferred();
manager.onHookLogin(101, '10001', sender);
vi.spyOn(manager.getBridge('10001')!.identity, 'close').mockReturnValue(first.promise);
manager.onPidDisconnected(101);
manager.onHookLogin(202, '10001', sender);
vi.spyOn(manager.getBridge('10001')!.identity, 'close').mockReturnValue(second.promise);
manager.onPidDisconnected(202);
manager.onHookLogin(303, '10001', sender);
const beforeOpen = vi.mocked(IdentityService.openForUin).mock.calls.at(-1)![2]!;
let ready = false;
const opening = beforeOpen.then(() => {
ready = true;
});
try {
second.resolve();
await nextTurn();
expect(ready).toBe(false);
} finally {
first.resolve();
await opening;
}
});
it('reopening is not poisoned by an already-failed close', async () => {
const failing = deferred();
manager.onHookLogin(101, '10001', sender);
vi.spyOn(manager.getBridge('10001')!.identity, 'close').mockReturnValue(failing.promise);
manager.onPidDisconnected(101);
failing.reject(new Error('close failed'));
await nextTurn();
// The failed close stays in the bookkeeping (dispose still reports
// it), but the reopening gate must absorb the rejection instead of
// passing it down — a rejected gate would keep the identity DB of
// every future session for this UIN closed until process restart.
manager.onHookLogin(202, '10001', sender);
const beforeOpen = vi.mocked(IdentityService.openForUin).mock.calls.at(-1)![2]!;
await expect(beforeOpen).resolves.toBeUndefined();
});
it('reopening waits for all closes to settle even when one of them fails', async () => {
const failing = deferred();
const slow = deferred();
manager.onHookLogin(101, '10001', sender);
vi.spyOn(manager.getBridge('10001')!.identity, 'close').mockReturnValue(failing.promise);
manager.onPidDisconnected(101);
manager.onHookLogin(202, '10001', sender);
vi.spyOn(manager.getBridge('10001')!.identity, 'close').mockReturnValue(slow.promise);
manager.onPidDisconnected(202);
failing.reject(new Error('close failed'));
await nextTurn();
manager.onHookLogin(303, '10001', sender);
const beforeOpen = vi.mocked(IdentityService.openForUin).mock.calls.at(-1)![2]!;
let ready = false;
const opening = beforeOpen.then(() => {
ready = true;
});
try {
await nextTurn();
// Still waiting for the pending close...
expect(ready).toBe(false);
} finally {
slow.resolve();
}
// ...and the already-failed close must not reject the gate.
await opening;
});
it('ignores late transport callbacks after shutdown starts', async () => {
const shutdown = manager.dispose();
manager.onHookLogin(101, '10001', sender);
manager.onPacket({
pid: 202,
uin: '20002',
serviceCmd: 'Test.Unhandled',
seqId: 1,
retCode: 0,
fromClient: false,
body: Buffer.alloc(0),
} satisfies PacketInfo);
manager.onPidReceiveHealthChanged(101, false);
await shutdown;
expect(IdentityService.openForUin).not.toHaveBeenCalled();
expect(manager.getBridge('10001')).toBeNull();
expect(manager.getBridge('20002')).toBeNull();
});
});