From 1ecb79476edab3082cde77d1faca8101c9fd2d5d Mon Sep 17 00:00:00 2001 From: indifferentketchup Date: Sat, 16 May 2026 04:35:31 +0000 Subject: [PATCH] test: vitest harness + unit tests for security-critical pure functions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds vitest 3.x (pinned to ^3 because vitest 4 requires Vite 6, while the web app pins Vite 5). Tests live under src/**/__tests__/**. Three target functions: - sanitizeFolderName (project_bootstrap.ts): 8 cases covering happy path, path-traversal stripping, empty-after-sanitize, control chars, truncation at 64, null bytes, leading/trailing dot/slash stripping. - resolveProjectPath (projects.ts): 7 cases including symlink-escape via realpath, outside-whitelist rejection, nonexistent path, AND a flagged BEHAVIOR GAP: passing the whitelist path itself currently returns success rather than erroring out (function early-exits the scope check when real === whitelistReal). Test asserts current behavior with explicit comment flagging the spec violation — function NOT silently patched. Function made exportable for testing (single keyword change). - buildMessagesPayload (inference.ts): 8 cases for compact-marker logic (no marker, marker present, multiple compacts, tool-message position). tsconfig.json excludes __tests__ + *.test.ts from emit so dist/ stays clean. pnpm -C apps/server test => 23 passed in ~340ms. Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/server/package.json | 6 +- .../src/routes/__tests__/projects.test.ts | 100 ++++++ apps/server/src/routes/projects.ts | 2 +- .../src/services/__tests__/inference.test.ts | 237 +++++++++++++ .../__tests__/project_bootstrap.test.ts | 47 +++ apps/server/tsconfig.json | 3 +- apps/server/vitest.config.ts | 9 + pnpm-lock.yaml | 322 ++++++++++++++++++ 8 files changed, 722 insertions(+), 4 deletions(-) create mode 100644 apps/server/src/routes/__tests__/projects.test.ts create mode 100644 apps/server/src/services/__tests__/inference.test.ts create mode 100644 apps/server/src/services/__tests__/project_bootstrap.test.ts create mode 100644 apps/server/vitest.config.ts diff --git a/apps/server/package.json b/apps/server/package.json index caeb3d4..d678f8b 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -7,7 +7,8 @@ "scripts": { "dev": "tsx watch src/index.ts", "build": "tsc && node -e \"import('node:fs').then(fs=>fs.copyFileSync('src/schema.sql','dist/schema.sql'))\"", - "typecheck": "tsc --noEmit" + "typecheck": "tsc --noEmit", + "test": "vitest run" }, "dependencies": { "@fastify/static": "^7.0.4", @@ -21,6 +22,7 @@ "@types/node": "^20.14.10", "@types/ws": "^8.5.10", "tsx": "^4.16.2", - "typescript": "^5.5.0" + "typescript": "^5.5.0", + "vitest": "^3.2.4" } } diff --git a/apps/server/src/routes/__tests__/projects.test.ts b/apps/server/src/routes/__tests__/projects.test.ts new file mode 100644 index 0000000..670ad93 --- /dev/null +++ b/apps/server/src/routes/__tests__/projects.test.ts @@ -0,0 +1,100 @@ +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import { mkdtemp, mkdir, rm, realpath, symlink, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { resolveProjectPath } from '../projects.js'; + +describe('resolveProjectPath', () => { + let scratch: string; + let whitelist: string; + let outside: string; + + beforeAll(async () => { + // mkdtemp returns a real path on most platforms, but symlink-resolve it + // anyway to defeat /tmp -> /private/tmp style indirection on macOS, and + // keep this stable for the comparisons below. + scratch = await realpath(await mkdtemp(join(tmpdir(), 'boocode-projects-test-'))); + whitelist = join(scratch, 'wl'); + outside = join(scratch, 'other'); + await mkdir(whitelist, { recursive: true }); + await mkdir(outside, { recursive: true }); + }); + + afterAll(async () => { + if (scratch) { + await rm(scratch, { recursive: true, force: true }); + } + }); + + it('returns real path and basename for a valid subdirectory under the whitelist', async () => { + const projectDir = join(whitelist, 'my-project'); + await mkdir(projectDir); + const result = await resolveProjectPath(projectDir, whitelist); + expect('error' in result).toBe(false); + if ('error' in result) return; // narrow + expect(result.real).toBe(projectDir); + expect(result.name).toBe('my-project'); + }); + + it('rejects a path outside the whitelist', async () => { + const projectDir = join(outside, 'foo'); + await mkdir(projectDir); + const result = await resolveProjectPath(projectDir, whitelist); + expect('error' in result).toBe(true); + if (!('error' in result)) return; + expect(result.error.toLowerCase()).toContain('path must be under'); + }); + + it('rejects relative paths', async () => { + const result = await resolveProjectPath('relative/path', whitelist); + expect('error' in result).toBe(true); + if (!('error' in result)) return; + expect(result.error.toLowerCase()).toContain('absolute'); + }); + + it('rejects nonexistent paths', async () => { + const result = await resolveProjectPath('/nonexistent/foo-boocode-test', whitelist); + expect('error' in result).toBe(true); + if (!('error' in result)) return; + expect(result.error.toLowerCase()).toContain('does not exist'); + }); + + it('rejects symlink escapes (realpath resolution catches traversal)', async () => { + // Create a symlink INSIDE the whitelist that points OUTSIDE. realpath + // should resolve through it and the resulting real path should fail the + // whitelist scope check. + const escapeLink = join(whitelist, 'escape-link'); + await symlink(outside, escapeLink); + const result = await resolveProjectPath(escapeLink, whitelist); + expect('error' in result).toBe(true); + if (!('error' in result)) return; + expect(result.error.toLowerCase()).toContain('path must be under'); + }); + + it('BEHAVIOR GAP: currently accepts the whitelist itself as a project root', async () => { + // SPEC says: the whitelist directory itself should be rejected — a + // project's parent can't be the project. The current implementation does + // NOT enforce this: the scope check is + // if (real !== whitelistReal && !real.startsWith(whitelistReal + sep)) + // which evaluates to false when real === whitelistReal, so the whitelist + // path falls through and is accepted as a valid project root. + // + // This test documents the ACTUAL current behavior. Reported as a bug in + // the harness report; not silently fixed here. To tighten the check, + // change the condition to: + // if (!real.startsWith(whitelistReal + sep)) + const result = await resolveProjectPath(whitelist, whitelist); + expect('error' in result).toBe(false); + if ('error' in result) return; + expect(result.real).toBe(whitelist); + }); + + it('rejects non-directory targets (file under whitelist)', async () => { + const filePath = join(whitelist, 'a-file.txt'); + await writeFile(filePath, 'content', 'utf8'); + const result = await resolveProjectPath(filePath, whitelist); + expect('error' in result).toBe(true); + if (!('error' in result)) return; + expect(result.error.toLowerCase()).toContain('not a directory'); + }); +}); diff --git a/apps/server/src/routes/projects.ts b/apps/server/src/routes/projects.ts index 703bc8b..e589d77 100644 --- a/apps/server/src/routes/projects.ts +++ b/apps/server/src/routes/projects.ts @@ -41,7 +41,7 @@ async function isDir(path: string): Promise { } } -async function resolveProjectPath( +export async function resolveProjectPath( raw: string, whitelist: string ): Promise<{ real: string; name: string } | { error: string }> { diff --git a/apps/server/src/services/__tests__/inference.test.ts b/apps/server/src/services/__tests__/inference.test.ts new file mode 100644 index 0000000..1012016 --- /dev/null +++ b/apps/server/src/services/__tests__/inference.test.ts @@ -0,0 +1,237 @@ +import { describe, it, expect } from 'vitest'; +import { buildMessagesPayload } from '../inference.js'; +import type { + Message, + MessageRole, + Project, + Session, + ToolCall, + ToolResult, +} from '../../types/api.js'; + +// ---- fixtures --------------------------------------------------------------- + +function makeSession(overrides: Partial = {}): Session { + return { + id: 'sess', + project_id: 'proj', + name: 'test session', + model: 'test-model', + system_prompt: '', + status: 'open', + created_at: new Date(0).toISOString(), + updated_at: new Date(0).toISOString(), + ...overrides, + }; +} + +function makeProject(overrides: Partial = {}): Project { + return { + id: 'proj', + name: 'test project', + path: '/tmp/proj', + added_at: new Date(0).toISOString(), + last_session_id: null, + status: 'open', + gitea_remote: null, + ...overrides, + }; +} + +let counter = 0; +function makeMessage( + role: MessageRole, + content: string, + overrides: Partial = {} +): Message { + counter += 1; + return { + id: `m${counter}`, + session_id: 'sess', + chat_id: 'chat', + role, + content, + kind: 'message', + tool_calls: null, + tool_results: null, + status: 'complete', + last_seq: 0, + tokens_used: null, + ctx_used: null, + ctx_max: null, + started_at: null, + finished_at: null, + created_at: new Date(counter * 1000).toISOString(), + ...overrides, + }; +} + +// ---- tests ------------------------------------------------------------------ + +describe('buildMessagesPayload', () => { + it('prepends a system prompt containing the project path', () => { + const session = makeSession(); + const project = makeProject({ path: '/tmp/my-proj' }); + const result = buildMessagesPayload(session, project, []); + expect(result).toHaveLength(1); + expect(result[0]!.role).toBe('system'); + expect(result[0]!.content).toContain('/tmp/my-proj'); + }); + + it('appends session.system_prompt to the system message when set', () => { + const session = makeSession({ system_prompt: 'Be terse.' }); + const project = makeProject(); + const result = buildMessagesPayload(session, project, []); + expect(result).toHaveLength(1); + expect(result[0]!.role).toBe('system'); + expect(result[0]!.content).toContain('Be terse.'); + }); + + it('returns user/assistant messages in order when no compact marker is present', () => { + const session = makeSession(); + const project = makeProject(); + const history: Message[] = [ + makeMessage('user', 'hi'), + makeMessage('assistant', 'hello'), + makeMessage('user', 'how are you'), + makeMessage('assistant', 'great'), + ]; + const result = buildMessagesPayload(session, project, history); + // 1 system + 4 history messages + expect(result).toHaveLength(5); + expect(result[0]!.role).toBe('system'); + expect(result[1]).toMatchObject({ role: 'user', content: 'hi' }); + expect(result[2]).toMatchObject({ role: 'assistant', content: 'hello' }); + expect(result[3]).toMatchObject({ role: 'user', content: 'how are you' }); + expect(result[4]).toMatchObject({ role: 'assistant', content: 'great' }); + }); + + it('starts from the latest compact marker, emitting it as a system message', () => { + const session = makeSession(); + const project = makeProject(); + const history: Message[] = [ + makeMessage('user', 'old1'), + makeMessage('assistant', 'oldreply1'), + makeMessage('user', 'old2'), + makeMessage('assistant', 'compacted summary text', { kind: 'compact' }), + makeMessage('user', 'new1'), + makeMessage('assistant', 'newreply1'), + ]; + const result = buildMessagesPayload(session, project, history); + // Expect: leading base-system prompt, then the compact as system, then + // the user/assistant pair following it. + expect(result).toHaveLength(4); + expect(result[0]!.role).toBe('system'); + expect(result[1]).toMatchObject({ + role: 'system', + content: 'compacted summary text', + }); + expect(result[2]).toMatchObject({ role: 'user', content: 'new1' }); + expect(result[3]).toMatchObject({ role: 'assistant', content: 'newreply1' }); + }); + + it('uses only the most recent compact when multiple are present', () => { + const session = makeSession(); + const project = makeProject(); + const history: Message[] = [ + makeMessage('user', 'u1'), + makeMessage('assistant', 'first compact summary', { kind: 'compact' }), + makeMessage('user', 'u2'), + makeMessage('assistant', 'second compact summary', { kind: 'compact' }), + makeMessage('user', 'u3'), + makeMessage('assistant', 'final reply'), + ]; + const result = buildMessagesPayload(session, project, history); + // Expect: base system + latest compact as system + the two messages + // following it. The earlier compact and pre-compact history are dropped. + expect(result).toHaveLength(4); + expect(result[0]!.role).toBe('system'); + expect(result[1]).toMatchObject({ + role: 'system', + content: 'second compact summary', + }); + expect(result[2]).toMatchObject({ role: 'user', content: 'u3' }); + expect(result[3]).toMatchObject({ role: 'assistant', content: 'final reply' }); + // None of the earlier content should leak through + const concatenated = result.map((m) => m.content ?? '').join(' '); + expect(concatenated).not.toContain('first compact summary'); + expect(concatenated).not.toContain('u1'); + expect(concatenated).not.toContain('u2'); + }); + + it('skips streaming and cancelled assistant rows', () => { + const session = makeSession(); + const project = makeProject(); + const history: Message[] = [ + makeMessage('user', 'hi'), + makeMessage('assistant', 'partial...', { status: 'streaming' }), + makeMessage('assistant', 'cancelled fragment', { status: 'cancelled' }), + makeMessage('assistant', 'final answer'), + ]; + const result = buildMessagesPayload(session, project, history); + // 1 system + 1 user + 1 assistant (only the complete one) + expect(result).toHaveLength(3); + expect(result[1]).toMatchObject({ role: 'user', content: 'hi' }); + expect(result[2]).toMatchObject({ role: 'assistant', content: 'final answer' }); + }); + + it('round-trips an assistant-with-tool_calls followed by its tool result', () => { + const session = makeSession(); + const project = makeProject(); + const toolCall: ToolCall = { + id: 'call_abc', + name: 'view_file', + args: { path: 'src/index.ts' }, + }; + const toolResult: ToolResult = { + tool_call_id: 'call_abc', + output: { contents: 'console.log(1)' }, + truncated: false, + }; + const history: Message[] = [ + makeMessage('user', 'show me the file'), + makeMessage('assistant', '', { tool_calls: [toolCall] }), + makeMessage('tool', '', { tool_results: toolResult }), + makeMessage('assistant', 'here it is'), + ]; + const result = buildMessagesPayload(session, project, history); + // 1 system + 1 user + 1 assistant(tool_calls) + 1 tool + 1 assistant + expect(result).toHaveLength(5); + expect(result[1]).toMatchObject({ role: 'user', content: 'show me the file' }); + expect(result[2]!.role).toBe('assistant'); + expect(result[2]!.tool_calls).toBeDefined(); + expect(result[2]!.tool_calls).toHaveLength(1); + expect(result[2]!.tool_calls![0]).toMatchObject({ + id: 'call_abc', + type: 'function', + function: { name: 'view_file' }, + }); + // The OpenAI shape stringifies args. + expect(result[2]!.tool_calls![0]!.function.arguments).toBe( + JSON.stringify({ path: 'src/index.ts' }) + ); + // assistant with empty content should be serialized as content: null + expect(result[2]!.content).toBeNull(); + expect(result[3]).toMatchObject({ + role: 'tool', + tool_call_id: 'call_abc', + }); + // Non-string tool output is JSON-stringified. + expect(result[3]!.content).toBe(JSON.stringify({ contents: 'console.log(1)' })); + expect(result[4]).toMatchObject({ role: 'assistant', content: 'here it is' }); + }); + + it('skips tool rows with no tool_results', () => { + const session = makeSession(); + const project = makeProject(); + const history: Message[] = [ + makeMessage('user', 'do it'), + makeMessage('tool', '', { tool_results: null }), + makeMessage('assistant', 'done'), + ]; + const result = buildMessagesPayload(session, project, history); + // 1 system + 1 user + 1 assistant; the empty tool row is dropped. + expect(result).toHaveLength(3); + expect(result.find((m) => m.role === 'tool')).toBeUndefined(); + }); +}); diff --git a/apps/server/src/services/__tests__/project_bootstrap.test.ts b/apps/server/src/services/__tests__/project_bootstrap.test.ts new file mode 100644 index 0000000..a461c64 --- /dev/null +++ b/apps/server/src/services/__tests__/project_bootstrap.test.ts @@ -0,0 +1,47 @@ +import { describe, it, expect } from 'vitest'; +import { sanitizeFolderName } from '../project_bootstrap.js'; + +describe('sanitizeFolderName', () => { + it('passes through a normal slug-like name', () => { + expect(sanitizeFolderName('my-project')).toBe('my-project'); + }); + + it('lowercases and replaces whitespace with hyphens', () => { + expect(sanitizeFolderName('Hello World')).toBe('hello-world'); + }); + + it('strips path-traversal characters', () => { + // dots and slashes fall outside [a-z0-9-] and are removed entirely. + expect(sanitizeFolderName('../etc/passwd')).toBe('etcpasswd'); + }); + + it('strips trailing and leading dots and slashes', () => { + expect(sanitizeFolderName('./foo/')).toBe('foo'); + }); + + it('collapses runs of hyphens and strips leading/trailing ones', () => { + expect(sanitizeFolderName('---foo---')).toBe('foo'); + }); + + it('returns empty string when nothing survives sanitization', () => { + // NOTE: sanitizeFolderName itself does NOT throw — it returns ''. The + // BootstrapNameError is raised by the caller (bootstrapProject) when the + // sanitized result fails the SAFE_NAME regex. The spec's "throws" phrasing + // refers to that caller-level validation, not this pure function. + expect(sanitizeFolderName('...')).toBe(''); + expect(sanitizeFolderName(' ')).toBe(''); + }); + + it('strips control characters and null bytes', () => { + // Null bytes and control characters are not in [a-z0-9-] so they're + // filtered out (effectively rejected as folder-name content). + expect(sanitizeFolderName('my\x00proj\x01')).toBe('myproj'); + expect(sanitizeFolderName('foo\x00bar')).toBe('foobar'); + }); + + it('truncates names longer than 64 characters', () => { + const long = 'a'.repeat(100); + expect(sanitizeFolderName(long)).toBe('a'.repeat(64)); + expect(sanitizeFolderName(long)).toHaveLength(64); + }); +}); diff --git a/apps/server/tsconfig.json b/apps/server/tsconfig.json index eb9098f..fe31069 100644 --- a/apps/server/tsconfig.json +++ b/apps/server/tsconfig.json @@ -10,5 +10,6 @@ "declaration": false, "sourceMap": true }, - "include": ["src/**/*"] + "include": ["src/**/*"], + "exclude": ["src/**/__tests__/**", "**/*.test.ts"] } diff --git a/apps/server/vitest.config.ts b/apps/server/vitest.config.ts new file mode 100644 index 0000000..5802658 --- /dev/null +++ b/apps/server/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + environment: 'node', + globals: false, + include: ['src/**/__tests__/**/*.test.ts'], + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5087b97..a16e119 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -45,6 +45,9 @@ importers: typescript: specifier: ^5.5.0 version: 5.9.3 + vitest: + specifier: ^3.2.4 + version: 3.2.4(@types/debug@4.1.13)(@types/node@20.19.41)(lightningcss@1.32.0)(msw@2.14.6(@types/node@20.19.41)(typescript@5.9.3)) apps/web: dependencies: @@ -1697,9 +1700,15 @@ packages: '@types/babel__traverse@7.28.0': resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + '@types/debug@4.1.13': resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==} + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/estree-jsx@1.0.5': resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} @@ -1756,6 +1765,35 @@ packages: peerDependencies: vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + '@vitest/expect@3.2.4': + resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} + + '@vitest/mocker@3.2.4': + resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@3.2.4': + resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} + + '@vitest/runner@3.2.4': + resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==} + + '@vitest/snapshot@3.2.4': + resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==} + + '@vitest/spy@3.2.4': + resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} + + '@vitest/utils@3.2.4': + resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} + abstract-logging@2.0.1: resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==} @@ -1809,6 +1847,10 @@ packages: resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} engines: {node: '>=10'} + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + ast-types@0.16.1: resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==} engines: {node: '>=4'} @@ -1863,6 +1905,10 @@ packages: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} @@ -1881,6 +1927,10 @@ packages: ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + chalk@5.6.2: resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} @@ -1897,6 +1947,10 @@ packages: character-reference-invalid@2.0.1: resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} + class-variance-authority@0.7.1: resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} @@ -2021,6 +2075,10 @@ packages: babel-plugin-macros: optional: true + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + deepmerge@4.3.1: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} @@ -2121,6 +2179,9 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + es-object-atoms@1.1.1: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} @@ -2154,6 +2215,9 @@ packages: estree-util-is-identifier-name@3.0.0: resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==} + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + etag@1.8.1: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} @@ -2174,6 +2238,10 @@ packages: resolution: {integrity: sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==} engines: {node: ^18.19.0 || >=20.5.0} + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + express-rate-limit@8.5.2: resolution: {integrity: sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==} engines: {node: '>= 16'} @@ -2529,6 +2597,9 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + js-yaml@4.1.1: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true @@ -2653,6 +2724,9 @@ packages: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} @@ -2998,6 +3072,13 @@ packages: path-to-regexp@8.4.2: resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==} + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -3308,6 +3389,9 @@ packages: resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} engines: {node: '>= 0.4'} + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} @@ -3342,6 +3426,9 @@ packages: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + statuses@2.0.1: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} @@ -3350,6 +3437,9 @@ packages: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + stdin-discarder@0.2.2: resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==} engines: {node: '>=18'} @@ -3402,6 +3492,9 @@ packages: resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==} engines: {node: '>=18'} + strip-literal@3.1.0: + resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + style-to-js@1.1.21: resolution: {integrity: sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==} @@ -3428,6 +3521,28 @@ packages: tiny-invariant@1.3.3: resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyglobby@0.2.16: + resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} + engines: {node: '>=12.0.0'} + + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@2.0.0: + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} + engines: {node: '>=14.0.0'} + + tinyspy@4.0.4: + resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} + engines: {node: '>=14.0.0'} + tldts-core@7.0.30: resolution: {integrity: sha512-uiHN8PIB1VmWyS98eZYja4xzlYqeFZVjb4OuYlJQnZAuJhMw4PbKQOKgHKhBdJR3FE/t5mUQ1Kd80++B+qhD1Q==} @@ -3572,6 +3687,11 @@ packages: vfile@6.0.3: resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + vite-node@3.2.4: + resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + vite@5.4.21: resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==} engines: {node: ^18.0.0 || >=20.0.0} @@ -3603,6 +3723,34 @@ packages: terser: optional: true + vitest@3.2.4: + resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/debug': ^4.1.12 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@vitest/browser': 3.2.4 + '@vitest/ui': 3.2.4 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/debug': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + web-streams-polyfill@3.3.3: resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} engines: {node: '>= 8'} @@ -3617,6 +3765,11 @@ packages: engines: {node: ^16.13.0 || >=18.0.0} hasBin: true + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -5196,10 +5349,17 @@ snapshots: dependencies: '@babel/types': 7.29.0 + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + '@types/debug@4.1.13': dependencies: '@types/ms': 2.1.0 + '@types/deep-eql@4.0.2': {} + '@types/estree-jsx@1.0.5': dependencies: '@types/estree': 1.0.8 @@ -5261,6 +5421,49 @@ snapshots: transitivePeerDependencies: - supports-color + '@vitest/expect@3.2.4': + dependencies: + '@types/chai': 5.2.3 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.3.3 + tinyrainbow: 2.0.0 + + '@vitest/mocker@3.2.4(msw@2.14.6(@types/node@20.19.41)(typescript@5.9.3))(vite@5.4.21(@types/node@20.19.41)(lightningcss@1.32.0))': + dependencies: + '@vitest/spy': 3.2.4 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + msw: 2.14.6(@types/node@20.19.41)(typescript@5.9.3) + vite: 5.4.21(@types/node@20.19.41)(lightningcss@1.32.0) + + '@vitest/pretty-format@3.2.4': + dependencies: + tinyrainbow: 2.0.0 + + '@vitest/runner@3.2.4': + dependencies: + '@vitest/utils': 3.2.4 + pathe: 2.0.3 + strip-literal: 3.1.0 + + '@vitest/snapshot@3.2.4': + dependencies: + '@vitest/pretty-format': 3.2.4 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@3.2.4': + dependencies: + tinyspy: 4.0.4 + + '@vitest/utils@3.2.4': + dependencies: + '@vitest/pretty-format': 3.2.4 + loupe: 3.2.1 + tinyrainbow: 2.0.0 + abstract-logging@2.0.1: {} accepts@2.0.0: @@ -5301,6 +5504,8 @@ snapshots: dependencies: tslib: 2.8.1 + assertion-error@2.0.1: {} + ast-types@0.16.1: dependencies: tslib: 2.8.1 @@ -5360,6 +5565,8 @@ snapshots: bytes@3.1.2: {} + cac@6.7.14: {} + call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 @@ -5376,6 +5583,14 @@ snapshots: ccount@2.0.1: {} + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.3 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + chalk@5.6.2: {} character-entities-html4@2.1.0: {} @@ -5386,6 +5601,8 @@ snapshots: character-reference-invalid@2.0.1: {} + check-error@2.1.3: {} + class-variance-authority@0.7.1: dependencies: clsx: 2.1.1 @@ -5474,6 +5691,8 @@ snapshots: dedent@1.7.2: {} + deep-eql@5.0.2: {} + deepmerge@4.3.1: {} default-browser-id@5.0.1: {} @@ -5556,6 +5775,8 @@ snapshots: es-errors@1.3.0: {} + es-module-lexer@1.7.0: {} + es-object-atoms@1.1.1: dependencies: es-errors: 1.3.0 @@ -5625,6 +5846,10 @@ snapshots: estree-util-is-identifier-name@3.0.0: {} + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + etag@1.8.1: {} eventsource-parser@3.0.8: {} @@ -5660,6 +5885,8 @@ snapshots: strip-final-newline: 4.0.0 yoctocolors: 2.1.2 + expect-type@1.3.0: {} + express-rate-limit@8.5.2(express@5.2.1): dependencies: express: 5.2.1 @@ -6053,6 +6280,8 @@ snapshots: js-tokens@4.0.0: {} + js-tokens@9.0.1: {} + js-yaml@4.1.1: dependencies: argparse: 2.0.1 @@ -6149,6 +6378,8 @@ snapshots: dependencies: js-tokens: 4.0.0 + loupe@3.2.1: {} + lru-cache@10.4.3: {} lru-cache@5.1.1: @@ -6700,6 +6931,10 @@ snapshots: path-to-regexp@8.4.2: {} + pathe@2.0.3: {} + + pathval@2.0.1: {} + picocolors@1.1.1: {} picomatch@2.3.2: {} @@ -7178,6 +7413,8 @@ snapshots: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 + siginfo@2.0.0: {} + signal-exit@3.0.7: {} signal-exit@4.1.0: {} @@ -7201,10 +7438,14 @@ snapshots: split2@4.2.0: {} + stackback@0.0.2: {} + statuses@2.0.1: {} statuses@2.0.2: {} + std-env@3.10.0: {} + stdin-discarder@0.2.2: {} stream-shift@1.0.3: {} @@ -7258,6 +7499,10 @@ snapshots: strip-final-newline@4.0.0: {} + strip-literal@3.1.0: + dependencies: + js-tokens: 9.0.1 + style-to-js@1.1.21: dependencies: style-to-object: 1.0.14 @@ -7280,6 +7525,21 @@ snapshots: tiny-invariant@1.3.3: {} + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + + tinyglobby@0.2.16: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + tinypool@1.1.1: {} + + tinyrainbow@2.0.0: {} + + tinyspy@4.0.4: {} + tldts-core@7.0.30: {} tldts@7.0.30: @@ -7419,6 +7679,24 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 + vite-node@3.2.4(@types/node@20.19.41)(lightningcss@1.32.0): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 5.4.21(@types/node@20.19.41)(lightningcss@1.32.0) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + vite@5.4.21(@types/node@20.19.41)(lightningcss@1.32.0): dependencies: esbuild: 0.21.5 @@ -7429,6 +7707,45 @@ snapshots: fsevents: 2.3.3 lightningcss: 1.32.0 + vitest@3.2.4(@types/debug@4.1.13)(@types/node@20.19.41)(lightningcss@1.32.0)(msw@2.14.6(@types/node@20.19.41)(typescript@5.9.3)): + dependencies: + '@types/chai': 5.2.3 + '@vitest/expect': 3.2.4 + '@vitest/mocker': 3.2.4(msw@2.14.6(@types/node@20.19.41)(typescript@5.9.3))(vite@5.4.21(@types/node@20.19.41)(lightningcss@1.32.0)) + '@vitest/pretty-format': 3.2.4 + '@vitest/runner': 3.2.4 + '@vitest/snapshot': 3.2.4 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.3.0 + magic-string: 0.30.21 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.16 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 5.4.21(@types/node@20.19.41)(lightningcss@1.32.0) + vite-node: 3.2.4(@types/node@20.19.41)(lightningcss@1.32.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/debug': 4.1.13 + '@types/node': 20.19.41 + transitivePeerDependencies: + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + web-streams-polyfill@3.3.3: {} which@2.0.2: @@ -7439,6 +7756,11 @@ snapshots: dependencies: isexe: 3.1.5 + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0