From 6fea293e1afc1e3ad75342300906c40456ceee61 Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" <29069505+gewenyu99@users.noreply.github.com> Date: Fri, 12 Jun 2026 16:20:24 -0400 Subject: [PATCH] test: migrate unit test runner from jest to vitest MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Swaps the unit suite's runner from jest to vitest, removing jest's finicky ESM workarounds now that the wizard's source is ESM-native. - Add vitest.config.ts: resolve.alias mirrors the old jest moduleNameMapper (manual mocks + tsconfig path aliases), a small pre-resolve plugin maps NodeNext `.js` import specifiers back to their `.ts` source, globals + node environment, and v8 coverage. - Codemod every test/mock file: jest.fn/mock/spyOn/timers → vi.*, and the jest.Mock/Mocked/... ambient types → vitest equivalents (bridged globally in types/vitest-global-types.d.ts so call sites stay bare). - Port the APIs without a 1:1 vitest mapping: requireMock → mocked imports, isolateModules → resetModules + dynamic import, hoisted-factory vars → vi.hoisted, and partial module mocks that omit an accessed export → importOriginal (vitest throws where jest returned undefined). - Stabilise the CLI re-import tests: the dispatch fires detached async work that used to run synchronously under jest's babel-commonjs imports; settle() now anchors on the decisive sink before asserting, and env-var setup mutates process.env in place (reassigning it defeats yargs' .env() reader once the module graph is reset). - Keep jest for the standalone e2e-tests package (own jest.config + ts-jest): give it a self-contained tsconfig and drop it from the root tsconfig/eslint scope. jest and its babel deps stay installed for that suite. 737/737 unit tests pass; `pnpm build`, `pnpm typecheck`, and `pnpm lint` are clean; the postbuild smoke-test still runs via `pnpm test`. Generated-By: PostHog Code Task-Id: 4ced191a-7fea-49ed-a854-ab6330f1c83d --- .eslintrc.cjs | 37 +- __mocks__/@posthog/warlock.ts | 12 +- e2e-tests/tsconfig.json | 9 + package.json | 56 +- pnpm-lock.yaml | 1176 ++++++++++++++++- src/__tests__/cli.test.ts | 137 +- src/__tests__/mcp-cli.test.ts | 20 +- src/__tests__/programs-cli.test.ts | 10 +- src/__tests__/provision-cli.test.ts | 74 +- src/__tests__/skill-cli.test.ts | 10 +- src/__tests__/wizard-abort.test.ts | 50 +- src/lib/__tests__/agent-interface.test.ts | 74 +- .../__tests__/cloudflare-detection.test.ts | 2 +- src/lib/__tests__/wizard-ask-bridge.test.ts | 12 +- src/lib/__tests__/wizard-can-use-tool.test.ts | 6 +- src/lib/__tests__/yara-hooks.test.ts | 41 +- .../__snapshots__/commandments.test.ts.snap | 4 +- src/lib/agent/__tests__/agent-prompt.test.ts | 2 +- src/lib/detection/__tests__/context.test.ts | 8 +- .../__tests__/package-manager.test.ts | 8 +- .../__tests__/health-checks.test.ts | 55 +- .../programs/__tests__/program-step.test.ts | 8 +- .../revenue-analytics-detect.test.ts | 4 +- .../__tests__/source-maps-detect.test.ts | 4 +- .../web-analytics-doctor-detect.test.ts | 4 +- .../__tests__/posthog-destination.test.ts | 30 +- .../__tests__/task-stream-push.test.ts | 36 +- .../clients/__tests__/claude-code.test.ts | 24 +- .../clients/__tests__/claude-web.test.ts | 6 +- .../clients/__tests__/claude.test.ts | 46 +- .../clients/__tests__/codex.test.ts | 36 +- .../providers/__tests__/vercel.test.ts | 34 +- src/ui/tui/__tests__/mcp-installer.test.ts | 43 +- src/ui/tui/__tests__/store.test.ts | 57 +- .../tui/hooks/__tests__/file-watcher.test.ts | 14 +- src/utils/__tests__/analytics.test.ts | 20 +- src/utils/__tests__/provisioning.test.ts | 12 +- tsconfig.json | 10 +- types/vitest-global-types.d.ts | 31 + vitest.config.ts | 82 ++ 40 files changed, 1795 insertions(+), 509 deletions(-) create mode 100644 types/vitest-global-types.d.ts create mode 100644 vitest.config.ts diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 7eb9f10c..184dc942 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -1,11 +1,8 @@ -const jestPackageJson = require('jest/package.json'); - module.exports = { root: true, env: { es6: true, node: true, - jest: true, }, parser: '@typescript-eslint/parser', plugins: ['@typescript-eslint'], @@ -22,7 +19,9 @@ module.exports = { 'assets/**', 'scripts/**', 'coverage/**', - 'e2e-tests/test-applications/**', + // Standalone jest-based package, linted/typechecked in its own context and + // outside the root tsconfig the parser uses (parserOptions.project). + 'e2e-tests/**', ], extends: [ 'eslint:recommended', @@ -33,16 +32,31 @@ module.exports = { overrides: [ { files: [ - '**/e2e-tests/utils/**/*.ts', '*.test.js', '*.test.ts', '**/__tests__/**/*.ts', '**/__tests__/**/*.js', + '**/__mocks__/**/*.ts', ], - plugins: ['jest'], - extends: ['plugin:jest/recommended', 'plugin:jest/style'], - env: { - 'jest/globals': true, + globals: { + // vitest test APIs (test.globals: true) ... + describe: 'readonly', + it: 'readonly', + test: 'readonly', + expect: 'readonly', + suite: 'readonly', + beforeEach: 'readonly', + afterEach: 'readonly', + beforeAll: 'readonly', + afterAll: 'readonly', + vi: 'readonly', + vitest: 'readonly', + // ... and the ambient mock helper types from types/vitest-global-types.d.ts + Mock: 'readonly', + Mocked: 'readonly', + MockInstance: 'readonly', + MockedFunction: 'readonly', + MockedClass: 'readonly', }, rules: { '@typescript-eslint/unbound-method': 'off', @@ -55,11 +69,6 @@ module.exports = { }, }, ], - settings: { - jest: { - version: jestPackageJson.version, - }, - }, globals: { NodeJS: true, }, diff --git a/__mocks__/@posthog/warlock.ts b/__mocks__/@posthog/warlock.ts index 1b7b0686..9424cc50 100644 --- a/__mocks__/@posthog/warlock.ts +++ b/__mocks__/@posthog/warlock.ts @@ -1,9 +1,9 @@ // Mock for @posthog/warlock // // The real package is ESM-only and loads a YARA-X WASM binary at runtime, which -// jest cannot transform/execute. Unit tests exercise the wizard's own decision +// the unit test runner can't execute. Unit tests exercise the wizard's own decision // logic (scan_context filtering, severity/action mapping, triage handling, -// fail-closed behavior) against these jest.fn()s — never the real engine. Rule +// fail-closed behavior) against these vi.fn()s — never the real engine. Rule // matching itself is tested in the warlock repo. import type * as RealWarlock from '@posthog/warlock'; @@ -62,13 +62,13 @@ export const CATEGORIES = [ ] as const; // Default: nothing matches. Tests override per-case with mockResolvedValueOnce. -export const scan = jest.fn( +export const scan = vi.fn( (_content: string): Promise => Promise.resolve({ matched: false }), ); // Default: pass matches through as true positives (mirrors warlock's fail-safe). -export const triageMatches = jest.fn( +export const triageMatches = vi.fn( ( _content: string, matches: ScanMatch[], @@ -86,8 +86,8 @@ export const triageMatches = jest.fn( // ─── Compile-time drift guard ──────────────────────────────────── // If the real package's exports change shape, this assignment stops // type-checking and `pnpm test` fails — so the mock can't silently diverge -// from the engine the wizard actually ships with. It works because jest's -// moduleNameMapper is runtime-only: the `import type` above resolves to the +// from the engine the wizard actually ships with. It works because vitest's +// resolve.alias is runtime-only: the `import type` above resolves to the // REAL package under TypeScript and is fully erased at runtime. // (This guard caught warlock 0.2.2 adding the required `matchedStrings` // field to ScanMatch.) diff --git a/e2e-tests/tsconfig.json b/e2e-tests/tsconfig.json index 48610a17..53f073ac 100644 --- a/e2e-tests/tsconfig.json +++ b/e2e-tests/tsconfig.json @@ -1,3 +1,12 @@ { + // e2e-tests is a standalone, jest-based package (run via `jest -c + // jest.config.ts`). The root tsconfig moved the unit suite to vitest and + // dropped jest from its `types`/scope, so this config restores jest's ambient + // types and re-includes the e2e sources for ts-jest. "extends": "../tsconfig.json", + "compilerOptions": { + "types": ["node", "jest"] + }, + "include": ["**/*.ts"], + "exclude": ["node_modules", "test-applications/**/*"] } diff --git a/package.json b/package.json index 069117c2..3c65e2b3 100644 --- a/package.json +++ b/package.json @@ -76,6 +76,7 @@ "@types/yargs": "^16.0.9", "@typescript-eslint/eslint-plugin": "^5.13.0", "@typescript-eslint/parser": "^5.13.0", + "@vitest/coverage-v8": "^3.2.4", "babel-jest": "^29.7.0", "dotenv": "^16.4.7", "eslint": "^8.18.0", @@ -91,7 +92,8 @@ "ts-node": "^10.9.1", "tsdown": "^0.21.9", "tsx": "^4.20.3", - "typescript": "^5.0.4" + "typescript": "^5.0.4", + "vitest": "^3.2.4" }, "engines": { "node": "^20.20.0 || >=22.22.0", @@ -113,61 +115,15 @@ "fix": "pnpm fix:eslint && pnpm fix:prettier", "fix:prettier": "prettier --write \"{lib,src,test}/**/*.ts\"", "fix:eslint": "eslint . --cache --format stylish --fix", - "test": "pnpm build && jest", + "test": "pnpm build && vitest run", + "test:coverage": "pnpm build && vitest run --coverage", "test:e2e": "pnpm build && ./e2e-tests/run.sh", "test:e2e-record": "export RECORD_FIXTURES=true && pnpm build && ./e2e-tests/run.sh", "try": "tsx bin.ts", "dev": "pnpm build && pnpm link --global && pnpm build:watch", - "test:watch": "jest --watch", + "test:watch": "vitest", "prepare": "husky" }, - "jest": { - "collectCoverage": true, - "coveragePathIgnorePatterns": [ - "dist" - ], - "transform": { - "^.+\\.tsx?$": "ts-jest", - "node_modules/.+\\.js$": "babel-jest" - }, - "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$", - "moduleFileExtensions": [ - "ts", - "tsx", - "js", - "jsx", - "json" - ], - "modulePathIgnorePatterns": [ - "/dist/", - "/.claude/worktrees/" - ], - "testPathIgnorePatterns": [ - "/dist/", - "/node_modules/", - "\\.d\\.(jsx?|tsx?)$", - "\\.no-jest\\.(jsx?|tsx?)$", - "/e2e-tests/" - ], - "testEnvironment": "node", - "transformIgnorePatterns": [ - "node_modules/(?!(.pnpm/nanostores|nanostores))" - ], - "moduleNameMapper": { - "^@anthropic-ai/claude-agent-sdk$": "/__mocks__/@anthropic-ai/claude-agent-sdk.ts", - "^@posthog/warlock$": "/__mocks__/@posthog/warlock.ts", - "^ink$": "/__mocks__/ink.ts", - "^@env$": "/src/env.ts", - "^@lib/(.*)$": "/src/lib/$1", - "^@utils/(.*)$": "/src/utils/$1", - "^@ui$": "/src/ui/index.ts", - "^@ui/(.*)$": "/src/ui/$1", - "^@steps$": "/src/steps/index.ts", - "^@steps/(.*)$": "/src/steps/$1", - "^@frameworks/(.*)$": "/src/frameworks/$1", - "^(\\.{1,2}/.*)\\.js$": "$1" - } - }, "lint-staged": { ".claude/settings.json": "sh -c 'printf \"\\n\\033[31mDo not commit .claude/settings.json — use .claude/settings.local.json (gitignored).\\nUnstage with: git restore --staged .claude/settings.json\\033[0m\\n\\n\" >&2 && exit 1'", "*.{js,ts,tsx,jsx}": [ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 71360c10..bf1c07b1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -135,6 +135,9 @@ importers: '@typescript-eslint/parser': specifier: ^5.13.0 version: 5.62.0(eslint@8.57.1)(typescript@5.7.3) + '@vitest/coverage-v8': + specifier: ^3.2.4 + version: 3.2.6(vitest@3.2.6(@types/node@18.19.76)(msw@2.10.4(@types/node@18.19.76)(typescript@5.7.3))(tsx@4.20.3)(yaml@2.7.1)) babel-jest: specifier: ^29.7.0 version: 29.7.0(@babel/core@7.29.0) @@ -183,6 +186,9 @@ importers: typescript: specifier: ^5.0.4 version: 5.7.3 + vitest: + specifier: ^3.2.4 + version: 3.2.6(@types/node@18.19.76)(msw@2.10.4(@types/node@18.19.76)(typescript@5.7.3))(tsx@4.20.3)(yaml@2.7.1) packages: @@ -190,6 +196,10 @@ packages: resolution: {integrity: sha512-3NX/MpTdroi0aKz134A6RC2Gb2iXVECN4QaAXnvCIxxIm3C3AVB1mkUe8NaaiyvOpDfsrqWhYtj+Q6a62RrTsw==} engines: {node: '>=18'} + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + '@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.146': resolution: {integrity: sha512-0IIvlEaenq2CRSVx5Bo5BaCtHQXS87GancM35WKEYveGVLn6DI+5G7ikYuTE4AKRPkMnogFtY4BJt6LulWGj+A==} cpu: [arm64] @@ -898,6 +908,10 @@ packages: '@bcoe/v8-coverage@0.2.3': resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + '@bcoe/v8-coverage@1.0.2': + resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} + engines: {node: '>=18'} + '@bundled-es-modules/cookie@2.0.1': resolution: {integrity: sha512-8o+5fRPLNbjbdGRRmJj3h6Hh1AQJf2dk3qQ/5ZFb+PXkRNiSoMGGUKlsgLfrxneb72axVJyIYji64E2+nNfYyw==} @@ -929,156 +943,312 @@ packages: cpu: [ppc64] os: [aix] + '@esbuild/aix-ppc64@0.27.7': + resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/android-arm64@0.25.6': resolution: {integrity: sha512-hd5zdUarsK6strW+3Wxi5qWws+rJhCCbMiC9QZyzoxfk5uHRIE8T287giQxzVpEvCwuJ9Qjg6bEjcRJcgfLqoA==} engines: {node: '>=18'} cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.27.7': + resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm@0.25.6': resolution: {integrity: sha512-S8ToEOVfg++AU/bHwdksHNnyLyVM+eMVAOf6yRKFitnwnbwwPNqKr3srzFRe7nzV69RQKb5DgchIX5pt3L53xg==} engines: {node: '>=18'} cpu: [arm] os: [android] + '@esbuild/android-arm@0.27.7': + resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-x64@0.25.6': resolution: {integrity: sha512-0Z7KpHSr3VBIO9A/1wcT3NTy7EB4oNC4upJ5ye3R7taCc2GUdeynSLArnon5G8scPwaU866d3H4BCrE5xLW25A==} engines: {node: '>=18'} cpu: [x64] os: [android] + '@esbuild/android-x64@0.27.7': + resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/darwin-arm64@0.25.6': resolution: {integrity: sha512-FFCssz3XBavjxcFxKsGy2DYK5VSvJqa6y5HXljKzhRZ87LvEi13brPrf/wdyl/BbpbMKJNOr1Sd0jtW4Ge1pAA==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.27.7': + resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-x64@0.25.6': resolution: {integrity: sha512-GfXs5kry/TkGM2vKqK2oyiLFygJRqKVhawu3+DOCk7OxLy/6jYkWXhlHwOoTb0WqGnWGAS7sooxbZowy+pK9Yg==} engines: {node: '>=18'} cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.27.7': + resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/freebsd-arm64@0.25.6': resolution: {integrity: sha512-aoLF2c3OvDn2XDTRvn8hN6DRzVVpDlj2B/F66clWd/FHLiHaG3aVZjxQX2DYphA5y/evbdGvC6Us13tvyt4pWg==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.27.7': + resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-x64@0.25.6': resolution: {integrity: sha512-2SkqTjTSo2dYi/jzFbU9Plt1vk0+nNg8YC8rOXXea+iA3hfNJWebKYPs3xnOUf9+ZWhKAaxnQNUf2X9LOpeiMQ==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.27.7': + resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/linux-arm64@0.25.6': resolution: {integrity: sha512-b967hU0gqKd9Drsh/UuAm21Khpoh6mPBSgz8mKRq4P5mVK8bpA+hQzmm/ZwGVULSNBzKdZPQBRT3+WuVavcWsQ==} engines: {node: '>=18'} cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.27.7': + resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm@0.25.6': resolution: {integrity: sha512-SZHQlzvqv4Du5PrKE2faN0qlbsaW/3QQfUUc6yO2EjFcA83xnwm91UbEEVx4ApZ9Z5oG8Bxz4qPE+HFwtVcfyw==} engines: {node: '>=18'} cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.27.7': + resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-ia32@0.25.6': resolution: {integrity: sha512-aHWdQ2AAltRkLPOsKdi3xv0mZ8fUGPdlKEjIEhxCPm5yKEThcUjHpWB1idN74lfXGnZ5SULQSgtr5Qos5B0bPw==} engines: {node: '>=18'} cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.27.7': + resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-loong64@0.25.6': resolution: {integrity: sha512-VgKCsHdXRSQ7E1+QXGdRPlQ/e08bN6WMQb27/TMfV+vPjjTImuT9PmLXupRlC90S1JeNNW5lzkAEO/McKeJ2yg==} engines: {node: '>=18'} cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.27.7': + resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-mips64el@0.25.6': resolution: {integrity: sha512-WViNlpivRKT9/py3kCmkHnn44GkGXVdXfdc4drNmRl15zVQ2+D2uFwdlGh6IuK5AAnGTo2qPB1Djppj+t78rzw==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.27.7': + resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-ppc64@0.25.6': resolution: {integrity: sha512-wyYKZ9NTdmAMb5730I38lBqVu6cKl4ZfYXIs31Baf8aoOtB4xSGi3THmDYt4BTFHk7/EcVixkOV2uZfwU3Q2Jw==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.27.7': + resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-riscv64@0.25.6': resolution: {integrity: sha512-KZh7bAGGcrinEj4qzilJ4hqTY3Dg2U82c8bv+e1xqNqZCrCyc+TL9AUEn5WGKDzm3CfC5RODE/qc96OcbIe33w==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.27.7': + resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-s390x@0.25.6': resolution: {integrity: sha512-9N1LsTwAuE9oj6lHMyyAM+ucxGiVnEqUdp4v7IaMmrwb06ZTEVCIs3oPPplVsnjPfyjmxwHxHMF8b6vzUVAUGw==} engines: {node: '>=18'} cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.27.7': + resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-x64@0.25.6': resolution: {integrity: sha512-A6bJB41b4lKFWRKNrWoP2LHsjVzNiaurf7wyj/XtFNTsnPuxwEBWHLty+ZE0dWBKuSK1fvKgrKaNjBS7qbFKig==} engines: {node: '>=18'} cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.27.7': + resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + '@esbuild/netbsd-arm64@0.25.6': resolution: {integrity: sha512-IjA+DcwoVpjEvyxZddDqBY+uJ2Snc6duLpjmkXm/v4xuS3H+3FkLZlDm9ZsAbF9rsfP3zeA0/ArNDORZgrxR/Q==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] + '@esbuild/netbsd-arm64@0.27.7': + resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + '@esbuild/netbsd-x64@0.25.6': resolution: {integrity: sha512-dUXuZr5WenIDlMHdMkvDc1FAu4xdWixTCRgP7RQLBOkkGgwuuzaGSYcOpW4jFxzpzL1ejb8yF620UxAqnBrR9g==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.27.7': + resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + '@esbuild/openbsd-arm64@0.25.6': resolution: {integrity: sha512-l8ZCvXP0tbTJ3iaqdNf3pjaOSd5ex/e6/omLIQCVBLmHTlfXW3zAxQ4fnDmPLOB1x9xrcSi/xtCWFwCZRIaEwg==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] + '@esbuild/openbsd-arm64@0.27.7': + resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-x64@0.25.6': resolution: {integrity: sha512-hKrmDa0aOFOr71KQ/19JC7az1P0GWtCN1t2ahYAf4O007DHZt/dW8ym5+CUdJhQ/qkZmI1HAF8KkJbEFtCL7gw==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.27.7': + resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + '@esbuild/openharmony-arm64@0.25.6': resolution: {integrity: sha512-+SqBcAWoB1fYKmpWoQP4pGtx+pUUC//RNYhFdbcSA16617cchuryuhOCRpPsjCblKukAckWsV+aQ3UKT/RMPcA==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] + '@esbuild/openharmony-arm64@0.27.7': + resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + '@esbuild/sunos-x64@0.25.6': resolution: {integrity: sha512-dyCGxv1/Br7MiSC42qinGL8KkG4kX0pEsdb0+TKhmJZgCUDBGmyo1/ArCjNGiOLiIAgdbWgmWgib4HoCi5t7kA==} engines: {node: '>=18'} cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.27.7': + resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/win32-arm64@0.25.6': resolution: {integrity: sha512-42QOgcZeZOvXfsCBJF5Afw73t4veOId//XD3i+/9gSkhSV6Gk3VPlWncctI+JcOyERv85FUo7RxuxGy+z8A43Q==} engines: {node: '>=18'} cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.27.7': + resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-ia32@0.25.6': resolution: {integrity: sha512-4AWhgXmDuYN7rJI6ORB+uU9DHLq/erBbuMoAuB4VWJTu5KtCgcKYPynF0YI1VkBNuEfjNlLrFr9KZPJzrtLkrQ==} engines: {node: '>=18'} cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.27.7': + resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-x64@0.25.6': resolution: {integrity: sha512-NgJPHHbEpLQgDH2MjQu90pzW/5vvXIZ7KOnPyNBm92A6WgZ/7b6fJyUBjoumLqeOQQGqY2QjQxRo97ah4Sj0cA==} engines: {node: '>=18'} cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.27.7': + resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@eslint-community/eslint-utils@4.4.1': resolution: {integrity: sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -1153,6 +1323,10 @@ packages: '@types/node': optional: true + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + '@istanbuljs/load-nyc-config@1.1.0': resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} engines: {node: '>=8'} @@ -1248,6 +1422,9 @@ packages: '@jridgewell/sourcemap-codec@1.5.0': resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} @@ -1305,6 +1482,10 @@ packages: '@oxc-project/types@0.126.0': resolution: {integrity: sha512-oGfVtjAgwQVVpfBrbtk4e1XDyWHRFta6BS3GWVzrF8xYBT2VGQAk39yJS/wFSMrZqoiCU4oghT3Ch0HaHGIHcQ==} + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + '@posthog/core@1.23.1': resolution: {integrity: sha512-GViD5mOv/mcbZcyzz3z9CS0R79JzxVaqEz4sP5Dsea178M/j3ZWe6gaHDZB9yuyGfcmIMQ/8K14yv+7QrK4sQQ==} @@ -1413,6 +1594,144 @@ packages: '@rolldown/pluginutils@1.0.0-rc.16': resolution: {integrity: sha512-45+YtqxLYKDWQouLKCrpIZhke+nXxhsw+qAHVzHDVwttyBlHNBVs2K25rDXrZzhpTp9w1FlAlvweV1H++fdZoA==} + '@rollup/rollup-android-arm-eabi@4.61.1': + resolution: {integrity: sha512-JnBB8MdXj45cajvTuO5FmPlvFVJRQgvrz1uSEl3NwqFnReAPGwb8EanbGi4z2nRaqLzjJSv5/JmycoTKlRZxHA==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.61.1': + resolution: {integrity: sha512-Jx2g7iSjw4AOT0HDPHM9RV3GNjRXwybWtSFZiZAYUTjUwjVrYIwq3kBf+LnhqJlzXFAqTAh2F7IGI+O568exPw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.61.1': + resolution: {integrity: sha512-0F1L/Z3Eqv8mT2n3dCpeO8GcTvHvVqkP5/t6DMsn0KzhYVcg+s7Ncl5DS8qjKYEeio6Az0Gt6nyBORay5qIlCA==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.61.1': + resolution: {integrity: sha512-qLttcH871ujY4YcVfUSShhOw+CsoTatYz8gRbHO7Bb92QH059/P0y5do1KMs41fY0BpD2x4AJH/gID0zFiqVKQ==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.61.1': + resolution: {integrity: sha512-fUI4RapGE0Oh3mb8mgfvC1O2nU1RpDZUKnDQm3xB1Ipg7C2wTs5Kstz7G2uWK99a8S2yTMq8/P4uycwNa0nJyw==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.61.1': + resolution: {integrity: sha512-H5YrdvJaDtI/U9/emrD4b++xkvp3y/JvOe4rizHbxvkyMfRS/CiRYdji+Pl8D0brEaNFWUh1drQxgAGIl6Xudw==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.61.1': + resolution: {integrity: sha512-Q8CBCCQtDFrYtXoeUXSrnFXKOnyUhx6bz+SkL6A0E7V8kAiCJ5pamq1WtbfpVGhR5TSpXY6ak3avmDc5fHTyJA==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.61.1': + resolution: {integrity: sha512-nwnhk1581l0FBVellGcVCAT0Oi06onEA3WB53sf01VO3I0UPBkMH9sXONYME2K0ovXcNayJfNtHfm6mpJElatQ==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.61.1': + resolution: {integrity: sha512-x5Xr49hwt3hdW75UOZm3395YwwzPyauktslv29KpWL/T+vVAzoT3azLcTWv0eMciBNrx+DYjH4paehHoLpPvpg==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.61.1': + resolution: {integrity: sha512-unMS3H73DpaoPyyEVPjGKleM/s0mkmsauTENpw4INQY8y4+IuLNjkueQ5QCtC0D3N38Y38yhAU8OoZ20S2Tm6w==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.61.1': + resolution: {integrity: sha512-zNZzGRnAhwjFEYmvphJRV5XaQGjs62cCmeYYHUT//NbvEnHauw+I85nGG+SiVg5ld4GX8D1IbKIX+ozITQnhMQ==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.61.1': + resolution: {integrity: sha512-LdpWGL8X209B2SIvWjqlc8VZgM6PKfontSerGepuldQmHYrAOtnMCXeJkxXGbC+PPZVOuu5czJo7fNV6aeW8rQ==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.61.1': + resolution: {integrity: sha512-EC5kTtNaNGOmbMGqar8dvJy6y/hg99GAwjfBz++pxZhQATXGcRjd6c5en5wcbru0vkRmiMGsQKdMJOOf6sza4g==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.61.1': + resolution: {integrity: sha512-8hiwp6D4acEcNK78I4rP0/XtS1sknWIAMJBPdR4l6zUtyTm5KiTDr5bXmWt4foY7nAN7AThDHgkLIEZOWKbzWw==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.61.1': + resolution: {integrity: sha512-10dh/h/BqA7DuMPWSxkR8uks18FRwnwOEqr5zOTEl+NOwP/OMzKX8OFR/Of9xxDA7D5qef1Nzar5WDD2kCCr1g==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.61.1': + resolution: {integrity: sha512-YKJ5lg35DP17gcAOggnihe+APw9HLyj1Xn7gsmGumBJAUDa6NGXNixJzmkWLhcK9TOuuyQjdamzvJefkO7qHZQ==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.61.1': + resolution: {integrity: sha512-Mlil5G2Jj6a7B3LWGctg+XPL9vdXYuzCtNXfxOQ0nPjc2m6ueUktocPGH9bnAM0bNRKb/bAWTujUU7IJQdQA+g==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.61.1': + resolution: {integrity: sha512-bVWIOIk6pV01p4CdUbPP7CJ/434z+OooYjDuFcR+44N35YvKUC66G8MGnvcWx5mWKW3g61J+t74l3Kj15Kwn2Q==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.61.1': + resolution: {integrity: sha512-qy5pBvZbqNFheBz61R1rzsezjm0J7O2oNGoWtGoY89SZYLUfxAJTBAqDChqAIdB4rCiIbi9nF7yZ83GnNiLwSw==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.61.1': + resolution: {integrity: sha512-E83TXjI4zm0+5f2qO+UOudaCYIhYwpJ5jq6YCZNIZ+6CbfhKrkAGezeiASBL9ElxAxFsRS9ZhESv8mfnj6TKeg==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.61.1': + resolution: {integrity: sha512-fbWnKqVkjrJN38vNe3ahkbk6iejS/3b0Nt7EEtPpE6RBacZcGXNKbzfHN3GUUlXOPghUg0j6XUGrtjX9z1sIvA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.61.1': + resolution: {integrity: sha512-ArMl38iVAbk0New1ogihQNY6iphLi4ZaRsa037gUzv5yeKPY8TD3Dmy4x2RNC1VztU/uqm+G+/RwFrSka3Oy2g==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.61.1': + resolution: {integrity: sha512-0mYtjHS9ucAbcATycCNK9IGBk/cCe/ma7EmSLGZdsxnOA8cjRIyU04wDpVAD9NiOfLUR9KTxdiO53uOkherqjQ==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.61.1': + resolution: {integrity: sha512-gK1iCEPfpoSG9wfBihXxvBMi8ZfcWffYkEsC/Eih+iFENTaewvNcrEQ69lIOWYO5pePHKLHHO7nq5AILGO/HQQ==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.61.1': + resolution: {integrity: sha512-X+zaP2x+j4RXGfbp/seSoRHWnPxzApilDszisZxbYH5C/jTxFhCtDNdPGZb9lJyYPs24wGxruPF7Y+sIXt9Gzw==} + cpu: [x64] + os: [win32] + '@sinclair/typebox@0.27.8': resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} @@ -1452,12 +1771,21 @@ packages: '@types/chai@4.3.20': resolution: {integrity: sha512-/pC9HAB5I/xMlc5FP77qjCnI16ChlJfW0tGa0IUcFn38VJrTV6DeZ60NU5KZBtaOZqjdpwTWohz5HU1RrhiYxQ==} + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + '@types/cookie@0.6.0': resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + '@types/glob@7.2.0': resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==} @@ -1642,6 +1970,44 @@ packages: '@virustotal/yara-x@1.15.0': resolution: {integrity: sha512-tR+Ue5ci9bURbD7/qjXY0VLVXDUP+NK/uvvgjX6UvSXN+3msfMtpAMaiCdsltFEciU+tXaZeGbHu3N07bvfjsw==} + '@vitest/coverage-v8@3.2.6': + resolution: {integrity: sha512-LsAdmUapA0qSN306d8+zOyawM0hFm2m2Hg9IwVNIKBm+qJV8cijiq2c+gxKZcB1HCfIWAy+0qEZDCUQA58A1cw==} + peerDependencies: + '@vitest/browser': 3.2.6 + vitest: 3.2.6 + peerDependenciesMeta: + '@vitest/browser': + optional: true + + '@vitest/expect@3.2.6': + resolution: {integrity: sha512-1+7q9BtaKzEmO+fmNT3kYvoNn5Y71XWAx2Q5HRim4tTVRQVRv4uJFAQ5FbK0OPUeNP/WmVCpxYxoJdvuHVjzBQ==} + + '@vitest/mocker@3.2.6': + resolution: {integrity: sha512-EZOrpDbkKotFAP7wPAQV1UIyoGOk4oX7ynWhBhLB7v+meMHbQhU16oPpIYGTTe4oFlhpryGpgpcZP/sin3hYuw==} + 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.6': + resolution: {integrity: sha512-lb7XXXzmm2h2ASzFnRvQpDo6onT1NmMJA3tkGTWiBFtRJ9lxGY3d3mm/Apt36gej2bkkOVLL/yTOtufDaFa/jA==} + + '@vitest/runner@3.2.6': + resolution: {integrity: sha512-HYcoSj1w5tcgUnzoF0HcyaAQjpA1gj9ftUJ7iSJSuipc02jW9gKkigwZbjFldAfYHA1fa8UZVRftdMY5msWM9Q==} + + '@vitest/snapshot@3.2.6': + resolution: {integrity: sha512-H+ZjNTWGpObenh0YnlBctAPnJSI20P81PL8BPzWpx54YXLLTm8hEsWawtcYLMrwvpK48hGxLLbCS+1KRXhsKhw==} + + '@vitest/spy@3.2.6': + resolution: {integrity: sha512-oq6BbH68WzcWmwtBrU9nqLeaXTR4XwJF7FSLkKEZo4i6eoXcrxjcwSuTvWBIRUTC6VC72nXYunzqgZA+IKdtxg==} + + '@vitest/utils@3.2.6': + resolution: {integrity: sha512-lI23nIs4bnT3T8NIoh+vFaz5s2/DdP0Jgt2jxwgWljvwn82cLJtyi/If+fjFyoLMGIOz0U/fKvWE0d4jsNQEfg==} + '@xmldom/xmldom@0.8.10': resolution: {integrity: sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==} engines: {node: '>=10.0.0'} @@ -1755,6 +2121,10 @@ packages: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + ast-kit@3.0.0-beta.1: resolution: {integrity: sha512-trmleAnZ2PxN/loHWVhhx1qeOHSRXq4TDsBBxq3GqeJitfk3+jTQ+v/C1km/KYq9M7wKqCewMh+/NAvVH7m+bw==} engines: {node: '>=20.19.0'} @@ -1763,6 +2133,9 @@ packages: resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==} engines: {node: '>=4'} + ast-v8-to-istanbul@0.3.12: + resolution: {integrity: sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==} + async@3.2.6: resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} @@ -1819,6 +2192,10 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} @@ -1851,6 +2228,13 @@ packages: brace-expansion@2.0.1: resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + brace-expansion@2.1.1: + resolution: {integrity: sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==} + + brace-expansion@5.0.6: + resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} + engines: {node: 18 || 20 || >=22} + braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} @@ -1879,6 +2263,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'} + cac@7.0.0: resolution: {integrity: sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ==} engines: {node: '>=20.19.0'} @@ -1913,6 +2301,10 @@ packages: caniuse-lite@1.0.30001776: resolution: {integrity: sha512-sg01JDPzZ9jGshqKSckOQthXnYwOEP50jeVFhaSFbZcOy05TiuuaffDOfcwtCisJ9kNQuLBFibYywv2Bgm9osw==} + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + chalk@2.4.2: resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} engines: {node: '>=4'} @@ -1936,6 +2328,10 @@ packages: chardet@0.7.0: resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} + ci-info@3.9.0: resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} engines: {node: '>=8'} @@ -2101,6 +2497,10 @@ packages: babel-plugin-macros: optional: true + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -2156,6 +2556,9 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} @@ -2180,6 +2583,9 @@ packages: emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + empathic@2.0.0: resolution: {integrity: sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==} engines: {node: '>=14'} @@ -2203,6 +2609,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'} @@ -2219,6 +2628,11 @@ packages: engines: {node: '>=18'} hasBin: true + esbuild@0.27.7: + resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -2337,6 +2751,10 @@ packages: resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} engines: {node: '>= 0.8.0'} + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + expect@29.7.0: resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -2433,6 +2851,10 @@ packages: debug: optional: true + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + form-data@4.0.2: resolution: {integrity: sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==} engines: {node: '>= 6'} @@ -2506,6 +2928,11 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} + glob@10.5.0: + resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + hasBin: true + glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} deprecated: Glob versions prior to v9 are no longer supported @@ -2748,10 +3175,17 @@ packages: resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} engines: {node: '>=10'} + istanbul-lib-source-maps@5.0.6: + resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==} + engines: {node: '>=10'} + istanbul-reports@3.1.7: resolution: {integrity: sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==} engines: {node: '>=8'} + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + jake@10.9.2: resolution: {integrity: sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==} engines: {node: '>=10'} @@ -2892,9 +3326,15 @@ packages: js-tiktoken@1.0.19: resolution: {integrity: sha512-XC63YQeEcS47Y53gg950xiZ4IWmkfMe4p2V9OSaBt26q+p47WHn18izuXzSclCI73B7yGqtfRsT6jcZQI0y08g==} + js-tokens@10.0.0: + resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + js-yaml@3.14.1: resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} hasBin: true @@ -3001,15 +3441,24 @@ packages: resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} engines: {node: '>=18'} + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + magicast@0.2.11: resolution: {integrity: sha512-6saXbRDA1HMkqbsvHOU6HBjCVgZT460qheRkLhJQHWAbhXoWESI3Kn/dGGXyKs15FFKR85jsUqFx2sMK0wy/5g==} + magicast@0.3.5: + resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==} + make-dir@4.0.0: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} engines: {node: '>=10'} @@ -3075,6 +3524,10 @@ packages: resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} engines: {node: '>=18'} + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -3086,6 +3539,10 @@ packages: resolution: {integrity: sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA==} engines: {node: '>=16 || 14 >=14.17'} + minimatch@9.0.9: + resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} + engines: {node: '>=16 || 14 >=14.17'} + minipass@4.2.8: resolution: {integrity: sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==} engines: {node: '>=8'} @@ -3118,6 +3575,11 @@ packages: resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} engines: {node: ^18.17.0 || >=20.5.0} + nanoid@3.3.12: + resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + nanostores@1.1.1: resolution: {integrity: sha512-EYJqS25r2iBeTtGQCHidXl1VfZ1jXM7Q04zXJOrMlxVVmD0ptxJaNux92n1mJ7c5lN3zTq12MhH/8x59nP+qmg==} engines: {node: ^20.0.0 || >=22.0.0} @@ -3250,6 +3712,9 @@ packages: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -3302,6 +3767,10 @@ packages: 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==} @@ -3334,6 +3803,10 @@ packages: resolution: {integrity: sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==} engines: {node: '>=10.4.0'} + postcss@8.5.15: + resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} + engines: {node: ^10 || ^12 || >=14} + posthog-node@5.24.17: resolution: {integrity: sha512-mdb8TKt+YCRbGQdYar3AKNUPCyEiqcprScF4unYpGALF6HlBaEuO6wPuIqXXpCWkw4VclJYCKbb6lq6pH6bJeA==} engines: {node: ^20.20.0 || >=22.22.0} @@ -3523,6 +3996,11 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} hasBin: true + rollup@4.61.1: + resolution: {integrity: sha512-I4KW6iuRpuu2uHBLraZ1wNZe0DP7lnRha+VJ9tNaYVaVgKhW0aI3h4RYnoRPeql0flHm/Co55b7snEDcOfOJrA==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + router@2.2.0: resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} engines: {node: '>= 18'} @@ -3596,6 +4074,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==} @@ -3628,6 +4109,10 @@ packages: resolution: {integrity: sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==} engines: {node: '>=20'} + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + source-map-support@0.5.13: resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} @@ -3642,10 +4127,16 @@ packages: resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} engines: {node: '>=10'} + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + statuses@2.0.2: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + stream-buffers@2.2.0: resolution: {integrity: sha512-uyQK/mx5QjHun80FLJTfaWE7JtwfRMKBLkMne6udYOmvH0CawotVa7TfgYHzAnpphn4+TweIx1QKMnRIbipmUg==} engines: {node: '>= 0.10.0'} @@ -3669,6 +4160,10 @@ packages: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + string-width@7.2.0: resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} engines: {node: '>=18'} @@ -3713,6 +4208,9 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + strip-literal@3.1.0: + resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + supports-color@5.5.0: resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} engines: {node: '>=4'} @@ -3741,6 +4239,10 @@ packages: resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} engines: {node: '>=8'} + test-exclude@7.0.2: + resolution: {integrity: sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==} + engines: {node: '>=18'} + text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} @@ -3750,6 +4252,12 @@ 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==} + tinyexec@1.1.1: resolution: {integrity: sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==} engines: {node: '>=18'} @@ -3758,6 +4266,18 @@ packages: 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'} + tmp@0.0.33: resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} engines: {node: '>=0.6.0'} @@ -3985,6 +4505,79 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} + 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@7.3.5: + resolution: {integrity: sha512-KuOaNhcnGFN2zIPGA7wRmzF+lJA1sea7rHq17aiJ++9lzY1WWG6Jpwqwe1KNbRVPIqHmr8GLYx7jbrQcN/7/ww==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@3.2.6: + resolution: {integrity: sha512-xejya+bT/j/+R/AGa1XOfRxLmNUlLtlwjRsFUILF+xHfzElmGcmFydy2gqqIrd62ptIEfwVMofd19uNWD9L7Nw==} + 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.6 + '@vitest/ui': 3.2.6 + 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 + walker@1.0.8: resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} @@ -3993,6 +4586,11 @@ packages: engines: {node: '>= 8'} hasBin: true + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + widest-line@6.0.0: resolution: {integrity: sha512-U89AsyEeAsyoF0zVJBkG9zBgekjgjK7yk9sje3F4IQpXBJ10TF6ByLlIfjMhcmHMJgHZI4KHt4rdNfktzxIAMA==} engines: {node: '>=20'} @@ -4009,6 +4607,10 @@ packages: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + wrap-ansi@9.0.0: resolution: {integrity: sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==} engines: {node: '>=18'} @@ -4107,6 +4709,11 @@ snapshots: ansi-styles: 6.2.1 is-fullwidth-code-point: 5.0.0 + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + '@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.146': optional: true @@ -4188,10 +4795,10 @@ snapshots: '@babel/generator@7.26.9': dependencies: - '@babel/parser': 7.26.9 - '@babel/types': 7.26.9 - '@jridgewell/gen-mapping': 0.3.8 - '@jridgewell/trace-mapping': 0.3.25 + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 jsesc: 3.1.0 '@babel/generator@7.29.1': @@ -4341,7 +4948,7 @@ snapshots: '@babel/parser@7.26.9': dependencies: - '@babel/types': 7.26.9 + '@babel/types': 7.29.0 '@babel/parser@7.29.0': dependencies: @@ -4912,8 +5519,8 @@ snapshots: '@babel/template@7.26.9': dependencies: '@babel/code-frame': 7.26.2 - '@babel/parser': 7.26.9 - '@babel/types': 7.26.9 + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 '@babel/template@7.28.6': dependencies: @@ -4929,7 +5536,7 @@ snapshots: '@babel/parser': 7.29.0 '@babel/template': 7.28.6 '@babel/types': 7.29.0 - debug: 4.4.0 + debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -4956,6 +5563,8 @@ snapshots: '@bcoe/v8-coverage@0.2.3': {} + '@bcoe/v8-coverage@1.0.2': {} + '@bundled-es-modules/cookie@2.0.1': dependencies: cookie: 0.7.2 @@ -4994,81 +5603,159 @@ snapshots: '@esbuild/aix-ppc64@0.25.6': optional: true + '@esbuild/aix-ppc64@0.27.7': + optional: true + '@esbuild/android-arm64@0.25.6': optional: true + '@esbuild/android-arm64@0.27.7': + optional: true + '@esbuild/android-arm@0.25.6': optional: true + '@esbuild/android-arm@0.27.7': + optional: true + '@esbuild/android-x64@0.25.6': optional: true + '@esbuild/android-x64@0.27.7': + optional: true + '@esbuild/darwin-arm64@0.25.6': optional: true + '@esbuild/darwin-arm64@0.27.7': + optional: true + '@esbuild/darwin-x64@0.25.6': optional: true + '@esbuild/darwin-x64@0.27.7': + optional: true + '@esbuild/freebsd-arm64@0.25.6': optional: true + '@esbuild/freebsd-arm64@0.27.7': + optional: true + '@esbuild/freebsd-x64@0.25.6': optional: true + '@esbuild/freebsd-x64@0.27.7': + optional: true + '@esbuild/linux-arm64@0.25.6': optional: true - '@esbuild/linux-arm@0.25.6': + '@esbuild/linux-arm64@0.27.7': + optional: true + + '@esbuild/linux-arm@0.25.6': + optional: true + + '@esbuild/linux-arm@0.27.7': optional: true '@esbuild/linux-ia32@0.25.6': optional: true + '@esbuild/linux-ia32@0.27.7': + optional: true + '@esbuild/linux-loong64@0.25.6': optional: true + '@esbuild/linux-loong64@0.27.7': + optional: true + '@esbuild/linux-mips64el@0.25.6': optional: true + '@esbuild/linux-mips64el@0.27.7': + optional: true + '@esbuild/linux-ppc64@0.25.6': optional: true + '@esbuild/linux-ppc64@0.27.7': + optional: true + '@esbuild/linux-riscv64@0.25.6': optional: true + '@esbuild/linux-riscv64@0.27.7': + optional: true + '@esbuild/linux-s390x@0.25.6': optional: true + '@esbuild/linux-s390x@0.27.7': + optional: true + '@esbuild/linux-x64@0.25.6': optional: true + '@esbuild/linux-x64@0.27.7': + optional: true + '@esbuild/netbsd-arm64@0.25.6': optional: true + '@esbuild/netbsd-arm64@0.27.7': + optional: true + '@esbuild/netbsd-x64@0.25.6': optional: true + '@esbuild/netbsd-x64@0.27.7': + optional: true + '@esbuild/openbsd-arm64@0.25.6': optional: true + '@esbuild/openbsd-arm64@0.27.7': + optional: true + '@esbuild/openbsd-x64@0.25.6': optional: true + '@esbuild/openbsd-x64@0.27.7': + optional: true + '@esbuild/openharmony-arm64@0.25.6': optional: true + '@esbuild/openharmony-arm64@0.27.7': + optional: true + '@esbuild/sunos-x64@0.25.6': optional: true + '@esbuild/sunos-x64@0.27.7': + optional: true + '@esbuild/win32-arm64@0.25.6': optional: true + '@esbuild/win32-arm64@0.27.7': + optional: true + '@esbuild/win32-ia32@0.25.6': optional: true + '@esbuild/win32-ia32@0.27.7': + optional: true + '@esbuild/win32-x64@0.25.6': optional: true + '@esbuild/win32-x64@0.27.7': + optional: true + '@eslint-community/eslint-utils@4.4.1(eslint@8.57.1)': dependencies: eslint: 8.57.1 @@ -5079,7 +5766,7 @@ snapshots: '@eslint/eslintrc@2.1.4': dependencies: ajv: 6.12.6 - debug: 4.4.0 + debug: 4.4.3 espree: 9.6.1 globals: 13.24.0 ignore: 5.3.2 @@ -5099,7 +5786,7 @@ snapshots: '@humanwhocodes/config-array@0.13.0': dependencies: '@humanwhocodes/object-schema': 2.0.3 - debug: 4.4.0 + debug: 4.4.3 minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -5142,6 +5829,15 @@ snapshots: optionalDependencies: '@types/node': 18.19.76 + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.2.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + '@istanbuljs/load-nyc-config@1.1.0': dependencies: camelcase: 5.3.1 @@ -5239,7 +5935,7 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/trace-mapping': 0.3.31 '@types/node': 18.19.76 chalk: 4.1.2 collect-v8-coverage: 1.0.2 @@ -5267,7 +5963,7 @@ snapshots: '@jest/source-map@29.6.3': dependencies: - '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/trace-mapping': 0.3.31 callsites: 3.1.0 graceful-fs: 4.2.11 @@ -5323,7 +6019,7 @@ snapshots: dependencies: '@jridgewell/set-array': 1.2.1 '@jridgewell/sourcemap-codec': 1.5.0 - '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/trace-mapping': 0.3.31 '@jridgewell/remapping@2.3.5': dependencies: @@ -5336,6 +6032,8 @@ snapshots: '@jridgewell/sourcemap-codec@1.5.0': {} + '@jridgewell/sourcemap-codec@1.5.5': {} + '@jridgewell/trace-mapping@0.3.25': dependencies: '@jridgewell/resolve-uri': 3.1.2 @@ -5431,6 +6129,9 @@ snapshots: '@oxc-project/types@0.126.0': {} + '@pkgjs/parseargs@0.11.0': + optional: true + '@posthog/core@1.23.1': dependencies: cross-spawn: 7.0.6 @@ -5494,6 +6195,81 @@ snapshots: '@rolldown/pluginutils@1.0.0-rc.16': {} + '@rollup/rollup-android-arm-eabi@4.61.1': + optional: true + + '@rollup/rollup-android-arm64@4.61.1': + optional: true + + '@rollup/rollup-darwin-arm64@4.61.1': + optional: true + + '@rollup/rollup-darwin-x64@4.61.1': + optional: true + + '@rollup/rollup-freebsd-arm64@4.61.1': + optional: true + + '@rollup/rollup-freebsd-x64@4.61.1': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.61.1': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.61.1': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.61.1': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.61.1': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.61.1': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.61.1': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.61.1': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.61.1': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.61.1': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.61.1': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.61.1': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.61.1': + optional: true + + '@rollup/rollup-linux-x64-musl@4.61.1': + optional: true + + '@rollup/rollup-openbsd-x64@4.61.1': + optional: true + + '@rollup/rollup-openharmony-arm64@4.61.1': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.61.1': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.61.1': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.61.1': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.61.1': + optional: true + '@sinclair/typebox@0.27.8': {} '@sinonjs/commons@3.0.1': @@ -5531,7 +6307,7 @@ snapshots: '@types/babel__template@7.4.4': dependencies: - '@babel/parser': 7.26.9 + '@babel/parser': 7.29.0 '@babel/types': 7.21.5 '@types/babel__traverse@7.20.6': @@ -5540,10 +6316,19 @@ snapshots: '@types/chai@4.3.20': {} + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + '@types/cookie@0.6.0': {} + '@types/deep-eql@4.0.2': {} + '@types/estree@1.0.8': {} + '@types/estree@1.0.9': {} + '@types/glob@7.2.0': dependencies: '@types/minimatch': 5.1.2 @@ -5734,7 +6519,7 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.7.3) '@typescript-eslint/utils': 5.62.0(eslint@8.57.1)(typescript@5.7.3) - debug: 4.4.0 + debug: 4.4.3 eslint: 8.57.1 tsutils: 3.21.0(typescript@5.7.3) optionalDependencies: @@ -5748,7 +6533,7 @@ snapshots: dependencies: '@typescript-eslint/types': 5.62.0 '@typescript-eslint/visitor-keys': 5.62.0 - debug: 4.4.0 + debug: 4.4.3 globby: 11.1.0 is-glob: 4.0.3 semver: 7.7.1 @@ -5782,6 +6567,68 @@ snapshots: '@virustotal/yara-x@1.15.0': {} + '@vitest/coverage-v8@3.2.6(vitest@3.2.6(@types/node@18.19.76)(msw@2.10.4(@types/node@18.19.76)(typescript@5.7.3))(tsx@4.20.3)(yaml@2.7.1))': + dependencies: + '@ampproject/remapping': 2.3.0 + '@bcoe/v8-coverage': 1.0.2 + ast-v8-to-istanbul: 0.3.12 + debug: 4.4.3 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 5.0.6 + istanbul-reports: 3.1.7 + magic-string: 0.30.21 + magicast: 0.3.5 + std-env: 3.10.0 + test-exclude: 7.0.2 + tinyrainbow: 2.0.0 + vitest: 3.2.6(@types/node@18.19.76)(msw@2.10.4(@types/node@18.19.76)(typescript@5.7.3))(tsx@4.20.3)(yaml@2.7.1) + transitivePeerDependencies: + - supports-color + + '@vitest/expect@3.2.6': + dependencies: + '@types/chai': 5.2.3 + '@vitest/spy': 3.2.6 + '@vitest/utils': 3.2.6 + chai: 5.3.3 + tinyrainbow: 2.0.0 + + '@vitest/mocker@3.2.6(msw@2.10.4(@types/node@18.19.76)(typescript@5.7.3))(vite@7.3.5(@types/node@18.19.76)(tsx@4.20.3)(yaml@2.7.1))': + dependencies: + '@vitest/spy': 3.2.6 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + msw: 2.10.4(@types/node@18.19.76)(typescript@5.7.3) + vite: 7.3.5(@types/node@18.19.76)(tsx@4.20.3)(yaml@2.7.1) + + '@vitest/pretty-format@3.2.6': + dependencies: + tinyrainbow: 2.0.0 + + '@vitest/runner@3.2.6': + dependencies: + '@vitest/utils': 3.2.6 + pathe: 2.0.3 + strip-literal: 3.1.0 + + '@vitest/snapshot@3.2.6': + dependencies: + '@vitest/pretty-format': 3.2.6 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@3.2.6': + dependencies: + tinyspy: 4.0.4 + + '@vitest/utils@3.2.6': + dependencies: + '@vitest/pretty-format': 3.2.6 + loupe: 3.2.1 + tinyrainbow: 2.0.0 + '@xmldom/xmldom@0.8.10': {} accepts@2.0.0: @@ -5872,6 +6719,8 @@ snapshots: array-union@2.1.0: {} + assertion-error@2.0.1: {} + ast-kit@3.0.0-beta.1: dependencies: '@babel/parser': 8.0.0-rc.3 @@ -5882,6 +6731,12 @@ snapshots: dependencies: tslib: 2.8.1 + ast-v8-to-istanbul@0.3.12: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + estree-walker: 3.0.3 + js-tokens: 10.0.0 + async@3.2.6: {} asynckit@0.4.0: {} @@ -5977,6 +6832,8 @@ snapshots: balanced-match@1.0.2: {} + balanced-match@4.0.4: {} + base64-js@1.5.1: {} baseline-browser-mapping@2.10.0: {} @@ -6016,6 +6873,14 @@ snapshots: dependencies: balanced-match: 1.0.2 + brace-expansion@2.1.1: + dependencies: + balanced-match: 1.0.2 + + brace-expansion@5.0.6: + dependencies: + balanced-match: 4.0.4 + braces@3.0.3: dependencies: fill-range: 7.1.1 @@ -6047,6 +6912,8 @@ snapshots: bytes@3.1.2: {} + cac@6.7.14: {} + cac@7.0.0: {} call-bind-apply-helpers@1.0.2: @@ -6071,6 +6938,14 @@ snapshots: caniuse-lite@1.0.30001776: {} + 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@2.4.2: dependencies: ansi-styles: 3.2.1 @@ -6090,6 +6965,8 @@ snapshots: chardet@0.7.0: {} + check-error@2.1.3: {} + ci-info@3.9.0: {} cjs-module-lexer@1.4.3: {} @@ -6228,6 +7105,8 @@ snapshots: dedent@1.5.3: {} + deep-eql@5.0.2: {} + deep-is@0.1.4: {} deepmerge@4.3.1: {} @@ -6262,6 +7141,8 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 + eastasianwidth@0.2.0: {} + ee-first@1.1.1: {} ejs@3.1.10: @@ -6278,6 +7159,8 @@ snapshots: emoji-regex@8.0.0: {} + emoji-regex@9.2.2: {} + empathic@2.0.0: {} encodeurl@2.0.0: {} @@ -6292,6 +7175,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 @@ -6334,6 +7219,35 @@ snapshots: '@esbuild/win32-ia32': 0.25.6 '@esbuild/win32-x64': 0.25.6 + esbuild@0.27.7: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.7 + '@esbuild/android-arm': 0.27.7 + '@esbuild/android-arm64': 0.27.7 + '@esbuild/android-x64': 0.27.7 + '@esbuild/darwin-arm64': 0.27.7 + '@esbuild/darwin-x64': 0.27.7 + '@esbuild/freebsd-arm64': 0.27.7 + '@esbuild/freebsd-x64': 0.27.7 + '@esbuild/linux-arm': 0.27.7 + '@esbuild/linux-arm64': 0.27.7 + '@esbuild/linux-ia32': 0.27.7 + '@esbuild/linux-loong64': 0.27.7 + '@esbuild/linux-mips64el': 0.27.7 + '@esbuild/linux-ppc64': 0.27.7 + '@esbuild/linux-riscv64': 0.27.7 + '@esbuild/linux-s390x': 0.27.7 + '@esbuild/linux-x64': 0.27.7 + '@esbuild/netbsd-arm64': 0.27.7 + '@esbuild/netbsd-x64': 0.27.7 + '@esbuild/openbsd-arm64': 0.27.7 + '@esbuild/openbsd-x64': 0.27.7 + '@esbuild/openharmony-arm64': 0.27.7 + '@esbuild/sunos-x64': 0.27.7 + '@esbuild/win32-arm64': 0.27.7 + '@esbuild/win32-ia32': 0.27.7 + '@esbuild/win32-x64': 0.27.7 + escalade@3.2.0: {} escape-html@1.0.3: {} @@ -6478,6 +7392,8 @@ snapshots: exit@0.1.2: {} + expect-type@1.3.0: {} + expect@29.7.0: dependencies: '@jest/expect-utils': 29.7.0 @@ -6609,6 +7525,11 @@ snapshots: follow-redirects@1.15.9: {} + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + form-data@4.0.2: dependencies: asynckit: 0.4.0 @@ -6675,6 +7596,15 @@ snapshots: dependencies: is-glob: 4.0.3 + glob@10.5.0: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.9 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + glob@7.2.3: dependencies: fs.realpath: 1.0.0 @@ -6888,7 +7818,7 @@ snapshots: istanbul-lib-instrument@5.2.1: dependencies: '@babel/core': 7.29.0 - '@babel/parser': 7.26.9 + '@babel/parser': 7.29.0 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.2 semver: 6.3.1 @@ -6898,10 +7828,10 @@ snapshots: istanbul-lib-instrument@6.0.3: dependencies: '@babel/core': 7.29.0 - '@babel/parser': 7.26.9 + '@babel/parser': 7.29.0 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.2 - semver: 7.7.1 + semver: 7.7.4 transitivePeerDependencies: - supports-color @@ -6919,11 +7849,25 @@ snapshots: transitivePeerDependencies: - supports-color + istanbul-lib-source-maps@5.0.6: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + debug: 4.4.3 + istanbul-lib-coverage: 3.2.2 + transitivePeerDependencies: + - supports-color + istanbul-reports@3.1.7: dependencies: html-escaper: 2.0.2 istanbul-lib-report: 3.0.1 + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + jake@10.9.2: dependencies: async: 3.2.6 @@ -7246,8 +8190,12 @@ snapshots: dependencies: base64-js: 1.5.1 + js-tokens@10.0.0: {} + js-tokens@4.0.0: {} + js-tokens@9.0.1: {} + js-yaml@3.14.1: dependencies: argparse: 1.0.10 @@ -7357,21 +8305,33 @@ snapshots: strip-ansi: 7.1.0 wrap-ansi: 9.0.0 + loupe@3.2.1: {} + lru-cache@10.4.3: {} lru-cache@5.1.1: dependencies: yallist: 3.1.1 + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + magicast@0.2.11: dependencies: '@babel/parser': 7.26.9 '@babel/types': 7.26.9 recast: 0.23.9 + magicast@0.3.5: + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + source-map-js: 1.2.1 + make-dir@4.0.0: dependencies: - semver: 7.7.1 + semver: 7.7.4 make-error@1.3.6: {} @@ -7414,6 +8374,10 @@ snapshots: mimic-function@5.0.1: {} + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.6 + minimatch@3.1.2: dependencies: brace-expansion: 1.1.11 @@ -7426,6 +8390,10 @@ snapshots: dependencies: brace-expansion: 2.0.1 + minimatch@9.0.9: + dependencies: + brace-expansion: 2.1.1 + minipass@4.2.8: {} minipass@7.1.2: {} @@ -7463,6 +8431,8 @@ snapshots: mute-stream@2.0.0: {} + nanoid@3.3.12: {} + nanostores@1.1.1: {} natural-compare-lite@1.4.0: {} @@ -7574,6 +8544,8 @@ snapshots: p-try@2.2.0: {} + package-json-from-dist@1.0.1: {} + parent-module@1.0.1: dependencies: callsites: 3.1.0 @@ -7612,6 +8584,8 @@ snapshots: pathe@2.0.3: {} + pathval@2.0.1: {} + picocolors@1.1.1: {} picomatch@2.3.1: {} @@ -7634,6 +8608,12 @@ snapshots: base64-js: 1.5.1 xmlbuilder: 15.1.1 + postcss@8.5.15: + dependencies: + nanoid: 3.3.12 + picocolors: 1.1.1 + source-map-js: 1.2.1 + posthog-node@5.24.17: dependencies: '@posthog/core': 1.23.1 @@ -7823,6 +8803,37 @@ snapshots: '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.16 '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.16 + rollup@4.61.1: + dependencies: + '@types/estree': 1.0.9 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.61.1 + '@rollup/rollup-android-arm64': 4.61.1 + '@rollup/rollup-darwin-arm64': 4.61.1 + '@rollup/rollup-darwin-x64': 4.61.1 + '@rollup/rollup-freebsd-arm64': 4.61.1 + '@rollup/rollup-freebsd-x64': 4.61.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.61.1 + '@rollup/rollup-linux-arm-musleabihf': 4.61.1 + '@rollup/rollup-linux-arm64-gnu': 4.61.1 + '@rollup/rollup-linux-arm64-musl': 4.61.1 + '@rollup/rollup-linux-loong64-gnu': 4.61.1 + '@rollup/rollup-linux-loong64-musl': 4.61.1 + '@rollup/rollup-linux-ppc64-gnu': 4.61.1 + '@rollup/rollup-linux-ppc64-musl': 4.61.1 + '@rollup/rollup-linux-riscv64-gnu': 4.61.1 + '@rollup/rollup-linux-riscv64-musl': 4.61.1 + '@rollup/rollup-linux-s390x-gnu': 4.61.1 + '@rollup/rollup-linux-x64-gnu': 4.61.1 + '@rollup/rollup-linux-x64-musl': 4.61.1 + '@rollup/rollup-openbsd-x64': 4.61.1 + '@rollup/rollup-openharmony-arm64': 4.61.1 + '@rollup/rollup-win32-arm64-msvc': 4.61.1 + '@rollup/rollup-win32-ia32-msvc': 4.61.1 + '@rollup/rollup-win32-x64-gnu': 4.61.1 + '@rollup/rollup-win32-x64-msvc': 4.61.1 + fsevents: 2.3.3 + router@2.2.0: dependencies: debug: 4.4.3 @@ -7916,6 +8927,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: {} @@ -7947,6 +8960,8 @@ snapshots: ansi-styles: 6.2.3 is-fullwidth-code-point: 5.1.0 + source-map-js@1.2.1: {} + source-map-support@0.5.13: dependencies: buffer-from: 1.1.2 @@ -7960,8 +8975,12 @@ snapshots: dependencies: escape-string-regexp: 2.0.0 + stackback@0.0.2: {} + statuses@2.0.2: {} + std-env@3.10.0: {} + stream-buffers@2.2.0: {} strict-event-emitter@0.5.1: {} @@ -7984,6 +9003,12 @@ snapshots: is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.1 + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.2.0 + string-width@7.2.0: dependencies: emoji-regex: 10.4.0 @@ -8023,6 +9048,10 @@ snapshots: strip-json-comments@3.1.1: {} + strip-literal@3.1.0: + dependencies: + js-tokens: 9.0.1 + supports-color@5.5.0: dependencies: has-flag: 3.0.0 @@ -8047,12 +9076,22 @@ snapshots: glob: 7.2.3 minimatch: 3.1.2 + test-exclude@7.0.2: + dependencies: + '@istanbuljs/schema': 0.1.3 + glob: 10.5.0 + minimatch: 10.2.5 + text-table@0.2.0: {} through@2.3.8: {} tiny-invariant@1.3.3: {} + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + tinyexec@1.1.1: {} tinyglobby@0.2.16: @@ -8060,6 +9099,12 @@ snapshots: fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 + tinypool@1.1.1: {} + + tinyrainbow@2.0.0: {} + + tinyspy@4.0.4: {} + tmp@0.0.33: dependencies: os-tmpdir: 1.0.2 @@ -8246,12 +9291,88 @@ snapshots: v8-to-istanbul@9.3.0: dependencies: - '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/trace-mapping': 0.3.31 '@types/istanbul-lib-coverage': 2.0.6 convert-source-map: 2.0.0 vary@1.1.2: {} + vite-node@3.2.4(@types/node@18.19.76)(tsx@4.20.3)(yaml@2.7.1): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 7.3.5(@types/node@18.19.76)(tsx@4.20.3)(yaml@2.7.1) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vite@7.3.5(@types/node@18.19.76)(tsx@4.20.3)(yaml@2.7.1): + dependencies: + esbuild: 0.27.7 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + postcss: 8.5.15 + rollup: 4.61.1 + tinyglobby: 0.2.16 + optionalDependencies: + '@types/node': 18.19.76 + fsevents: 2.3.3 + tsx: 4.20.3 + yaml: 2.7.1 + + vitest@3.2.6(@types/node@18.19.76)(msw@2.10.4(@types/node@18.19.76)(typescript@5.7.3))(tsx@4.20.3)(yaml@2.7.1): + dependencies: + '@types/chai': 5.2.3 + '@vitest/expect': 3.2.6 + '@vitest/mocker': 3.2.6(msw@2.10.4(@types/node@18.19.76)(typescript@5.7.3))(vite@7.3.5(@types/node@18.19.76)(tsx@4.20.3)(yaml@2.7.1)) + '@vitest/pretty-format': 3.2.6 + '@vitest/runner': 3.2.6 + '@vitest/snapshot': 3.2.6 + '@vitest/spy': 3.2.6 + '@vitest/utils': 3.2.6 + 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: 7.3.5(@types/node@18.19.76)(tsx@4.20.3)(yaml@2.7.1) + vite-node: 3.2.4(@types/node@18.19.76)(tsx@4.20.3)(yaml@2.7.1) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 18.19.76 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + walker@1.0.8: dependencies: makeerror: 1.0.12 @@ -8260,6 +9381,11 @@ snapshots: dependencies: isexe: 2.0.0 + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + widest-line@6.0.0: dependencies: string-width: 8.2.0 @@ -8278,6 +9404,12 @@ snapshots: string-width: 4.2.3 strip-ansi: 6.0.1 + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 5.1.2 + strip-ansi: 7.2.0 + wrap-ansi@9.0.0: dependencies: ansi-styles: 6.2.1 diff --git a/src/__tests__/cli.test.ts b/src/__tests__/cli.test.ts index 728ed6ad..38082ecd 100644 --- a/src/__tests__/cli.test.ts +++ b/src/__tests__/cli.test.ts @@ -1,92 +1,110 @@ -// Mock functions must be defined before imports (jest hoists jest.mock calls; -// variables starting with "mock" are allowed in the factory scope). +// Mock functions are created via vi.hoisted so they exist before the hoisted +// vi.mock factories that reference them run. // NOTE: variable names must be unique across test files because .test.ts // files without top-level imports/exports share a single TS project scope. -const mockBuildSessionCli = jest.fn((args: Record) => args); -const mockProvisionNewAccountCli = jest.fn(); +const { mockBuildSessionCli, mockProvisionNewAccountCli } = vi.hoisted(() => ({ + mockBuildSessionCli: vi.fn((args: Record) => args), + mockProvisionNewAccountCli: vi.fn(), +})); -jest.mock('semver', () => ({ satisfies: () => true })); -jest.mock('../lib/wizard-session', () => ({ +vi.mock('semver', () => ({ satisfies: () => true })); +// importOriginal keeps real exports (e.g. RunPhase) while overriding +// buildSession — vitest throws on access to exports a partial mock omits. +vi.mock('../lib/wizard-session', async (importOriginal) => ({ + ...(await importOriginal()), buildSession: mockBuildSessionCli, })); -jest.mock('../utils/provisioning', () => ({ +vi.mock('../utils/provisioning', () => ({ provisionNewAccount: mockProvisionNewAccountCli, })); -jest.mock('../ui/tui/start-tui', () => ({ +vi.mock('../ui/tui/start-tui', () => ({ startTUI: () => ({ - unmount: jest.fn(), + unmount: vi.fn(), store: { session: {}, - runReadyHooks: jest.fn().mockResolvedValue(undefined), + runReadyHooks: vi.fn().mockResolvedValue(undefined), // eslint-disable-next-line @typescript-eslint/no-empty-function - getGate: jest.fn().mockReturnValue(new Promise(() => {})), - subscribe: jest.fn(), - onEnterScreen: jest.fn(), + getGate: vi.fn().mockReturnValue(new Promise(() => {})), + subscribe: vi.fn(), + onEnterScreen: vi.fn(), }, }), })); -jest.mock('../lib/programs/posthog-integration/index', () => ({ +vi.mock('../lib/programs/posthog-integration/index', () => ({ posthogIntegrationConfig: { id: 'posthog-integration', steps: [], run: null, }, })); -jest.mock('../utils/environment', () => ({ +vi.mock('../utils/environment', () => ({ isNonInteractiveEnvironment: () => false, readEnvironment: () => ({}), })); // CI-path dynamic imports need mocks to prevent unhandled rejections -jest.mock('../utils/env-api-key', () => ({ +vi.mock('../utils/env-api-key', () => ({ readApiKeyFromEnv: () => undefined, })); -jest.mock('../utils/debug', () => ({ - configureLogFileFromEnvironment: jest.fn(), - logToFile: jest.fn(), +vi.mock('../utils/debug', () => ({ + configureLogFileFromEnvironment: vi.fn(), + logToFile: vi.fn(), +})); +vi.mock('../lib/registry', () => ({ FRAMEWORK_REGISTRY: {} })); +vi.mock('../lib/detection/index', () => ({ + detectFramework: vi.fn().mockResolvedValue(null), + gatherFrameworkContext: vi.fn().mockResolvedValue({}), })); -jest.mock('../lib/registry', () => ({ FRAMEWORK_REGISTRY: {} })); -jest.mock('../lib/detection/index', () => ({ - detectFramework: jest.fn().mockResolvedValue(null), - gatherFrameworkContext: jest.fn().mockResolvedValue({}), +vi.mock('../utils/analytics', () => ({ + analytics: { setTag: vi.fn() }, })); -jest.mock('../utils/analytics', () => ({ - analytics: { setTag: jest.fn() }, +vi.mock('../utils/wizard-abort', async (importOriginal) => ({ + ...(await importOriginal()), + wizardAbort: vi.fn(), })); -jest.mock('../utils/wizard-abort', () => ({ wizardAbort: jest.fn() })); -jest.mock('../lib/agent/agent-runner', () => ({ - runAgent: jest.fn().mockResolvedValue(undefined), +vi.mock('../lib/agent/agent-runner', () => ({ + runAgent: vi.fn().mockResolvedValue(undefined), })); describe('CLI argument parsing', () => { const originalArgv = process.argv; // eslint-disable-next-line @typescript-eslint/unbound-method const originalExit = process.exit; - const originalEnv = { ...process.env }; + + // The wizard's env vars that individual tests set. Cleared around each test + // by mutating the live process.env in place — reassigning `process.env` to a + // fresh object (as this used to) defeats yargs' `.env()` reader once the + // module graph has been reset and yargs re-imported, so env-only flags stop + // being picked up. + const WIZARD_ENV_KEYS = [ + 'POSTHOG_WIZARD_REGION', + 'POSTHOG_WIZARD_DEFAULT', + 'POSTHOG_WIZARD_CI', + 'POSTHOG_WIZARD_API_KEY', + 'POSTHOG_WIZARD_INSTALL_DIR', + ]; + const clearWizardEnv = () => { + for (const key of WIZARD_ENV_KEYS) delete process.env[key]; + }; beforeEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); // Reset environment - process.env = { ...originalEnv }; - delete process.env.POSTHOG_WIZARD_REGION; - delete process.env.POSTHOG_WIZARD_DEFAULT; - delete process.env.POSTHOG_WIZARD_CI; - delete process.env.POSTHOG_WIZARD_API_KEY; - delete process.env.POSTHOG_WIZARD_INSTALL_DIR; + clearWizardEnv(); // Mock process.exit so the test runner doesn't exit. The CLI dispatch is // async (it dynamically imports the matched command file), so a throwing // mock would escape as an unhandled rejection rather than halting the // handler. A no-op suffices: validation failures `return` right after // calling exit, and tests assert on the recorded exit code. - process.exit = jest.fn() as unknown as typeof process.exit; + process.exit = vi.fn() as unknown as typeof process.exit; }); afterEach(() => { process.argv = originalArgv; process.exit = originalExit; - process.env = originalEnv; - jest.resetModules(); + clearWizardEnv(); + vi.resetModules(); }); /** @@ -96,15 +114,44 @@ describe('CLI argument parsing', () => { process.argv = ['node', 'bin.ts', ...args]; try { - jest.isolateModules(() => { - require('../../bin.ts'); - }); + // vi.resetModules() (afterEach) clears the registry, so this re-evaluates + // bin.ts fresh on every call — the vitest equivalent of isolateModules. + await import('../../bin'); } catch { // process.exit mock throws to halt handler execution } - // Allow yargs + async handlers to process - await new Promise((resolve) => setImmediate(resolve)); + await settle(); + } + + async function settle() { + // The CLI dispatch fires detached async work (`void (async () => …)()`) + // that awaits a deep chain of dynamic imports before reaching a mocked + // sink. Under jest these imports were synchronous (babel-commonjs), so the + // chain completed within runCLI; under the real ESM runner it spans many + // async turns. + // + // First anchor: pump the event loop until this run reaches a sink — + // buildSession (success paths) or process.exit (validation-failure paths). + // This guarantees the run has acted before we return, so it can't leak a + // first sink call into the next test. + // Poll on a real timer (not a fixed event-loop-turn count): afterEach's + // vi.resetModules() forces a full graph reload from disk on every run, so + // the chain is I/O-bound and can be starved when vitest runs files in + // parallel. A wall-clock budget tolerates that load; it returns as soon as + // the sink fires, so the budget is only spent in the worst case. + const sank = () => + mockBuildSessionCli.mock.calls.length > 0 || + (process.exit as unknown as Mock).mock.calls.length > 0; + for (let i = 0; i < 300 && !sank(); i++) { + await new Promise((resolve) => setTimeout(resolve, 10)); + } + // Then drain: process.exit is a no-op here, so a validation-failure chain + // keeps running past it and may still call buildSession. A short wait lets + // that trailing work finish inside this test rather than leaking into the + // next one. (Success chains past their sink only park on the never-resolving + // intro gate or hit mocked no-ops.) + await new Promise((resolve) => setTimeout(resolve, 150)); } /** @@ -278,7 +325,7 @@ describe('CLI argument parsing', () => { // handler's exit calls are always terminal, so "continuing" past them is // harmless and lets us assert on both the exit code and mock state. beforeEach(() => { - process.exit = jest.fn() as unknown as typeof process.exit; + process.exit = vi.fn() as unknown as typeof process.exit; }); const successResult = { diff --git a/src/__tests__/mcp-cli.test.ts b/src/__tests__/mcp-cli.test.ts index 0f4ce3aa..fe1b3762 100644 --- a/src/__tests__/mcp-cli.test.ts +++ b/src/__tests__/mcp-cli.test.ts @@ -1,21 +1,21 @@ // Mock variable names must be unique across .test.ts files (shared TS scope). -const mockBuildSessionMcp = jest.fn((args: Record) => args); -const mockStartTUIMcp = jest.fn(() => ({ - unmount: jest.fn(), +const mockBuildSessionMcp = vi.fn((args: Record) => args); +const mockStartTUIMcp = vi.fn(() => ({ + unmount: vi.fn(), store: { session: {} }, })); -const mockReadApiKeyFromEnvMcp = jest.fn(() => undefined as string | undefined); +const mockReadApiKeyFromEnvMcp = vi.fn(() => undefined as string | undefined); -jest.mock('@lib/wizard-session', () => ({ +vi.mock('@lib/wizard-session', () => ({ buildSession: mockBuildSessionMcp, })); -jest.mock('@ui/tui/start-tui', () => ({ +vi.mock('@ui/tui/start-tui', () => ({ startTUI: mockStartTUIMcp, })); -jest.mock('@utils/env-api-key', () => ({ +vi.mock('@utils/env-api-key', () => ({ readApiKeyFromEnv: mockReadApiKeyFromEnvMcp, })); -jest.mock('@lib/programs/program-registry', () => ({ +vi.mock('@lib/programs/program-registry', () => ({ Program: { McpAdd: 'mcp-add', McpRemove: 'mcp-remove', @@ -56,7 +56,7 @@ describe('mcpCommand (parent)', () => { describe('mcp add handler', () => { beforeEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); test('starts the TUI with the McpAdd program id', async () => { @@ -118,7 +118,7 @@ describe('mcp parsing (end-to-end yargs)', () => { describe('mcp remove handler', () => { beforeEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); test('starts the TUI with the McpRemove program id', async () => { diff --git a/src/__tests__/programs-cli.test.ts b/src/__tests__/programs-cli.test.ts index 17743981..46ffde36 100644 --- a/src/__tests__/programs-cli.test.ts +++ b/src/__tests__/programs-cli.test.ts @@ -1,7 +1,9 @@ -const mockRunWizard = jest.fn(); -const mockRunWizardCI = jest.fn(); +const { mockRunWizard, mockRunWizardCI } = vi.hoisted(() => ({ + mockRunWizard: vi.fn(), + mockRunWizardCI: vi.fn(), +})); -jest.mock('@lib/runners', () => ({ +vi.mock('@lib/runners', () => ({ runWizard: mockRunWizard, runWizardCI: mockRunWizardCI, })); @@ -19,7 +21,7 @@ function makeArgv(extra: Record = {}): Arguments { describe('program commands', () => { beforeEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); test('each command exposes its CLI name', () => { diff --git a/src/__tests__/provision-cli.test.ts b/src/__tests__/provision-cli.test.ts index 58160d48..148ba20c 100644 --- a/src/__tests__/provision-cli.test.ts +++ b/src/__tests__/provision-cli.test.ts @@ -1,60 +1,62 @@ -// Mock functions must be defined before imports (jest hoists jest.mock calls; -// variables starting with "mock" are allowed in the factory scope). +// Mock functions are created via vi.hoisted so they exist before the hoisted +// vi.mock factories that reference them run. // Name-scoped to this file because .test.ts files share TS project scope when // they have no top-level imports/exports. -const mockProvisionNewAccountSubcmd = jest.fn(); +const { mockProvisionNewAccountSubcmd } = vi.hoisted(() => ({ + mockProvisionNewAccountSubcmd: vi.fn(), +})); -jest.mock('semver', () => ({ satisfies: () => true })); -jest.mock('../utils/provisioning', () => ({ +vi.mock('semver', () => ({ satisfies: () => true })); +vi.mock('../utils/provisioning', () => ({ provisionNewAccount: mockProvisionNewAccountSubcmd, })); // Same supporting mocks as src/__tests__/cli.test.ts — bin.ts imports these // at module load regardless of which subcommand yargs dispatches. -jest.mock('../lib/wizard-session', () => ({ - buildSession: jest.fn((args: Record) => args), +vi.mock('../lib/wizard-session', () => ({ + buildSession: vi.fn((args: Record) => args), })); -jest.mock('../ui/tui/start-tui', () => ({ +vi.mock('../ui/tui/start-tui', () => ({ startTUI: () => ({ - unmount: jest.fn(), + unmount: vi.fn(), store: { session: {}, - runReadyHooks: jest.fn().mockResolvedValue(undefined), + runReadyHooks: vi.fn().mockResolvedValue(undefined), // eslint-disable-next-line @typescript-eslint/no-empty-function - getGate: jest.fn().mockReturnValue(new Promise(() => {})), - subscribe: jest.fn(), - onEnterScreen: jest.fn(), + getGate: vi.fn().mockReturnValue(new Promise(() => {})), + subscribe: vi.fn(), + onEnterScreen: vi.fn(), }, }), })); -jest.mock('../lib/programs/posthog-integration/index', () => ({ +vi.mock('../lib/programs/posthog-integration/index', () => ({ posthogIntegrationConfig: { id: 'posthog-integration', steps: [], run: null, }, })); -jest.mock('../utils/environment', () => ({ +vi.mock('../utils/environment', () => ({ isNonInteractiveEnvironment: () => false, readEnvironment: () => ({}), })); -jest.mock('../utils/env-api-key', () => ({ +vi.mock('../utils/env-api-key', () => ({ readApiKeyFromEnv: () => undefined, })); -jest.mock('../utils/debug', () => ({ - configureLogFileFromEnvironment: jest.fn(), - logToFile: jest.fn(), +vi.mock('../utils/debug', () => ({ + configureLogFileFromEnvironment: vi.fn(), + logToFile: vi.fn(), })); -jest.mock('../lib/registry', () => ({ FRAMEWORK_REGISTRY: {} })); -jest.mock('../lib/detection/index', () => ({ - detectFramework: jest.fn().mockResolvedValue(null), - gatherFrameworkContext: jest.fn().mockResolvedValue({}), +vi.mock('../lib/registry', () => ({ FRAMEWORK_REGISTRY: {} })); +vi.mock('../lib/detection/index', () => ({ + detectFramework: vi.fn().mockResolvedValue(null), + gatherFrameworkContext: vi.fn().mockResolvedValue({}), })); -jest.mock('../utils/analytics', () => ({ - analytics: { setTag: jest.fn() }, +vi.mock('../utils/analytics', () => ({ + analytics: { setTag: vi.fn() }, })); -jest.mock('../utils/wizard-abort', () => ({ wizardAbort: jest.fn() })); -jest.mock('../lib/agent/agent-runner', () => ({ - runAgent: jest.fn().mockResolvedValue(undefined), +vi.mock('../utils/wizard-abort', () => ({ wizardAbort: vi.fn() })); +vi.mock('../lib/agent/agent-runner', () => ({ + runAgent: vi.fn().mockResolvedValue(undefined), })); import { provisionCommand } from '../commands/provision'; @@ -87,7 +89,7 @@ describe('wizard provision subcommand', () => { let stdoutChunks: string[]; let stderrChunks: string[]; - let consoleLogSpy: jest.SpyInstance; + let consoleLogSpy: MockInstance; const successResult = { projectApiKey: 'phc_test', @@ -100,7 +102,7 @@ describe('wizard provision subcommand', () => { }; beforeEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); stdoutChunks = []; stderrChunks = []; @@ -113,14 +115,14 @@ describe('wizard provision subcommand', () => { return true; }) as typeof process.stderr.write; - consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(() => { + consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => { // suppress LoggingUI output during tests }); // process.exit is always the final call in each branch of the provision // handler, so a silent no-op is enough. Throwing here would escape the // void async IIFE and become an unhandled rejection. - process.exit = jest.fn() as unknown as typeof process.exit; + process.exit = vi.fn() as unknown as typeof process.exit; }); afterEach(() => { @@ -133,7 +135,7 @@ describe('wizard provision subcommand', () => { configurable: true, }); consoleLogSpy.mockRestore(); - jest.resetModules(); + vi.resetModules(); }); function setTTY(isTTY: boolean) { @@ -146,9 +148,9 @@ describe('wizard provision subcommand', () => { async function runCLI(args: string[]) { process.argv = ['node', 'bin.ts', 'provision', ...args]; try { - jest.isolateModules(() => { - require('../../bin.ts'); - }); + // vi.resetModules() (afterEach) clears the registry, so this re-evaluates + // bin.ts fresh on every call — the vitest equivalent of isolateModules. + await import('../../bin'); } catch { // process.exit mock throws to halt handler execution } diff --git a/src/__tests__/skill-cli.test.ts b/src/__tests__/skill-cli.test.ts index 47bf8de9..3036becf 100644 --- a/src/__tests__/skill-cli.test.ts +++ b/src/__tests__/skill-cli.test.ts @@ -1,7 +1,7 @@ import type { Arguments } from 'yargs'; -jest.mock('../commands/basic-integration/skill', () => ({ - runSkillMode: jest.fn(), +vi.mock('../commands/basic-integration/skill', () => ({ + runSkillMode: vi.fn(), })); import { runSkillMode } from '../commands/basic-integration/skill'; @@ -54,18 +54,18 @@ describe('skill command validation', () => { }); describe('skill command handler', () => { - beforeEach(() => jest.clearAllMocks()); + beforeEach(() => vi.clearAllMocks()); test('bridges the positional onto runSkillMode as the skill id', () => { skillCommand.handler!(makeArgv({ skillName: 'audit-events', ci: false })); expect(runSkillMode).toHaveBeenCalledTimes(1); - const passed = (runSkillMode as jest.Mock).mock.calls[0][0]; + const passed = (runSkillMode as Mock).mock.calls[0][0]; expect(passed.skill).toBe('audit-events'); }); test('trims surrounding whitespace from the skill id', () => { skillCommand.handler!(makeArgv({ skillName: ' audit-events ' })); - const passed = (runSkillMode as jest.Mock).mock.calls[0][0]; + const passed = (runSkillMode as Mock).mock.calls[0][0]; expect(passed.skill).toBe('audit-events'); }); }); diff --git a/src/__tests__/wizard-abort.test.ts b/src/__tests__/wizard-abort.test.ts index d6c5efb8..f5942d32 100644 --- a/src/__tests__/wizard-abort.test.ts +++ b/src/__tests__/wizard-abort.test.ts @@ -6,33 +6,44 @@ import { clearCleanup, } from '@utils/wizard-abort'; import { analytics } from '@utils/analytics'; +import { getUI } from '../ui'; -jest.mock('../utils/analytics'); -jest.mock('../ui', () => ({ - getUI: jest.fn().mockReturnValue({ - outroError: jest.fn(), - waitForOutroDismissed: jest.fn().mockResolvedValue(undefined), +vi.mock('../utils/analytics'); +vi.mock('../ui', () => ({ + getUI: vi.fn().mockReturnValue({ + outroError: vi.fn(), + waitForOutroDismissed: vi.fn().mockResolvedValue(undefined), }), })); -const mockAnalytics = analytics as jest.Mocked; -const { getUI } = jest.requireMock('../ui'); +const mockAnalytics = analytics as Mocked; + +// vitest's restoreAllMocks() (afterEach) wipes the getUI() factory mock's +// return value (unlike jest, which only restores spyOn mocks), so re-seed it +// before each test. +const seedGetUI = () => { + (getUI as Mock).mockReturnValue({ + outroError: vi.fn(), + waitForOutroDismissed: vi.fn().mockResolvedValue(undefined), + }); +}; describe('wizardAbort', () => { beforeEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); clearCleanup(); + seedGetUI(); - mockAnalytics.captureException = jest.fn(); - mockAnalytics.shutdown = jest.fn().mockResolvedValue(undefined); + mockAnalytics.captureException = vi.fn(); + mockAnalytics.shutdown = vi.fn().mockResolvedValue(undefined); - jest.spyOn(process, 'exit').mockImplementation(() => { + vi.spyOn(process, 'exit').mockImplementation(() => { throw new Error('process.exit called'); }); }); afterEach(() => { - jest.restoreAllMocks(); + vi.restoreAllMocks(); }); it('calls analytics.shutdown, getUI().outroError, and process.exit in order', async () => { @@ -40,7 +51,7 @@ describe('wizardAbort', () => { mockAnalytics.shutdown.mockImplementation(async () => { callOrder.push('shutdown'); }); - getUI().outroError.mockImplementation(() => { + (getUI().outroError as unknown as Mock).mockImplementation(() => { callOrder.push('outroError'); }); @@ -128,7 +139,7 @@ describe('wizardAbort', () => { mockAnalytics.shutdown.mockImplementation(async () => { callOrder.push('shutdown'); }); - getUI().outroError.mockImplementation(() => { + (getUI().outroError as unknown as Mock).mockImplementation(() => { callOrder.push('outroError'); }); @@ -168,19 +179,20 @@ describe('wizardAbort', () => { describe('abort() delegates to wizardAbort()', () => { beforeEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); clearCleanup(); + seedGetUI(); - mockAnalytics.captureException = jest.fn(); - mockAnalytics.shutdown = jest.fn().mockResolvedValue(undefined); + mockAnalytics.captureException = vi.fn(); + mockAnalytics.shutdown = vi.fn().mockResolvedValue(undefined); - jest.spyOn(process, 'exit').mockImplementation(() => { + vi.spyOn(process, 'exit').mockImplementation(() => { throw new Error('process.exit called'); }); }); afterEach(() => { - jest.restoreAllMocks(); + vi.restoreAllMocks(); }); it('abort() calls wizardAbort with message and exitCode', async () => { diff --git a/src/lib/__tests__/agent-interface.test.ts b/src/lib/__tests__/agent-interface.test.ts index 366bced5..fdb69409 100644 --- a/src/lib/__tests__/agent-interface.test.ts +++ b/src/lib/__tests__/agent-interface.test.ts @@ -8,54 +8,54 @@ import { } from '@lib/wizard-session'; // Mock dependencies -jest.mock('../../utils/analytics'); -jest.mock('../../utils/debug'); +vi.mock('../../utils/analytics'); +vi.mock('../../utils/debug'); // Mock the SDK module -const mockQuery = jest.fn(); -jest.mock('@anthropic-ai/claude-agent-sdk', () => ({ +const mockQuery = vi.fn(); +vi.mock('@anthropic-ai/claude-agent-sdk', () => ({ query: (...args: unknown[]) => mockQuery(...args), })); // Mock the UI layer const mockUIInstance = { log: { - step: jest.fn(), - success: jest.fn(), - error: jest.fn(), - warn: jest.fn(), - info: jest.fn(), + step: vi.fn(), + success: vi.fn(), + error: vi.fn(), + warn: vi.fn(), + info: vi.fn(), }, - spinner: jest.fn(), - select: jest.fn(), - confirm: jest.fn(), - text: jest.fn(), - intro: jest.fn(), - outro: jest.fn(), - cancel: jest.fn(), - note: jest.fn(), - isCancel: jest.fn(), - setDetectedFramework: jest.fn(), - setCredentials: jest.fn(), - pushStatus: jest.fn(), - setLoginUrl: jest.fn(), - showBlockingOutage: jest.fn(), - setReadinessWarnings: jest.fn(), - showSettingsOverride: jest.fn(), - startRun: jest.fn(), - syncTodos: jest.fn(), - groupMultiselect: jest.fn(), - multiselect: jest.fn(), + spinner: vi.fn(), + select: vi.fn(), + confirm: vi.fn(), + text: vi.fn(), + intro: vi.fn(), + outro: vi.fn(), + cancel: vi.fn(), + note: vi.fn(), + isCancel: vi.fn(), + setDetectedFramework: vi.fn(), + setCredentials: vi.fn(), + pushStatus: vi.fn(), + setLoginUrl: vi.fn(), + showBlockingOutage: vi.fn(), + setReadinessWarnings: vi.fn(), + showSettingsOverride: vi.fn(), + startRun: vi.fn(), + syncTodos: vi.fn(), + groupMultiselect: vi.fn(), + multiselect: vi.fn(), }; -jest.mock('../../ui', () => ({ +vi.mock('../../ui', () => ({ getUI: () => mockUIInstance, })); describe('runAgent', () => { let mockSpinner: { - start: jest.Mock; - stop: jest.Mock; - message: jest.Mock; + start: Mock; + stop: Mock; + message: Mock; }; const defaultOptions: WizardRunOptions = { @@ -76,12 +76,12 @@ describe('runAgent', () => { }; beforeEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); mockSpinner = { - start: jest.fn(), - stop: jest.fn(), - message: jest.fn(), + start: vi.fn(), + stop: vi.fn(), + message: vi.fn(), }; mockUIInstance.spinner.mockReturnValue(mockSpinner); diff --git a/src/lib/__tests__/cloudflare-detection.test.ts b/src/lib/__tests__/cloudflare-detection.test.ts index 540cbd44..7879b532 100644 --- a/src/lib/__tests__/cloudflare-detection.test.ts +++ b/src/lib/__tests__/cloudflare-detection.test.ts @@ -3,7 +3,7 @@ import * as path from 'path'; import * as os from 'os'; import { detectCloudflareTarget } from '@lib/cloudflare-detection'; -jest.mock('../../utils/debug'); +vi.mock('../../utils/debug'); function makeTmpDir(): string { return fs.mkdtempSync(path.join(os.tmpdir(), 'cloudflare-detect-')); diff --git a/src/lib/__tests__/wizard-ask-bridge.test.ts b/src/lib/__tests__/wizard-ask-bridge.test.ts index 11a8122d..43a940a4 100644 --- a/src/lib/__tests__/wizard-ask-bridge.test.ts +++ b/src/lib/__tests__/wizard-ask-bridge.test.ts @@ -5,13 +5,13 @@ import { import { analytics } from '@utils/analytics'; import type { AskAnswers, PendingQuestion } from '@lib/wizard-session'; -jest.mock('../../utils/analytics', () => ({ +vi.mock('../../utils/analytics', () => ({ analytics: { - wizardCapture: jest.fn(), + wizardCapture: vi.fn(), }, })); -const wizardCaptureMock = analytics.wizardCapture as jest.Mock; +const wizardCaptureMock = analytics.wizardCapture as Mock; beforeEach(() => { wizardCaptureMock.mockClear(); @@ -164,7 +164,7 @@ describe('createWizardAskBridge', () => { describe('timeout', () => { it('resolves every field with the cancelled sentinel when the user does not answer in time', async () => { - jest.useFakeTimers(); + vi.useFakeTimers(); try { // showQuestion intentionally never resolves — the timeout has to win. const bridge = createWizardAskBridge({ @@ -180,7 +180,7 @@ describe('createWizardAskBridge', () => { ], }); - jest.advanceTimersByTime(1000); + vi.advanceTimersByTime(1000); await expect(promise).resolves.toEqual({ goal: CANCELLED_SENTINEL, @@ -192,7 +192,7 @@ describe('createWizardAskBridge', () => { ); expect(cancelledCall?.[1]).toMatchObject({ timed_out: true }); } finally { - jest.useRealTimers(); + vi.useRealTimers(); } }); }); diff --git a/src/lib/__tests__/wizard-can-use-tool.test.ts b/src/lib/__tests__/wizard-can-use-tool.test.ts index 10036fb2..376c03fc 100644 --- a/src/lib/__tests__/wizard-can-use-tool.test.ts +++ b/src/lib/__tests__/wizard-can-use-tool.test.ts @@ -1,11 +1,11 @@ import { wizardCanUseTool } from '@lib/agent/agent-interface'; -jest.mock('../../utils/analytics', () => ({ +vi.mock('../../utils/analytics', () => ({ analytics: { - wizardCapture: jest.fn(), + wizardCapture: vi.fn(), }, })); -jest.mock('../../utils/debug'); +vi.mock('../../utils/debug'); describe('wizardCanUseTool — wizard_ask pending guard', () => { for (const tool of ['Write', 'Edit'] as const) { diff --git a/src/lib/__tests__/yara-hooks.test.ts b/src/lib/__tests__/yara-hooks.test.ts index 53915c4f..a2c0d457 100644 --- a/src/lib/__tests__/yara-hooks.test.ts +++ b/src/lib/__tests__/yara-hooks.test.ts @@ -7,29 +7,32 @@ import { resetScanReport, } from '@lib/yara-hooks'; import { scan, triageMatches } from '@posthog/warlock'; +import fs from 'fs'; +import fg from 'fast-glob'; +import * as analyticsModule from '../../utils/analytics'; // Mock dependencies -jest.mock('../../utils/debug'); -jest.mock('../../utils/analytics'); -jest.mock('fs'); -jest.mock('fast-glob'); +vi.mock('../../utils/debug'); +vi.mock('../../utils/analytics'); +vi.mock('fs'); +vi.mock('fast-glob'); // Mock isSkillInstallCommand from skill-install (extracted to break circular dep) -jest.mock('../skill-install', () => ({ +vi.mock('../skill-install', () => ({ isSkillInstallCommand: (command: string) => command.startsWith('mkdir -p .claude/skills/') && command.includes('curl -sL') && command.includes('github.com/PostHog/context-mill/releases/'), })); -const mockFs = jest.requireMock('fs'); -const mockFg = jest.requireMock('fast-glob'); -const mockAnalytics = jest.requireMock('../../utils/analytics'); +const mockFs = vi.mocked(fs); +const mockFg = vi.mocked(fg); +const mockAnalytics = vi.mocked(analyticsModule, true); // @posthog/warlock is mapped to __mocks__/@posthog/warlock.ts (ESM + WASM can't -// load under jest). These are the jest.fn()s the hooks call via dynamic import. -const mockScan = scan as jest.Mock; -const mockTriage = triageMatches as jest.Mock; +// load under vitest). These are the vi.fn()s the hooks call via dynamic import. +const mockScan = scan as Mock; +const mockTriage = triageMatches as Mock; const dummySignal = new AbortController().signal; @@ -38,7 +41,7 @@ const dummyProvider = () => Promise.resolve('[]'); // onTerminate is required by createPostToolUseYaraHooks. Tests that don't // exercise terminate paths can pass this no-op so the signature is satisfied; -// tests that DO exercise terminate paths pass their own jest.fn() spy. +// tests that DO exercise terminate paths pass their own vi.fn() spy. // eslint-disable-next-line @typescript-eslint/no-empty-function const noopTerminate = (_reason: string): void => {}; @@ -83,7 +86,7 @@ function input(fields: Record) { describe('yara-hooks', () => { beforeEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); resetScanReport(); // Default: nothing matches; triage passes everything through as real. mockScan.mockReset().mockResolvedValue(noMatch); @@ -310,7 +313,7 @@ describe('yara-hooks', () => { it('fails closed (terminate) when the scanner throws', async () => { mockScan.mockRejectedValueOnce(new Error('boom')); - const terminateSpy = jest.fn(); + const terminateSpy = vi.fn(); const hook = createPostToolUseYaraHooks(undefined, terminateSpy)[0] .hooks[0]; const result = await hook( @@ -826,7 +829,7 @@ describe('yara-hooks', () => { }), ), ); - const onTerminate = jest.fn(); + const onTerminate = vi.fn(); const hook = createPostToolUseYaraHooks(undefined, onTerminate)[1] .hooks[0]; const result = await hook( @@ -844,7 +847,7 @@ describe('yara-hooks', () => { it('fires fail-closed when the read scan throws', async () => { mockScan.mockRejectedValueOnce(new Error('boom')); - const onTerminate = jest.fn(); + const onTerminate = vi.fn(); const hook = createPostToolUseYaraHooks(undefined, onTerminate)[1] .hooks[0]; await hook(input({ tool_name: 'Read', tool_response: 'x' }), 'x', { @@ -866,7 +869,7 @@ describe('yara-hooks', () => { }), ), ); - const onTerminate = jest.fn(); + const onTerminate = vi.fn(); const hook = createPostToolUseYaraHooks(undefined, onTerminate)[2] .hooks[0]; await hook( @@ -889,7 +892,7 @@ describe('yara-hooks', () => { }), ), ); - const onTerminate = jest.fn(); + const onTerminate = vi.fn(); const hook = createPostToolUseYaraHooks(undefined, onTerminate)[1] .hooks[0]; await hook(input({ tool_name: 'Grep', tool_response: 'x' }), 'x', { @@ -907,7 +910,7 @@ describe('yara-hooks', () => { }), ), ); - const onTerminate = jest.fn(); + const onTerminate = vi.fn(); const hook = createPostToolUseYaraHooks(undefined, onTerminate)[0] .hooks[0]; await hook( diff --git a/src/lib/agent/__tests__/__snapshots__/commandments.test.ts.snap b/src/lib/agent/__tests__/__snapshots__/commandments.test.ts.snap index c4d89d8d..f5f4f468 100644 --- a/src/lib/agent/__tests__/__snapshots__/commandments.test.ts.snap +++ b/src/lib/agent/__tests__/__snapshots__/commandments.test.ts.snap @@ -1,6 +1,6 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`getWizardCommandments matches the published commandment list 1`] = ` +exports[`getWizardCommandments > matches the published commandment list 1`] = ` "Never hallucinate a PostHog project token, host, or any other secret. Always use the real values that have been configured for this project (for example via environment variables). Never write API keys, access tokens, or other secrets directly into source code. Always reference environment variables instead, and rely on the wizard-tools MCP server (check_env_keys / set_env_values) to create or update .env files. Always use the detect_package_manager tool from the wizard-tools MCP server to determine the package manager. Do not guess based on lockfiles or hard-code npm, yarn, pnpm, bun, pip, etc. diff --git a/src/lib/agent/__tests__/agent-prompt.test.ts b/src/lib/agent/__tests__/agent-prompt.test.ts index 206efc8a..18609e29 100644 --- a/src/lib/agent/__tests__/agent-prompt.test.ts +++ b/src/lib/agent/__tests__/agent-prompt.test.ts @@ -29,7 +29,7 @@ describe('assemblePrompt', () => { }); it('composes three sections in order: default → custom → skill', () => { - const customFn = jest.fn(() => 'CUSTOM_INSTRUCTIONS'); + const customFn = vi.fn(() => 'CUSTOM_INSTRUCTIONS'); const runDef = makeRunDef({ customPrompt: customFn }); const ctx: PromptContext = { ...baseCtx, skillPath: '/skills/test' }; diff --git a/src/lib/detection/__tests__/context.test.ts b/src/lib/detection/__tests__/context.test.ts index f92e3e97..730e1787 100644 --- a/src/lib/detection/__tests__/context.test.ts +++ b/src/lib/detection/__tests__/context.test.ts @@ -20,7 +20,7 @@ describe('gatherFrameworkContext', () => { it('calls gatherContext and returns the result', async () => { const config = { metadata: { - gatherContext: jest + gatherContext: vi .fn() .mockResolvedValue({ routerType: 'app', srcDir: 'src' }), }, @@ -38,7 +38,7 @@ describe('gatherFrameworkContext', () => { const throws = { metadata: { - gatherContext: jest.fn().mockRejectedValue(new Error('fail')), + gatherContext: vi.fn().mockRejectedValue(new Error('fail')), }, } as unknown as FrameworkConfig; expect(await gatherFrameworkContext(throws, baseOptions)).toEqual({}); @@ -56,7 +56,7 @@ describe('checkFrameworkVersion', () => { const config = { detection: { minimumVersion: '14.0.0', - getInstalledVersion: jest.fn().mockResolvedValue('15.2.3'), + getInstalledVersion: vi.fn().mockResolvedValue('15.2.3'), }, metadata: { docsUrl: 'https://example.com/docs' }, } as unknown as FrameworkConfig; @@ -70,7 +70,7 @@ describe('checkFrameworkVersion', () => { const config = { detection: { minimumVersion: '14.0.0', - getInstalledVersion: jest.fn().mockResolvedValue('13.5.0'), + getInstalledVersion: vi.fn().mockResolvedValue('13.5.0'), }, metadata: { docsUrl: 'https://example.com/docs' }, } as unknown as FrameworkConfig; diff --git a/src/lib/detection/__tests__/package-manager.test.ts b/src/lib/detection/__tests__/package-manager.test.ts index 76c92387..27b468ad 100644 --- a/src/lib/detection/__tests__/package-manager.test.ts +++ b/src/lib/detection/__tests__/package-manager.test.ts @@ -9,12 +9,12 @@ import { gradlePackageManager, } from '@lib/detection/package-manager'; -jest.mock('../../../utils/debug'); -jest.mock('../../../telemetry', () => ({ +vi.mock('../../../utils/debug'); +vi.mock('../../../telemetry', () => ({ withProgress: (_name: string, fn: () => unknown) => fn(), })); -jest.mock('../../../utils/analytics', () => ({ - analytics: { setTag: jest.fn() }, +vi.mock('../../../utils/analytics', () => ({ + analytics: { setTag: vi.fn() }, })); function makeTmpDir(): string { diff --git a/src/lib/health-checks/__tests__/health-checks.test.ts b/src/lib/health-checks/__tests__/health-checks.test.ts index e6c97c70..fbd3b882 100644 --- a/src/lib/health-checks/__tests__/health-checks.test.ts +++ b/src/lib/health-checks/__tests__/health-checks.test.ts @@ -303,9 +303,9 @@ describe('health-checks', () => { const originalFetch = global.fetch; beforeEach(() => { - jest.restoreAllMocks(); + vi.restoreAllMocks(); resetPosthogHealthCache(); - (global as any).fetch = jest.fn(allHealthyFetchMock); + (global as any).fetch = vi.fn(allHealthyFetchMock); }); afterAll(() => { @@ -331,7 +331,7 @@ describe('health-checks', () => { indicator: 'minor', description: 'Minor Service Outage', }); - (global.fetch as jest.Mock).mockImplementation( + (global.fetch as Mock).mockImplementation( overrideFetch({ [URLS.anthropicStatus]: () => Promise.resolve( @@ -352,7 +352,7 @@ describe('health-checks', () => { indicator: 'major', description: 'Partial System Outage', }); - (global.fetch as jest.Mock).mockImplementation( + (global.fetch as Mock).mockImplementation( overrideFetch({ [URLS.anthropicStatus]: () => Promise.resolve( @@ -372,7 +372,7 @@ describe('health-checks', () => { indicator: 'critical', description: 'Major Service Outage', }); - (global.fetch as jest.Mock).mockImplementation( + (global.fetch as Mock).mockImplementation( overrideFetch({ [URLS.anthropicStatus]: () => Promise.resolve( @@ -385,7 +385,7 @@ describe('health-checks', () => { }); it('returns degraded when statuspage returns HTTP 500', async () => { - (global.fetch as jest.Mock).mockImplementation( + (global.fetch as Mock).mockImplementation( overrideFetch({ [URLS.anthropicStatus]: () => Promise.resolve( @@ -399,7 +399,7 @@ describe('health-checks', () => { }); it('returns degraded when fetch throws (network failure)', async () => { - (global.fetch as jest.Mock).mockImplementation( + (global.fetch as Mock).mockImplementation( overrideFetch({ [URLS.anthropicStatus]: () => Promise.reject( @@ -442,7 +442,7 @@ describe('health-checks', () => { }, ], }; - (global.fetch as jest.Mock).mockImplementation( + (global.fetch as Mock).mockImplementation( overrideFetch({ [URLS.posthogIncidentIo]: () => Promise.resolve( @@ -470,7 +470,7 @@ describe('health-checks', () => { }, ], }; - (global.fetch as jest.Mock).mockImplementation( + (global.fetch as Mock).mockImplementation( overrideFetch({ [URLS.posthogIncidentIo]: () => Promise.resolve( @@ -511,7 +511,7 @@ describe('health-checks', () => { indicator: 'minor', description: 'Minor Service Outage', }); - (global.fetch as jest.Mock).mockImplementation( + (global.fetch as Mock).mockImplementation( overrideFetch({ [URLS.cloudflareStatus]: () => Promise.resolve( @@ -564,7 +564,7 @@ describe('health-checks', () => { }, ], }; - (global.fetch as jest.Mock).mockImplementation( + (global.fetch as Mock).mockImplementation( overrideFetch({ [URLS.posthogIncidentIo]: () => Promise.resolve( @@ -609,7 +609,7 @@ describe('health-checks', () => { }, ], }; - (global.fetch as jest.Mock).mockImplementation( + (global.fetch as Mock).mockImplementation( overrideFetch({ [URLS.posthogIncidentIo]: () => Promise.resolve( @@ -658,7 +658,7 @@ describe('health-checks', () => { }, ], }); - (global.fetch as jest.Mock).mockImplementation( + (global.fetch as Mock).mockImplementation( overrideFetch({ [URLS.npmSummary]: () => Promise.resolve( @@ -697,7 +697,7 @@ describe('health-checks', () => { }); it('returns down when gateway responds 503 (e.g. deploying)', async () => { - (global.fetch as jest.Mock).mockImplementation( + (global.fetch as Mock).mockImplementation( overrideFetch({ [URLS.llmGatewayLiveness]: () => Promise.resolve( @@ -711,7 +711,7 @@ describe('health-checks', () => { }); it('returns down when gateway responds 502 (bad gateway)', async () => { - (global.fetch as jest.Mock).mockImplementation( + (global.fetch as Mock).mockImplementation( overrideFetch({ [URLS.llmGatewayLiveness]: () => Promise.resolve(new Response('Bad Gateway', { status: 502 })), @@ -723,7 +723,7 @@ describe('health-checks', () => { }); it('returns down on DNS resolution failure', async () => { - (global.fetch as jest.Mock).mockImplementation( + (global.fetch as Mock).mockImplementation( overrideFetch({ [URLS.llmGatewayLiveness]: () => Promise.reject( @@ -739,7 +739,7 @@ describe('health-checks', () => { it('returns down on timeout (AbortError)', async () => { const abortError = new Error('The operation was aborted.'); abortError.name = 'AbortError'; - (global.fetch as jest.Mock).mockImplementation( + (global.fetch as Mock).mockImplementation( overrideFetch({ [URLS.llmGatewayLiveness]: () => Promise.reject(abortError), }), @@ -766,7 +766,7 @@ describe('health-checks', () => { }); it('returns down when worker responds 500', async () => { - (global.fetch as jest.Mock).mockImplementation( + (global.fetch as Mock).mockImplementation( overrideFetch({ [URLS.mcpLanding]: () => Promise.resolve( @@ -780,7 +780,7 @@ describe('health-checks', () => { }); it('returns down when Cloudflare returns 522 (connection timed out)', async () => { - (global.fetch as jest.Mock).mockImplementation( + (global.fetch as Mock).mockImplementation( overrideFetch({ [URLS.mcpLanding]: () => Promise.resolve(new Response('', { status: 522 })), @@ -792,7 +792,7 @@ describe('health-checks', () => { }); it('returns down on network failure', async () => { - (global.fetch as jest.Mock).mockImplementation( + (global.fetch as Mock).mockImplementation( overrideFetch({ [URLS.mcpLanding]: () => Promise.reject(new Error('fetch failed')), }), @@ -834,9 +834,8 @@ describe('health-checks', () => { it('fires all fetch calls in parallel', async () => { await checkAllExternalServices(); - const calledUrls = (global.fetch as jest.Mock).mock.calls.map( - (c: unknown[]) => - typeof c[0] === 'string' ? c[0] : (c[0] as URL).toString(), + const calledUrls = (global.fetch as Mock).mock.calls.map((c: unknown[]) => + typeof c[0] === 'string' ? c[0] : (c[0] as URL).toString(), ); // PostHog uses a single incident.io endpoint for both overall + components expect(calledUrls).toHaveLength(10); @@ -867,7 +866,7 @@ describe('health-checks', () => { indicator: 'minor', description: 'Minor Service Outage', }); - (global.fetch as jest.Mock).mockImplementation( + (global.fetch as Mock).mockImplementation( overrideFetch({ [URLS.anthropicStatus]: () => Promise.resolve( @@ -883,7 +882,7 @@ describe('health-checks', () => { }); it('returns No when LLM Gateway is down (downBlocksRun)', async () => { - (global.fetch as jest.Mock).mockImplementation( + (global.fetch as Mock).mockImplementation( overrideFetch({ [URLS.llmGatewayLiveness]: () => Promise.resolve( @@ -899,7 +898,7 @@ describe('health-checks', () => { }); it('returns No when MCP is down (downBlocksRun)', async () => { - (global.fetch as jest.Mock).mockImplementation( + (global.fetch as Mock).mockImplementation( overrideFetch({ [URLS.mcpLanding]: () => Promise.resolve(new Response('Bad Gateway', { status: 502 })), @@ -920,7 +919,7 @@ describe('health-checks', () => { indicator: 'critical', description: 'Major Service Outage', }); - (global.fetch as jest.Mock).mockImplementation( + (global.fetch as Mock).mockImplementation( overrideFetch({ [URLS.npmStatus]: () => Promise.resolve( @@ -943,7 +942,7 @@ describe('health-checks', () => { indicator: 'minor', description: 'Minor Service Outage', }); - (global.fetch as jest.Mock).mockImplementation( + (global.fetch as Mock).mockImplementation( overrideFetch({ [URLS.cloudflareStatus]: () => Promise.resolve( diff --git a/src/lib/programs/__tests__/program-step.test.ts b/src/lib/programs/__tests__/program-step.test.ts index d2147223..57c4ac05 100644 --- a/src/lib/programs/__tests__/program-step.test.ts +++ b/src/lib/programs/__tests__/program-step.test.ts @@ -18,8 +18,8 @@ describe('createProgramSequence', () => { }); it('falls back isComplete to gate, preferring explicit isComplete', () => { - const gateFn = jest.fn(); - const isCompleteFn = jest.fn(); + const gateFn = vi.fn(); + const isCompleteFn = vi.fn(); const steps: ProgramStep[] = [ { id: 'a', label: 'A', screenId: 'a', gate: gateFn }, @@ -47,8 +47,8 @@ describe('createProgramSequence', () => { label: 'Welcome', screenId: 'intro', gate: () => true, - onInit: jest.fn(), - onReady: jest.fn(), + onInit: vi.fn(), + onReady: vi.fn(), }, ]; diff --git a/src/lib/programs/__tests__/revenue-analytics-detect.test.ts b/src/lib/programs/__tests__/revenue-analytics-detect.test.ts index d043aa09..5907dc4f 100644 --- a/src/lib/programs/__tests__/revenue-analytics-detect.test.ts +++ b/src/lib/programs/__tests__/revenue-analytics-detect.test.ts @@ -25,12 +25,12 @@ function writePackageJson( describe('detectRevenuePrerequisites', () => { let tmpDir: string; let ctx: Record; - let setCtx: jest.Mock; + let setCtx: Mock; beforeEach(() => { tmpDir = makeTmpDir(); ctx = {}; - setCtx = jest.fn((key: string, value: unknown) => { + setCtx = vi.fn((key: string, value: unknown) => { ctx[key] = value; }); }); diff --git a/src/lib/programs/__tests__/source-maps-detect.test.ts b/src/lib/programs/__tests__/source-maps-detect.test.ts index 813e8a2a..456933ad 100644 --- a/src/lib/programs/__tests__/source-maps-detect.test.ts +++ b/src/lib/programs/__tests__/source-maps-detect.test.ts @@ -33,12 +33,12 @@ function writePackageJson( describe('detectSourceMapsPrerequisites', () => { let tmpDir: string; let ctx: Record; - let setCtx: jest.Mock; + let setCtx: Mock; beforeEach(() => { tmpDir = makeTmpDir(); ctx = {}; - setCtx = jest.fn((key: string, value: unknown) => { + setCtx = vi.fn((key: string, value: unknown) => { ctx[key] = value; }); }); diff --git a/src/lib/programs/__tests__/web-analytics-doctor-detect.test.ts b/src/lib/programs/__tests__/web-analytics-doctor-detect.test.ts index 9b47194c..2d689c11 100644 --- a/src/lib/programs/__tests__/web-analytics-doctor-detect.test.ts +++ b/src/lib/programs/__tests__/web-analytics-doctor-detect.test.ts @@ -30,12 +30,12 @@ function writePackageJson( describe('detectWebAnalyticsPrerequisites', () => { let tmpDir: string; let ctx: Record; - let setCtx: jest.Mock; + let setCtx: Mock; beforeEach(() => { tmpDir = makeTmpDir(); ctx = {}; - setCtx = jest.fn((key: string, value: unknown) => { + setCtx = vi.fn((key: string, value: unknown) => { ctx[key] = value; }); }); diff --git a/src/lib/task-stream/__tests__/posthog-destination.test.ts b/src/lib/task-stream/__tests__/posthog-destination.test.ts index acaf7454..266b18b3 100644 --- a/src/lib/task-stream/__tests__/posthog-destination.test.ts +++ b/src/lib/task-stream/__tests__/posthog-destination.test.ts @@ -29,9 +29,9 @@ function makeResponse( }); } -function makeFetch(responses: Array): jest.Mock { +function makeFetch(responses: Array): Mock { let i = 0; - return jest.fn(() => { + return vi.fn(() => { const next = responses[i++]; if (next instanceof Error) return Promise.reject(next); if (!next) return Promise.resolve(makeResponse(500)); @@ -84,7 +84,7 @@ describe('PostHogDestination', () => { makeResponse(201), makeResponse(201), ]); - const onError = jest.fn(); + const onError = vi.fn(); const dest = new PostHogDestination({ getCredentials: () => SAMPLE_CREDS, fetchImpl, @@ -123,10 +123,10 @@ describe('PostHogDestination', () => { makeResponse(500), makeResponse(500), ]); - const sleep: jest.Mock, [number]> = jest.fn((_ms: number) => + const sleep: Mock<(ms: number) => Promise> = vi.fn((_ms: number) => Promise.resolve(), ); - const onError = jest.fn(); + const onError = vi.fn(); const dest = new PostHogDestination({ getCredentials: () => SAMPLE_CREDS, fetchImpl, @@ -151,10 +151,10 @@ describe('PostHogDestination', () => { new Error('ECONNREFUSED'), new Error('ECONNREFUSED'), ]); - const sleep: jest.Mock, [number]> = jest.fn((_ms: number) => + const sleep: Mock<(ms: number) => Promise> = vi.fn((_ms: number) => Promise.resolve(), ); - const onError = jest.fn(); + const onError = vi.fn(); const dest = new PostHogDestination({ getCredentials: () => SAMPLE_CREDS, fetchImpl, @@ -170,7 +170,7 @@ describe('PostHogDestination', () => { it('5xx succeeds on retry', async () => { const fetchImpl = makeFetch([makeResponse(503), makeResponse(201)]); - const sleep: jest.Mock, [number]> = jest.fn((_ms: number) => + const sleep: Mock<(ms: number) => Promise> = vi.fn((_ms: number) => Promise.resolve(), ); const dest = new PostHogDestination({ @@ -190,10 +190,10 @@ describe('PostHogDestination', () => { makeResponse(429, { headers: { 'Retry-After': '1' } }), makeResponse(429, { headers: { 'Retry-After': '1' } }), ]); - const sleep: jest.Mock, [number]> = jest.fn((_ms: number) => + const sleep: Mock<(ms: number) => Promise> = vi.fn((_ms: number) => Promise.resolve(), ); - const onError = jest.fn(); + const onError = vi.fn(); const dest = new PostHogDestination({ getCredentials: () => SAMPLE_CREDS, fetchImpl, @@ -215,7 +215,7 @@ describe('PostHogDestination', () => { makeResponse(429, { headers: { 'Retry-After': '1' } }), makeResponse(201), ]); - const sleep: jest.Mock, [number]> = jest.fn((_ms: number) => + const sleep: Mock<(ms: number) => Promise> = vi.fn((_ms: number) => Promise.resolve(), ); const dest = new PostHogDestination({ @@ -235,7 +235,7 @@ describe('PostHogDestination', () => { makeResponse(400, { body: 'invalid run_phase' }), makeResponse(201), ]); - const onError = jest.fn(); + const onError = vi.fn(); const dest = new PostHogDestination({ getCredentials: () => SAMPLE_CREDS, fetchImpl, @@ -258,7 +258,7 @@ describe('PostHogDestination', () => { new Error('boom'), new Error('boom'), ]); - const onError = jest.fn(); + const onError = vi.fn(); const dest = new PostHogDestination({ getCredentials: () => SAMPLE_CREDS, fetchImpl, @@ -293,7 +293,7 @@ describe('PostHogDestination', () => { makeResponse(429, { headers: { 'Retry-After': '999999' } }), makeResponse(201), ]); - const sleep: jest.Mock, [number]> = jest.fn((_ms: number) => + const sleep: Mock<(ms: number) => Promise> = vi.fn((_ms: number) => Promise.resolve(), ); const dest = new PostHogDestination({ @@ -311,7 +311,7 @@ describe('PostHogDestination', () => { it('treats 200 (upsert update) as success, same as 201 (created)', async () => { const fetchImpl = makeFetch([makeResponse(200)]); - const onError = jest.fn(); + const onError = vi.fn(); const dest = new PostHogDestination({ getCredentials: () => SAMPLE_CREDS, fetchImpl, diff --git a/src/lib/task-stream/__tests__/task-stream-push.test.ts b/src/lib/task-stream/__tests__/task-stream-push.test.ts index b2334830..ebdc3c61 100644 --- a/src/lib/task-stream/__tests__/task-stream-push.test.ts +++ b/src/lib/task-stream/__tests__/task-stream-push.test.ts @@ -72,7 +72,7 @@ function createMockDestination(name = 'test'): TaskStreamDestination & { return { name, calls, - send: jest.fn((event: StreamEvent, payload: TaskStreamUpdate) => { + send: vi.fn((event: StreamEvent, payload: TaskStreamUpdate) => { calls.push([event, payload]); return Promise.resolve(); }), @@ -98,7 +98,7 @@ function createPush( describe('TaskStreamPush', () => { afterEach(() => { - jest.useRealTimers(); + vi.useRealTimers(); }); // ── Existing event-sequencing behaviour ──────────────────────── @@ -242,7 +242,7 @@ describe('TaskStreamPush', () => { const good = createMockDestination('good'); const bad: TaskStreamDestination = { name: 'bad', - send: jest.fn(() => Promise.reject(new Error('network down'))), + send: vi.fn(() => Promise.reject(new Error('network down'))), }; const push = new TaskStreamPush({ store, @@ -260,7 +260,7 @@ describe('TaskStreamPush', () => { const store = createMockStore(); const bad: TaskStreamDestination = { name: 'bad', - send: jest.fn(() => Promise.reject(new Error('fail'))), + send: vi.fn(() => Promise.reject(new Error('fail'))), }; const push = new TaskStreamPush({ store, @@ -277,17 +277,17 @@ describe('TaskStreamPush', () => { describe('spec: deterministic session_id', () => { it('session_id is locked at construction with second-precision ISO', async () => { const fixedNow = new Date('2026-05-20T17:00:00.123Z'); - jest.useFakeTimers(); - jest.setSystemTime(fixedNow); + vi.useFakeTimers(); + vi.setSystemTime(fixedNow); const store = createMockStore(); const { push, dest } = createPush(store); // Even if real time advances before the first push, session_id // is fixed. - jest.setSystemTime(new Date('2026-05-20T17:05:00.999Z')); + vi.setSystemTime(new Date('2026-05-20T17:05:00.999Z')); await push.push(); - jest.setSystemTime(new Date('2026-05-20T17:10:00.000Z')); + vi.setSystemTime(new Date('2026-05-20T17:10:00.000Z')); await push.push(); expect(dest.calls[0][1].session_id).toBe( @@ -296,7 +296,7 @@ describe('TaskStreamPush', () => { expect(dest.calls[1][1].session_id).toBe(dest.calls[0][1].session_id); expect(dest.calls[0][1].started_at).toBe('2026-05-20T17:00:00Z'); - jest.useRealTimers(); + vi.useRealTimers(); }); }); @@ -340,7 +340,7 @@ describe('TaskStreamPush', () => { describe('spec: debounces task updates', () => { it('five rapid emits in the running phase produce one HTTP call with the latest task list', async () => { - jest.useFakeTimers(); + vi.useFakeTimers(); const store = createMockStore({ runPhase: RunPhase.Running }); const { push, dest } = createPush(store); push.attach(); @@ -354,12 +354,12 @@ describe('TaskStreamPush', () => { // Five rapid task emits within 100ms — none fire synchronously. for (let i = 1; i <= 5; i++) { store._setAndEmit({ tasks: tasksUpTo(i) }); - await jest.advanceTimersByTimeAsync(20); + await vi.advanceTimersByTimeAsync(20); } expect(dest.calls).toHaveLength(1); // Advance past the 250ms debounce window — one push with the latest list. - await jest.advanceTimersByTimeAsync(250); + await vi.advanceTimersByTimeAsync(250); await flushMicrotasks(); expect(dest.calls).toHaveLength(2); @@ -369,7 +369,7 @@ describe('TaskStreamPush', () => { describe('spec: phase change bypasses debounce', () => { it('Running → Completed produces an immediate push', async () => { - jest.useFakeTimers(); + vi.useFakeTimers(); const store = createMockStore({ runPhase: RunPhase.Running }); const { push, dest } = createPush(store); push.attach(); @@ -401,7 +401,7 @@ describe('TaskStreamPush', () => { } = { name: 'slow', calls: [], - send: jest.fn((event: StreamEvent, payload: TaskStreamUpdate) => { + send: vi.fn((event: StreamEvent, payload: TaskStreamUpdate) => { dest.calls.push([event, payload]); if (dest.calls.length === 1) { return new Promise((resolve) => { @@ -464,11 +464,11 @@ describe('TaskStreamPush', () => { describe('spec: shutdown honours timeout', () => { it('shutdown returns even when destination hangs forever', async () => { - jest.useFakeTimers(); + vi.useFakeTimers(); const store = createMockStore({ runPhase: RunPhase.Completed }); const hanging: TaskStreamDestination = { name: 'hangs', - send: jest.fn(() => new Promise(() => undefined)), + send: vi.fn(() => new Promise(() => undefined)), }; const push = new TaskStreamPush({ store, @@ -477,9 +477,9 @@ describe('TaskStreamPush', () => { }); const shutdown = push.shutdown(500); - jest.advanceTimersByTime(500); + vi.advanceTimersByTime(500); await expect(shutdown).resolves.toBeUndefined(); - jest.useRealTimers(); + vi.useRealTimers(); }); }); }); diff --git a/src/steps/add-mcp-server-to-clients/clients/__tests__/claude-code.test.ts b/src/steps/add-mcp-server-to-clients/clients/__tests__/claude-code.test.ts index 63611489..432b28d3 100644 --- a/src/steps/add-mcp-server-to-clients/clients/__tests__/claude-code.test.ts +++ b/src/steps/add-mcp-server-to-clients/clients/__tests__/claude-code.test.ts @@ -1,28 +1,28 @@ import { ClaudeCodeMCPClient } from '@steps/add-mcp-server-to-clients/clients/claude-code'; +import { execSync } from 'child_process'; +import { analytics } from '@utils/analytics'; -jest.mock('child_process', () => ({ - execSync: jest.fn(), +vi.mock('child_process', () => ({ + execSync: vi.fn(), })); -jest.mock('fs', () => ({ - existsSync: jest.fn().mockReturnValue(false), +vi.mock('fs', () => ({ + existsSync: vi.fn().mockReturnValue(false), })); -jest.mock('../../../../utils/analytics', () => ({ - analytics: { captureException: jest.fn() }, +vi.mock('../../../../utils/analytics', () => ({ + analytics: { captureException: vi.fn() }, })); -jest.mock('../../../../utils/debug', () => ({ - debug: jest.fn(), +vi.mock('../../../../utils/debug', () => ({ + debug: vi.fn(), })); describe('ClaudeCodeMCPClient — plugin methods', () => { - const { execSync } = require('child_process'); - const { analytics } = require('@utils/analytics'); - const execSyncMock = execSync as jest.Mock; + const execSyncMock = execSync as Mock; beforeEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); // Make binary discoverable via PATH by default execSyncMock.mockImplementation((cmd: string) => { if (cmd === 'command -v claude') return Buffer.from(''); diff --git a/src/steps/add-mcp-server-to-clients/clients/__tests__/claude-web.test.ts b/src/steps/add-mcp-server-to-clients/clients/__tests__/claude-web.test.ts index 646ee74d..0a647896 100644 --- a/src/steps/add-mcp-server-to-clients/clients/__tests__/claude-web.test.ts +++ b/src/steps/add-mcp-server-to-clients/clients/__tests__/claude-web.test.ts @@ -2,9 +2,9 @@ import opn from 'opn'; import { ClaudeWebMCPClient } from '@steps/add-mcp-server-to-clients/clients/claude-web'; import { isBrowserFinishable } from '@steps/add-mcp-server-to-clients/browser-client'; -jest.mock('opn', () => jest.fn(() => Promise.resolve())); +vi.mock('opn', () => ({ default: vi.fn(() => Promise.resolve()) })); -const opnMock = opn as unknown as jest.Mock; +const opnMock = opn as unknown as Mock; const CONNECTOR_URL = 'https://claude.ai/directory/connectors/posthog'; @@ -13,7 +13,7 @@ describe('ClaudeWebMCPClient', () => { beforeEach(() => { client = new ClaudeWebMCPClient(); - jest.clearAllMocks(); + vi.clearAllMocks(); }); it('has the expected name and connector metadata', () => { diff --git a/src/steps/add-mcp-server-to-clients/clients/__tests__/claude.test.ts b/src/steps/add-mcp-server-to-clients/clients/__tests__/claude.test.ts index 626dbf5e..6e2cca39 100644 --- a/src/steps/add-mcp-server-to-clients/clients/__tests__/claude.test.ts +++ b/src/steps/add-mcp-server-to-clients/clients/__tests__/claude.test.ts @@ -3,26 +3,29 @@ import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; import { ClaudeMCPClient } from '@steps/add-mcp-server-to-clients/clients/claude'; -import { getDefaultServerConfig } from '@steps/add-mcp-server-to-clients/defaults'; +import { + getDefaultServerConfig, + DefaultMCPClientConfig, +} from '@steps/add-mcp-server-to-clients/defaults'; -jest.mock('fs', () => ({ +vi.mock('fs', () => ({ promises: { - mkdir: jest.fn(), - readFile: jest.fn(), - writeFile: jest.fn(), + mkdir: vi.fn(), + readFile: vi.fn(), + writeFile: vi.fn(), }, - existsSync: jest.fn(), + existsSync: vi.fn(), })); -jest.mock('os', () => ({ - homedir: jest.fn(), +vi.mock('os', () => ({ + homedir: vi.fn(), })); -jest.mock('../../defaults', () => ({ +vi.mock('../../defaults', () => ({ DefaultMCPClientConfig: { - parse: jest.fn(), + parse: vi.fn(), }, - getDefaultServerConfig: jest.fn(), + getDefaultServerConfig: vi.fn(), })); describe('ClaudeMCPClient', () => { @@ -35,26 +38,25 @@ describe('ClaudeMCPClient', () => { env: { POSTHOG_AUTH_HEADER: `Bearer ${mockApiKey}` }, }; - const mkdirMock = fs.promises.mkdir as jest.Mock; - const readFileMock = fs.promises.readFile as jest.Mock; - const writeFileMock = fs.promises.writeFile as jest.Mock; - const existsSyncMock = fs.existsSync as jest.Mock; - const homedirMock = os.homedir as jest.Mock; - const getDefaultServerConfigMock = getDefaultServerConfig as jest.Mock; + const mkdirMock = fs.promises.mkdir as Mock; + const readFileMock = fs.promises.readFile as Mock; + const writeFileMock = fs.promises.writeFile as Mock; + const existsSyncMock = fs.existsSync as Mock; + const homedirMock = os.homedir as Mock; + const getDefaultServerConfigMock = getDefaultServerConfig as Mock; const originalPlatform = process.platform; beforeEach(() => { client = new ClaudeMCPClient(); - jest.clearAllMocks(); + vi.clearAllMocks(); homedirMock.mockReturnValue(mockHomeDir); getDefaultServerConfigMock.mockReturnValue(mockServerConfig); // Mock the Zod schema parse method - const { - DefaultMCPClientConfig, - } = require('@steps/add-mcp-server-to-clients/defaults'); - DefaultMCPClientConfig.parse.mockImplementation((data: any) => data); + (DefaultMCPClientConfig.parse as Mock).mockImplementation( + (data: any) => data, + ); }); afterEach(() => { diff --git a/src/steps/add-mcp-server-to-clients/clients/__tests__/codex.test.ts b/src/steps/add-mcp-server-to-clients/clients/__tests__/codex.test.ts index 07208d87..95126637 100644 --- a/src/steps/add-mcp-server-to-clients/clients/__tests__/codex.test.ts +++ b/src/steps/add-mcp-server-to-clients/clients/__tests__/codex.test.ts @@ -1,33 +1,32 @@ import { CodexMCPClient } from '@steps/add-mcp-server-to-clients/clients/codex'; +import { execSync, spawnSync } from 'node:child_process'; +import * as fs from 'node:fs'; +import { analytics } from '@utils/analytics'; -jest.mock('node:child_process', () => ({ - execSync: jest.fn(), - spawnSync: jest.fn(), +vi.mock('node:child_process', () => ({ + execSync: vi.fn(), + spawnSync: vi.fn(), })); -jest.mock('node:fs', () => ({ - existsSync: jest.fn(), - readFileSync: jest.fn(), - rmSync: jest.fn(), +vi.mock('node:fs', () => ({ + existsSync: vi.fn(), + readFileSync: vi.fn(), + rmSync: vi.fn(), })); -jest.mock('../../../../utils/analytics', () => ({ - analytics: { captureException: jest.fn() }, +vi.mock('../../../../utils/analytics', () => ({ + analytics: { captureException: vi.fn() }, })); describe('CodexMCPClient', () => { - const { execSync, spawnSync } = require('node:child_process'); - const fs = require('node:fs'); - const analytics = require('@utils/analytics').analytics; - - const spawnSyncMock = spawnSync as jest.Mock; - const execSyncMock = execSync as jest.Mock; - const readFileSyncMock = fs.readFileSync as jest.Mock; + const spawnSyncMock = spawnSync as Mock; + const execSyncMock = execSync as Mock; + const readFileSyncMock = fs.readFileSync as Mock; const CODEX_PATH = '/usr/local/bin/codex'; beforeEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); // Default: codex found via command -v execSyncMock.mockReturnValue(Buffer.from(CODEX_PATH + '\n')); }); @@ -148,7 +147,6 @@ describe('CodexMCPClient', () => { }); it('clears stale cache and retries when marketplace is already added from a different source', async () => { - const { rmSync } = require('node:fs'); spawnSyncMock .mockReturnValueOnce({ status: 1, @@ -158,7 +156,7 @@ describe('CodexMCPClient', () => { .mockReturnValueOnce({ status: 0, stderr: '' }); const client = new CodexMCPClient(); await expect(client.installPlugin()).resolves.toEqual({ success: true }); - expect(rmSync).toHaveBeenCalledWith( + expect(fs.rmSync).toHaveBeenCalledWith( expect.stringContaining('marketplaces/posthog'), { recursive: true, force: true }, ); diff --git a/src/steps/upload-environment-variables/providers/__tests__/vercel.test.ts b/src/steps/upload-environment-variables/providers/__tests__/vercel.test.ts index 7966c9d5..97c8f73b 100644 --- a/src/steps/upload-environment-variables/providers/__tests__/vercel.test.ts +++ b/src/steps/upload-environment-variables/providers/__tests__/vercel.test.ts @@ -2,8 +2,8 @@ import { VercelEnvironmentProvider } from '@steps/upload-environment-variables/p import * as fs from 'fs'; import * as child_process from 'child_process'; -jest.mock('fs'); -jest.mock('child_process'); +vi.mock('fs'); +vi.mock('child_process'); const mockOptions = { installDir: '/tmp/project' }; @@ -12,17 +12,17 @@ describe('VercelEnvironmentProvider', () => { beforeEach(() => { provider = new VercelEnvironmentProvider(mockOptions as any); - jest.clearAllMocks(); + vi.clearAllMocks(); }); it('should detect Vercel CLI, project link, and authentication', async () => { - (child_process.execSync as jest.Mock).mockReturnValue(undefined); - (fs.existsSync as jest.Mock).mockImplementation((p: string) => { + (child_process.execSync as Mock).mockReturnValue(undefined); + (fs.existsSync as Mock).mockImplementation((p: string) => { if (p.endsWith('.vercel')) return true; if (p.endsWith('project.json')) return true; return false; }); - (child_process.spawnSync as jest.Mock).mockReturnValue({ + (child_process.spawnSync as Mock).mockReturnValue({ stdout: 'testuser', stderr: '', status: 0, @@ -32,22 +32,22 @@ describe('VercelEnvironmentProvider', () => { }); it('should return false if Vercel CLI is missing', async () => { - (child_process.execSync as jest.Mock).mockImplementation(() => { + (child_process.execSync as Mock).mockImplementation(() => { throw new Error(); }); await expect(provider.detect()).resolves.toBe(false); }); it('should return false if project is not linked', async () => { - (child_process.execSync as jest.Mock).mockReturnValue(undefined); - (fs.existsSync as jest.Mock).mockReturnValue(false); + (child_process.execSync as Mock).mockReturnValue(undefined); + (fs.existsSync as Mock).mockReturnValue(false); await expect(provider.detect()).resolves.toBe(false); }); it('should return false if not authenticated', async () => { - (child_process.execSync as jest.Mock).mockReturnValue(undefined); - (fs.existsSync as jest.Mock).mockReturnValue(true); - (child_process.spawnSync as jest.Mock).mockReturnValue({ + (child_process.execSync as Mock).mockReturnValue(undefined); + (fs.existsSync as Mock).mockReturnValue(true); + (child_process.spawnSync as Mock).mockReturnValue({ stdout: 'Log in to Vercel', stderr: '', status: 0, @@ -56,21 +56,21 @@ describe('VercelEnvironmentProvider', () => { }); it('should return false if env var already exists', async () => { - const stdinMock = { write: jest.fn(), end: jest.fn() }; + const stdinMock = { write: vi.fn(), end: vi.fn() }; let closeCallback: ((code: number) => void) | undefined; - const onMock = jest.fn((event, cb) => { + const onMock = vi.fn((event, cb) => { if (event === 'close') closeCallback = cb; }); // Simulate a process with a writable stderr stream let stderrListener: ((data: Buffer | string) => void) | undefined; const stderr = { - on: jest.fn((event, cb) => { + on: vi.fn((event, cb) => { if (event === 'data') stderrListener = cb; }), }; - (child_process.spawn as jest.Mock).mockReturnValue({ + (child_process.spawn as Mock).mockReturnValue({ stdin: stdinMock, on: onMock, stderr, @@ -86,7 +86,7 @@ describe('VercelEnvironmentProvider', () => { }); it('should attempt to upload environment variables', async () => { - (child_process.spawn as jest.Mock).mockReturnValue({}); + (child_process.spawn as Mock).mockReturnValue({}); await provider.uploadEnvVars({ FOO: 'bar' }); diff --git a/src/ui/tui/__tests__/mcp-installer.test.ts b/src/ui/tui/__tests__/mcp-installer.test.ts index 906bff45..f0bf9412 100644 --- a/src/ui/tui/__tests__/mcp-installer.test.ts +++ b/src/ui/tui/__tests__/mcp-installer.test.ts @@ -1,36 +1,38 @@ import { createMcpInstaller } from '@ui/tui/services/mcp-installer'; - -jest.mock('../../../steps/add-mcp-server-to-clients/index.js', () => ({ - getSupportedClients: jest.fn(), - getInstalledClients: jest.fn(), - removeMCPServer: jest.fn(), - getSupportedPluginClients: jest.fn(), - installPlugins: jest.fn(), +import * as mcpModuleReal from '@steps/add-mcp-server-to-clients/index'; +import { analytics } from '@utils/analytics'; + +// The module is mocked below. Expose its exports as plain Mocks so the tests +// can drive them with lightweight partial fixtures (the same loose access the +// previous `require()` form gave) while still typing the .mock* helpers. +const mcpModule = mcpModuleReal as unknown as Record; + +vi.mock('../../../steps/add-mcp-server-to-clients/index.js', () => ({ + getSupportedClients: vi.fn(), + getInstalledClients: vi.fn(), + removeMCPServer: vi.fn(), + getSupportedPluginClients: vi.fn(), + installPlugins: vi.fn(), })); -jest.mock('../../../steps/add-mcp-server-to-clients/defaults.js', () => ({ +vi.mock('../../../steps/add-mcp-server-to-clients/defaults.js', () => ({ ALL_FEATURE_VALUES: ['feature-a'], })); -jest.mock('../../../utils/debug.js', () => ({ - logToFile: jest.fn(), +vi.mock('../../../utils/debug.js', () => ({ + logToFile: vi.fn(), })); -jest.mock('../../../utils/analytics.js', () => ({ - analytics: { wizardCapture: jest.fn() }, +vi.mock('../../../utils/analytics.js', () => ({ + analytics: { wizardCapture: vi.fn() }, })); describe('createMcpInstaller — installPlugins', () => { - // eslint-disable-next-line @typescript-eslint/no-require-imports - const mcpModule = require('@steps/add-mcp-server-to-clients/index'); - // eslint-disable-next-line @typescript-eslint/no-require-imports - const { analytics } = require('@utils/analytics'); - const mockClaudeClient = { name: 'Claude Code' }; const mockCursorClient = { name: 'Cursor' }; beforeEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); mcpModule.getSupportedClients.mockResolvedValue([ mockClaudeClient, mockCursorClient, @@ -117,11 +119,8 @@ describe('createMcpInstaller — installPlugins', () => { }); describe('createMcpInstaller — detectClients', () => { - // eslint-disable-next-line @typescript-eslint/no-require-imports - const mcpModule = require('@steps/add-mcp-server-to-clients/index'); - beforeEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); it('surfaces the finish note for browser-finishable clients only', async () => { diff --git a/src/ui/tui/__tests__/store.test.ts b/src/ui/tui/__tests__/store.test.ts index 4c2d0447..455408c6 100644 --- a/src/ui/tui/__tests__/store.test.ts +++ b/src/ui/tui/__tests__/store.test.ts @@ -18,18 +18,18 @@ import { buildSession } from '@lib/wizard-session'; import { Integration } from '@lib/constants'; import { analytics } from '@utils/analytics'; -jest.mock('../../../utils/analytics.js', () => ({ +vi.mock('../../../utils/analytics.js', () => ({ analytics: { - capture: jest.fn(), - wizardCapture: jest.fn(), - setTag: jest.fn(), - shutdown: jest.fn().mockResolvedValue(undefined), + capture: vi.fn(), + wizardCapture: vi.fn(), + setTag: vi.fn(), + shutdown: vi.fn().mockResolvedValue(undefined), }, - sessionProperties: jest.fn(() => ({})), + sessionProperties: vi.fn(() => ({})), })); -jest.mock('../../../lib/health-checks/readiness.js', () => ({ - evaluateWizardReadiness: jest.fn().mockResolvedValue({ +vi.mock('../../../lib/health-checks/readiness.js', () => ({ + evaluateWizardReadiness: vi.fn().mockResolvedValue({ decision: 'yes', health: {}, reasons: [], @@ -46,11 +46,10 @@ function createStore(program?: ProgramId): WizardStore { return new WizardStore(program); } -const wizardCaptureMock = analytics.wizardCapture as jest.Mock; -const evaluateWizardReadinessMock = - evaluateWizardReadiness as jest.MockedFunction< - typeof evaluateWizardReadiness - >; +const wizardCaptureMock = analytics.wizardCapture as Mock; +const evaluateWizardReadinessMock = evaluateWizardReadiness as MockedFunction< + typeof evaluateWizardReadiness +>; async function flushMicrotasks(): Promise { await Promise.resolve(); @@ -59,7 +58,7 @@ async function flushMicrotasks(): Promise { describe('WizardStore', () => { beforeEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); evaluateWizardReadinessMock.mockResolvedValue({ decision: WizardReadiness.Yes, health: {} as never, @@ -99,7 +98,7 @@ describe('WizardStore', () => { describe('change notification', () => { it('emitChange increments version and notifies subscribers', () => { const store = createStore(); - const listener = jest.fn(); + const listener = vi.fn(); store.subscribe(listener); store.emitChange(); @@ -122,7 +121,7 @@ describe('WizardStore', () => { describe('subscribe / getSnapshot', () => { it('subscribe registers a listener that fires on change', () => { const store = createStore(); - const cb = jest.fn(); + const cb = vi.fn(); store.subscribe(cb); store.emitChange(); @@ -131,7 +130,7 @@ describe('WizardStore', () => { it('subscribe returns an unsubscribe function', () => { const store = createStore(); - const cb = jest.fn(); + const cb = vi.fn(); const unsub = store.subscribe(cb); unsub(); @@ -148,7 +147,7 @@ describe('WizardStore', () => { it('is compatible with useSyncExternalStore contract', () => { const store = createStore(); - const cb = jest.fn(); + const cb = vi.fn(); const unsub = store.subscribe(cb); const v1 = store.getSnapshot(); @@ -166,7 +165,7 @@ describe('WizardStore', () => { describe('session setters', () => { it('completeSetup sets setupConfirmed and resolves intro gate', async () => { const store = createStore(); - const cb = jest.fn(); + const cb = vi.fn(); store.subscribe(cb); store.completeSetup(); @@ -277,7 +276,7 @@ describe('WizardStore', () => { it('every setter emits exactly one change event', () => { const store = createStore(); - const cb = jest.fn(); + const cb = vi.fn(); store.subscribe(cb); store.completeSetup(); @@ -473,7 +472,7 @@ describe('WizardStore', () => { it('pushOverlay emits change and increments version', () => { const store = createStore(); - const cb = jest.fn(); + const cb = vi.fn(); store.subscribe(cb); store.pushOverlay(Overlay.SettingsOverride); @@ -486,7 +485,7 @@ describe('WizardStore', () => { const store = createStore(); store.pushOverlay(Overlay.SettingsOverride); - const cb = jest.fn(); + const cb = vi.fn(); store.subscribe(cb); store.popOverlay(); @@ -602,7 +601,7 @@ describe('WizardStore', () => { it('pushStatus emits change', () => { const store = createStore(); - const cb = jest.fn(); + const cb = vi.fn(); store.subscribe(cb); store.pushStatus('msg'); @@ -666,7 +665,7 @@ describe('WizardStore', () => { { label: 'Install SDK', status: TaskStatus.Pending, done: false }, ]); - const cb = jest.fn(); + const cb = vi.fn(); store.subscribe(cb); store.updateTask(99, true); @@ -741,7 +740,7 @@ describe('WizardStore', () => { it('emits change', () => { const store = createStore(); - const cb = jest.fn(); + const cb = vi.fn(); store.subscribe(cb); store.syncTodos([{ content: 'task', status: 'pending' }]); expect(cb).toHaveBeenCalled(); @@ -768,7 +767,7 @@ describe('WizardStore', () => { describe('concurrent mutations', () => { it('rapid-fire setters each increment version by 1', () => { const store = createStore(); - const cb = jest.fn(); + const cb = vi.fn(); store.subscribe(cb); store.completeSetup(); @@ -892,7 +891,7 @@ describe('WizardStore', () => { describe('multiple subscribers', () => { it('supports many concurrent subscribers', () => { const store = createStore(); - const callbacks = Array.from({ length: 50 }, () => jest.fn()); + const callbacks = Array.from({ length: 50 }, () => vi.fn()); const unsubs = callbacks.map((cb) => store.subscribe(cb)); store.emitChange(); @@ -909,7 +908,7 @@ describe('WizardStore', () => { it('double-unsubscribe is safe', () => { const store = createStore(); - const cb = jest.fn(); + const cb = vi.fn(); const unsub = store.subscribe(cb); unsub(); @@ -969,7 +968,7 @@ describe('WizardStore', () => { store.setTasks([ { label: 'Task', status: TaskStatus.Pending, done: false }, ]); - const cb = jest.fn(); + const cb = vi.fn(); store.subscribe(cb); store.updateTask(-1, true); expect(cb).not.toHaveBeenCalled(); diff --git a/src/ui/tui/hooks/__tests__/file-watcher.test.ts b/src/ui/tui/hooks/__tests__/file-watcher.test.ts index 5d3cda4b..8a467d25 100644 --- a/src/ui/tui/hooks/__tests__/file-watcher.test.ts +++ b/src/ui/tui/hooks/__tests__/file-watcher.test.ts @@ -23,7 +23,7 @@ describe('startFileWatcher', () => { }); it('fires onUpdate when the watched file becomes valid JSON', async () => { - const onUpdate = jest.fn(); + const onUpdate = vi.fn(); const target = path.join(workdir, 'data.json'); handle = startFileWatcher(target, onUpdate, { @@ -38,7 +38,7 @@ describe('startFileWatcher', () => { }); it('skips re-parsing when mtime is unchanged across polls', async () => { - const onUpdate = jest.fn(); + const onUpdate = vi.fn(); const target = path.join(workdir, 'data.json'); writeFileSync(target, JSON.stringify({ a: 1 })); @@ -53,7 +53,7 @@ describe('startFileWatcher', () => { }); it('handles atomic-rename writes (write to .tmp + rename)', async () => { - const onUpdate = jest.fn(); + const onUpdate = vi.fn(); const target = path.join(workdir, 'data.json'); writeFileSync(target, JSON.stringify({ v: 1 })); @@ -71,7 +71,7 @@ describe('startFileWatcher', () => { }); it('polls and attaches once the file appears later', async () => { - const onUpdate = jest.fn(); + const onUpdate = vi.fn(); const target = path.join(workdir, 'late.json'); handle = startFileWatcher(target, onUpdate, { @@ -89,7 +89,7 @@ describe('startFileWatcher', () => { }); it('swallows invalid JSON without throwing', async () => { - const onUpdate = jest.fn(); + const onUpdate = vi.fn(); const target = path.join(workdir, 'data.json'); writeFileSync(target, '{not valid'); @@ -100,7 +100,7 @@ describe('startFileWatcher', () => { }); it('stop() halts polling and watchers', async () => { - const onUpdate = jest.fn(); + const onUpdate = vi.fn(); const target = path.join(workdir, 'data.json'); writeFileSync(target, JSON.stringify({ a: 1 })); @@ -116,7 +116,7 @@ describe('startFileWatcher', () => { }); it('survives the file being deleted and recreated', async () => { - const onUpdate = jest.fn(); + const onUpdate = vi.fn(); const target = path.join(workdir, 'data.json'); writeFileSync(target, JSON.stringify({ v: 1 })); diff --git a/src/utils/__tests__/analytics.test.ts b/src/utils/__tests__/analytics.test.ts index c968cd1e..3c150cd7 100644 --- a/src/utils/__tests__/analytics.test.ts +++ b/src/utils/__tests__/analytics.test.ts @@ -4,25 +4,25 @@ import { v4 as uuidv4 } from 'uuid'; import { ANALYTICS_TEAM_TAG } from '@lib/constants'; import type { ApiUser } from '@lib/api'; -jest.mock('posthog-node'); -jest.mock('uuid'); +vi.mock('posthog-node'); +vi.mock('uuid'); -const mockUuidv4 = uuidv4 as jest.MockedFunction; -const MockedPostHog = PostHog as jest.MockedClass; +const mockUuidv4 = uuidv4 as unknown as MockedFunction; +const MockedPostHog = PostHog as MockedClass; describe('Analytics', () => { let analytics: Analytics; - let mockPostHogInstance: jest.Mocked; + let mockPostHogInstance: Mocked; beforeEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); mockUuidv4.mockReturnValue('test-uuid' as any); mockPostHogInstance = { - capture: jest.fn(), - captureException: jest.fn(), - alias: jest.fn(), - shutdown: jest.fn().mockResolvedValue(undefined), + capture: vi.fn(), + captureException: vi.fn(), + alias: vi.fn(), + shutdown: vi.fn().mockResolvedValue(undefined), } as any; MockedPostHog.mockImplementation(() => mockPostHogInstance); diff --git a/src/utils/__tests__/provisioning.test.ts b/src/utils/__tests__/provisioning.test.ts index f89f2888..1ab56a79 100644 --- a/src/utils/__tests__/provisioning.test.ts +++ b/src/utils/__tests__/provisioning.test.ts @@ -1,17 +1,17 @@ import axios from 'axios'; import { provisionNewAccount } from '@utils/provisioning'; -jest.mock('axios'); -jest.mock('../debug', () => ({ logToFile: jest.fn() })); -jest.mock('../analytics', () => ({ - analytics: { captureException: jest.fn() }, +vi.mock('axios'); +vi.mock('../debug', () => ({ logToFile: vi.fn() })); +vi.mock('../analytics', () => ({ + analytics: { captureException: vi.fn() }, })); -const mockedAxios = axios as jest.Mocked; +const mockedAxios = axios as Mocked; describe('provisionNewAccount', () => { beforeEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); it('completes the full PKCE flow and returns credentials', async () => { diff --git a/tsconfig.json b/tsconfig.json index 882ce510..2c468136 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,7 +2,7 @@ "extends": "./tsconfig.build.json", "compilerOptions": { - "types": ["node", "jest"], + "types": ["node"], "typeRoots": ["./node_modules/@types", "./types"], "declaration": true, "resolveJsonModule": true, @@ -14,15 +14,19 @@ "index.ts", "bin.ts", "tsdown.config.ts", + "vitest.config.ts", "src/lib/**/*", "spec/**/*", "src/**/*", "test/**/*", - "e2e-tests/**/*", "types/**/*" ], "exclude": [ - "e2e-tests/test-applications/**/*" + // e2e-tests is a standalone package (own package.json, own jest + ts-jest, + // excluded from the pnpm workspace) and typechecks itself in its own + // context. The root unit-test typecheck no longer carries jest types, so it + // must not pull e2e's jest-based config/specs into scope. + "e2e-tests/**/*" ], "ts-node": { "files": true diff --git a/types/vitest-global-types.d.ts b/types/vitest-global-types.d.ts new file mode 100644 index 00000000..a9d44c22 --- /dev/null +++ b/types/vitest-global-types.d.ts @@ -0,0 +1,31 @@ +/* eslint-disable @typescript-eslint/no-explicit-any -- mock helper type + constraints mirror vitest's own `(...args: any[]) => any` procedure shape. */ +// Makes vitest's global test APIs (describe/it/expect/vi/…) ambiently available +// project-wide, matching `test.globals: true` in vitest.config.ts. Referenced +// here rather than via tsconfig `types` because the project's `typeRoots` is +// constrained and can't resolve the `vitest/globals` subpath export. +/// + +// Bridges jest's ambient mock helper types to vitest's equivalents so test +// files can keep using the bare `Mock`, `Mocked`, ... names (migrated from +// `jest.Mock`, `jest.Mocked`, ...) without a per-file import from 'vitest'. +import type { + Mock as ViMock, + Mocked as ViMocked, + MockedFunction as ViMockedFunction, + MockedClass as ViMockedClass, + MockInstance as ViMockInstance, +} from 'vitest'; + +declare global { + type Mock< + T extends (...args: any[]) => any = (...args: any[]) => any, + > = ViMock; + type Mocked = ViMocked; + type MockedFunction any> = ViMockedFunction; + type MockedClass any> = + ViMockedClass; + type MockInstance< + T extends (...args: any[]) => any = (...args: any[]) => any, + > = ViMockInstance; +} diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 00000000..a91ac723 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,82 @@ +import * as path from 'path'; +import { defineConfig, type Plugin } from 'vitest/config'; + +const r = (...p: string[]) => path.resolve(__dirname, ...p); + +/** + * The source uses NodeNext-style relative imports that carry a `.js` extension + * (e.g. `import { x } from './foo.js'`) while the files on disk are `.ts`/`.tsx`. + * Vite does not strip the extension on its own, so this plugin retries a failed + * `.js` resolution against the matching TypeScript source. It replaces jest's + * `"^(\\.{1,2}/.*)\\.js$": "$1"` moduleNameMapper entry. + */ +function resolveTsForJs(): Plugin { + return { + name: 'resolve-ts-for-js', + enforce: 'pre', + async resolveId(id, importer, options) { + if (!id.endsWith('.js')) return null; + for (const ext of ['.ts', '.tsx'] as const) { + const candidate = `${id.slice(0, -3)}${ext}`; + const resolved = await this.resolve(candidate, importer, { + ...options, + skipSelf: true, + }); + if (resolved) return resolved; + } + return null; + }, + }; +} + +export default defineConfig({ + plugins: [resolveTsForJs()], + // The source targets the React 19 automatic JSX runtime (tsconfig + // `"jsx": "react-jsx"`); mirror that here so the TUI `.tsx` tests transform. + esbuild: { jsx: 'automatic' }, + resolve: { + alias: [ + // Module mocks — parity with jest's moduleNameMapper. Every import of + // these resolves to the hand-written manual mock; a per-test `vi.mock(...)` + // factory still takes precedence within that test file. + { + find: /^@anthropic-ai\/claude-agent-sdk$/, + replacement: r('__mocks__/@anthropic-ai/claude-agent-sdk.ts'), + }, + { + find: /^@posthog\/warlock$/, + replacement: r('__mocks__/@posthog/warlock.ts'), + }, + { find: /^ink$/, replacement: r('__mocks__/ink.ts') }, + // Path aliases — mirror tsconfig `paths`. + { find: /^@env$/, replacement: r('src/env.ts') }, + { find: /^@lib\/(.*)$/, replacement: `${r('src/lib')}/$1` }, + { find: /^@utils\/(.*)$/, replacement: `${r('src/utils')}/$1` }, + { find: /^@ui$/, replacement: r('src/ui/index.ts') }, + { find: /^@ui\/(.*)$/, replacement: `${r('src/ui')}/$1` }, + { find: /^@steps$/, replacement: r('src/steps/index.ts') }, + { find: /^@steps\/(.*)$/, replacement: `${r('src/steps')}/$1` }, + { find: /^@frameworks\/(.*)$/, replacement: `${r('src/frameworks')}/$1` }, + ], + }, + test: { + globals: true, + environment: 'node', + include: [ + '**/__tests__/**/*.{test,spec}.{js,jsx,ts,tsx}', + '**/__tests__/**/*.{js,jsx,ts,tsx}', + '**/*.{test,spec}.{js,jsx,ts,tsx}', + ], + exclude: [ + '**/node_modules/**', + '**/dist/**', + '**/e2e-tests/**', + '**/*.no-jest.*', + '**/*.d.ts', + ], + coverage: { + provider: 'v8', + exclude: ['dist/**'], + }, + }, +});