@hasna/recordings 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,322 @@
1
+ import { describe, test, expect, beforeEach, afterEach, mock } from "bun:test";
2
+ import { tmpdir } from "os";
3
+ import { join } from "path";
4
+ import { mkdirSync, rmSync, existsSync, writeFileSync } from "fs";
5
+ import { resetClient } from "../lib/transcriber.js";
6
+ import { TranscriptionError } from "../types/index.js";
7
+ import { DEFAULT_CONFIG } from "../lib/config.js";
8
+ import type { RecordingsConfig } from "../types/index.js";
9
+
10
+ const config: RecordingsConfig = {
11
+ ...DEFAULT_CONFIG,
12
+ openai_api_key: "sk-test-key",
13
+ enhancement_api_key: "sk-test-key",
14
+ };
15
+
16
+ let tempDir: string;
17
+ let tempAudioFile: string;
18
+
19
+ beforeEach(() => {
20
+ resetClient();
21
+ tempDir = join(tmpdir(), `open-recordings-test-trans-${Date.now()}-${Math.random().toString(36).slice(2)}`);
22
+ mkdirSync(tempDir, { recursive: true });
23
+ // Create a fake audio file for tests that need to open a file
24
+ tempAudioFile = join(tempDir, "test.wav");
25
+ writeFileSync(tempAudioFile, Buffer.from("fake-audio-content"));
26
+ });
27
+
28
+ afterEach(() => {
29
+ resetClient();
30
+ if (existsSync(tempDir)) {
31
+ rmSync(tempDir, { recursive: true, force: true });
32
+ }
33
+ });
34
+
35
+ describe("transcribeAudio", () => {
36
+ test("throws TranscriptionError when no API key", async () => {
37
+ const noKeyConfig = { ...config, openai_api_key: "" };
38
+ const { transcribeAudio } = await import("../lib/transcriber.js");
39
+ resetClient();
40
+
41
+ try {
42
+ await transcribeAudio(tempAudioFile, noKeyConfig);
43
+ expect(true).toBe(false);
44
+ } catch (err) {
45
+ expect(err).toBeInstanceOf(TranscriptionError);
46
+ expect((err as Error).message).toContain("API key not configured");
47
+ }
48
+ });
49
+
50
+ test("calls OpenAI transcription API and returns result", async () => {
51
+ mock.module("openai", () => ({
52
+ default: class MockOpenAI {
53
+ audio = {
54
+ transcriptions: {
55
+ create: mock(() =>
56
+ Promise.resolve({
57
+ text: "Hello world transcribed",
58
+ language: "en",
59
+ })
60
+ ),
61
+ },
62
+ };
63
+ },
64
+ }));
65
+
66
+ resetClient();
67
+ const { transcribeAudio } = await import("../lib/transcriber.js");
68
+ resetClient();
69
+
70
+ const result = await transcribeAudio(tempAudioFile, config);
71
+ expect(result.text).toBe("Hello world transcribed");
72
+ expect(result.model).toBe(config.transcription_model);
73
+ expect(result.duration_ms).toBeGreaterThanOrEqual(0);
74
+ expect(result.language).toBe("en");
75
+
76
+ resetClient();
77
+ });
78
+
79
+ test("handles null language from API", async () => {
80
+ mock.module("openai", () => ({
81
+ default: class MockOpenAI {
82
+ audio = {
83
+ transcriptions: {
84
+ create: mock(() =>
85
+ Promise.resolve({
86
+ text: "Transcribed text",
87
+ })
88
+ ),
89
+ },
90
+ };
91
+ },
92
+ }));
93
+
94
+ resetClient();
95
+ const { transcribeAudio } = await import("../lib/transcriber.js");
96
+ resetClient();
97
+
98
+ const result = await transcribeAudio(tempAudioFile, config);
99
+ expect(result.text).toBe("Transcribed text");
100
+ expect(result.language).toBeUndefined(); // property not present
101
+
102
+ resetClient();
103
+ });
104
+
105
+ test("wraps API errors in TranscriptionError", async () => {
106
+ mock.module("openai", () => ({
107
+ default: class MockOpenAI {
108
+ audio = {
109
+ transcriptions: {
110
+ create: mock(() => Promise.reject(new Error("API rate limit exceeded"))),
111
+ },
112
+ };
113
+ },
114
+ }));
115
+
116
+ resetClient();
117
+ const { transcribeAudio } = await import("../lib/transcriber.js");
118
+ resetClient();
119
+
120
+ try {
121
+ await transcribeAudio(tempAudioFile, config);
122
+ expect(true).toBe(false);
123
+ } catch (err) {
124
+ expect(err).toBeInstanceOf(TranscriptionError);
125
+ expect((err as Error).message).toContain("Transcription failed");
126
+ expect((err as Error).message).toContain("API rate limit exceeded");
127
+ }
128
+
129
+ resetClient();
130
+ });
131
+
132
+ test("wraps non-Error exceptions in TranscriptionError", async () => {
133
+ mock.module("openai", () => ({
134
+ default: class MockOpenAI {
135
+ audio = {
136
+ transcriptions: {
137
+ create: mock(() => Promise.reject("string error")),
138
+ },
139
+ };
140
+ },
141
+ }));
142
+
143
+ resetClient();
144
+ const { transcribeAudio } = await import("../lib/transcriber.js");
145
+ resetClient();
146
+
147
+ try {
148
+ await transcribeAudio(tempAudioFile, config);
149
+ expect(true).toBe(false);
150
+ } catch (err) {
151
+ expect(err).toBeInstanceOf(TranscriptionError);
152
+ expect((err as Error).message).toContain("Transcription failed");
153
+ }
154
+
155
+ resetClient();
156
+ });
157
+
158
+ test("uses config language when provided", async () => {
159
+ let capturedOpts: any = null;
160
+ mock.module("openai", () => ({
161
+ default: class MockOpenAI {
162
+ audio = {
163
+ transcriptions: {
164
+ create: mock((opts: any) => {
165
+ capturedOpts = opts;
166
+ return Promise.resolve({ text: "test", language: "fr" });
167
+ }),
168
+ },
169
+ };
170
+ },
171
+ }));
172
+
173
+ resetClient();
174
+ const { transcribeAudio } = await import("../lib/transcriber.js");
175
+ resetClient();
176
+
177
+ const frConfig = { ...config, language: "fr" };
178
+ await transcribeAudio(tempAudioFile, frConfig);
179
+ expect(capturedOpts.language).toBe("fr");
180
+ expect(capturedOpts.model).toBe(config.transcription_model);
181
+ expect(capturedOpts.response_format).toBe("json");
182
+
183
+ resetClient();
184
+ });
185
+
186
+ test("omits language when not in config", async () => {
187
+ let capturedOpts: any = null;
188
+ mock.module("openai", () => ({
189
+ default: class MockOpenAI {
190
+ audio = {
191
+ transcriptions: {
192
+ create: mock((opts: any) => {
193
+ capturedOpts = opts;
194
+ return Promise.resolve({ text: "test" });
195
+ }),
196
+ },
197
+ };
198
+ },
199
+ }));
200
+
201
+ resetClient();
202
+ const { transcribeAudio } = await import("../lib/transcriber.js");
203
+ resetClient();
204
+
205
+ const noLangConfig = { ...config, language: "" };
206
+ await transcribeAudio(tempAudioFile, noLangConfig);
207
+ expect(capturedOpts.language).toBeUndefined();
208
+
209
+ resetClient();
210
+ });
211
+ });
212
+
213
+ describe("transcribeBuffer", () => {
214
+ test("throws TranscriptionError when no API key", async () => {
215
+ const noKeyConfig = { ...config, openai_api_key: "" };
216
+ const { transcribeBuffer } = await import("../lib/transcriber.js");
217
+ resetClient();
218
+
219
+ try {
220
+ await transcribeBuffer(Buffer.from("test"), "test.wav", noKeyConfig);
221
+ expect(true).toBe(false);
222
+ } catch (err) {
223
+ expect(err).toBeInstanceOf(TranscriptionError);
224
+ expect((err as Error).message).toContain("API key not configured");
225
+ }
226
+ });
227
+
228
+ test("creates File and calls transcription API", async () => {
229
+ mock.module("openai", () => ({
230
+ default: class MockOpenAI {
231
+ audio = {
232
+ transcriptions: {
233
+ create: mock(() =>
234
+ Promise.resolve({
235
+ text: "Buffer transcription result",
236
+ language: "en",
237
+ })
238
+ ),
239
+ },
240
+ };
241
+ },
242
+ }));
243
+
244
+ resetClient();
245
+ const { transcribeBuffer } = await import("../lib/transcriber.js");
246
+ resetClient();
247
+
248
+ const result = await transcribeBuffer(
249
+ Buffer.from("fake audio data"),
250
+ "test.wav",
251
+ config
252
+ );
253
+ expect(result.text).toBe("Buffer transcription result");
254
+ expect(result.model).toBe(config.transcription_model);
255
+ expect(result.duration_ms).toBeGreaterThanOrEqual(0);
256
+
257
+ resetClient();
258
+ });
259
+
260
+ test("wraps API errors in TranscriptionError for buffer", async () => {
261
+ mock.module("openai", () => ({
262
+ default: class MockOpenAI {
263
+ audio = {
264
+ transcriptions: {
265
+ create: mock(() => Promise.reject(new Error("Buffer API error"))),
266
+ },
267
+ };
268
+ },
269
+ }));
270
+
271
+ resetClient();
272
+ const { transcribeBuffer } = await import("../lib/transcriber.js");
273
+ resetClient();
274
+
275
+ try {
276
+ await transcribeBuffer(Buffer.from("test"), "test.mp3", config);
277
+ expect(true).toBe(false);
278
+ } catch (err) {
279
+ expect(err).toBeInstanceOf(TranscriptionError);
280
+ expect((err as Error).message).toContain("Transcription failed");
281
+ expect((err as Error).message).toContain("Buffer API error");
282
+ }
283
+
284
+ resetClient();
285
+ });
286
+
287
+ test("handles various file extensions for MIME type", async () => {
288
+ let capturedFile: any = null;
289
+ mock.module("openai", () => ({
290
+ default: class MockOpenAI {
291
+ audio = {
292
+ transcriptions: {
293
+ create: mock((opts: any) => {
294
+ capturedFile = opts.file;
295
+ return Promise.resolve({ text: "ok" });
296
+ }),
297
+ },
298
+ };
299
+ },
300
+ }));
301
+
302
+ resetClient();
303
+ const { transcribeBuffer } = await import("../lib/transcriber.js");
304
+
305
+ // Test different extensions - each creates a File with the correct MIME type
306
+ const extensions = ["wav", "mp3", "m4a", "webm", "mp4", "mpeg", "mpga", "xyz"];
307
+ for (const ext of extensions) {
308
+ resetClient();
309
+ await transcribeBuffer(Buffer.from("test"), `test.${ext}`, config);
310
+ expect(capturedFile).toBeDefined();
311
+ }
312
+
313
+ resetClient();
314
+ });
315
+ });
316
+
317
+ describe("resetClient", () => {
318
+ test("clears the singleton client safely", () => {
319
+ resetClient();
320
+ resetClient(); // Double reset is fine
321
+ });
322
+ });
@@ -0,0 +1,75 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import {
3
+ RecordingNotFoundError,
4
+ RecordingError,
5
+ TranscriptionError,
6
+ EnhancementError,
7
+ } from "../types/index.js";
8
+
9
+ describe("RecordingNotFoundError", () => {
10
+ test("sets correct message with id", () => {
11
+ const err = new RecordingNotFoundError("abc-123");
12
+ expect(err.message).toBe("Recording not found: abc-123");
13
+ });
14
+
15
+ test("sets name to RecordingNotFoundError", () => {
16
+ const err = new RecordingNotFoundError("xyz");
17
+ expect(err.name).toBe("RecordingNotFoundError");
18
+ });
19
+
20
+ test("is an instance of Error", () => {
21
+ const err = new RecordingNotFoundError("id");
22
+ expect(err).toBeInstanceOf(Error);
23
+ });
24
+ });
25
+
26
+ describe("RecordingError", () => {
27
+ test("sets correct message", () => {
28
+ const err = new RecordingError("something went wrong");
29
+ expect(err.message).toBe("something went wrong");
30
+ });
31
+
32
+ test("sets name to RecordingError", () => {
33
+ const err = new RecordingError("msg");
34
+ expect(err.name).toBe("RecordingError");
35
+ });
36
+
37
+ test("is an instance of Error", () => {
38
+ const err = new RecordingError("msg");
39
+ expect(err).toBeInstanceOf(Error);
40
+ });
41
+ });
42
+
43
+ describe("TranscriptionError", () => {
44
+ test("sets correct message", () => {
45
+ const err = new TranscriptionError("transcription failed");
46
+ expect(err.message).toBe("transcription failed");
47
+ });
48
+
49
+ test("sets name to TranscriptionError", () => {
50
+ const err = new TranscriptionError("msg");
51
+ expect(err.name).toBe("TranscriptionError");
52
+ });
53
+
54
+ test("is an instance of Error", () => {
55
+ const err = new TranscriptionError("msg");
56
+ expect(err).toBeInstanceOf(Error);
57
+ });
58
+ });
59
+
60
+ describe("EnhancementError", () => {
61
+ test("sets correct message", () => {
62
+ const err = new EnhancementError("enhancement failed");
63
+ expect(err.message).toBe("enhancement failed");
64
+ });
65
+
66
+ test("sets name to EnhancementError", () => {
67
+ const err = new EnhancementError("msg");
68
+ expect(err.name).toBe("EnhancementError");
69
+ });
70
+
71
+ test("is an instance of Error", () => {
72
+ const err = new EnhancementError("msg");
73
+ expect(err).toBeInstanceOf(Error);
74
+ });
75
+ });