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
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:
commit
fa9c73363d
1367 files changed
+355735
No files matched your search
@@ -0,0 +1,212 @@
|
||||
import { createHash } from 'node:crypto';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import type { Server } from 'node:http';
|
||||
import type { AddressInfo } from 'node:net';
|
||||
import { Readable } from 'node:stream';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import {
|
||||
SnowLumaApiError,
|
||||
SnowLumaHttpClient,
|
||||
SnowLumaWebSocketClient,
|
||||
type SnowLumaApiClient,
|
||||
} from '../../sdk/src/index';
|
||||
import type { ApiActionContext } from '../src/api-handler';
|
||||
import { actions as uploadActions, __test as uploadTest } from '../src/actions/stream-file';
|
||||
import { actions as downloadActions } from '../src/actions/stream-download';
|
||||
import { actions as messageActions } from '../src/actions/message';
|
||||
import { HttpServerAdapter } from '../src/network/http-server-adapter';
|
||||
import { okResponse, type HttpServerNetwork } from '../src/types';
|
||||
import { createCompiledTestHandler, testAction } from './helpers/compiled-action-handler';
|
||||
|
||||
// Real server adapters, parsers and disk handlers, isolated by tests/setup.ts.
|
||||
// No QQ session or protocol requests are involved.
|
||||
describe('SDK streaming against OneBot', () => {
|
||||
let adapter: HttpServerAdapter | undefined;
|
||||
let ws: SnowLumaWebSocketClient | undefined;
|
||||
|
||||
afterEach(async () => {
|
||||
ws?.close();
|
||||
ws = undefined;
|
||||
await adapter?.close();
|
||||
adapter = undefined;
|
||||
|
||||
for (const key of uploadTest.uploads.keys()) {
|
||||
uploadTest.cleanupUpload(key, true);
|
||||
}
|
||||
});
|
||||
|
||||
async function connect(
|
||||
transport: 'http' | 'ws',
|
||||
context = {} as ApiActionContext,
|
||||
): Promise<SnowLumaApiClient> {
|
||||
const api = createCompiledTestHandler(context, [
|
||||
...uploadActions,
|
||||
...downloadActions,
|
||||
...messageActions.filter((action) =>
|
||||
action.names.some((name) => ['send_group_msg', 'send_private_msg'].includes(name)),
|
||||
),
|
||||
testAction('sdk_ping', () => okResponse({ pong: true })),
|
||||
]);
|
||||
const config: HttpServerNetwork = {
|
||||
name: 'sdk-streaming',
|
||||
host: '127.0.0.1',
|
||||
port: 0,
|
||||
path: '/',
|
||||
enabled: true,
|
||||
enableWebSocket: true,
|
||||
accessToken: 'sdk-test',
|
||||
messageFormat: 'array',
|
||||
reportSelfMessage: false,
|
||||
};
|
||||
adapter = new HttpServerAdapter(config.name, config, {
|
||||
uin: '10001',
|
||||
api,
|
||||
buildLifecycleEvent: () => ({}),
|
||||
buildHeartbeatEvent: () => ({}),
|
||||
});
|
||||
await adapter.open();
|
||||
const { port } = (adapter as unknown as { server: Server }).server.address() as AddressInfo;
|
||||
|
||||
if (transport === 'http') {
|
||||
return new SnowLumaHttpClient({
|
||||
baseUrl: `http://127.0.0.1:${port}/`,
|
||||
accessToken: 'sdk-test',
|
||||
});
|
||||
}
|
||||
|
||||
ws = new SnowLumaWebSocketClient({
|
||||
url: `ws://127.0.0.1:${port}/api`,
|
||||
accessToken: 'sdk-test',
|
||||
});
|
||||
await ws.connect();
|
||||
return ws;
|
||||
}
|
||||
|
||||
it.each(['http', 'ws'] as const)(
|
||||
'%s uploads and downloads binary data incrementally',
|
||||
async (transport) => {
|
||||
const client = await connect(transport);
|
||||
const bytes = Uint8Array.from({ length: 210007 }, (_, index) => index % 256);
|
||||
const sha256 = createHash('sha256').update(bytes).digest('hex');
|
||||
const result = await client.uploadFileStream(
|
||||
Readable.from([
|
||||
bytes.subarray(0, 17),
|
||||
bytes.subarray(17, 100009),
|
||||
bytes.subarray(100009),
|
||||
]),
|
||||
{
|
||||
fileSize: bytes.length,
|
||||
chunkSize: 16384,
|
||||
filename: '二进制.bin',
|
||||
expectedSha256: sha256,
|
||||
fileRetention: 0,
|
||||
},
|
||||
);
|
||||
expect(result.sha256).toBe(sha256);
|
||||
expect(result.total_chunks).toBe(Math.ceil(bytes.length / 16384));
|
||||
expect(await readFile(result.file_path)).toEqual(Buffer.from(bytes));
|
||||
|
||||
for (const action of [
|
||||
'download_file_stream',
|
||||
'download_file_image_stream',
|
||||
'download_file_record_stream',
|
||||
] as const) {
|
||||
const stream = client.downloadFileStream(
|
||||
{ file: result.file_path, chunk_size: 8192 },
|
||||
{ action },
|
||||
);
|
||||
const output: Uint8Array[] = [];
|
||||
|
||||
while (true) {
|
||||
const next = await stream.next();
|
||||
|
||||
if (next.done) {
|
||||
expect(next.value).toMatchObject({
|
||||
total_bytes: bytes.length,
|
||||
total_chunks: output.length,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
expect(next.value.byteLength).toBeLessThanOrEqual(8192);
|
||||
output.push(next.value);
|
||||
}
|
||||
|
||||
expect(output.length).toBeGreaterThan(1);
|
||||
expect(Buffer.concat(output)).toEqual(Buffer.from(bytes));
|
||||
}
|
||||
|
||||
await expect(client.raw('sdk_ping')).resolves.toEqual({ pong: true });
|
||||
},
|
||||
);
|
||||
|
||||
it.each(['http', 'ws'] as const)('%s handles empty files', async (transport) => {
|
||||
const client = await connect(transport);
|
||||
const result = await client.uploadFileStream(new Uint8Array(), { fileRetention: 0 });
|
||||
expect(result.file_size).toBe(0);
|
||||
const stream = client.downloadFileStream({ file: result.file_path });
|
||||
expect(await stream.next()).toMatchObject({
|
||||
done: true,
|
||||
value: { total_bytes: 0, total_chunks: 0 },
|
||||
});
|
||||
});
|
||||
|
||||
it.each(['http', 'ws'] as const)(
|
||||
'%s sends staged media through the real upload handler and message parser',
|
||||
async (transport) => {
|
||||
const sent: unknown[] = [];
|
||||
const send = vi.fn(async (id: number, message: unknown, autoEscape: boolean) => {
|
||||
expect(id).toBe(12345);
|
||||
expect(autoEscape).toBe(false);
|
||||
const segments = message as Array<{ type: string; data: { file: string } }>;
|
||||
expect(segments).toHaveLength(1);
|
||||
expect(segments[0].type).toBe('image');
|
||||
expect(await readFile(segments[0].data.file)).toEqual(Buffer.from([1, 2, 3]));
|
||||
sent.push(message);
|
||||
return { messageId: -42 };
|
||||
});
|
||||
const client = await connect(transport, {
|
||||
sendGroupMessage: send,
|
||||
sendPrivateMessage: send,
|
||||
} as unknown as ApiActionContext);
|
||||
|
||||
await expect(
|
||||
client.sendGroupImage(12345, new Uint8Array([1, 2, 3]), {
|
||||
chunkSize: 2,
|
||||
filename: 'picture.bin',
|
||||
}),
|
||||
).resolves.toEqual({ message_id: -42 });
|
||||
await expect(
|
||||
client.sendPrivateImage(12345, new Blob([new Uint8Array([1, 2, 3])])),
|
||||
).resolves.toEqual({ message_id: -42 });
|
||||
expect(sent).toHaveLength(2);
|
||||
},
|
||||
);
|
||||
|
||||
it.each(['http', 'ws'] as const)(
|
||||
'%s preserves a server error after partial download output',
|
||||
async (transport) => {
|
||||
const client = await connect(transport);
|
||||
const received: unknown[] = [];
|
||||
|
||||
const run = async () => {
|
||||
for await (const frame of client.stream('test_download_stream', { error: true })) {
|
||||
received.push(frame);
|
||||
}
|
||||
};
|
||||
|
||||
await expect(run()).rejects.toBeInstanceOf(SnowLumaApiError);
|
||||
expect(received).toHaveLength(10);
|
||||
},
|
||||
);
|
||||
|
||||
it.each(['http', 'ws'] as const)(
|
||||
'%s propagates server hash validation failure',
|
||||
async (transport) => {
|
||||
const client = await connect(transport);
|
||||
await expect(
|
||||
client.uploadFileStream(new Uint8Array([1]), { expectedSha256: '0'.repeat(64) }),
|
||||
).rejects.toThrow('sha256 mismatch');
|
||||
},
|
||||
);
|
||||
});
|
||||
Reference in new issue
Block a user