Skip to content
Source
Sign inSign up

stream.test.ts

108 lines3.9 KB

1import { DEFAULT_SPEECH_TRANSCRIPTION_OPTIONS } from "@t3tools/contracts";
2import { expect, it, vi } from "vite-plus/test";
3import { SPEECH_STREAM_MAX_QUEUED_BYTES, type SpeechStreamEvent } from "@t3tools/contracts";
4import { openSpeechStream } from "./stream.ts";
5​
6class TestSocket extends EventTarget {
7 sent: (string | Uint8Array)[] = [];
8 closed = false;
9 send(data: string | Uint8Array) {
10 this.sent.push(data);
11 }
12 close() {
13 this.closed = true;
14 this.dispatchEvent(new Event("close"));
15 }
16 receive(event: SpeechStreamEvent) {
17 this.dispatchEvent(new MessageEvent("message", { data: JSON.stringify(event) }));
18 }
20​
21function connect(bufferedAudio?: Float32Array, canSend = () => true) {
22 const socket = new TestSocket();
23 const abort = new AbortController();
24 const onText = vi.fn();
25 const onError = vi.fn();
26 const stream = openSpeechStream({
27 url: "wss://environment.test/ws/voice",
28 options: DEFAULT_SPEECH_TRANSCRIPTION_OPTIONS,
29 signal: abort.signal,
30 canSend,
31 onText,
32 onError,
33 createSocket: () => socket as unknown as WebSocket,
34 });
35 socket.dispatchEvent(new Event("open"));
36 expect(JSON.parse(socket.sent.shift() as string)).toEqual({
37 type: "start",
38 options: DEFAULT_SPEECH_TRANSCRIPTION_OPTIONS,
39 });
40 if (bufferedAudio) {
41 stream.feed(bufferedAudio);
42 expect(socket.sent).toHaveLength(0);
43 }
44 socket.receive({ type: "ready" });
45 return { socket, abort, onText, onError, stream };
47​
48it("drains ordered PCM before finishing and replaces tentative hypotheses", async () => {
49 const { socket, stream, onText } = connect(new Float32Array([0.1]));
50 stream.feed(new Float32Array([0.2]));
51 const final = stream.finish();
52 expect(socket.sent).toHaveLength(2);
53 socket.receive({ type: "update", revision: 1, text: { committed: "hello ", tentative: "were" } });
54 expect(socket.sent).toHaveLength(2);
55 expect(new Float32Array((socket.sent[1] as Uint8Array).slice().buffer)[0]).toBeCloseTo(0.2);
56 socket.receive({
57 type: "update",
58 revision: 2,
59 text: { committed: "hello ", tentative: "world" },
60 });
61 expect(socket.sent[2]).toBe('{"type":"finish"}');
62 expect(onText.mock.calls.map(([text]) => text)).toEqual([
63 { committed: "hello ", tentative: "were" },
64 { committed: "hello ", tentative: "world" },
65 ]);
66 socket.receive({ type: "finished", text: "hello world" });
67 await expect(final).resolves.toBe("hello world");
68 expect(socket.closed).toBe(true);
69});
70​
71it("rejects recordings beyond the duration limit", async () => {
72 const { socket, stream, onError } = connect();
73 stream.feed(new Float32Array(SPEECH_STREAM_MAX_QUEUED_BYTES / 4 + 1));
74 await expect(stream.finish()).rejects.toThrow("limited to five minutes");
75 expect(onError).toHaveBeenCalledOnce();
76 expect(socket.sent).toHaveLength(0);
77 expect(socket.closed).toBe(true);
78});
79​
80it.each(["cancel", "revoke"])("does not send queued frames after %s", async (action) => {
81 let allowed = true;
82 const { socket, stream, abort, onText } = connect(undefined, () => allowed);
83 stream.feed(new Float32Array([0.1]));
84 stream.feed(new Float32Array([0.2]));
85 stream.feed(new Float32Array([0.3]));
86 if (action === "cancel") abort.abort();
87 else allowed = false;
88 socket.receive({ type: "update", revision: 1, text: { committed: "late", tentative: "" } });
89 expect(socket.sent).toHaveLength(2);
90 expect(onText).not.toHaveBeenCalled();
91 await expect(stream.finish()).rejects.toThrow(action === "cancel" ? "cancelled" : "revoked");
92});
93​
94it("fails on disconnect and times out stalled inference", async () => {
95 vi.useFakeTimers();
96 try {
97 const first = connect();
98 first.socket.close();
99 await expect(first.stream.finish()).rejects.toThrow("connection closed");
100 const second = connect();
101 second.stream.feed(new Float32Array([0.1]));
102 await vi.advanceTimersByTimeAsync(120_000);
103 await expect(second.stream.finish()).rejects.toThrow("stopped responding");
104 expect(second.socket.closed).toBe(true);
105 } finally {
106 vi.useRealTimers();
107 }
108});