Files
TanTan fa9c73363d
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
SnowLuma v2.4.2 source snapshot
2026-11-13 09:02:00 +08:00

98 lines
3.7 KiB
TypeScript

import { describe, it, expect, vi } from 'vitest';
import { OneBotNetworkManager } from '../src/network/network-manager';
import type { ApiActionContext } from '../src/api-handler';
import { createCompiledTestHandler, testAction } from './helpers/compiled-action-handler';
// ApiHandler's register* only define handlers (closures) at construction — they
// don't touch ctx — so a throwing proxy is a safe minimal context.
function fakeCtx(): ApiActionContext {
return new Proxy({} as ApiActionContext, {
get(_t, prop) {
throw new Error(`ctx.${String(prop)} not stubbed`);
},
});
}
describe('OneBotNetworkManager.subscribeDebug', () => {
it('notifies debug subscribers on emitEvent even with no active adapters', async () => {
const nm = new OneBotNetworkManager();
const seen: unknown[] = [];
nm.subscribeDebug((e) => seen.push(e));
await nm.emitEvent({ post_type: 'meta_event', x: 1 });
expect(seen).toEqual([{ post_type: 'meta_event', x: 1 }]);
});
it('stops notifying after unsubscribe', async () => {
const nm = new OneBotNetworkManager();
const cb = vi.fn();
const off = nm.subscribeDebug(cb);
await nm.emitEvent({ a: 1 });
off();
await nm.emitEvent({ a: 2 });
expect(cb).toHaveBeenCalledTimes(1);
expect(cb).toHaveBeenCalledWith({ a: 1 });
});
it('a throwing subscriber does not break emitEvent', async () => {
const nm = new OneBotNetworkManager();
nm.subscribeDebug(() => {
throw new Error('boom');
});
await expect(nm.emitEvent({ a: 1 })).resolves.toBeUndefined();
});
});
describe('ApiHandler.setObserver', () => {
it('fires after a successful action with {action, params, response, ms}', async () => {
const h = createCompiledTestHandler(fakeCtx(), [
testAction('ping', async () => ({ status: 'ok', retcode: 0, data: 'pong' })),
]);
const recs: Array<{ action: string; response: { status: string } }> = [];
h.setObserver((r) => recs.push(r as any));
const res = await h.handle('ping', { x: 1 });
expect(res).toMatchObject({ status: 'ok', data: 'pong' });
expect(recs).toHaveLength(1);
expect(recs[0]).toMatchObject({
action: 'ping',
params: { x: 1 },
response: { status: 'ok' },
});
expect(typeof (recs[0] as any).ms).toBe('number');
});
it('fires on a thrown action with the failure response', async () => {
const h = createCompiledTestHandler(fakeCtx(), [
testAction('boom', async () => {
throw new Error('kaboom');
}),
]);
const recs: any[] = [];
h.setObserver((r) => recs.push(r));
await h.handle('boom', {});
expect(recs).toHaveLength(1);
expect(recs[0].response.status).toBe('failed');
});
it('unsubscribe stops observation', async () => {
const h = createCompiledTestHandler(fakeCtx(), [
testAction('ping', async () => ({ status: 'ok', retcode: 0 })),
]);
const cb = vi.fn();
const off = h.setObserver(cb);
await h.handle('ping', {});
off();
await h.handle('ping', {});
expect(cb).toHaveBeenCalledTimes(1);
});
it('a throwing observer does not break handle', async () => {
const h = createCompiledTestHandler(fakeCtx(), [
testAction('ping', async () => ({ status: 'ok', retcode: 0 })),
]);
h.setObserver(() => {
throw new Error('observer boom');
});
await expect(h.handle('ping', {})).resolves.toMatchObject({ status: 'ok' });
});
});