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,236 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
import http, { type IncomingMessage } from 'node:http';
import type { AddressInfo } from 'node:net';
import { HttpServerAdapter } from '../src/network/http-server-adapter';
import type { NetworkAdapterContext } from '../src/network/adapter';
import { buildDispatchPayload } from '../src/event-filter';
import type { HttpServerNetwork, JsonObject } from '../src/types';
const adapters: HttpServerAdapter[] = [];
const responses: IncomingMessage[] = [];
async function start(overrides: Partial<HttpServerNetwork> = {}, uin = '10001') {
const config: HttpServerNetwork = {
name: 'sse',
host: '127.0.0.1',
port: 0,
path: '/api',
enableSse: true,
accessToken: 'test-secret',
...overrides,
};
const api = {
isAcceptingActions: true,
isStreamAction: () => false,
handle: vi.fn(async () => ({ status: 'ok', retcode: 0, data: { self_id: uin } })),
};
const adapter = new HttpServerAdapter(config.name, config, {
uin,
api,
buildLifecycleEvent: (subType: string) => ({
self_id: uin,
post_type: 'meta_event',
meta_event_type: 'lifecycle',
sub_type: subType,
}),
buildHeartbeatEvent: () => ({
self_id: uin,
post_type: 'meta_event',
meta_event_type: 'heartbeat',
}),
} as unknown as NetworkAdapterContext);
adapters.push(adapter);
await adapter.open();
const port = ((adapter as unknown as { server: http.Server }).server.address() as AddressInfo)
.port;
const request = (
path = '/api/_events?access_token=test-secret',
headers: Record<string, string> = {},
method = 'GET',
): Promise<IncomingMessage> =>
new Promise((resolve, reject) => {
const req = http.request(
{ hostname: '127.0.0.1', port, path, headers, method },
(res) => {
responses.push(res);
// A stream closed intentionally by the test/server can be aborted.
res.on('error', () => undefined);
resolve(res);
},
);
req.on('error', reject);
req.end();
});
const emit = (id: number, postType = 'message') => {
const event: JsonObject = {
self_id: uin,
post_type: postType,
message_id: id,
message: [{ type: 'text', data: { text: `line\n${id}` } }],
raw_message: `line\n${id}`,
};
adapter.onEvent(event, buildDispatchPayload(event));
};
return { adapter, config, request, emit, api };
}
function collect(response: IncomingMessage) {
let text = '';
response.setEncoding('utf8');
response.on('data', (chunk: string) => {
text += chunk;
});
const frames = () =>
text
.split('\n\n')
.filter((frame) => frame.includes('data: '))
.map((frame) => {
const id = /^id: (.+)$/m.exec(frame)?.[1];
const data = JSON.parse(/^data: (.+)$/m.exec(frame)![1]) as JsonObject;
return { id, data };
});
return { frames, text: () => text };
}
afterEach(async () => {
responses.splice(0).forEach((res) => res.destroy());
await Promise.all(adapters.splice(0).map((adapter) => adapter.close()));
vi.useRealTimers();
});
describe('HTTP OneBot SSE', () => {
it('shares HTTP actions and prefix, authenticates all supported token forms, and frames events', async () => {
const { adapter, request, emit, api } = await start();
expect((await request('/wrong/_events?access_token=test-secret')).statusCode).toBe(404);
expect((await request('/api/_events')).statusCode).toBe(401);
expect((await request('/api/_events?access_token=wrong')).statusCode).toBe(401);
expect((await request(undefined, {}, 'POST')).statusCode).toBe(405);
const streams = [];
for (const [path, headers] of [
['/api/_events?access_token=test-secret', {}],
['/api/_events', { authorization: 'Bearer test-secret' }],
['/api/_events', { authorization: 'test-secret' }],
] as Array<[string, Record<string, string>]>) {
const res = await request(path, headers);
expect(res.statusCode).toBe(200);
expect(res.headers['content-type']).toContain('text/event-stream');
expect(res.headers['cache-control']).toBe('no-cache, no-transform');
expect(res.headers['x-accel-buffering']).toBe('no');
streams.push(collect(res));
}
emit(1);
emit(2, 'message_sent');
emit(3);
await vi.waitFor(() =>
expect(streams.every((stream) => stream.frames().length === 5)).toBe(true),
);
expect(
streams[0]
.frames()
.slice(3)
.map((frame) => frame.data.message_id),
).toEqual([1, 3]);
expect(streams[0].frames()[3].data.message).toEqual([
{ type: 'text', data: { text: 'line\n1' } },
]);
expect(streams[0].text()).toContain('retry: 3000\n\n');
const action = await request('/api/get_status', { authorization: 'test-secret' }, 'POST');
expect(action.statusCode).toBe(200);
expect(api.handle).toHaveBeenCalledWith('get_status', {});
expect(adapter.describeStatus().detail).toContain('SSE 3 个客户端');
});
it('replays exactly the missing events and maintains report format and self-message policy', async () => {
const { request, emit } = await start({ messageFormat: 'string', reportSelfMessage: true });
const initial = await request();
const first = collect(initial);
emit(1);
await vi.waitFor(() => expect(first.frames()).toHaveLength(4));
const cursor = first.frames()[3].id!;
initial.destroy();
emit(2, 'message_sent');
emit(3);
const resumed = collect(await request(undefined, { 'Last-Event-ID': cursor }));
await vi.waitFor(() => expect(resumed.frames()).toHaveLength(5));
expect(
resumed
.frames()
.slice(0, 2)
.map((frame) => frame.data.message_id),
).toEqual([2, 3]);
expect(resumed.frames()[0].data.message).toBe('line\n2');
expect(resumed.frames()[0].data.post_type).toBe('message_sent');
});
it('rejects expired, malformed, future and other-account cursors without silently losing events', async () => {
const first = await start();
const second = await start({}, '10002');
const stream = collect(await first.request());
await vi.waitFor(() => expect(stream.frames()).toHaveLength(3));
const cursor = stream.frames()[0].id!;
const header = { 'Last-Event-ID': cursor };
expect((await second.request(undefined, header)).statusCode).toBe(409);
expect((await first.request(undefined, { 'Last-Event-ID': 'broken' })).statusCode).toBe(
400,
);
expect(
(await first.request(undefined, { 'Last-Event-ID': cursor.replace(/:0$/, ':99') }))
.statusCode,
).toBe(409);
for (let i = 0; i < 257; i += 1) {
first.emit(i);
}
expect((await first.request(undefined, header)).statusCode).toBe(409);
// A new subscription remains possible after explicit resynchronization.
expect((await first.request()).statusCode).toBe(200);
});
it('revokes live streams on token/report policy changes and closes without waiting on SSE', async () => {
const { adapter, config, request } = await start();
const res = await request();
const closed = new Promise<void>((resolve) => res.once('close', resolve));
await adapter.reload({ ...config, accessToken: 'rotated' });
await closed;
expect((await request()).statusCode).toBe(401);
const next = await request('/api/_events?access_token=rotated');
const changed = new Promise<void>((resolve) => next.once('close', resolve));
await adapter.reload({ ...config, accessToken: 'rotated', reportSelfMessage: true });
await changed;
const last = await request('/api/_events?access_token=rotated');
const ended = new Promise<void>((resolve) => last.once('close', resolve));
await adapter.close();
await ended;
expect(adapter.describeStatus().status).toBe('disabled');
});
it('is opt-in and can be enabled/disabled live without replacing the HTTP listener', async () => {
const { adapter, config, request } = await start({ enableSse: false });
expect((await request()).statusCode).toBe(404);
await adapter.reload({ ...config, enableSse: true });
const stream = await request();
expect(stream.statusCode).toBe(200);
const closed = new Promise<void>((resolve) => stream.once('close', resolve));
await adapter.reload(config);
await closed;
expect((await request()).statusCode).toBe(404);
});
it('sends comment heartbeats and releases their timer when the client leaves', async () => {
vi.useFakeTimers({ toFake: ['setInterval', 'clearInterval'] });
const { adapter, request } = await start();
const res = await request();
const stream = collect(res);
await vi.waitFor(() => expect(stream.frames()).toHaveLength(3));
await vi.advanceTimersByTimeAsync(15_000);
await vi.waitFor(() => expect(stream.text()).toContain(': ping\n\n'));
res.destroy();
await vi.waitFor(() => expect(adapter.describeStatus().detail).toContain('SSE 0 个客户端'));
expect(vi.getTimerCount()).toBe(0);
});
});