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,115 @@
// E2E for Docker "QQ never logs in + pipe keeps reconnecting" scenario.
//
// Drives the real PipeWatcher + HookManager through repeated same-PID
// pipe up/down cycles via tickNow() and asserts the BridgeManager mock
// is never touched (no login → no Bridge / OneBotInstance / Identity).
import { describe, it, expect, vi } from 'vitest';
import { EventEmitter } from 'events';
import { HookManager } from '../src/hook-manager';
import { PipeWatcher } from '../src/pipe-watcher';
import type { ManualMapHandle } from '../src/injector';
import type { BridgeManagerSink } from '../src/hook-manager';
import type { QqHookClient } from '../src/qq-hook-client';
const DUMMY_HANDLE: ManualMapHandle = { base: 0n, entry: 0n, exceptionTable: 0n, size: 0 };
const flush = () => new Promise<void>((r) => setImmediate(r));
class FakeClient extends EventEmitter {
isClosed = false;
isLoggedIn = false;
async connectAll(_opts: { recv: boolean }) {
if (this.isClosed) {
throw new Error('client is closed');
}
}
getLoginState() {
return { loggedIn: false, uin: '0', uinNumber: 0n };
}
close() {
if (this.isClosed) {
return;
}
this.isClosed = true;
this.emit('close');
}
}
describe('HookManager — Docker reconnect-without-login leak guard', () => {
it('after N pipe up/down cycles: no Bridge calls, no client accumulation', async () => {
const PID = 61;
const liveSet = new Set<number>([PID]);
const clients: FakeClient[] = [];
const pipeWatcher = new PipeWatcher({
listProcesses: async () => [{ pid: PID, name: 'qq', path: '' }],
listLivePipes: async () => new Set(liveSet),
intervalMs: 60_000, // manual ticks only
});
const bridgeManager = {
onPacket: vi.fn(),
onHookLogin: vi.fn(),
onPidDisconnected: vi.fn(),
} as unknown as BridgeManagerSink;
const manager = new HookManager({
bridgeManager,
pipeWatcher,
injector: {
inject: vi.fn(async () => ({
method: 'loadModuleManual' as const,
handle: DUMMY_HANDLE,
})),
unload: vi.fn(async () => {}),
},
makeClient: () => {
const c = new FakeClient();
clients.push(c);
return c as unknown as QqHookClient;
},
autoLoadOnDiscovery: true,
listProcesses: async () => [{ pid: PID, name: 'qq', path: '' }],
detectAccounts: async () => [],
watchAccounts: () => () => {},
});
// Initial discovery + first connect.
await pipeWatcher.start();
await flush();
await flush();
// Drive 50 pipe up/down cycles with the QQ process staying alive but
// the pipe (= QQ control socket) bouncing every "tick".
const CYCLES = 50;
for (let i = 0; i < CYCLES; i++) {
liveSet.delete(PID);
await pipeWatcher.tickNow();
await flush();
liveSet.add(PID);
await pipeWatcher.tickNow();
await flush();
}
// The "QQ never logs in" hypothesis: BridgeManager should never have
// been told about a login or a disconnect.
expect(bridgeManager.onHookLogin).not.toHaveBeenCalled();
expect(bridgeManager.onPidDisconnected).not.toHaveBeenCalled();
expect(bridgeManager.onPacket).not.toHaveBeenCalled();
// Hook layer should hold at most one live client.
const alive = clients.filter((c) => !c.isClosed);
expect(
alive.length,
`live clients after ${CYCLES} cycles: ${alive.length}`,
).toBeLessThanOrEqual(1);
// Total clients minted = roughly one per up cycle. Sanity bound:
// we must have actually exercised the reconnect path.
expect(clients.length).toBeGreaterThan(1);
manager.dispose();
});
});