93 lines
2.9 KiB
TypeScript
93 lines
2.9 KiB
TypeScript
|
|
import { describe, test, expect } from 'bun:test';
|
||
|
|
import { ValidationError } from '@shade/core';
|
||
|
|
import {
|
||
|
|
encodeStreamControl,
|
||
|
|
parseStreamControl,
|
||
|
|
isStreamControlMessage,
|
||
|
|
} from '../src/index.js';
|
||
|
|
import type {
|
||
|
|
StreamInitMessage,
|
||
|
|
StreamFinishMessage,
|
||
|
|
StreamAbortMessage,
|
||
|
|
} from '../src/index.js';
|
||
|
|
|
||
|
|
describe('control envelope encode/parse roundtrip', () => {
|
||
|
|
test('stream-init', () => {
|
||
|
|
const msg: StreamInitMessage = {
|
||
|
|
kind: 'shade.stream-init/v1',
|
||
|
|
streamId: 'AAAAAAAAAAAAAAAAAAAAAA',
|
||
|
|
streamSecret: 'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBA',
|
||
|
|
metadata: {
|
||
|
|
name: 'world.zip',
|
||
|
|
sizeBytes: 1024,
|
||
|
|
contentType: 'application/zip',
|
||
|
|
chunkSize: 256,
|
||
|
|
totalChunks: 4,
|
||
|
|
sentAt: 1730000000000,
|
||
|
|
},
|
||
|
|
lanes: [
|
||
|
|
{
|
||
|
|
laneId: 0,
|
||
|
|
partition: { kind: 'range', startByte: 0, endByte: 512, startChunk: 0 },
|
||
|
|
},
|
||
|
|
{
|
||
|
|
laneId: 1,
|
||
|
|
partition: { kind: 'range', startByte: 512, endByte: 1024, startChunk: 0 },
|
||
|
|
},
|
||
|
|
],
|
||
|
|
};
|
||
|
|
const json = encodeStreamControl(msg);
|
||
|
|
expect(parseStreamControl(json)).toEqual(msg);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('stream-finish', () => {
|
||
|
|
const msg: StreamFinishMessage = {
|
||
|
|
kind: 'shade.stream-finish/v1',
|
||
|
|
streamId: 'AAAAAAAAAAAAAAAAAAAAAA',
|
||
|
|
laneSha256: [{ laneId: 0, sha256: 'abcd', chunkCount: 1, byteCount: 256 }],
|
||
|
|
overallSha256: 'efgh',
|
||
|
|
finishedAt: 1730000001000,
|
||
|
|
};
|
||
|
|
expect(parseStreamControl(encodeStreamControl(msg))).toEqual(msg);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('stream-abort', () => {
|
||
|
|
const msg: StreamAbortMessage = {
|
||
|
|
kind: 'shade.stream-abort/v1',
|
||
|
|
streamId: 'AAAAAAAAAAAAAAAAAAAAAA',
|
||
|
|
reason: 'sender-cancel',
|
||
|
|
message: 'user clicked cancel',
|
||
|
|
abortedAt: 1730000002000,
|
||
|
|
};
|
||
|
|
expect(parseStreamControl(encodeStreamControl(msg))).toEqual(msg);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('rejects malformed JSON', () => {
|
||
|
|
expect(() => parseStreamControl('not-json')).toThrow(ValidationError);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('rejects messages without a kind field', () => {
|
||
|
|
expect(() => parseStreamControl(JSON.stringify({ foo: 'bar' }))).toThrow(ValidationError);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('rejects messages whose kind does not start with shade.stream-', () => {
|
||
|
|
expect(() => parseStreamControl(JSON.stringify({ kind: 'other.kind' }))).toThrow(
|
||
|
|
ValidationError,
|
||
|
|
);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('isStreamControlMessage', () => {
|
||
|
|
test('returns true for valid shapes', () => {
|
||
|
|
expect(isStreamControlMessage({ kind: 'shade.stream-init/v1' })).toBe(true);
|
||
|
|
expect(isStreamControlMessage({ kind: 'shade.stream-finish/v1' })).toBe(true);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('returns false for non-objects and missing kind', () => {
|
||
|
|
expect(isStreamControlMessage(null)).toBe(false);
|
||
|
|
expect(isStreamControlMessage(42)).toBe(false);
|
||
|
|
expect(isStreamControlMessage({})).toBe(false);
|
||
|
|
expect(isStreamControlMessage({ kind: 'unrelated' })).toBe(false);
|
||
|
|
});
|
||
|
|
});
|