···66 "scripts": {
77 "dev": "bun run src/index.ts --hot",
88 "clean": "rm -rf transcripts uploads thistle.db",
99- "test": "bun test",
1010- "test:integration": "bun test src/index.test.ts",
99+ "test": "NODE_ENV=test bun test",
1010+ "test:integration": "NODE_ENV=test bun test src/index.test.ts",
1111 "ngrok": "ngrok http 3000 --domain casual-renewing-reptile.ngrok-free.app"
1212 },
1313 "devDependencies": {
+5-1
src/db/schema.ts
···11import { Database } from "bun:sqlite";
2233-export const db = new Database("thistle.db");
33+// Use test database when NODE_ENV is test
44+const dbPath = process.env.NODE_ENV === "test" ? "thistle.test.db" : "thistle.db";
55+export const db = new Database(dbPath);
66+77+console.log(`[Database] Using database: ${dbPath}`);
4859// Schema version tracking
610db.run(`
+171-117
src/index.test.ts
···66 expect,
77 test,
88} from "bun:test";
99-import db from "./db/schema";
109import { hashPasswordClient } from "./lib/client-auth";
1010+import type { Subprocess } from "bun";
11111212-// Test server URL - uses port 3001 for testing to avoid conflicts
1212+// Test server configuration
1313const TEST_PORT = 3001;
1414const BASE_URL = `http://localhost:${TEST_PORT}`;
1515+const TEST_DB_PATH = "./thistle.test.db";
15161616-// Check if server is available
1717-let serverAvailable = false;
1717+// Test server process
1818+let serverProcess: Subprocess | null = null;
18191920beforeAll(async () => {
2121+ // Clean up any existing test database
2022 try {
2121- const response = await fetch(`${BASE_URL}/api/health`, {
2222- signal: AbortSignal.timeout(1000),
2323- });
2424- serverAvailable = response.ok || response.status === 404;
2323+ await import("node:fs/promises").then((fs) => fs.unlink(TEST_DB_PATH));
2524 } catch {
2626- console.warn(
2727- `\n⚠️ Test server not running on port ${TEST_PORT}. Start it with:\n PORT=${TEST_PORT} bun run src/index.ts\n Then run tests in another terminal.\n`,
2828- );
2929- serverAvailable = false;
2525+ // Ignore if doesn't exist
3026 }
2727+2828+ // Start test server as subprocess
2929+ serverProcess = Bun.spawn(["bun", "run", "src/index.ts"], {
3030+ env: {
3131+ ...process.env,
3232+ NODE_ENV: "test",
3333+ PORT: TEST_PORT.toString(),
3434+ SKIP_EMAILS: "true",
3535+ SKIP_POLAR_SYNC: "true",
3636+ // Dummy env vars to pass startup validation (won't be used due to SKIP_EMAILS/SKIP_POLAR_SYNC)
3737+ MAILCHANNELS_API_KEY: "test-key",
3838+ DKIM_PRIVATE_KEY: "test-key",
3939+ LLM_API_KEY: "test-key",
4040+ LLM_API_BASE_URL: "https://test.com",
4141+ LLM_MODEL: "test-model",
4242+ POLAR_ACCESS_TOKEN: "test-token",
4343+ POLAR_ORGANIZATION_ID: "test-org",
4444+ POLAR_PRODUCT_ID: "test-product",
4545+ POLAR_SUCCESS_URL: "http://localhost:3001/success",
4646+ POLAR_WEBHOOK_SECRET: "test-webhook-secret",
4747+ ORIGIN: "http://localhost:3001",
4848+ },
4949+ stdout: "pipe",
5050+ stderr: "pipe",
5151+ });
5252+5353+ // Log server output for debugging
5454+ const stdoutReader = serverProcess.stdout.getReader();
5555+ const stderrReader = serverProcess.stderr.getReader();
5656+ const decoder = new TextDecoder();
5757+5858+ (async () => {
5959+ try {
6060+ while (true) {
6161+ const { value, done } = await stdoutReader.read();
6262+ if (done) break;
6363+ const text = decoder.decode(value);
6464+ console.log("[SERVER OUT]", text.trim());
6565+ }
6666+ } catch {}
6767+ })();
6868+6969+ (async () => {
7070+ try {
7171+ while (true) {
7272+ const { value, done } = await stderrReader.read();
7373+ if (done) break;
7474+ const text = decoder.decode(value);
7575+ console.error("[SERVER ERR]", text.trim());
7676+ }
7777+ } catch {}
7878+ })();
7979+8080+ // Wait for server to be ready
8181+ let retries = 30;
8282+ let ready = false;
8383+ while (retries > 0 && !ready) {
8484+ try {
8585+ const response = await fetch(`${BASE_URL}/api/health`, {
8686+ signal: AbortSignal.timeout(1000),
8787+ });
8888+ if (response.ok) {
8989+ ready = true;
9090+ break;
9191+ }
9292+ } catch {
9393+ // Server not ready yet
9494+ }
9595+ await new Promise((resolve) => setTimeout(resolve, 500));
9696+ retries--;
9797+ }
9898+9999+ if (!ready) {
100100+ throw new Error("Test server failed to start within 15 seconds");
101101+ }
102102+103103+ console.log(`✓ Test server running on port ${TEST_PORT}`);
104104+});
105105+106106+afterAll(async () => {
107107+ // Kill test server
108108+ if (serverProcess) {
109109+ serverProcess.kill();
110110+ await new Promise((resolve) => setTimeout(resolve, 1000));
111111+ }
112112+113113+ // Clean up test database
114114+ try {
115115+ await import("node:fs/promises").then((fs) => fs.unlink(TEST_DB_PATH));
116116+ } catch {
117117+ // Ignore if doesn't exist
118118+ }
119119+120120+ console.log("✓ Test server stopped and test database cleaned up");
31121});
3212233123// Test user credentials
···81171 });
82172}
831738484-// Cleanup helpers
8585-function cleanupTestData() {
8686- // Delete test users and their related data (cascade will handle most of it)
8787- // Include 'newemail%' to catch users whose emails were updated during tests
8888- db.run(
8989- "DELETE FROM sessions WHERE user_id IN (SELECT id FROM users WHERE email LIKE 'test%' OR email LIKE 'admin@%' OR email LIKE 'newemail%')",
9090- );
9191- db.run(
9292- "DELETE FROM passkeys WHERE user_id IN (SELECT id FROM users WHERE email LIKE 'test%' OR email LIKE 'admin@%' OR email LIKE 'newemail%')",
9393- );
9494- db.run(
9595- "DELETE FROM transcriptions WHERE user_id IN (SELECT id FROM users WHERE email LIKE 'test%' OR email LIKE 'admin@%' OR email LIKE 'newemail%')",
9696- );
9797- db.run(
9898- "DELETE FROM users WHERE email LIKE 'test%' OR email LIKE 'admin@%' OR email LIKE 'newemail%'",
9999- );
100100-101101- // Clear ALL rate limit data to prevent accumulation across tests
102102- // (IP-based rate limits don't contain test/admin in the key)
103103- db.run("DELETE FROM rate_limit_attempts");
104104-}
105105-106106-beforeEach(() => {
107107- if (serverAvailable) {
108108- cleanupTestData();
109109- }
110110-});
111111-112112-afterAll(() => {
113113- if (serverAvailable) {
114114- cleanupTestData();
115115- }
116116-});
117117-118118-// Helper to skip tests if server is not available
119119-function serverTest(name: string, fn: () => void | Promise<void>) {
120120- test(name, async () => {
121121- if (!serverAvailable) {
122122- console.log(`⏭️ Skipping: ${name} (server not running)`);
123123- return;
124124- }
125125- await fn();
126126- });
127127-}
174174+// All tests run against a fresh database, no cleanup needed
128175129176describe("API Endpoints - Authentication", () => {
130177 describe("POST /api/auth/register", () => {
131131- serverTest("should register a new user successfully", async () => {
178178+ test("should register a new user successfully", async () => {
132179 const hashedPassword = await clientHashPassword(
133180 TEST_USER.email,
134181 TEST_USER.password,
···144191 }),
145192 });
146193194194+ if (response.status !== 200) {
195195+ const error = await response.json();
196196+ console.error("Registration failed:", response.status, error);
197197+ }
198198+147199 expect(response.status).toBe(200);
200200+201201+ // Extract session before consuming response body
202202+ const sessionCookie = extractSessionCookie(response);
203203+148204 const data = await response.json();
149205 expect(data.user).toBeDefined();
150206 expect(data.user.email).toBe(TEST_USER.email);
151151- expect(extractSessionCookie(response)).toBeTruthy();
207207+ expect(sessionCookie).toBeTruthy();
152208 });
153209154154- serverTest("should reject registration with missing email", async () => {
210210+ test("should reject registration with missing email", async () => {
155211 const response = await fetch(`${BASE_URL}/api/auth/register`, {
156212 method: "POST",
157213 headers: { "Content-Type": "application/json" },
···165221 expect(data.error).toBe("Email and password required");
166222 });
167223168168- serverTest(
224224+ test(
169225 "should reject registration with invalid password format",
170226 async () => {
171227 const response = await fetch(`${BASE_URL}/api/auth/register`, {
···183239 },
184240 );
185241186186- serverTest("should reject duplicate email registration", async () => {
242242+ test("should reject duplicate email registration", async () => {
187243 const hashedPassword = await clientHashPassword(
188244 TEST_USER.email,
189245 TEST_USER.password,
···216272 expect(data.error).toBe("Email already registered");
217273 });
218274219219- serverTest("should enforce rate limiting on registration", async () => {
275275+ test("should enforce rate limiting on registration", async () => {
220276 const hashedPassword = await clientHashPassword(
221277 "test@example.com",
222278 "password",
···246302 });
247303248304 describe("POST /api/auth/login", () => {
249249- serverTest("should login successfully with valid credentials", async () => {
305305+ test("should login successfully with valid credentials", async () => {
250306 // Register user first
251307 const hashedPassword = await clientHashPassword(
252308 TEST_USER.email,
···279335 expect(extractSessionCookie(response)).toBeTruthy();
280336 });
281337282282- serverTest("should reject login with invalid credentials", async () => {
338338+ test("should reject login with invalid credentials", async () => {
283339 // Register user first
284340 const hashedPassword = await clientHashPassword(
285341 TEST_USER.email,
···313369 expect(data.error).toBe("Invalid email or password");
314370 });
315371316316- serverTest("should reject login with missing fields", async () => {
372372+ test("should reject login with missing fields", async () => {
317373 const response = await fetch(`${BASE_URL}/api/auth/login`, {
318374 method: "POST",
319375 headers: { "Content-Type": "application/json" },
···327383 expect(data.error).toBe("Email and password required");
328384 });
329385330330- serverTest("should enforce rate limiting on login attempts", async () => {
386386+ test("should enforce rate limiting on login attempts", async () => {
331387 const hashedPassword = await clientHashPassword(
332388 TEST_USER.email,
333389 TEST_USER.password,
···357413 });
358414359415 describe("POST /api/auth/logout", () => {
360360- serverTest("should logout successfully", async () => {
416416+ test("should logout successfully", async () => {
361417 // Register and login
362418 const hashedPassword = await clientHashPassword(
363419 TEST_USER.email,
···391447 expect(setCookie).toContain("Max-Age=0");
392448 });
393449394394- serverTest("should logout even without valid session", async () => {
450450+ test("should logout even without valid session", async () => {
395451 const response = await fetch(`${BASE_URL}/api/auth/logout`, {
396452 method: "POST",
397453 });
···403459 });
404460405461 describe("GET /api/auth/me", () => {
406406- serverTest(
462462+ test(
407463 "should return current user info when authenticated",
408464 async () => {
409465 // Register user
···436492 },
437493 );
438494439439- serverTest("should return 401 when not authenticated", async () => {
495495+ test("should return 401 when not authenticated", async () => {
440496 const response = await fetch(`${BASE_URL}/api/auth/me`);
441497442498 expect(response.status).toBe(401);
···444500 expect(data.error).toBe("Not authenticated");
445501 });
446502447447- serverTest("should return 401 with invalid session", async () => {
503503+ test("should return 401 with invalid session", async () => {
448504 const response = await authRequest(
449505 `${BASE_URL}/api/auth/me`,
450506 "invalid-session",
···459515460516describe("API Endpoints - Session Management", () => {
461517 describe("GET /api/sessions", () => {
462462- serverTest("should return user sessions", async () => {
518518+ test("should return user sessions", async () => {
463519 // Register user
464520 const hashedPassword = await clientHashPassword(
465521 TEST_USER.email,
···490546 expect(data.sessions[0]).toHaveProperty("user_agent");
491547 });
492548493493- serverTest("should require authentication", async () => {
549549+ test("should require authentication", async () => {
494550 const response = await fetch(`${BASE_URL}/api/sessions`);
495551496552 expect(response.status).toBe(401);
···498554 });
499555500556 describe("DELETE /api/sessions", () => {
501501- serverTest("should delete specific session", async () => {
557557+ test("should delete specific session", async () => {
502558 // Register user and create multiple sessions
503559 const hashedPassword = await clientHashPassword(
504560 TEST_USER.email,
···557613 expect(verifyResponse.status).toBe(401);
558614 });
559615560560- serverTest("should not delete another user's session", async () => {
616616+ test("should not delete another user's session", async () => {
561617 // Register two users
562618 const hashedPassword1 = await clientHashPassword(
563619 TEST_USER.email,
···601657 expect(response.status).toBe(404);
602658 });
603659604604- serverTest("should not delete current session", async () => {
660660+ test("should not delete current session", async () => {
605661 // Register user
606662 const hashedPassword = await clientHashPassword(
607663 TEST_USER.email,
···637693638694describe("API Endpoints - User Management", () => {
639695 describe("DELETE /api/user", () => {
640640- serverTest("should delete user account", async () => {
696696+ test("should delete user account", async () => {
641697 // Register user
642698 const hashedPassword = await clientHashPassword(
643699 TEST_USER.email,
···674730 expect(verifyResponse.status).toBe(401);
675731 });
676732677677- serverTest("should require authentication", async () => {
733733+ test("should require authentication", async () => {
678734 const response = await fetch(`${BASE_URL}/api/user`, {
679735 method: "DELETE",
680736 });
···684740 });
685741686742 describe("PUT /api/user/email", () => {
687687- serverTest("should update user email", async () => {
743743+ test("should update user email", async () => {
688744 // Register user
689745 const hashedPassword = await clientHashPassword(
690746 TEST_USER.email,
···725781 expect(meData.email).toBe(newEmail);
726782 });
727783728728- serverTest("should reject duplicate email", async () => {
784784+ test("should reject duplicate email", async () => {
729785 // Register two users
730786 const hashedPassword1 = await clientHashPassword(
731787 TEST_USER.email,
···772828 });
773829774830 describe("PUT /api/user/password", () => {
775775- serverTest("should update user password", async () => {
831831+ test("should update user password", async () => {
776832 // Register user
777833 const hashedPassword = await clientHashPassword(
778834 TEST_USER.email,
···819875 expect(loginResponse.status).toBe(200);
820876 });
821877822822- serverTest("should reject invalid password format", async () => {
878878+ test("should reject invalid password format", async () => {
823879 // Register user
824880 const hashedPassword = await clientHashPassword(
825881 TEST_USER.email,
···853909 });
854910855911 describe("PUT /api/user/name", () => {
856856- serverTest("should update user name", async () => {
912912+ test("should update user name", async () => {
857913 // Register user
858914 const hashedPassword = await clientHashPassword(
859915 TEST_USER.email,
···895951 expect(meData.name).toBe(newName);
896952 });
897953898898- serverTest("should reject missing name", async () => {
954954+ test("should reject missing name", async () => {
899955 // Register user
900956 const hashedPassword = await clientHashPassword(
901957 TEST_USER.email,
···926982 });
927983928984 describe("PUT /api/user/avatar", () => {
929929- serverTest("should update user avatar", async () => {
985985+ test("should update user avatar", async () => {
930986 // Register user
931987 const hashedPassword = await clientHashPassword(
932988 TEST_USER.email,
···97110279721028describe("API Endpoints - Health", () => {
9731029 describe("GET /api/health", () => {
974974- serverTest(
10301030+ test(
9751031 "should return service health status with details",
9761032 async () => {
9771033 const response = await fetch(`${BASE_URL}/api/health`);
···99110479921048describe("API Endpoints - Transcriptions", () => {
9931049 describe("GET /api/transcriptions", () => {
994994- serverTest("should return user transcriptions", async () => {
10501050+ test("should return user transcriptions", async () => {
9951051 // Register user
9961052 const hashedPassword = await clientHashPassword(
9971053 TEST_USER.email,
···10191075 expect(Array.isArray(data.jobs)).toBe(true);
10201076 });
1021107710221022- serverTest("should require authentication", async () => {
10781078+ test("should require authentication", async () => {
10231079 const response = await fetch(`${BASE_URL}/api/transcriptions`);
1024108010251081 expect(response.status).toBe(401);
···10271083 });
1028108410291085 describe("POST /api/transcriptions", () => {
10301030- serverTest("should upload audio file and start transcription", async () => {
10861086+ test("should upload audio file and start transcription", async () => {
10311087 // Register user
10321088 const hashedPassword = await clientHashPassword(
10331089 TEST_USER.email,
···10651121 expect(data.message).toContain("Upload successful");
10661122 });
1067112310681068- serverTest("should reject non-audio files", async () => {
11241124+ test("should reject non-audio files", async () => {
10691125 // Register user
10701126 const hashedPassword = await clientHashPassword(
10711127 TEST_USER.email,
···10981154 expect(response.status).toBe(400);
10991155 });
1100115611011101- serverTest("should reject files exceeding size limit", async () => {
11571157+ test("should reject files exceeding size limit", async () => {
11021158 // Register user
11031159 const hashedPassword = await clientHashPassword(
11041160 TEST_USER.email,
···11351191 expect(data.error).toContain("File size must be less than");
11361192 });
1137119311381138- serverTest("should require authentication", async () => {
11941194+ test("should require authentication", async () => {
11391195 const audioBlob = new Blob(["fake audio data"], { type: "audio/mp3" });
11401196 const formData = new FormData();
11411197 formData.append("audio", audioBlob, "test.mp3");
···11561212 let userId: number;
1157121311581214 beforeEach(async () => {
11591159- if (!serverAvailable) return;
1160121511611216 // Create admin user
11621217 const adminHash = await clientHashPassword(
···12031258 });
1204125912051260 describe("GET /api/admin/users", () => {
12061206- serverTest("should return all users for admin", async () => {
12611261+ test("should return all users for admin", async () => {
12071262 const response = await authRequest(
12081263 `${BASE_URL}/api/admin/users`,
12091264 adminCookie,
···12151270 expect(data.length).toBeGreaterThan(0);
12161271 });
1217127212181218- serverTest("should reject non-admin users", async () => {
12731273+ test("should reject non-admin users", async () => {
12191274 const response = await authRequest(
12201275 `${BASE_URL}/api/admin/users`,
12211276 userCookie,
···12241279 expect(response.status).toBe(403);
12251280 });
1226128112271227- serverTest("should require authentication", async () => {
12821282+ test("should require authentication", async () => {
12281283 const response = await fetch(`${BASE_URL}/api/admin/users`);
1229128412301285 expect(response.status).toBe(401);
···12321287 });
1233128812341289 describe("GET /api/admin/transcriptions", () => {
12351235- serverTest("should return all transcriptions for admin", async () => {
12901290+ test("should return all transcriptions for admin", async () => {
12361291 const response = await authRequest(
12371292 `${BASE_URL}/api/admin/transcriptions`,
12381293 adminCookie,
···12431298 expect(Array.isArray(data)).toBe(true);
12441299 });
1245130012461246- serverTest("should reject non-admin users", async () => {
13011301+ test("should reject non-admin users", async () => {
12471302 const response = await authRequest(
12481303 `${BASE_URL}/api/admin/transcriptions`,
12491304 userCookie,
···12541309 });
1255131012561311 describe("DELETE /api/admin/users/:id", () => {
12571257- serverTest("should delete user as admin", async () => {
13121312+ test("should delete user as admin", async () => {
12581313 const response = await authRequest(
12591314 `${BASE_URL}/api/admin/users/${userId}`,
12601315 adminCookie,
···12751330 expect(verifyResponse.status).toBe(401);
12761331 });
1277133212781278- serverTest("should reject non-admin users", async () => {
13331333+ test("should reject non-admin users", async () => {
12791334 const response = await authRequest(
12801335 `${BASE_URL}/api/admin/users/${userId}`,
12811336 userCookie,
···12891344 });
1290134512911346 describe("PUT /api/admin/users/:id/role", () => {
12921292- serverTest("should update user role as admin", async () => {
13471347+ test("should update user role as admin", async () => {
12931348 const response = await authRequest(
12941349 `${BASE_URL}/api/admin/users/${userId}/role`,
12951350 adminCookie,
···13131368 expect(meData.role).toBe("admin");
13141369 });
1315137013161316- serverTest("should reject invalid roles", async () => {
13711371+ test("should reject invalid roles", async () => {
13171372 const response = await authRequest(
13181373 `${BASE_URL}/api/admin/users/${userId}/role`,
13191374 adminCookie,
···13291384 });
1330138513311386 describe("GET /api/admin/users/:id/details", () => {
13321332- serverTest("should return user details for admin", async () => {
13871387+ test("should return user details for admin", async () => {
13331388 const response = await authRequest(
13341389 `${BASE_URL}/api/admin/users/${userId}/details`,
13351390 adminCookie,
···13431398 expect(data).toHaveProperty("sessions");
13441399 });
1345140013461346- serverTest("should reject non-admin users", async () => {
14011401+ test("should reject non-admin users", async () => {
13471402 const response = await authRequest(
13481403 `${BASE_URL}/api/admin/users/${userId}/details`,
13491404 userCookie,
···13541409 });
1355141013561411 describe("PUT /api/admin/users/:id/name", () => {
13571357- serverTest("should update user name as admin", async () => {
14121412+ test("should update user name as admin", async () => {
13581413 const newName = "Admin Updated Name";
13591414 const response = await authRequest(
13601415 `${BASE_URL}/api/admin/users/${userId}/name`,
···13711426 expect(data.success).toBe(true);
13721427 });
1373142813741374- serverTest("should reject empty names", async () => {
14291429+ test("should reject empty names", async () => {
13751430 const response = await authRequest(
13761431 `${BASE_URL}/api/admin/users/${userId}/name`,
13771432 adminCookie,
···13871442 });
1388144313891444 describe("PUT /api/admin/users/:id/email", () => {
13901390- serverTest("should update user email as admin", async () => {
14451445+ test("should update user email as admin", async () => {
13911446 const newEmail = "newemail@admin.com";
13921447 const response = await authRequest(
13931448 `${BASE_URL}/api/admin/users/${userId}/email`,
···14041459 expect(data.success).toBe(true);
14051460 });
1406146114071407- serverTest("should reject duplicate emails", async () => {
14621462+ test("should reject duplicate emails", async () => {
14081463 const response = await authRequest(
14091464 `${BASE_URL}/api/admin/users/${userId}/email`,
14101465 adminCookie,
···14221477 });
1423147814241479 describe("GET /api/admin/users/:id/sessions", () => {
14251425- serverTest("should return user sessions as admin", async () => {
14801480+ test("should return user sessions as admin", async () => {
14261481 const response = await authRequest(
14271482 `${BASE_URL}/api/admin/users/${userId}/sessions`,
14281483 adminCookie,
···14351490 });
1436149114371492 describe("DELETE /api/admin/users/:id/sessions", () => {
14381438- serverTest("should delete all user sessions as admin", async () => {
14931493+ test("should delete all user sessions as admin", async () => {
14391494 const response = await authRequest(
14401495 `${BASE_URL}/api/admin/users/${userId}/sessions`,
14411496 adminCookie,
···14621517 let sessionCookie: string;
1463151814641519 beforeEach(async () => {
14651465- if (!serverAvailable) return;
1466152014671521 // Register user
14681522 const hashedPassword = await clientHashPassword(
···14811535 });
1482153614831537 describe("GET /api/passkeys", () => {
14841484- serverTest("should return user passkeys", async () => {
15381538+ test("should return user passkeys", async () => {
14851539 const response = await authRequest(
14861540 `${BASE_URL}/api/passkeys`,
14871541 sessionCookie,
···14931547 expect(Array.isArray(data.passkeys)).toBe(true);
14941548 });
1495154914961496- serverTest("should require authentication", async () => {
15501550+ test("should require authentication", async () => {
14971551 const response = await fetch(`${BASE_URL}/api/passkeys`);
1498155214991553 expect(response.status).toBe(401);
···15011555 });
1502155615031557 describe("POST /api/passkeys/register/options", () => {
15041504- serverTest(
15581558+ test(
15051559 "should return registration options for authenticated user",
15061560 async () => {
15071561 const response = await authRequest(
···15201574 },
15211575 );
1522157615231523- serverTest("should require authentication", async () => {
15771577+ test("should require authentication", async () => {
15241578 const response = await fetch(
15251579 `${BASE_URL}/api/passkeys/register/options`,
15261580 {
···15331587 });
1534158815351589 describe("POST /api/passkeys/authenticate/options", () => {
15361536- serverTest("should return authentication options for email", async () => {
15901590+ test("should return authentication options for email", async () => {
15371591 const response = await fetch(
15381592 `${BASE_URL}/api/passkeys/authenticate/options`,
15391593 {
···15481602 expect(data).toHaveProperty("challenge");
15491603 });
1550160415511551- serverTest("should handle non-existent email", async () => {
16051605+ test("should handle non-existent email", async () => {
15521606 const response = await fetch(
15531607 `${BASE_URL}/api/passkeys/authenticate/options`,
15541608 {
···55555656test("cleanVTT preserves empty VTT", async () => {
5757 const emptyVTT = "WEBVTT\n\n";
5858+5959+ // Save and remove API key to avoid burning tokens
6060+ const originalKey = process.env.LLM_API_KEY;
6161+ delete process.env.LLM_API_KEY;
6262+5863 const result = await cleanVTT("test-empty", emptyVTT);
59646065 expect(result).toBe(emptyVTT);
6666+6767+ // Restore original key
6868+ if (originalKey) {
6969+ process.env.LLM_API_KEY = originalKey;
7070+ }
6171});
62726373// AI integration test - skip by default to avoid burning credits
+6
src/lib/vtt-cleaner.ts
···338338 `[VTTCleaner] Processing ${segments.length} segments for ${transcriptionId}`,
339339 );
340340341341+ // Check if API key is available, return original if not
342342+ if (!process.env.LLM_API_KEY) {
343343+ console.warn("[VTTCleaner] LLM_API_KEY not set, returning original VTT");
344344+ return vttContent;
345345+ }
346346+341347 // Validated at startup
342348 const apiKey = process.env.LLM_API_KEY as string;
343349 const apiBaseUrl = process.env.LLM_API_BASE_URL as string;