Skip to content
Source
Sign inSign up
Open History to see the last commit

CloudSpeech.test.ts

200 lines8.5 KB

1import { it } from "@effect/vitest";
2import { DEFAULT_SPEECH_TRANSCRIPTION_OPTIONS, SecretProviderId } from "@t3tools/contracts";
3import * as SecretResolver from "@t3tools/secret-provider-core/server/SecretResolver";
4import * as Effect from "effect/Effect";
5import * as Fiber from "effect/Fiber";
6import * as Layer from "effect/Layer";
7import * as Redacted from "effect/Redacted";
8import * as FetchHttpClient from "effect/http/FetchHttpClient";
9import { expect, vi } from "vite-plus/test";
10import * as ServerSettings from "../serverSettings.ts";
11import * as CloudSpeech from "./CloudSpeech.ts";
12​
13it.effect.each(["openai", "xai"] as const)("sends only to %s using its own saved key", (provider) =>
14 Effect.gen(function* () {
15 const reference = {
16 provider: SecretProviderId.make("work"),
17 reference: "op://Work/Voice/key",
18 };
19 let resolved: string | undefined;
20 let cached: string | undefined;
21 const read = vi.fn(() => Effect.sync(() => [(cached ??= resolved)]));
22 const upstream = vi.fn<typeof fetch>(async () => Response.json({ text: "hello" }));
23 const service = yield* CloudSpeech.CloudSpeech.pipe(
24 Effect.provide(
25 CloudSpeech.layer.pipe(
26 Layer.provide(FetchHttpClient.layer),
27 Layer.provide(ServerSettings.ServerSettingsService.layerTest()),
28 Layer.provide(
29 Layer.mock(SecretResolver.SecretResolver)({
30 read,
31 label: () => Effect.succeed("Work"),
32 invalidate: () =>
33 Effect.sync(() => {
34 cached = undefined;
35 }),
36 }),
37 ),
38 ),
39 ),
40 );
41 const transcribe = (pcm = new Uint8Array(new Float32Array([0, 0.5]).buffer)) =>
42 service
43 .transcribe(provider, pcm, {
44 ...DEFAULT_SPEECH_TRANSCRIPTION_OPTIONS,
45 speechLanguage: "en",
46 speechCustomWords: [{ term: "T3 Code", aliases: [] }],
47 })
48 .pipe(Effect.provideService(FetchHttpClient.Fetch, upstream));
49 yield* service.setKey(provider === "openai" ? "xai" : "openai", Redacted.make("other"));
50 expect((yield* Effect.flip(transcribe()))._tag).toBe("CloudSpeechError");
51 yield* service.setKey(provider, Redacted.make("chosen"));
52 expect(yield* service.configured).toEqual({ openai: true, xai: true, openrouter: false });
53 yield* Effect.flip(transcribe(new Uint8Array(3)));
54 expect(upstream).not.toHaveBeenCalled();
55 expect(yield* transcribe()).toBe("hello");
56 const [url, init] = upstream.mock.calls[0]!;
57 expect(String(url)).toBe(
58 {
59 openai: "https://api.openai.com/v1/audio/transcriptions",
60 xai: "https://api.x.ai/v1/stt",
61 }[provider],
62 );
63 expect(init?.redirect).toBe("error");
64 expect(new Headers(init?.headers).get("authorization")).toBe("Bearer chosen");
65 const form = init?.body as FormData;
66 expect(form.get("model")).toBe(
67 provider === "openai" ? "gpt-transcribe" : "grok-voice-transcribe-2.0",
68 );
69 expect(form.getAll(provider === "openai" ? "keywords[]" : "keyterm")).toEqual(["T3 Code"]);
70 expect(form.get(provider === "openai" ? "languages[]" : "language")).toBe("en");
71 expect([...form.keys()].at(-1)).toBe("file");
72 const wav = new DataView(yield* Effect.promise(() => (form.get("file") as Blob).arrayBuffer()));
73 expect(wav.getUint32(24, true)).toBe(16_000);
74 expect(wav.getInt16(46, true)).toBe(16384);
75 upstream.mockResolvedValueOnce(new Response("sensitive upstream diagnostic", { status: 401 }));
76 expect(yield* Effect.flip(transcribe())).toMatchObject({ provider, reason: "rejected-key" });
77 yield* service.setKey(provider, null);
78 expect((yield* service.configured)[provider]).toBe(false);
79 yield* Effect.flip(transcribe());
80 expect(upstream).toHaveBeenCalledTimes(2);
81 yield* service.setKey(provider, reference);
82 expect((yield* service.configured)[provider]).toBe(true);
83 expect(read).not.toHaveBeenCalled();
84 resolved = "resolved-fixture";
85 expect(yield* transcribe()).toBe("hello");
86 expect(read).toHaveBeenCalledWith([reference]);
87 expect(new Headers(upstream.mock.calls[2]?.[1]?.headers).get("authorization")).toBe(
88 "Bearer resolved-fixture",
89 );
90 resolved = undefined;
91 expect(yield* Effect.flip(transcribe())).toMatchObject({
92 provider,
93 reason: "unreadable-key",
94 });
95 expect(upstream).toHaveBeenCalledTimes(3);
96 }),
97);
98​
99it.effect("uploads to OpenRouter only for a listed exact model, as zero-retention JSON WAV", () =>
100 Effect.gen(function* () {
101 const upstream = vi.fn<typeof fetch>(async (input) =>
102 new URL(String(input)).pathname.endsWith("/models")
103 ? Response.json({ data: [{ id: "acme/transcribe-1" }, { id: "acme/transcribe-2" }] })
104 : Response.json({ text: "hello" }),
105 );
106 const service = yield* CloudSpeech.CloudSpeech.pipe(
107 Effect.provide(
108 CloudSpeech.layer.pipe(
109 Layer.provide(FetchHttpClient.layer),
110 Layer.provide(ServerSettings.ServerSettingsService.layerTest()),
111 Layer.provide(Layer.mock(SecretResolver.SecretResolver)({})),
112 ),
113 ),
114 );
115 const uploads = () =>
116 upstream.mock.calls.filter(([url]) => String(url).endsWith("/audio/transcriptions"));
117 const transcribe = (model: string | undefined) =>
118 service
119 .transcribe(
120 "openrouter",
121 new Uint8Array(new Float32Array([0, 0.5]).buffer),
122 { ...DEFAULT_SPEECH_TRANSCRIPTION_OPTIONS, speechLanguage: "en" },
123 model,
124 )
125 .pipe(Effect.provideService(FetchHttpClient.Fetch, upstream));
126 yield* service.setKey("openrouter", Redacted.make("router-key"));
127 expect(yield* Effect.flip(transcribe(undefined))).toMatchObject({ provider: "openrouter" });
128 upstream.mockResolvedValueOnce(new Response(null, { status: 503 }));
129 expect(yield* Effect.flip(transcribe("acme/transcribe-2"))).toMatchObject({
130 reason: "unavailable",
131 });
132 expect(yield* Effect.flip(transcribe("acme/delisted"))).toMatchObject({
133 provider: "openrouter",
134 });
135 expect(uploads()).toEqual([]);
136 expect(yield* transcribe("acme/transcribe-2")).toBe("hello");
137 const [[url, init]] = uploads() as [[URL, RequestInit]];
138 expect(new URL(String(upstream.mock.calls[0]![0])).searchParams.get("zdr")).toBe("true");
139 expect(String(url)).toBe("https://openrouter.ai/api/v1/audio/transcriptions");
140 expect(init.redirect).toBe("error");
141 const headers = new Headers(init.headers);
142 expect(headers.get("authorization")).toBe("Bearer router-key");
143 expect(headers.get("content-type")).toContain("application/json");
144 const body = JSON.parse(yield* Effect.promise(() => new Response(init.body).text())) as {
145 model: string;
146 models?: unknown;
147 provider: { zdr: boolean };
148 input_audio: { format: string; data: string };
149 };
150 expect(body).toMatchObject({
151 model: "acme/transcribe-2",
152 provider: { zdr: true },
153 input_audio: { format: "wav" },
154 });
155 expect(body.models).toBeUndefined();
156 const wav = Buffer.from(body.input_audio.data, "base64");
157 expect(wav.toString("ascii", 0, 4)).toBe("RIFF");
158 expect(wav.readInt16LE(46)).toBe(16384);
159 }),
160);
161​
162it.effect("removing a key while a catalog read is pending prevents the upload", () =>
163 Effect.gen(function* () {
164 const service = yield* CloudSpeech.CloudSpeech.pipe(
165 Effect.provide(
166 CloudSpeech.layer.pipe(
167 Layer.provide(FetchHttpClient.layer),
168 Layer.provide(ServerSettings.ServerSettingsService.layerTest()),
169 Layer.provide(Layer.mock(SecretResolver.SecretResolver)({})),
170 ),
171 ),
172 );
173 const catalogStarted = Promise.withResolvers<void>();
174 const catalog = Promise.withResolvers<Response>();
175 const uploads: string[] = [];
176 const upstream: typeof fetch = async (input) => {
177 if (new URL(String(input)).pathname.endsWith("/models")) {
178 catalogStarted.resolve();
179 return catalog.promise;
180 }
181 uploads.push(String(input));
182 return Response.json({ text: "unexpected" });
183 };
184 yield* service.setKey("openrouter", Redacted.make("old-key"));
185 const job = yield* service
186 .transcribe(
187 "openrouter",
188 new Uint8Array(4),
189 DEFAULT_SPEECH_TRANSCRIPTION_OPTIONS,
190 "acme/voice",
191 )
192 .pipe(Effect.provideService(FetchHttpClient.Fetch, upstream), Effect.forkChild);
193 yield* Effect.promise(() => catalogStarted.promise);
194 yield* service.setKey("openrouter", null);
195 catalog.resolve(Response.json({ data: [{ id: "acme/voice" }] }));
196 const exit = yield* Fiber.await(job);
197 expect(uploads).toEqual([]);
198 expect(exit._tag).toBe("Failure");
199 }),
200);