Files
SnowLuma/packages/sdk/tests/http-streaming.test.ts
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

223 lines
8.3 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from 'vitest';
import {
SnowLumaAbortError,
SnowLumaApiError,
SnowLumaAuthError,
SnowLumaHttpClient,
SnowLumaParseError,
SnowLumaTimeoutError,
} from '../src/index.js';
const data = { type: 'stream', data_type: 'data_chunk', data: '中文🙂' };
const complete = { type: 'response', data_type: 'data_complete', data: 'done' };
const frame = (payload: unknown) => ({
status: 'ok',
retcode: 0,
stream: 'stream-action',
data: payload,
echo: 'stream',
});
const wire = (payload: unknown) => JSON.stringify(payload) + '\r\n\r\n';
async function collect<T>(stream: AsyncIterable<T>): Promise<T[]> {
const results: T[] = [];
for await (const result of stream) {
results.push(result);
}
return results;
}
function clientFor(body: BodyInit | null) {
return new SnowLumaHttpClient({ fetch: async () => new Response(body) });
}
afterEach(() => vi.useRealTimers());
describe('HTTP streaming', () => {
it('does not send an already-aborted request', async () => {
const fetch = vi.fn();
const client = new SnowLumaHttpClient({ fetch });
await expect(
client.stream('test_download_stream', {}, { signal: AbortSignal.abort() }).next(),
).rejects.toBeInstanceOf(SnowLumaAbortError);
expect(fetch).not.toHaveBeenCalled();
});
it('honors cancellation between frames buffered in the same HTTP read', async () => {
const controller = new AbortController();
const client = clientFor(wire(frame(data)) + wire(frame(complete)));
const stream = client.stream('test_download_stream', {}, { signal: controller.signal });
await stream.next();
controller.abort();
await expect(stream.next()).rejects.toBeInstanceOf(SnowLumaAbortError);
});
it.each(['one byte', 'one read'] as const)(
'decodes split UTF-8 and delimiters: %s',
async (mode) => {
const bytes = new TextEncoder().encode(
wire(frame(data)) + wire(frame(data)) + wire(frame(complete)),
);
let offset = 0;
const body = new ReadableStream<Uint8Array>({
pull(controller) {
if (offset === bytes.length) {
controller.close();
return;
}
const end = mode === 'one byte' ? offset + 1 : bytes.length;
controller.enqueue(bytes.subarray(offset, end));
offset = end;
},
});
const fetch = vi.fn(async () => new Response(body));
const client = new SnowLumaHttpClient({ fetch, accessToken: 'test-token' });
const results = await collect(
client.requestStream('test_download_stream', {}, { echo: 'stream' }),
);
expect(results.map((result) => result.data)).toEqual([data, data, complete]);
expect(
results.every(
(result) => result.stream === 'stream-action' && result.echo === 'stream',
),
).toBe(true);
expect(fetch).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
headers: expect.objectContaining({ Authorization: 'Bearer test-token' }),
body: JSON.stringify({
action: 'test_download_stream',
params: {},
echo: 'stream',
}),
}),
);
expect(body.locked).toBe(false);
},
);
it('delivers a frame before EOF and cancels the reader when iteration stops', async () => {
const cancel = vi.fn();
const body = new ReadableStream<Uint8Array>({
start(controller) {
controller.enqueue(new TextEncoder().encode(wire(frame(data))));
},
cancel,
});
const stream = clientFor(body).stream('test_download_stream', {});
expect(await stream.next()).toMatchObject({ value: data, done: false });
await stream.return();
expect(cancel).toHaveBeenCalledOnce();
expect(body.locked).toBe(false);
});
it.each([
['', 'terminal'],
[wire(frame(data)), 'terminal'],
[wire({ status: 'ok', retcode: 0, data }), 'marker'],
[wire({ status: 'ok', data }), 'envelope'],
[wire(frame({ type: 'error' })), 'frame'],
['{bad json}\r\n\r\n', 'JSON'],
])('rejects incomplete or invalid responses: %s', async (body, message) => {
await expect(collect(clientFor(body).stream('test_download_stream', {}))).rejects.toThrow(
message,
);
});
it('rejects malformed UTF-8', async () => {
await expect(
collect(clientFor(new Uint8Array([0xff])).stream('test_download_stream', {})),
).rejects.toBeInstanceOf(SnowLumaParseError);
});
it('bounds individual frames while allowing multiple frames in one large read', async () => {
const text = wire(frame(data)).repeat(10) + wire(frame(complete));
await expect(
collect(clientFor(text).stream('test_download_stream', {}, { maxBufferBytes: 200 })),
).resolves.toHaveLength(11);
await expect(
collect(clientFor(text).stream('test_download_stream', {}, { maxBufferBytes: 10 })),
).rejects.toThrow('maxBufferBytes');
await expect(
collect(
clientFor('x'.repeat(500)).stream(
'test_download_stream',
{},
{ maxBufferBytes: 10 },
),
),
).rejects.toThrow('maxBufferBytes');
});
it('returns raw failures and throws typed API errors from stream()', async () => {
const failure = { status: 'failed', retcode: 1401, data: null, wording: 'denied' };
expect(
await collect(
clientFor(JSON.stringify(failure)).requestStream('test_download_stream', {}),
),
).toMatchObject([failure]);
await expect(
collect(clientFor(JSON.stringify(failure)).stream('test_download_stream', {})),
).rejects.toBeInstanceOf(SnowLumaAuthError);
const stream = clientFor(wire(frame(data)) + wire({ ...failure, retcode: 1500 })).stream(
'test_download_stream',
{},
);
expect((await stream.next()).value).toEqual(data);
await expect(stream.next()).rejects.toBeInstanceOf(SnowLumaApiError);
});
it.each(['abort', 'timeout'] as const)('releases a pending body read on %s', async (reason) => {
vi.useFakeTimers();
const cancel = vi.fn();
const body = new ReadableStream<Uint8Array>({ cancel });
const controller = new AbortController();
const remove = vi.spyOn(controller.signal, 'removeEventListener');
const stream = clientFor(body).stream(
'test_download_stream',
{},
{ signal: controller.signal, timeoutMs: 100 },
);
const next = expect(stream.next()).rejects.toBeInstanceOf(
reason === 'abort' ? SnowLumaAbortError : SnowLumaTimeoutError,
);
await vi.advanceTimersByTimeAsync(0);
if (reason === 'abort') {
controller.abort();
} else {
await vi.advanceTimersByTimeAsync(100);
}
await next;
expect(cancel).toHaveBeenCalledOnce();
expect(remove).toHaveBeenCalled();
expect(body.locked).toBe(false);
expect(vi.getTimerCount()).toBe(0);
});
it('ends an upload ACK without waiting for a response-type frame or HTTP EOF', async () => {
const ack = {
type: 'stream',
status: 'chunk_received',
stream_id: 'id',
received_chunks: 1,
total_chunks: 2,
};
const cancel = vi.fn();
const body = new ReadableStream<Uint8Array>({
start(controller) {
controller.enqueue(new TextEncoder().encode(wire(frame(ack))));
},
cancel,
});
expect(
await collect(clientFor(body).stream('upload_file_stream', { stream_id: 'id' })),
).toEqual([ack]);
expect(cancel).toHaveBeenCalledOnce();
});
});