diff --git a/apps/memoro/apps/audio-server/src/index.ts b/apps/memoro/apps/audio-server/src/index.ts index 46e60de24..0abf4a4e8 100644 --- a/apps/memoro/apps/audio-server/src/index.ts +++ b/apps/memoro/apps/audio-server/src/index.ts @@ -7,34 +7,36 @@ const app = new Hono(); // ─── Service key middleware ─────────────────────────────────────────────────── function serviceKeyMiddleware(): MiddlewareHandler { - return async (c, next) => { - const expectedKey = process.env.SERVICE_KEY; + return async (c, next) => { + const expectedKey = process.env.SERVICE_KEY; - if (!expectedKey) { - console.error('[Auth] SERVICE_KEY env var is not configured'); - return c.json({ error: 'Server misconfiguration' }, 500); - } + if (!expectedKey) { + console.error('[Auth] SERVICE_KEY env var is not configured'); + return c.json({ error: 'Server misconfiguration' }, 500); + } - const providedKey = c.req.header('X-Service-Key'); + const providedKey = c.req.header('X-Service-Key'); - if (!providedKey || providedKey !== expectedKey) { - console.warn(`[Auth] Unauthorized request to ${c.req.path} — invalid or missing X-Service-Key`); - return c.json({ error: 'Unauthorized' }, 401); - } + if (!providedKey || providedKey !== expectedKey) { + console.warn( + `[Auth] Unauthorized request to ${c.req.path} — invalid or missing X-Service-Key` + ); + return c.json({ error: 'Unauthorized' }, 401); + } - await next(); - }; + await next(); + }; } // ─── Health check (no auth) ─────────────────────────────────────────────────── app.get('/health', (c) => { - return c.json({ - status: 'ok', - service: 'memoro-audio-server', - version: '1.0.0', - timestamp: new Date().toISOString(), - }); + return c.json({ + status: 'ok', + service: 'memoro-audio-server', + version: '1.0.0', + timestamp: new Date().toISOString(), + }); }); // ─── Protected routes ───────────────────────────────────────────────────────── @@ -47,14 +49,14 @@ app.route('/api/v1/transcribe', transcribeRoutes); // ─── 404 handler ───────────────────────────────────────────────────────────── app.notFound((c) => { - return c.json({ error: `Not found: ${c.req.method} ${c.req.path}` }, 404); + return c.json({ error: `Not found: ${c.req.method} ${c.req.path}` }, 404); }); // ─── Error handler ──────────────────────────────────────────────────────────── app.onError((err, c) => { - console.error(`[Error] Unhandled error on ${c.req.method} ${c.req.path}:`, err); - return c.json({ error: 'Internal server error' }, 500); + console.error(`[Error] Unhandled error on ${c.req.method} ${c.req.path}:`, err); + return c.json({ error: 'Internal server error' }, 500); }); // ─── Start ──────────────────────────────────────────────────────────────────── @@ -63,7 +65,8 @@ const port = parseInt(process.env.PORT ?? '3016', 10); console.log(`[Server] Memoro Audio Server starting on port ${port}`); +export { app }; export default { - port, - fetch: app.fetch, + port, + fetch: app.fetch, }; diff --git a/apps/memoro/apps/audio-server/src/lib/azure.test.ts b/apps/memoro/apps/audio-server/src/lib/azure.test.ts new file mode 100644 index 000000000..a697ec2fb --- /dev/null +++ b/apps/memoro/apps/audio-server/src/lib/azure.test.ts @@ -0,0 +1,109 @@ +/** + * Tests for Azure Speech config utilities. + */ + +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { getAvailableSpeechServices, pickRandomService, BATCH_ENDPOINT_BASE } from './azure'; +import type { SpeechServiceConfig } from './azure'; + +describe('BATCH_ENDPOINT_BASE', () => { + it('points to swedencentral', () => { + expect(BATCH_ENDPOINT_BASE).toBe( + 'https://swedencentral.api.cognitive.microsoft.com/speechtotext' + ); + }); +}); + +describe('getAvailableSpeechServices', () => { + const originalEnv = process.env; + + beforeEach(() => { + process.env = { ...originalEnv }; + // Clear all Azure keys + delete process.env.AZURE_SPEECH_KEY; + delete process.env.AZURE_SPEECH_KEY_1; + delete process.env.AZURE_SPEECH_KEY_2; + delete process.env.AZURE_SPEECH_KEY_3; + delete process.env.AZURE_SPEECH_KEY_4; + delete process.env.AZURE_SPEECH_REGION; + }); + + afterEach(() => { + process.env = originalEnv; + }); + + it('throws if no keys configured', () => { + expect(() => getAvailableSpeechServices()).toThrow('No Azure Speech credentials configured'); + }); + + it('uses single AZURE_SPEECH_KEY as fallback', () => { + process.env.AZURE_SPEECH_KEY = 'single-key'; + + const services = getAvailableSpeechServices(); + expect(services).toHaveLength(1); + expect(services[0].key).toBe('single-key'); + expect(services[0].name).toBe('azure-speech-default'); + }); + + it('uses numbered keys when available', () => { + process.env.AZURE_SPEECH_KEY_1 = 'key-1'; + process.env.AZURE_SPEECH_KEY_2 = 'key-2'; + + const services = getAvailableSpeechServices(); + expect(services).toHaveLength(2); + expect(services[0].name).toBe('azure-speech-1'); + expect(services[1].name).toBe('azure-speech-2'); + }); + + it('prefers numbered keys over single key', () => { + process.env.AZURE_SPEECH_KEY = 'fallback-key'; + process.env.AZURE_SPEECH_KEY_1 = 'key-1'; + + const services = getAvailableSpeechServices(); + expect(services).toHaveLength(1); + expect(services[0].key).toBe('key-1'); + }); + + it('uses custom region', () => { + process.env.AZURE_SPEECH_KEY = 'key'; + process.env.AZURE_SPEECH_REGION = 'westeurope'; + + const services = getAvailableSpeechServices(); + expect(services[0].region).toBe('westeurope'); + expect(services[0].endpoint).toContain('westeurope'); + }); + + it('defaults to swedencentral region', () => { + process.env.AZURE_SPEECH_KEY = 'key'; + + const services = getAvailableSpeechServices(); + expect(services[0].region).toBe('swedencentral'); + }); +}); + +describe('pickRandomService', () => { + it('throws for empty array', () => { + expect(() => pickRandomService([])).toThrow('No speech services available'); + }); + + it('returns the only service for single-element array', () => { + const service: SpeechServiceConfig = { + key: 'key-1', + endpoint: 'https://example.com', + region: 'swedencentral', + name: 'azure-speech-1', + }; + + expect(pickRandomService([service])).toBe(service); + }); + + it('returns a service from the array', () => { + const services: SpeechServiceConfig[] = [ + { key: 'key-1', endpoint: 'https://example.com', region: 'swedencentral', name: 'svc-1' }, + { key: 'key-2', endpoint: 'https://example.com', region: 'swedencentral', name: 'svc-2' }, + ]; + + const result = pickRandomService(services); + expect(services).toContainEqual(result); + }); +}); diff --git a/apps/memoro/apps/audio-server/src/routes/transcribe.test.ts b/apps/memoro/apps/audio-server/src/routes/transcribe.test.ts new file mode 100644 index 000000000..fabb1eabc --- /dev/null +++ b/apps/memoro/apps/audio-server/src/routes/transcribe.test.ts @@ -0,0 +1,224 @@ +/** + * Tests for audio-server transcription routes and health check. + */ + +import { describe, it, expect, vi } from 'vitest'; +import { app } from '../index'; + +// ── Mocks ──────────────────────────────────────────────────────────────────── + +vi.mock('../lib/supabase.ts', () => ({ + downloadAudioFromStorage: vi.fn().mockResolvedValue(Buffer.from('fake-audio')), +})); + +vi.mock('../services/transcription.ts', () => { + class MockTranscriptionService { + transcribeWithFallback = vi.fn().mockResolvedValue({ transcript: 'Hello world' }); + } + return { TranscriptionService: MockTranscriptionService }; +}); + +const SERVICE_KEY = 'test-service-key'; + +function post(path: string, body: unknown, headers?: Record) { + return app.request(path, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...headers, + }, + body: JSON.stringify(body), + }); +} + +// ── Health ─────────────────────────────────────────────────────────────────── + +describe('GET /health', () => { + it('returns 200 with service info', async () => { + const res = await app.request('/health'); + expect(res.status).toBe(200); + + const data = await res.json(); + expect(data.status).toBe('ok'); + expect(data.service).toBe('memoro-audio-server'); + expect(data.version).toBe('1.0.0'); + expect(data.timestamp).toBeDefined(); + }); +}); + +// ── Auth ───────────────────────────────────────────────────────────────────── + +describe('Service key authentication', () => { + it('rejects requests without X-Service-Key', async () => { + const res = await post('/api/v1/transcribe', { + audioPath: 'test.m4a', + memoId: 'memo-1', + userId: 'user-1', + }); + expect(res.status).toBe(401); + + const data = await res.json(); + expect(data.error).toBe('Unauthorized'); + }); + + it('rejects requests with wrong service key', async () => { + const res = await post( + '/api/v1/transcribe', + { audioPath: 'test.m4a', memoId: 'memo-1', userId: 'user-1' }, + { 'X-Service-Key': 'wrong-key' } + ); + expect(res.status).toBe(401); + }); + + it('accepts requests with valid service key', async () => { + const res = await post( + '/api/v1/transcribe', + { audioPath: 'test.m4a', memoId: 'memo-1', userId: 'user-1' }, + { 'X-Service-Key': SERVICE_KEY } + ); + expect(res.status).toBe(200); + }); +}); + +// ── POST /api/v1/transcribe ────────────────────────────────────────────────── + +describe('POST /api/v1/transcribe', () => { + it('starts transcription with valid input', async () => { + const res = await post( + '/api/v1/transcribe', + { audioPath: 'user/recording.m4a', memoId: 'memo-1', userId: 'user-1' }, + { 'X-Service-Key': SERVICE_KEY } + ); + expect(res.status).toBe(200); + + const data = await res.json(); + expect(data.success).toBe(true); + expect(data.memoId).toBe('memo-1'); + expect(data.message).toBe('Transcription started'); + }); + + it('accepts optional fields', async () => { + const res = await post( + '/api/v1/transcribe', + { + audioPath: 'user/recording.m4a', + memoId: 'memo-1', + userId: 'user-1', + spaceId: 'space-1', + recordingLanguages: ['de-DE', 'en-US'], + enableDiarization: true, + recordingIndex: 0, + }, + { 'X-Service-Key': SERVICE_KEY } + ); + expect(res.status).toBe(200); + }); + + it('rejects missing audioPath', async () => { + const res = await post( + '/api/v1/transcribe', + { memoId: 'memo-1', userId: 'user-1' }, + { 'X-Service-Key': SERVICE_KEY } + ); + expect(res.status).toBe(400); + + const data = await res.json(); + expect(data.error).toContain('Missing required fields'); + }); + + it('rejects missing memoId', async () => { + const res = await post( + '/api/v1/transcribe', + { audioPath: 'test.m4a', userId: 'user-1' }, + { 'X-Service-Key': SERVICE_KEY } + ); + expect(res.status).toBe(400); + }); + + it('rejects missing userId', async () => { + const res = await post( + '/api/v1/transcribe', + { audioPath: 'test.m4a', memoId: 'memo-1' }, + { 'X-Service-Key': SERVICE_KEY } + ); + expect(res.status).toBe(400); + }); + + it('rejects invalid JSON', async () => { + const res = await app.request('/api/v1/transcribe', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Service-Key': SERVICE_KEY, + }, + body: 'not json', + }); + expect(res.status).toBe(400); + + const data = await res.json(); + expect(data.error).toBe('Invalid JSON body'); + }); +}); + +// ── POST /api/v1/transcribe/append ─────────────────────────────────────────── + +describe('POST /api/v1/transcribe/append', () => { + it('starts append transcription with valid input', async () => { + const res = await post( + '/api/v1/transcribe/append', + { + audioPath: 'user/append.m4a', + memoId: 'memo-1', + userId: 'user-1', + recordingIndex: 1, + }, + { 'X-Service-Key': SERVICE_KEY } + ); + expect(res.status).toBe(200); + + const data = await res.json(); + expect(data.success).toBe(true); + expect(data.memoId).toBe('memo-1'); + expect(data.message).toBe('Append transcription started'); + }); + + it('rejects missing required fields', async () => { + const res = await post( + '/api/v1/transcribe/append', + { audioPath: 'test.m4a' }, + { 'X-Service-Key': SERVICE_KEY } + ); + expect(res.status).toBe(400); + }); + + it('rejects invalid JSON', async () => { + const res = await app.request('/api/v1/transcribe/append', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Service-Key': SERVICE_KEY, + }, + body: '{invalid', + }); + expect(res.status).toBe(400); + }); +}); + +// ── 404 ────────────────────────────────────────────────────────────────────── + +describe('404 handler', () => { + it('returns 404 for unknown routes', async () => { + const res = await app.request('/nonexistent'); + expect(res.status).toBe(404); + + const data = await res.json(); + expect(data.error).toContain('Not found'); + }); + + it('returns 404 for unknown API routes', async () => { + const res = await app.request('/api/v1/unknown', { + headers: { 'X-Service-Key': SERVICE_KEY }, + }); + expect(res.status).toBe(404); + }); +}); diff --git a/apps/memoro/apps/audio-server/src/test-setup.ts b/apps/memoro/apps/audio-server/src/test-setup.ts new file mode 100644 index 000000000..35bad647f --- /dev/null +++ b/apps/memoro/apps/audio-server/src/test-setup.ts @@ -0,0 +1,3 @@ +process.env.NODE_ENV = 'test'; +process.env.SERVICE_KEY = 'test-service-key'; +process.env.MEMORO_SERVER_URL = 'http://localhost:3015'; diff --git a/apps/memoro/apps/audio-server/tsconfig.json b/apps/memoro/apps/audio-server/tsconfig.json index 079bc3f25..f79b74bd1 100644 --- a/apps/memoro/apps/audio-server/tsconfig.json +++ b/apps/memoro/apps/audio-server/tsconfig.json @@ -1,19 +1,19 @@ { - "compilerOptions": { - "target": "ESNext", - "module": "ESNext", - "moduleResolution": "bundler", - "lib": ["ESNext"], - "strict": true, - "skipLibCheck": true, - "noEmit": true, - "esModuleInterop": true, - "allowSyntheticDefaultImports": true, - "resolveJsonModule": true, - "allowImportingTsExtensions": true, - "outDir": "dist", - "types": ["bun-types", "node"] - }, - "include": ["src/**/*.ts"], - "exclude": ["node_modules", "dist"] + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ESNext"], + "strict": true, + "skipLibCheck": true, + "noEmit": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "resolveJsonModule": true, + "allowImportingTsExtensions": true, + "outDir": "dist", + "types": ["bun-types", "node"] + }, + "include": ["src/**/*.ts", "vitest.config.ts"], + "exclude": ["node_modules", "dist"] } diff --git a/apps/memoro/apps/audio-server/vitest.config.ts b/apps/memoro/apps/audio-server/vitest.config.ts new file mode 100644 index 000000000..d84744d7e --- /dev/null +++ b/apps/memoro/apps/audio-server/vitest.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + include: ['src/**/*.test.ts'], + setupFiles: ['./src/test-setup.ts'], + clearMocks: true, + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 83e86e28a..71d9a62ab 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2897,6 +2897,9 @@ importers: typescript: specifier: ^5.5.0 version: 5.9.3 + vitest: + specifier: ^4.1.2 + version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@20.19.25)(jsdom@29.0.1(@noble/hashes@2.0.1))(vite@7.2.4(@types/node@20.19.25)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) apps/memoro/apps/landing: dependencies: @@ -3196,9 +3199,6 @@ importers: apps/memoro/apps/server: dependencies: - '@manacore/notify-client': - specifier: workspace:^ - version: link:../../../../packages/notify-client '@manacore/shared-hono': specifier: workspace:* version: link:../../../../packages/shared-hono @@ -14453,6 +14453,9 @@ packages: '@vitest/expect@4.1.1': resolution: {integrity: sha512-xAV0fqBTk44Rn6SjJReEQkHP3RrqbJo6JQ4zZ7/uVOiJZRarBtblzrOfFIZeYUrukp2YD6snZG6IBqhOoHTm+A==} + '@vitest/expect@4.1.2': + resolution: {integrity: sha512-gbu+7B0YgUJ2nkdsRJrFFW6X7NTP44WlhiclHniUhxADQJH5Szt9mZ9hWnJPJ8YwOK5zUOSSlSvyzRf0u1DSBQ==} + '@vitest/mocker@2.1.9': resolution: {integrity: sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==} peerDependencies: @@ -14519,6 +14522,17 @@ packages: vite: optional: true + '@vitest/mocker@4.1.2': + resolution: {integrity: sha512-Ize4iQtEALHDttPRCmN+FKqOl2vxTiNUhzobQFFt/BM1lRUTG7zRCLOykG/6Vo4E4hnUdfVLo5/eqKPukcWW7Q==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + '@vitest/pretty-format@2.1.9': resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==} @@ -14537,6 +14551,9 @@ packages: '@vitest/pretty-format@4.1.1': resolution: {integrity: sha512-GM+TEQN5WhOygr1lp7skeVjdLPqqWMHsfzXrcHAqZJi/lIVh63H0kaRCY8MDhNWikx19zBUK8ceaLB7X5AH9NQ==} + '@vitest/pretty-format@4.1.2': + resolution: {integrity: sha512-dwQga8aejqeuB+TvXCMzSQemvV9hNEtDDpgUKDzOmNQayl2OG241PSWeJwKRH3CiC+sESrmoFd49rfnq7T4RnA==} + '@vitest/runner@1.6.1': resolution: {integrity: sha512-3nSnYXkVkf3mXFfE7vVyPmi3Sazhb/2cfZGGs0JRzFsPFvAMBEcrweV1V1GsrstdXeKCTXlJbvnQwGWgEIHmOA==} @@ -14558,6 +14575,9 @@ packages: '@vitest/runner@4.1.1': resolution: {integrity: sha512-f7+FPy75vN91QGWsITueq0gedwUZy1fLtHOCMeQpjs8jTekAHeKP80zfDEnhrleviLHzVSDXIWuCIOFn3D3f8A==} + '@vitest/runner@4.1.2': + resolution: {integrity: sha512-Gr+FQan34CdiYAwpGJmQG8PgkyFVmARK8/xSijia3eTFgVfpcpztWLuP6FttGNfPLJhaZVP/euvujeNYar36OQ==} + '@vitest/snapshot@1.6.1': resolution: {integrity: sha512-WvidQuWAzU2p95u8GAKlRMqMyN1yOJkGHnx3M1PL9Raf7AQ1kwLKg04ADlCa3+OXUZE7BceOhVZiuWAbzCKcUQ==} @@ -14579,6 +14599,9 @@ packages: '@vitest/snapshot@4.1.1': resolution: {integrity: sha512-kMVSgcegWV2FibXEx9p9WIKgje58lcTbXgnJixfcg15iK8nzCXhmalL0ZLtTWLW9PH1+1NEDShiFFedB3tEgWg==} + '@vitest/snapshot@4.1.2': + resolution: {integrity: sha512-g7yfUmxYS4mNxk31qbOYsSt2F4m1E02LFqO53Xpzg3zKMhLAPZAjjfyl9e6z7HrW6LvUdTwAQR3HHfLjpko16A==} + '@vitest/spy@1.6.1': resolution: {integrity: sha512-MGcMmpGkZebsMZhbQKkAf9CX5zGvjkBTqf8Zx3ApYWXr3wG+QvEu2eXWfnIIWYSJExIp4V9FCKDEeygzkYrXMw==} @@ -14600,6 +14623,9 @@ packages: '@vitest/spy@4.1.1': resolution: {integrity: sha512-6Ti/KT5OVaiupdIZEuZN7l3CZcR0cxnxt70Z0//3CtwgObwA6jZhmVBA3yrXSVN3gmwjgd7oDNLlsXz526gpRA==} + '@vitest/spy@4.1.2': + resolution: {integrity: sha512-DU4fBnbVCJGNBwVA6xSToNXrkZNSiw59H8tcuUspVMsBDBST4nfvsPsEHDHGtWRRnqBERBQu7TrTKskmjqTXKA==} + '@vitest/ui@3.2.4': resolution: {integrity: sha512-hGISOaP18plkzbWEcP/QvtRW1xDXF2+96HbEX6byqQhAUbiS5oH6/9JwW+QsQCIYON2bI6QZBF+2PvOmrRZ9wA==} peerDependencies: @@ -14631,6 +14657,9 @@ packages: '@vitest/utils@4.1.1': resolution: {integrity: sha512-cNxAlaB3sHoCdL6pj6yyUXv9Gry1NHNg0kFTXdvSIZXLHsqKH7chiWOkwJ5s5+d/oMwcoG9T0bKU38JZWKusrQ==} + '@vitest/utils@4.1.2': + resolution: {integrity: sha512-xw2/TiX82lQHA06cgbqRKFb5lCAy3axQ4H4SoUFhUsg+wztiet+co86IAMDtF6Vm1hc7J6j09oh/rgDn+JdKIQ==} + '@volar/kit@2.4.26': resolution: {integrity: sha512-shgNg7PbV8SIxxQLOQh5zMr8KV0JvdG9If0MwJb5L1HMrBU91jBxR0ANi2OJPMMme6/l1vIYm4hCaO6W2JaEcQ==} peerDependencies: @@ -23801,6 +23830,10 @@ packages: resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==} engines: {node: '>=14.0.0'} + tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} + engines: {node: '>=14.0.0'} + tinyspy@2.2.1: resolution: {integrity: sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==} engines: {node: '>=14.0.0'} @@ -24905,6 +24938,41 @@ packages: jsdom: optional: true + vitest@4.1.2: + resolution: {integrity: sha512-xjR1dMTVHlFLh98JE3i/f/WePqJsah4A0FK9cc8Ehp9Udk0AZk6ccpIZhh1qJ/yxVWRZ+Q54ocnD8TXmkhspGg==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.2 + '@vitest/browser-preview': 4.1.2 + '@vitest/browser-webdriverio': 4.1.2 + '@vitest/ui': 4.1.2 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + vlq@1.0.1: resolution: {integrity: sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w==} @@ -37170,7 +37238,7 @@ snapshots: magic-string: 0.30.21 sirv: 3.0.2 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@20.19.25)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@29.0.1(@noble/hashes@2.0.1))(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@20.19.25)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) ws: 8.18.3 optionalDependencies: playwright: 1.57.0 @@ -37201,11 +37269,11 @@ snapshots: - vite optional: true - '@vitest/browser@3.2.4(playwright@1.57.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@3.2.4)': + '@vitest/browser@3.2.4(playwright@1.57.0)(vite@6.4.1(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@3.2.4)': dependencies: '@testing-library/dom': 10.4.1 '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.1) - '@vitest/mocker': 3.2.4(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + '@vitest/mocker': 3.2.4(vite@6.4.1(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) '@vitest/utils': 3.2.4 magic-string: 0.30.21 sirv: 3.0.2 @@ -37221,6 +37289,26 @@ snapshots: - vite optional: true + '@vitest/browser@3.2.4(playwright@1.57.0)(vite@7.2.4(@types/node@20.19.25)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@3.2.4)': + dependencies: + '@testing-library/dom': 10.4.1 + '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.1) + '@vitest/mocker': 3.2.4(vite@7.2.4(@types/node@20.19.25)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + '@vitest/utils': 3.2.4 + magic-string: 0.30.21 + sirv: 3.0.2 + tinyrainbow: 2.0.0 + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@20.19.25)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@29.0.1(@noble/hashes@2.0.1))(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) + ws: 8.18.3 + optionalDependencies: + playwright: 1.57.0 + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite + optional: true + '@vitest/coverage-v8@4.0.14(vitest@4.0.14)': dependencies: '@bcoe/v8-coverage': 1.0.2 @@ -37295,6 +37383,15 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.0.3 + '@vitest/expect@4.1.2': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.2 + '@vitest/utils': 4.1.2 + chai: 6.2.2 + tinyrainbow: 3.1.0 + '@vitest/mocker@2.1.9(vite@5.4.21(@types/node@20.19.25)(lightningcss@1.30.2)(terser@5.44.1))': dependencies: '@vitest/spy': 2.1.9 @@ -37335,13 +37432,13 @@ snapshots: optionalDependencies: vite: 6.4.1(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) - '@vitest/mocker@3.2.4(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))': + '@vitest/mocker@3.2.4(vite@7.2.4(@types/node@20.19.25)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) + vite: 7.2.4(@types/node@20.19.25)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) optional: true '@vitest/mocker@4.0.14(vite@6.4.1(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))': @@ -37392,6 +37489,14 @@ snapshots: optionalDependencies: vite: 6.4.1(@types/node@20.19.25)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) + '@vitest/mocker@4.1.2(vite@7.2.4(@types/node@20.19.25)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))': + dependencies: + '@vitest/spy': 4.1.2 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.2.4(@types/node@20.19.25)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) + '@vitest/pretty-format@2.1.9': dependencies: tinyrainbow: 1.2.0 @@ -37416,6 +37521,10 @@ snapshots: dependencies: tinyrainbow: 3.0.3 + '@vitest/pretty-format@4.1.2': + dependencies: + tinyrainbow: 3.1.0 + '@vitest/runner@1.6.1': dependencies: '@vitest/utils': 1.6.1 @@ -37453,6 +37562,11 @@ snapshots: '@vitest/utils': 4.1.1 pathe: 2.0.3 + '@vitest/runner@4.1.2': + dependencies: + '@vitest/utils': 4.1.2 + pathe: 2.0.3 + '@vitest/snapshot@1.6.1': dependencies: magic-string: 0.30.21 @@ -37497,6 +37611,13 @@ snapshots: magic-string: 0.30.21 pathe: 2.0.3 + '@vitest/snapshot@4.1.2': + dependencies: + '@vitest/pretty-format': 4.1.2 + '@vitest/utils': 4.1.2 + magic-string: 0.30.21 + pathe: 2.0.3 + '@vitest/spy@1.6.1': dependencies: tinyspy: 2.2.1 @@ -37517,6 +37638,8 @@ snapshots: '@vitest/spy@4.1.1': {} + '@vitest/spy@4.1.2': {} + '@vitest/ui@3.2.4(vitest@3.2.4)': dependencies: '@vitest/utils': 3.2.4 @@ -37526,7 +37649,7 @@ snapshots: sirv: 3.0.2 tinyglobby: 0.2.15 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@20.19.25)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@29.0.1(@noble/hashes@2.0.1))(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.1)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@29.0.1(@noble/hashes@2.0.1))(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) optional: true '@vitest/ui@4.0.14(vitest@4.0.14)': @@ -37581,6 +37704,12 @@ snapshots: convert-source-map: 2.0.0 tinyrainbow: 3.0.3 + '@vitest/utils@4.1.2': + dependencies: + '@vitest/pretty-format': 4.1.2 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 + '@volar/kit@2.4.26(typescript@5.9.3)': dependencies: '@volar/language-service': 2.4.26 @@ -52302,6 +52431,8 @@ snapshots: tinyrainbow@3.0.3: {} + tinyrainbow@3.1.0: {} + tinyspy@2.2.1: {} tinyspy@3.0.2: {} @@ -53690,7 +53821,7 @@ snapshots: optionalDependencies: '@types/debug': 4.1.12 '@types/node': 20.19.25 - '@vitest/browser': 3.2.4(playwright@1.57.0)(vite@6.4.1(@types/node@20.19.25)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@3.2.4) + '@vitest/browser': 3.2.4(playwright@1.57.0)(vite@7.2.4(@types/node@20.19.25)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@3.2.4) '@vitest/ui': 3.2.4(vitest@3.2.4) jsdom: 29.0.1(@noble/hashes@2.0.1) transitivePeerDependencies: @@ -53780,7 +53911,7 @@ snapshots: optionalDependencies: '@types/debug': 4.1.12 '@types/node': 24.10.1 - '@vitest/browser': 3.2.4(playwright@1.57.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@3.2.4) + '@vitest/browser': 3.2.4(playwright@1.57.0)(vite@6.4.1(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@3.2.4) '@vitest/ui': 3.2.4(vitest@3.2.4) jsdom: 29.0.1(@noble/hashes@2.0.1) transitivePeerDependencies: @@ -54050,6 +54181,35 @@ snapshots: transitivePeerDependencies: - msw + vitest@4.1.2(@opentelemetry/api@1.9.0)(@types/node@20.19.25)(jsdom@29.0.1(@noble/hashes@2.0.1))(vite@7.2.4(@types/node@20.19.25)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)): + dependencies: + '@vitest/expect': 4.1.2 + '@vitest/mocker': 4.1.2(vite@7.2.4(@types/node@20.19.25)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + '@vitest/pretty-format': 4.1.2 + '@vitest/runner': 4.1.2 + '@vitest/snapshot': 4.1.2 + '@vitest/spy': 4.1.2 + '@vitest/utils': 4.1.2 + es-module-lexer: 2.0.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.3 + std-env: 4.0.0 + tinybench: 2.9.0 + tinyexec: 1.0.2 + tinyglobby: 0.2.15 + tinyrainbow: 3.1.0 + vite: 7.2.4(@types/node@20.19.25)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) + why-is-node-running: 2.3.0 + optionalDependencies: + '@opentelemetry/api': 1.9.0 + '@types/node': 20.19.25 + jsdom: 29.0.1(@noble/hashes@2.0.1) + transitivePeerDependencies: + - msw + vlq@1.0.1: {} void-elements@3.1.0: {}