feat(memoro/audio-server): add vitest setup and 25 API + config tests

- Health endpoint, service key auth, 404 handler tests
- Transcribe and append endpoint validation tests
- Azure speech service config tests (getAvailableSpeechServices, pickRandomService)
- Export app for testability

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-01 16:31:28 +02:00
parent cb0e67ddd2
commit c582f164ba
7 changed files with 562 additions and 52 deletions

View file

@ -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,
};

View file

@ -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);
});
});

View file

@ -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<string, string>) {
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);
});
});

View file

@ -0,0 +1,3 @@
process.env.NODE_ENV = 'test';
process.env.SERVICE_KEY = 'test-service-key';
process.env.MEMORO_SERVER_URL = 'http://localhost:3015';

View file

@ -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"]
}

View file

@ -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,
},
});

182
pnpm-lock.yaml generated
View file

@ -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: {}