From 6fb22bb311b3090df3bab69920326699601c1a4f Mon Sep 17 00:00:00 2001 From: Jiahan Chen Date: Mon, 22 Jun 2026 16:12:49 +0800 Subject: [PATCH] test(frontend): migrate unit tests to rstest (#3703) * test(frontend): migrate unit tests to rstest * docs: updates AGENTS.md * test(frontend): fix rstest lint formatting --- frontend/AGENTS.md | 2 +- frontend/CLAUDE.md | 4 +- frontend/README.md | 2 +- frontend/package.json | 7 +- frontend/pnpm-lock.yaml | 441 ++++++++++-------- frontend/rstest.config.ts | 19 + .../gateway-offline-banner-helpers.test.ts | 2 +- frontend/tests/unit/core/agents/api.test.ts | 10 +- .../tests/unit/core/api/api-client.test.ts | 28 +- .../tests/unit/core/api/stream-mode.test.ts | 2 +- .../tests/unit/core/artifacts/preview.test.ts | 2 +- .../tests/unit/core/artifacts/utils.test.ts | 11 +- .../unit/core/auth/gateway-config.test.ts | 11 +- frontend/tests/unit/core/auth/server.test.ts | 45 +- frontend/tests/unit/core/channels/api.test.ts | 10 +- .../unit/core/channels/connect-poll.test.ts | 41 +- .../core/channels/open-connect-url.test.ts | 18 +- .../unit/core/channels/provider-state.test.ts | 2 +- frontend/tests/unit/core/clipboard.test.ts | 154 +++--- frontend/tests/unit/core/mcp/api.test.ts | 10 +- frontend/tests/unit/core/mcp/hooks.test.ts | 8 +- .../unit/core/messages/usage-model.test.ts | 2 +- .../tests/unit/core/messages/usage.test.ts | 2 +- .../tests/unit/core/messages/utils.test.ts | 2 +- .../tests/unit/core/reasoning-trigger.test.ts | 4 +- .../tests/unit/core/settings/local.test.ts | 2 +- .../unit/core/streamdown/mermaid.test.ts | 2 +- .../unit/core/streamdown/plugins.test.ts | 2 +- .../unit/core/streamdown/preprocess.test.ts | 2 +- .../unit/core/tasks/subtask-result.test.ts | 14 +- frontend/tests/unit/core/threads/api.test.ts | 6 +- .../tests/unit/core/threads/export.test.ts | 2 +- .../tests/unit/core/threads/infinite.test.ts | 2 +- .../unit/core/threads/message-merge.test.ts | 2 +- .../core/threads/thread-search-query.test.ts | 4 +- .../unit/core/threads/token-usage.test.ts | 2 +- .../tests/unit/core/threads/utils.test.ts | 2 +- .../unit/core/uploads/file-validation.test.ts | 2 +- .../core/uploads/prompt-input-files.test.ts | 22 +- .../unit/hooks/use-global-shortcuts.test.ts | 20 +- frontend/vitest.config.ts | 14 - 41 files changed, 500 insertions(+), 439 deletions(-) create mode 100644 frontend/rstest.config.ts delete mode 100644 frontend/vitest.config.ts diff --git a/frontend/AGENTS.md b/frontend/AGENTS.md index 0d4e770b0..ba4a5fe7f 100644 --- a/frontend/AGENTS.md +++ b/frontend/AGENTS.md @@ -38,7 +38,7 @@ DeerFlow is built on a sophisticated agent-based architecture using the [LangGra ``` tests/ ├── e2e/ # E2E tests (Playwright, Chromium, mocked backend) -└── unit/ # Unit tests (mirrors src/ layout, powered by Vitest) +└── unit/ # Unit tests (mirrors src/ layout, powered by Rstest) src/ ├── app/ # Next.js App Router pages │ ├── api/ # API routes diff --git a/frontend/CLAUDE.md b/frontend/CLAUDE.md index 33da1f276..c545c92a3 100644 --- a/frontend/CLAUDE.md +++ b/frontend/CLAUDE.md @@ -17,12 +17,12 @@ DeerFlow Frontend is a Next.js 16 web interface for an AI agent system. It commu | `pnpm check` | Lint + type check (run before committing) | | `pnpm lint` | ESLint only | | `pnpm lint:fix` | ESLint with auto-fix | -| `pnpm test` | Run unit tests with Vitest | +| `pnpm test` | Run unit tests with Rstest | | `pnpm test:e2e` | Run E2E tests with Playwright (Chromium) | | `pnpm typecheck` | TypeScript type check (`tsc --noEmit`) | | `pnpm start` | Start production server | -Unit tests live under `tests/unit/` and mirror the `src/` layout (e.g., `tests/unit/core/api/stream-mode.test.ts` tests `src/core/api/stream-mode.ts`). Powered by Vitest; import source modules via the `@/` path alias. +Unit tests live under `tests/unit/` and mirror the `src/` layout (e.g., `tests/unit/core/api/stream-mode.test.ts` tests `src/core/api/stream-mode.ts`). Powered by Rstest; import source modules via the `@/` path alias. E2E tests live under `tests/e2e/` and use Playwright with Chromium. They mock all backend APIs via `page.route()` network interception and test real page interactions (navigation, chat input, streaming responses). Config: `playwright.config.ts`. diff --git a/frontend/README.md b/frontend/README.md index 4ad70fb1f..76c450232 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -131,7 +131,7 @@ src/ | `pnpm dev` | Start development server with Turbopack | | `pnpm build` | Build for production | | `pnpm start` | Start production server | -| `pnpm test` | Run unit tests with Vitest | +| `pnpm test` | Run unit tests with Rstest | | `pnpm test:e2e` | Run E2E tests with Playwright | | `pnpm format` | Check formatting with Prettier | | `pnpm format:write` | Apply formatting with Prettier | diff --git a/frontend/package.json b/frontend/package.json index c6904daf8..e76ad94e1 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,7 +14,7 @@ "lint:fix": "eslint . --ext .ts,.tsx --fix", "preview": "next build && next start", "start": "next start", - "test": "vitest run", + "test": "rstest", "test:e2e": "playwright test", "typecheck": "tsc --noEmit" }, @@ -94,6 +94,8 @@ "devDependencies": { "@eslint/eslintrc": "^3.3.1", "@playwright/test": "^1.59.1", + "@rsbuild/plugin-react": "^2.1.0", + "@rstest/core": "^0.10.6", "@tailwindcss/postcss": "^4.0.15", "@types/gsap": "^3.0.0", "@types/node": "^20.14.10", @@ -107,8 +109,7 @@ "tailwindcss": "^4.0.15", "tw-animate-css": "^1.4.0", "typescript": "^5.8.2", - "typescript-eslint": "^8.27.0", - "vitest": "^4.1.4" + "typescript-eslint": "^8.27.0" }, "ct3aMetadata": { "initVersion": "7.40.0" diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 4d2e8331b..746da8bb6 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -228,6 +228,12 @@ importers: '@playwright/test': specifier: ^1.59.1 version: 1.59.1 + '@rsbuild/plugin-react': + specifier: ^2.1.0 + version: 2.1.0(@rsbuild/core@2.0.15)(@rspack/core@2.0.8(@swc/helpers@0.5.23)) + '@rstest/core': + specifier: ^0.10.6 + version: 0.10.6 '@tailwindcss/postcss': specifier: ^4.0.15 version: 4.1.18 @@ -270,9 +276,6 @@ importers: typescript-eslint: specifier: ^8.27.0 version: 8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - vitest: - specifier: ^4.1.4 - version: 4.1.4(@opentelemetry/api@1.9.0)(@types/node@20.19.33)(vite@7.3.1(@types/node@20.19.33)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.3)) packages: @@ -434,6 +437,9 @@ packages: '@codemirror/view@6.39.13': resolution: {integrity: sha512-QBO8ZsgJLCbI28KdY0/oDy5NQLqOQVZCozBknxc2/7L98V+TVYFHnfaCsnGh1U+alpd2LOkStVwYY7nW2R1xbw==} + '@emnapi/core@1.10.0': + resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} + '@emnapi/core@1.8.1': resolution: {integrity: sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==} @@ -443,6 +449,9 @@ packages: '@emnapi/wasi-threads@1.1.0': resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + '@esbuild/aix-ppc64@0.27.7': resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} engines: {node: '>=18'} @@ -1018,6 +1027,12 @@ packages: '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} + '@napi-rs/wasm-runtime@1.1.4': + resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + '@next/env@16.2.6': resolution: {integrity: sha512-gd8HoHN4ufj73WmR3JmVolrpJR47ILK6LouP5xElPglaVxir6e1a7VzvTvDWkOoPXT9rkkTzyCxBu4yeZfZwcw==} @@ -1856,6 +1871,110 @@ packages: cpu: [x64] os: [win32] + '@rsbuild/core@2.0.15': + resolution: {integrity: sha512-O8vmMhZu1YImO6jOqt/K/vlJSvkq7UtSq5YM1DIlcEd9LW8Gf6/dkQ1B2KPI6F+hSMFBnTTTumdcIowSLCw97g==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + core-js: '>= 3.0.0' + peerDependenciesMeta: + core-js: + optional: true + + '@rsbuild/plugin-react@2.1.0': + resolution: {integrity: sha512-RQTIAWB/CwPjoWt9iAl+8HixeQVgZ7kEIBrWPCixfITyHdiD84h0YpUTpEUuz6kGHw1KXT9mHZ3Rwy6WG7aRDA==} + peerDependencies: + '@rsbuild/core': ^2.0.0 + peerDependenciesMeta: + '@rsbuild/core': + optional: true + + '@rspack/binding-darwin-arm64@2.0.8': + resolution: {integrity: sha512-vCgbgH7B7qom+uID+RCZsTCOYFb9wC4/4+1U6rMfytrXGVJ72eNQs2tbdjOl0lb18CT3N/n+VkWynUiLk84GwA==} + cpu: [arm64] + os: [darwin] + + '@rspack/binding-darwin-x64@2.0.8': + resolution: {integrity: sha512-satPm2PD4B7jDTVlVAdvMVdUszwLvWUEnUDzLb77mvVkezKNDZmuhb+e8s+FfKs8hJpNbZ9VAejuA2rr8o985w==} + cpu: [x64] + os: [darwin] + + '@rspack/binding-linux-arm64-gnu@2.0.8': + resolution: {integrity: sha512-pSI+npPQE/uDtiboqvcOIRJbEV2+B+H1xffmko/gw50la92oTUW60kVULFwsb6L0+GVCzIcwX3yq60GtYIn+Ug==} + cpu: [arm64] + os: [linux] + + '@rspack/binding-linux-arm64-musl@2.0.8': + resolution: {integrity: sha512-igjJ43yxWQ72GZqjDDZSSHax9/Vg+6rLMmOvFglTJUkQpB4Tyvu/YjW+WRjYj2xRw6blOjLxUSJWASvuSqqlvg==} + cpu: [arm64] + os: [linux] + + '@rspack/binding-linux-x64-gnu@2.0.8': + resolution: {integrity: sha512-zrkoEOnqj1hOEBO5T2I/2Ts2HSJsYFh1qXwMpK4dMJFGGNWDfNeUa6/LF5uq3VINF3JUl7RL47AgrucoSZJXPA==} + cpu: [x64] + os: [linux] + + '@rspack/binding-linux-x64-musl@2.0.8': + resolution: {integrity: sha512-6CtDaGZjNDvJd9TBp7a9zABbrPORO21W96+3ZcGBn0YNUPUk4ARxIxrTTpeJ/1F41QDM8AYIkGDdqEYMqTYBsA==} + cpu: [x64] + os: [linux] + + '@rspack/binding-wasm32-wasi@2.0.8': + resolution: {integrity: sha512-Yf4SiqTUroT5Ju+te0YAY2xxKOb35tECsO21v7hYyGa705wrgoAK/MmF7enOvs9GR1iZIqgiLD/wxsIxl8GjJw==} + cpu: [wasm32] + + '@rspack/binding-win32-arm64-msvc@2.0.8': + resolution: {integrity: sha512-8NCuiQsAhXrwRBy57QZoypqrws/zLBkaQVGiB8hksr6v++8hNigNjqpQARLbd0iyMuHsQQ++8+auGk6xlDXmzw==} + cpu: [arm64] + os: [win32] + + '@rspack/binding-win32-ia32-msvc@2.0.8': + resolution: {integrity: sha512-bxiekytbX7V9KFAra+HkwtNWC6pYfHEBBZFpiT0xUs3mCFOmAAFVBsBSQsoCP9AdCEXoMAvNdnrHNw3iov4OZw==} + cpu: [ia32] + os: [win32] + + '@rspack/binding-win32-x64-msvc@2.0.8': + resolution: {integrity: sha512-7zPs8YCe/ZVJTwd+5lpB0CP0tkn2pONf/T1ycmVY76u21Nrwt8mXQGc/2yH2eWP4B7fikYBr3hGr7mpR2fajqQ==} + cpu: [x64] + os: [win32] + + '@rspack/binding@2.0.8': + resolution: {integrity: sha512-3uZ+y8aQxq33ty2srMxg2Nu0XuBI6vVrG50rkDaXqwWqOohfgGUSfFuQK7EnSUNy4aFUQlCG6NHialQHJov0wg==} + + '@rspack/core@2.0.8': + resolution: {integrity: sha512-+NLGJf8gZxihDmMFzjlly3toc2SMjeDmuvz0/Cai9AMdV4F+Pqcnt2BA9V4e3SY2jmhJQtPwgyyLtR1RiJO77g==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + '@module-federation/runtime-tools': ^0.24.1 || ^2.0.0 + '@swc/helpers': ^0.5.23 + peerDependenciesMeta: + '@module-federation/runtime-tools': + optional: true + '@swc/helpers': + optional: true + + '@rspack/plugin-react-refresh@2.0.2': + resolution: {integrity: sha512-dGNZiCxQxgAUI9sah7gd8u+O7OJZRCmqtEJNDOd8xW5RqcieC86F7p5qcShyw6onH5pKf57evpr2VjGbaFGkZg==} + peerDependencies: + '@rspack/core': ^2.0.0 + react-refresh: '>=0.10.0 <1.0.0' + peerDependenciesMeta: + '@rspack/core': + optional: true + + '@rstest/core@0.10.6': + resolution: {integrity: sha512-nX61dw2Tnxfwy/jGZEuNbh2tzapgaH6Iwj4Aw5d2wxUwxptUL3ddUxFF5M1ZSWoLEVG30VaPg3ITzrSo5sdbTg==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + happy-dom: ^20.8.3 + jsdom: '*' + peerDependenciesMeta: + happy-dom: + optional: true + jsdom: + optional: true + '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} @@ -1915,6 +2034,9 @@ packages: '@swc/helpers@0.5.21': resolution: {integrity: sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg==} + '@swc/helpers@0.5.23': + resolution: {integrity: sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==} + '@t3-oss/env-core@0.12.0': resolution: {integrity: sha512-lOPj8d9nJJTt81mMuN9GMk8x5veOt7q9m11OSnCBJhwp1QrL/qR+M8Y467ULBSm9SunosryWNbmQQbgoiMgcdw==} peerDependencies: @@ -2459,35 +2581,6 @@ packages: resolution: {integrity: sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w==} engines: {node: '>= 20'} - '@vitest/expect@4.1.4': - resolution: {integrity: sha512-iPBpra+VDuXmBFI3FMKHSFXp3Gx5HfmSCE8X67Dn+bwephCnQCaB7qWK2ldHa+8ncN8hJU8VTMcxjPpyMkUjww==} - - '@vitest/mocker@4.1.4': - resolution: {integrity: sha512-R9HTZBhW6yCSGbGQnDnH3QHfJxokKN4KB+Yvk9Q1le7eQNYwiCyKxmLmurSpFy6BzJanSLuEUDrD+j97Q+ZLPg==} - peerDependencies: - msw: ^2.4.9 - vite: ^6.0.0 || ^7.0.0 || ^8.0.0 - peerDependenciesMeta: - msw: - optional: true - vite: - optional: true - - '@vitest/pretty-format@4.1.4': - resolution: {integrity: sha512-ddmDHU0gjEUyEVLxtZa7xamrpIefdEETu3nZjWtHeZX4QxqJ7tRxSteHVXJOcr8jhiLoGAhkK4WJ3WqBpjx42A==} - - '@vitest/runner@4.1.4': - resolution: {integrity: sha512-xTp7VZ5aXP5ZJrn15UtJUWlx6qXLnGtF6jNxHepdPHpMfz/aVPx+htHtgcAL2mDXJgKhpoo2e9/hVJsIeFbytQ==} - - '@vitest/snapshot@4.1.4': - resolution: {integrity: sha512-MCjCFgaS8aZz+m5nTcEcgk/xhWv0rEH4Yl53PPlMXOZ1/Ka2VcZU6CJ+MgYCZbcJvzGhQRjVrGQNZqkGPttIKw==} - - '@vitest/spy@4.1.4': - resolution: {integrity: sha512-XxNdAsKW7C+FLydqFJLb5KhJtl3PGCMmYwFRfhvIgxJvLSXhhVI1zM8f1qD3Zg7RCjTSzDVyct6sghs9UEgBEQ==} - - '@vitest/utils@4.1.4': - resolution: {integrity: sha512-13QMT+eysM5uVGa1rG4kegGYNp6cnQcsTc67ELFbhNLQO+vgsygtYJx2khvdt4gVQqSSpC/KT5FZZxUpP3Oatw==} - '@vue/compiler-core@3.5.28': resolution: {integrity: sha512-kviccYxTgoE8n6OCw96BNdYlBg2GOWfBuOW4Vqwrt7mSKWKwFVvI8egdTltqRgITGPsTFYtKYfxIG8ptX2PJHQ==} @@ -2722,10 +2815,6 @@ packages: ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} - chai@6.2.2: - resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} - engines: {node: '>=18'} - chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -2842,9 +2931,6 @@ packages: console-table-printer@2.15.0: resolution: {integrity: sha512-SrhBq4hYVjLCkBVOWaTzceJalvn5K1Zq5aQA6wXC/cYjI3frKWNPEMK3sZsJfNNQApvCQmgBcc13ZKmFj8qExw==} - convert-source-map@2.0.0: - resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - cookie-es@1.2.2: resolution: {integrity: sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==} @@ -3191,9 +3277,6 @@ packages: resolution: {integrity: sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w==} engines: {node: '>= 0.4'} - es-module-lexer@2.0.0: - resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==} - es-object-atoms@1.1.1: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} @@ -3397,10 +3480,6 @@ packages: resolution: {integrity: sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==} engines: {node: ^18.19.0 || >=20.5.0} - expect-type@1.3.0: - resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} - engines: {node: '>=12.0.0'} - exsolve@1.0.8: resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} @@ -4504,9 +4583,6 @@ packages: resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} engines: {node: '>= 0.4'} - obug@2.1.1: - resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} - ofetch@1.5.1: resolution: {integrity: sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==} @@ -4800,6 +4876,10 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-refresh@0.18.0: + resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==} + engines: {node: '>=0.10.0'} + react-remove-scroll-bar@2.3.8: resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} engines: {node: '>=10'} @@ -5067,9 +5147,6 @@ packages: resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} engines: {node: '>= 0.4'} - siginfo@2.0.0: - resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} - signal-exit@4.1.0: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} @@ -5114,15 +5191,9 @@ packages: stable-hash@0.0.5: resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==} - stackback@0.0.2: - resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} - std-env@3.10.0: resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} - std-env@4.0.0: - resolution: {integrity: sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==} - stop-iteration-iterator@1.1.0: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} @@ -5233,9 +5304,6 @@ packages: tiny-inflate@1.0.3: resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==} - tinybench@2.9.0: - resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} - tinyexec@1.0.2: resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} engines: {node: '>=18'} @@ -5248,10 +5316,6 @@ packages: resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} engines: {node: '>=12.0.0'} - tinyrainbow@3.1.0: - resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} - engines: {node: '>=14.0.0'} - title@4.0.1: resolution: {integrity: sha512-xRnPkJx9nvE5MF6LkB5e8QJjE2FW8269wTu/LQdf7zZqBgPly0QJPf/CWAo7srj5so4yXfoLEdCFgurlpi47zg==} hasBin: true @@ -5578,47 +5642,6 @@ packages: yaml: optional: true - vitest@4.1.4: - resolution: {integrity: sha512-tFuJqTxKb8AvfyqMfnavXdzfy3h3sWZRWwfluGbkeR7n0HUev+FmNgZ8SDrRBTVrVCjgH5cA21qGbCffMNtWvg==} - engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} - hasBin: true - peerDependencies: - '@edge-runtime/vm': '*' - '@opentelemetry/api': ^1.9.0 - '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 - '@vitest/browser-playwright': 4.1.4 - '@vitest/browser-preview': 4.1.4 - '@vitest/browser-webdriverio': 4.1.4 - '@vitest/coverage-istanbul': 4.1.4 - '@vitest/coverage-v8': 4.1.4 - '@vitest/ui': 4.1.4 - happy-dom: '*' - jsdom: '*' - vite: ^6.0.0 || ^7.0.0 || ^8.0.0 - peerDependenciesMeta: - '@edge-runtime/vm': - optional: true - '@opentelemetry/api': - optional: true - '@types/node': - optional: true - '@vitest/browser-playwright': - optional: true - '@vitest/browser-preview': - optional: true - '@vitest/browser-webdriverio': - optional: true - '@vitest/coverage-istanbul': - optional: true - '@vitest/coverage-v8': - optional: true - '@vitest/ui': - optional: true - happy-dom: - optional: true - jsdom: - optional: true - vscode-jsonrpc@8.2.0: resolution: {integrity: sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==} engines: {node: '>=14.0.0'} @@ -5677,11 +5700,6 @@ packages: engines: {node: '>= 8'} hasBin: true - why-is-node-running@2.3.0: - resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} - engines: {node: '>=8'} - hasBin: true - wicked-good-xpath@1.3.0: resolution: {integrity: sha512-Gd9+TUn5nXdwj/hFsPVx5cuHHiF5Bwuc30jZ4+ronF1qHK5O7HD0sgmXWSEgwKquT3ClLoKPVbO6qGwVwLzvAw==} @@ -6068,6 +6086,12 @@ snapshots: style-mod: 4.1.3 w3c-keyname: 2.2.8 + '@emnapi/core@1.10.0': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + '@emnapi/core@1.8.1': dependencies: '@emnapi/wasi-threads': 1.1.0 @@ -6084,6 +6108,11 @@ snapshots: tslib: 2.8.1 optional: true + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 + optional: true + '@esbuild/aix-ppc64@0.27.7': optional: true @@ -6610,6 +6639,13 @@ snapshots: '@tybys/wasm-util': 0.10.1 optional: true + '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@tybys/wasm-util': 0.10.1 + optional: true + '@next/env@16.2.6': {} '@next/eslint-plugin-next@15.5.12': @@ -7372,6 +7408,89 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.60.4': optional: true + '@rsbuild/core@2.0.15': + dependencies: + '@rspack/core': 2.0.8(@swc/helpers@0.5.23) + '@swc/helpers': 0.5.23 + transitivePeerDependencies: + - '@module-federation/runtime-tools' + + '@rsbuild/plugin-react@2.1.0(@rsbuild/core@2.0.15)(@rspack/core@2.0.8(@swc/helpers@0.5.23))': + dependencies: + '@rspack/plugin-react-refresh': 2.0.2(@rspack/core@2.0.8(@swc/helpers@0.5.23))(react-refresh@0.18.0) + react-refresh: 0.18.0 + optionalDependencies: + '@rsbuild/core': 2.0.15 + transitivePeerDependencies: + - '@rspack/core' + + '@rspack/binding-darwin-arm64@2.0.8': + optional: true + + '@rspack/binding-darwin-x64@2.0.8': + optional: true + + '@rspack/binding-linux-arm64-gnu@2.0.8': + optional: true + + '@rspack/binding-linux-arm64-musl@2.0.8': + optional: true + + '@rspack/binding-linux-x64-gnu@2.0.8': + optional: true + + '@rspack/binding-linux-x64-musl@2.0.8': + optional: true + + '@rspack/binding-wasm32-wasi@2.0.8': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + optional: true + + '@rspack/binding-win32-arm64-msvc@2.0.8': + optional: true + + '@rspack/binding-win32-ia32-msvc@2.0.8': + optional: true + + '@rspack/binding-win32-x64-msvc@2.0.8': + optional: true + + '@rspack/binding@2.0.8': + optionalDependencies: + '@rspack/binding-darwin-arm64': 2.0.8 + '@rspack/binding-darwin-x64': 2.0.8 + '@rspack/binding-linux-arm64-gnu': 2.0.8 + '@rspack/binding-linux-arm64-musl': 2.0.8 + '@rspack/binding-linux-x64-gnu': 2.0.8 + '@rspack/binding-linux-x64-musl': 2.0.8 + '@rspack/binding-wasm32-wasi': 2.0.8 + '@rspack/binding-win32-arm64-msvc': 2.0.8 + '@rspack/binding-win32-ia32-msvc': 2.0.8 + '@rspack/binding-win32-x64-msvc': 2.0.8 + + '@rspack/core@2.0.8(@swc/helpers@0.5.23)': + dependencies: + '@rspack/binding': 2.0.8 + optionalDependencies: + '@swc/helpers': 0.5.23 + + '@rspack/plugin-react-refresh@2.0.2(@rspack/core@2.0.8(@swc/helpers@0.5.23))(react-refresh@0.18.0)': + dependencies: + react-refresh: 0.18.0 + optionalDependencies: + '@rspack/core': 2.0.8(@swc/helpers@0.5.23) + + '@rstest/core@0.10.6': + dependencies: + '@rsbuild/core': 2.0.15 + '@types/chai': 5.2.3 + transitivePeerDependencies: + - '@module-federation/runtime-tools' + - core-js + '@rtsao/scc@1.1.0': {} '@rushstack/eslint-patch@1.15.0': {} @@ -7449,6 +7568,10 @@ snapshots: dependencies: tslib: 2.8.1 + '@swc/helpers@0.5.23': + dependencies: + tslib: 2.8.1 + '@t3-oss/env-core@0.12.0(typescript@5.9.3)(zod@3.25.76)': optionalDependencies: typescript: 5.9.3 @@ -8004,47 +8127,6 @@ snapshots: '@vercel/oidc@3.1.0': {} - '@vitest/expect@4.1.4': - dependencies: - '@standard-schema/spec': 1.1.0 - '@types/chai': 5.2.3 - '@vitest/spy': 4.1.4 - '@vitest/utils': 4.1.4 - chai: 6.2.2 - tinyrainbow: 3.1.0 - - '@vitest/mocker@4.1.4(vite@7.3.1(@types/node@20.19.33)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.3))': - dependencies: - '@vitest/spy': 4.1.4 - estree-walker: 3.0.3 - magic-string: 0.30.21 - optionalDependencies: - vite: 7.3.1(@types/node@20.19.33)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.3) - - '@vitest/pretty-format@4.1.4': - dependencies: - tinyrainbow: 3.1.0 - - '@vitest/runner@4.1.4': - dependencies: - '@vitest/utils': 4.1.4 - pathe: 2.0.3 - - '@vitest/snapshot@4.1.4': - dependencies: - '@vitest/pretty-format': 4.1.4 - '@vitest/utils': 4.1.4 - magic-string: 0.30.21 - pathe: 2.0.3 - - '@vitest/spy@4.1.4': {} - - '@vitest/utils@4.1.4': - dependencies: - '@vitest/pretty-format': 4.1.4 - convert-source-map: 2.0.0 - tinyrainbow: 3.1.0 - '@vue/compiler-core@3.5.28': dependencies: '@babel/parser': 7.29.7 @@ -8331,8 +8413,6 @@ snapshots: ccount@2.0.1: {} - chai@6.2.2: {} - chalk@4.1.2: dependencies: ansi-styles: 4.3.0 @@ -8451,8 +8531,6 @@ snapshots: dependencies: simple-wcswidth: 1.1.2 - convert-source-map@2.0.0: {} - cookie-es@1.2.2: {} cookie-es@1.2.3: {} @@ -8872,8 +8950,6 @@ snapshots: iterator.prototype: 1.1.5 safe-array-concat: 1.1.3 - es-module-lexer@2.0.0: {} - es-object-atoms@1.1.1: dependencies: es-errors: 1.3.0 @@ -9214,8 +9290,6 @@ snapshots: strip-final-newline: 4.0.0 yoctocolors: 2.1.2 - expect-type@1.3.0: {} - exsolve@1.0.8: {} extend@3.0.2: {} @@ -10771,8 +10845,6 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.1 - obug@2.1.1: {} - ofetch@1.5.1: dependencies: destr: 2.0.5 @@ -11023,6 +11095,8 @@ snapshots: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) + react-refresh@0.18.0: {} + react-remove-scroll-bar@2.3.8(@types/react@19.2.13)(react@19.2.4): dependencies: react: 19.2.4 @@ -11480,8 +11554,6 @@ snapshots: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 - siginfo@2.0.0: {} - signal-exit@4.1.0: {} simple-wcswidth@1.1.2: {} @@ -11518,12 +11590,8 @@ snapshots: stable-hash@0.0.5: {} - stackback@0.0.2: {} - std-env@3.10.0: {} - std-env@4.0.0: {} - stop-iteration-iterator@1.1.0: dependencies: es-errors: 1.3.0 @@ -11653,8 +11721,6 @@ snapshots: tiny-inflate@1.0.3: {} - tinybench@2.9.0: {} - tinyexec@1.0.2: {} tinyglobby@0.2.15: @@ -11667,8 +11733,6 @@ snapshots: fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 - tinyrainbow@3.1.0: {} - title@4.0.1: dependencies: arg: 5.0.2 @@ -11995,34 +12059,6 @@ snapshots: lightningcss: 1.30.2 yaml: 2.8.3 - vitest@4.1.4(@opentelemetry/api@1.9.0)(@types/node@20.19.33)(vite@7.3.1(@types/node@20.19.33)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.3)): - dependencies: - '@vitest/expect': 4.1.4 - '@vitest/mocker': 4.1.4(vite@7.3.1(@types/node@20.19.33)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.3)) - '@vitest/pretty-format': 4.1.4 - '@vitest/runner': 4.1.4 - '@vitest/snapshot': 4.1.4 - '@vitest/spy': 4.1.4 - '@vitest/utils': 4.1.4 - es-module-lexer: 2.0.0 - expect-type: 1.3.0 - magic-string: 0.30.21 - obug: 2.1.1 - pathe: 2.0.3 - picomatch: 4.0.4 - std-env: 4.0.0 - tinybench: 2.9.0 - tinyexec: 1.0.2 - tinyglobby: 0.2.16 - tinyrainbow: 3.1.0 - vite: 7.3.1(@types/node@20.19.33)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.3) - why-is-node-running: 2.3.0 - optionalDependencies: - '@opentelemetry/api': 1.9.0 - '@types/node': 20.19.33 - transitivePeerDependencies: - - msw - vscode-jsonrpc@8.2.0: {} vscode-languageserver-protocol@3.17.5: @@ -12101,11 +12137,6 @@ snapshots: dependencies: isexe: 2.0.0 - why-is-node-running@2.3.0: - dependencies: - siginfo: 2.0.0 - stackback: 0.0.2 - wicked-good-xpath@1.3.0: {} word-wrap@1.2.5: {} diff --git a/frontend/rstest.config.ts b/frontend/rstest.config.ts new file mode 100644 index 000000000..1e9cded4d --- /dev/null +++ b/frontend/rstest.config.ts @@ -0,0 +1,19 @@ +import { resolve } from "path"; + +import { pluginReact } from "@rsbuild/plugin-react"; +import { defineConfig } from "@rstest/core"; + +export default defineConfig({ + plugins: [pluginReact()], + resolve: { + alias: { + "@": resolve(__dirname, "src"), + }, + }, + output: { + // Streamdown imports KaTeX CSS as a side effect. Bundle these packages so + // Rsbuild processes that CSS import instead of Node trying to load it. + bundleDependencies: ["streamdown", "katex"], + }, + include: ["tests/unit/**/*.test.ts"], +}); diff --git a/frontend/tests/unit/components/workspace/gateway-offline-banner-helpers.test.ts b/frontend/tests/unit/components/workspace/gateway-offline-banner-helpers.test.ts index 952a749ff..11b205421 100644 --- a/frontend/tests/unit/components/workspace/gateway-offline-banner-helpers.test.ts +++ b/frontend/tests/unit/components/workspace/gateway-offline-banner-helpers.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, it } from "@rstest/core"; import { OFFLINE_BANNER_AUTH_FAILURE_THRESHOLD, diff --git a/frontend/tests/unit/core/agents/api.test.ts b/frontend/tests/unit/core/agents/api.test.ts index 9ea61b5e7..297f8ae59 100644 --- a/frontend/tests/unit/core/agents/api.test.ts +++ b/frontend/tests/unit/core/agents/api.test.ts @@ -16,20 +16,20 @@ * These tests pin both halves of the contract so a future refactor doesn't * silently drop the detail or leak the generated fallback into the UI. */ -import { beforeEach, describe, expect, test, vi } from "vitest"; +import { beforeEach, describe, expect, test, rs } from "@rstest/core"; -vi.mock("@/core/api/fetcher", () => ({ - fetch: vi.fn(), +rs.mock("@/core/api/fetcher", () => ({ + fetch: rs.fn(), })); -vi.mock("@/core/config", () => ({ +rs.mock("@/core/config", () => ({ getBackendBaseURL: () => "", })); import { AgentsApiDisabledError, checkAgentName } from "@/core/agents/api"; import { fetch as fetcher } from "@/core/api/fetcher"; -const mockedFetch = vi.mocked(fetcher); +const mockedFetch = rs.mocked(fetcher); function jsonResponse(status: number, body: unknown): Response { return new Response(JSON.stringify(body), { diff --git a/frontend/tests/unit/core/api/api-client.test.ts b/frontend/tests/unit/core/api/api-client.test.ts index 7e8e595f1..45c5af394 100644 --- a/frontend/tests/unit/core/api/api-client.test.ts +++ b/frontend/tests/unit/core/api/api-client.test.ts @@ -1,4 +1,4 @@ -import { afterEach, expect, test, vi } from "vitest"; +import { afterEach, expect, test, rs } from "@rstest/core"; import { clearReconnectRun, @@ -9,18 +9,18 @@ import { function makeSessionStorage() { const values = new Map(); return { - getItem: vi.fn((key: string) => values.get(key) ?? null), - removeItem: vi.fn((key: string) => { + getItem: rs.fn((key: string) => values.get(key) ?? null), + removeItem: rs.fn((key: string) => { values.delete(key); }), - setItem: vi.fn((key: string, value: string) => { + setItem: rs.fn((key: string, value: string) => { values.set(key, value); }), }; } afterEach(() => { - vi.unstubAllGlobals(); + rs.unstubAllGlobals(); }); test("identifies inactive run stream errors", () => { @@ -45,7 +45,7 @@ test("does not classify unrelated conflict errors as inactive streams", () => { test("clears matching reconnect metadata", () => { const sessionStorage = makeSessionStorage(); sessionStorage.setItem("lg:stream:thread-1", "run-1"); - vi.stubGlobal("window", { sessionStorage }); + rs.stubGlobal("window", { sessionStorage }); clearReconnectRun("thread-1", "run-1"); @@ -55,7 +55,7 @@ test("clears matching reconnect metadata", () => { test("keeps newer reconnect metadata", () => { const sessionStorage = makeSessionStorage(); sessionStorage.setItem("lg:stream:thread-1", "newer-run"); - vi.stubGlobal("window", { sessionStorage }); + rs.stubGlobal("window", { sessionStorage }); clearReconnectRun("thread-1", "stale-run"); @@ -63,7 +63,7 @@ test("keeps newer reconnect metadata", () => { }); test("ignores reconnect metadata storage access failures", () => { - vi.stubGlobal("window", { + rs.stubGlobal("window", { get sessionStorage() { throw new DOMException("Blocked", "SecurityError"); }, @@ -75,13 +75,13 @@ test("ignores reconnect metadata storage access failures", () => { test("clears stale reconnect metadata when join stream cannot be resumed", async () => { const sessionStorage = makeSessionStorage(); sessionStorage.setItem("lg:stream:thread-1", "run-1"); - vi.stubGlobal("window", { + rs.stubGlobal("window", { location: { origin: "http://localhost:2026" }, sessionStorage, }); - vi.stubGlobal( + rs.stubGlobal( "fetch", - vi.fn(async () => { + rs.fn(async () => { return new Response( JSON.stringify({ detail: @@ -102,13 +102,13 @@ test("clears stale reconnect metadata when join stream cannot be resumed", async test("rethrows unrelated streaming errors", async () => { const sessionStorage = makeSessionStorage(); sessionStorage.setItem("lg:stream:thread-1", "run-1"); - vi.stubGlobal("window", { + rs.stubGlobal("window", { location: { origin: "http://localhost:2026" }, sessionStorage, }); - vi.stubGlobal( + rs.stubGlobal( "fetch", - vi.fn(async () => { + rs.fn(async () => { return new Response(JSON.stringify({ detail: "run is still active" }), { status: 409, }); diff --git a/frontend/tests/unit/core/api/stream-mode.test.ts b/frontend/tests/unit/core/api/stream-mode.test.ts index 818906c31..d94980549 100644 --- a/frontend/tests/unit/core/api/stream-mode.test.ts +++ b/frontend/tests/unit/core/api/stream-mode.test.ts @@ -1,4 +1,4 @@ -import { expect, test } from "vitest"; +import { expect, test } from "@rstest/core"; import { sanitizeRunStreamOptions } from "@/core/api/stream-mode"; diff --git a/frontend/tests/unit/core/artifacts/preview.test.ts b/frontend/tests/unit/core/artifacts/preview.test.ts index 123f6bca4..8544188be 100644 --- a/frontend/tests/unit/core/artifacts/preview.test.ts +++ b/frontend/tests/unit/core/artifacts/preview.test.ts @@ -1,4 +1,4 @@ -import { expect, test } from "vitest"; +import { expect, test } from "@rstest/core"; import { appendHtmlPreviewBaseHref, diff --git a/frontend/tests/unit/core/artifacts/utils.test.ts b/frontend/tests/unit/core/artifacts/utils.test.ts index c0400b371..23a45d70d 100644 --- a/frontend/tests/unit/core/artifacts/utils.test.ts +++ b/frontend/tests/unit/core/artifacts/utils.test.ts @@ -1,4 +1,11 @@ -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; +import { + afterEach, + beforeEach, + describe, + expect, + test, + rs, +} from "@rstest/core"; const ENV_KEYS = [ "NEXT_PUBLIC_BACKEND_BASE_URL", @@ -33,7 +40,7 @@ function restoreEnv(snapshot: EnvSnapshot) { } async function loadFreshArtifactUtils() { - vi.resetModules(); + rs.resetModules(); return await import("@/core/artifacts/utils"); } diff --git a/frontend/tests/unit/core/auth/gateway-config.test.ts b/frontend/tests/unit/core/auth/gateway-config.test.ts index 4fa116a1a..3fca80f10 100644 --- a/frontend/tests/unit/core/auth/gateway-config.test.ts +++ b/frontend/tests/unit/core/auth/gateway-config.test.ts @@ -1,4 +1,11 @@ -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; +import { + afterEach, + beforeEach, + describe, + expect, + test, + rs, +} from "@rstest/core"; const ENV_KEYS = [ "NODE_ENV", @@ -36,7 +43,7 @@ function restoreEnv(snapshot: EnvSnapshot) { } async function loadFreshConfig() { - vi.resetModules(); + rs.resetModules(); return await import("@/core/auth/gateway-config"); } diff --git a/frontend/tests/unit/core/auth/server.test.ts b/frontend/tests/unit/core/auth/server.test.ts index dac90cacc..ba9d2f225 100644 --- a/frontend/tests/unit/core/auth/server.test.ts +++ b/frontend/tests/unit/core/auth/server.test.ts @@ -1,10 +1,17 @@ -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; +import { + afterEach, + beforeEach, + describe, + expect, + test, + rs, +} from "@rstest/core"; import { AUTH_DISABLED_USER } from "@/core/auth/auth-disabled-user"; import { STATIC_WEBSITE_USER } from "@/core/auth/static-user"; -vi.mock("next/headers", () => ({ - cookies: vi.fn(() => { +rs.mock("next/headers", () => ({ + cookies: rs.fn(() => { throw new Error("cookies should not be read in static website mode"); }), })); @@ -44,7 +51,7 @@ function restoreEnv(snapshot: EnvSnapshot) { } async function loadFreshServerAuth() { - vi.resetModules(); + rs.resetModules(); return await import("@/core/auth/server"); } @@ -61,15 +68,15 @@ describe("getServerSideUser", () => { afterEach(() => { restoreEnv(saved); - vi.unstubAllGlobals(); + rs.unstubAllGlobals(); }); test("bypasses gateway auth in static website mode", async () => { setEnv("NEXT_PUBLIC_STATIC_WEBSITE_ONLY", "true"); - const fetchSpy = vi.fn(() => { + const fetchSpy = rs.fn(() => { throw new Error("fetch should not be called in static website mode"); }); - vi.stubGlobal("fetch", fetchSpy); + rs.stubGlobal("fetch", fetchSpy); const { getServerSideUser } = await loadFreshServerAuth(); @@ -82,10 +89,10 @@ describe("getServerSideUser", () => { test("bypasses gateway auth in auth-disabled mode", async () => { setEnv("DEER_FLOW_AUTH_DISABLED", "1"); - const fetchSpy = vi.fn(() => { + const fetchSpy = rs.fn(() => { throw new Error("fetch should not be called in auth-disabled mode"); }); - vi.stubGlobal("fetch", fetchSpy); + rs.stubGlobal("fetch", fetchSpy); const { getServerSideUser } = await loadFreshServerAuth(); @@ -118,21 +125,21 @@ describe("getServerSideUser — gateway_unavailable contract (issue #3493)", () afterEach(() => { restoreEnv(saved); - vi.unstubAllGlobals(); - vi.doUnmock("next/headers"); + rs.unstubAllGlobals(); + rs.doUnmock("next/headers"); }); test("returns gateway_unavailable when /auth/me fetch rejects (e.g. AbortError)", async () => { - vi.doMock("next/headers", () => ({ - cookies: vi.fn(async () => ({ + rs.doMock("next/headers", () => ({ + cookies: rs.fn(async () => ({ get: (name: string) => name === "access_token" ? { value: "stub-token" } : undefined, })), })); const abortErr = new DOMException("Aborted", "AbortError"); - vi.stubGlobal( + rs.stubGlobal( "fetch", - vi.fn(() => Promise.reject(abortErr)), + rs.fn(() => Promise.reject(abortErr)), ); const { getServerSideUser } = await loadFreshServerAuth(); @@ -143,15 +150,15 @@ describe("getServerSideUser — gateway_unavailable contract (issue #3493)", () }); test("returns gateway_unavailable when /auth/me responds with a 5xx", async () => { - vi.doMock("next/headers", () => ({ - cookies: vi.fn(async () => ({ + rs.doMock("next/headers", () => ({ + cookies: rs.fn(async () => ({ get: (name: string) => name === "access_token" ? { value: "stub-token" } : undefined, })), })); - vi.stubGlobal( + rs.stubGlobal( "fetch", - vi.fn(() => + rs.fn(() => Promise.resolve( new Response("upstream error", { status: 503, diff --git a/frontend/tests/unit/core/channels/api.test.ts b/frontend/tests/unit/core/channels/api.test.ts index c8fa80aac..333029425 100644 --- a/frontend/tests/unit/core/channels/api.test.ts +++ b/frontend/tests/unit/core/channels/api.test.ts @@ -1,10 +1,10 @@ -import { beforeEach, describe, expect, test, vi } from "vitest"; +import { beforeEach, describe, expect, test, rs } from "@rstest/core"; -vi.mock("@/core/api/fetcher", () => ({ - fetch: vi.fn(), +rs.mock("@/core/api/fetcher", () => ({ + fetch: rs.fn(), })); -vi.mock("@/core/config", () => ({ +rs.mock("@/core/config", () => ({ getBackendBaseURL: () => "/backend", })); @@ -18,7 +18,7 @@ import { listChannelProviders, } from "@/core/channels/api"; -const mockedFetch = vi.mocked(fetcher); +const mockedFetch = rs.mocked(fetcher); function jsonResponse(status: number, body: unknown): Response { return new Response(JSON.stringify(body), { diff --git a/frontend/tests/unit/core/channels/connect-poll.test.ts b/frontend/tests/unit/core/channels/connect-poll.test.ts index 056066e48..d5b0ed1ae 100644 --- a/frontend/tests/unit/core/channels/connect-poll.test.ts +++ b/frontend/tests/unit/core/channels/connect-poll.test.ts @@ -1,4 +1,11 @@ -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; +import { + afterEach, + beforeEach, + describe, + expect, + test, + rs, +} from "@rstest/core"; import { startConnectionPoll } from "@/core/channels/connect-poll"; import type { ChannelConnection } from "@/core/channels/types"; @@ -14,11 +21,11 @@ function connection(provider: string, status: string): ChannelConnection { } beforeEach(() => { - vi.useFakeTimers(); + rs.useFakeTimers(); }); afterEach(() => { - vi.useRealTimers(); + rs.useRealTimers(); }); describe("startConnectionPoll", () => { @@ -27,8 +34,8 @@ describe("startConnectionPoll", () => { [connection("telegram", "pending")], [connection("telegram", "connected")], ]; - const fetchConnections = vi.fn(async () => responses.shift() ?? []); - const onConnected = vi.fn(); + const fetchConnections = rs.fn(async () => responses.shift() ?? []); + const onConnected = rs.fn(); startConnectionPoll({ provider: "telegram", @@ -38,41 +45,41 @@ describe("startConnectionPoll", () => { intervalMs: 1000, }); - await vi.advanceTimersByTimeAsync(1000); + await rs.advanceTimersByTimeAsync(1000); expect(fetchConnections).toHaveBeenCalledTimes(1); expect(onConnected).not.toHaveBeenCalled(); - await vi.advanceTimersByTimeAsync(1000); + await rs.advanceTimersByTimeAsync(1000); expect(fetchConnections).toHaveBeenCalledTimes(2); expect(onConnected).toHaveBeenCalledTimes(1); // No further polling after the connection resolves. - await vi.advanceTimersByTimeAsync(5000); + await rs.advanceTimersByTimeAsync(5000); expect(fetchConnections).toHaveBeenCalledTimes(2); }); test("cancel() stops scheduled polling and fires no further fetches", async () => { - const fetchConnections = vi.fn(async () => [ + const fetchConnections = rs.fn(async () => [ connection("telegram", "pending"), ]); const handle = startConnectionPoll({ provider: "telegram", expiresInSeconds: 600, fetchConnections, - onConnected: vi.fn(), + onConnected: rs.fn(), intervalMs: 1000, }); - await vi.advanceTimersByTimeAsync(1000); + await rs.advanceTimersByTimeAsync(1000); expect(fetchConnections).toHaveBeenCalledTimes(1); handle.cancel(); - await vi.advanceTimersByTimeAsync(10000); + await rs.advanceTimersByTimeAsync(10000); expect(fetchConnections).toHaveBeenCalledTimes(1); }); test("a non-finite expires_in falls back to a finite deadline and terminates", async () => { - const fetchConnections = vi.fn(async () => [ + const fetchConnections = rs.fn(async () => [ connection("telegram", "pending"), ]); let nowValue = 0; @@ -80,22 +87,22 @@ describe("startConnectionPoll", () => { provider: "telegram", expiresInSeconds: Number.NaN, fetchConnections, - onConnected: vi.fn(), + onConnected: rs.fn(), intervalMs: 1000, now: () => nowValue, }); nowValue = 1; - await vi.advanceTimersByTimeAsync(1000); + await rs.advanceTimersByTimeAsync(1000); expect(fetchConnections).toHaveBeenCalledTimes(1); // Jump past the fallback expiry window: the loop must stop instead of // running forever (Date.now() >= NaN would otherwise never be true). nowValue = 10_000_000; - await vi.advanceTimersByTimeAsync(1000); + await rs.advanceTimersByTimeAsync(1000); expect(fetchConnections).toHaveBeenCalledTimes(2); - await vi.advanceTimersByTimeAsync(10000); + await rs.advanceTimersByTimeAsync(10000); expect(fetchConnections).toHaveBeenCalledTimes(2); }); }); diff --git a/frontend/tests/unit/core/channels/open-connect-url.test.ts b/frontend/tests/unit/core/channels/open-connect-url.test.ts index 5175da710..e03b53318 100644 --- a/frontend/tests/unit/core/channels/open-connect-url.test.ts +++ b/frontend/tests/unit/core/channels/open-connect-url.test.ts @@ -1,4 +1,4 @@ -import { afterEach, describe, expect, test, vi } from "vitest"; +import { afterEach, describe, expect, test, rs } from "@rstest/core"; import { closeConnectWindow, @@ -8,17 +8,17 @@ import { type PopupStub = { closed: boolean; - close: ReturnType; + close: ReturnType; location: { - replace: ReturnType; + replace: ReturnType; }; opener: unknown; }; function stubWindow(openResult: PopupStub | null) { - const assign = vi.fn(); - const open = vi.fn(() => openResult); - vi.stubGlobal("window", { + const assign = rs.fn(); + const open = rs.fn(() => openResult); + rs.stubGlobal("window", { open, location: { assign }, }); @@ -28,14 +28,14 @@ function stubWindow(openResult: PopupStub | null) { function makePopup(): PopupStub { return { closed: false, - close: vi.fn(), - location: { replace: vi.fn() }, + close: rs.fn(), + location: { replace: rs.fn() }, opener: {}, }; } afterEach(() => { - vi.unstubAllGlobals(); + rs.unstubAllGlobals(); }); describe("channel connect window helpers", () => { diff --git a/frontend/tests/unit/core/channels/provider-state.test.ts b/frontend/tests/unit/core/channels/provider-state.test.ts index c391b910d..b690ae1df 100644 --- a/frontend/tests/unit/core/channels/provider-state.test.ts +++ b/frontend/tests/unit/core/channels/provider-state.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, it } from "@rstest/core"; import { providerCanConnect, diff --git a/frontend/tests/unit/core/clipboard.test.ts b/frontend/tests/unit/core/clipboard.test.ts index 2329a17dc..fba6dba44 100644 --- a/frontend/tests/unit/core/clipboard.test.ts +++ b/frontend/tests/unit/core/clipboard.test.ts @@ -1,4 +1,4 @@ -import { afterEach, expect, test, vi } from "vitest"; +import { afterEach, expect, test, rs } from "@rstest/core"; import { installClipboardFallback, @@ -15,7 +15,7 @@ const originalClipboardItemDescriptor = Object.getOwnPropertyDescriptor( ); afterEach(() => { - vi.restoreAllMocks(); + rs.restoreAllMocks(); if (!hadOriginalNavigator) { Reflect.deleteProperty(globalThis, "navigator"); } else { @@ -46,7 +46,7 @@ afterEach(() => { }); test("writes text with the Clipboard API when available", async () => { - const writeText = vi.fn().mockResolvedValue(undefined); + const writeText = rs.fn().mockResolvedValue(undefined); Object.defineProperty(globalThis, "navigator", { configurable: true, value: { @@ -75,14 +75,14 @@ test("returns false when Clipboard API is unavailable", async () => { test("falls back to execCommand when Clipboard API is unavailable", async () => { const textarea = { - remove: vi.fn(), - select: vi.fn(), - setAttribute: vi.fn(), + remove: rs.fn(), + select: rs.fn(), + setAttribute: rs.fn(), style: {}, value: "", }; - const appendChild = vi.fn(); - const execCommand = vi.fn().mockReturnValue(true); + const appendChild = rs.fn(); + const execCommand = rs.fn().mockReturnValue(true); Object.defineProperty(globalThis, "navigator", { configurable: true, @@ -94,7 +94,7 @@ test("falls back to execCommand when Clipboard API is unavailable", async () => body: { appendChild, }, - createElement: vi.fn().mockReturnValue(textarea), + createElement: rs.fn().mockReturnValue(textarea), execCommand, }, }); @@ -109,16 +109,16 @@ test("falls back to execCommand when Clipboard API is unavailable", async () => test("falls back to parent removal when textarea.remove is unavailable", async () => { const parentNode = { - removeChild: vi.fn(), + removeChild: rs.fn(), }; const textarea = { parentNode, - select: vi.fn(), - setAttribute: vi.fn(), + select: rs.fn(), + setAttribute: rs.fn(), style: {}, value: "", }; - const execCommand = vi.fn().mockReturnValue(true); + const execCommand = rs.fn().mockReturnValue(true); Object.defineProperty(globalThis, "navigator", { configurable: true, @@ -128,9 +128,9 @@ test("falls back to parent removal when textarea.remove is unavailable", async ( configurable: true, value: { body: { - appendChild: vi.fn(), + appendChild: rs.fn(), }, - createElement: vi.fn().mockReturnValue(textarea), + createElement: rs.fn().mockReturnValue(textarea), execCommand, }, }); @@ -142,8 +142,8 @@ test("falls back to parent removal when textarea.remove is unavailable", async ( test("does not fail cleanup when textarea removal APIs are unavailable", async () => { const textarea = { parentNode: {}, - select: vi.fn(), - setAttribute: vi.fn(), + select: rs.fn(), + setAttribute: rs.fn(), style: {}, value: "", }; @@ -156,10 +156,10 @@ test("does not fail cleanup when textarea removal APIs are unavailable", async ( configurable: true, value: { body: { - appendChild: vi.fn(), + appendChild: rs.fn(), }, - createElement: vi.fn().mockReturnValue(textarea), - execCommand: vi.fn().mockReturnValue(true), + createElement: rs.fn().mockReturnValue(textarea), + execCommand: rs.fn().mockReturnValue(true), }, }); @@ -168,11 +168,11 @@ test("does not fail cleanup when textarea removal APIs are unavailable", async ( test("cleans up the textarea when selecting text fails", async () => { const textarea = { - remove: vi.fn(), - select: vi.fn(() => { + remove: rs.fn(), + select: rs.fn(() => { throw new Error("selection failed"); }), - setAttribute: vi.fn(), + setAttribute: rs.fn(), style: {}, value: "", }; @@ -185,10 +185,10 @@ test("cleans up the textarea when selecting text fails", async () => { configurable: true, value: { body: { - appendChild: vi.fn(), + appendChild: rs.fn(), }, - createElement: vi.fn().mockReturnValue(textarea), - execCommand: vi.fn(), + createElement: rs.fn().mockReturnValue(textarea), + execCommand: rs.fn(), }, }); @@ -198,9 +198,9 @@ test("cleans up the textarea when selecting text fails", async () => { test("returns false when execCommand fallback fails", async () => { const textarea = { - remove: vi.fn(), - select: vi.fn(), - setAttribute: vi.fn(), + remove: rs.fn(), + select: rs.fn(), + setAttribute: rs.fn(), style: {}, value: "", }; @@ -213,10 +213,10 @@ test("returns false when execCommand fallback fails", async () => { configurable: true, value: { body: { - appendChild: vi.fn(), + appendChild: rs.fn(), }, - createElement: vi.fn().mockReturnValue(textarea), - execCommand: vi.fn().mockReturnValue(false), + createElement: rs.fn().mockReturnValue(textarea), + execCommand: rs.fn().mockReturnValue(false), }, }); @@ -233,9 +233,9 @@ test("returns false when execCommand fallback cannot create an element", async ( configurable: true, value: { body: { - appendChild: vi.fn(), + appendChild: rs.fn(), }, - execCommand: vi.fn(), + execCommand: rs.fn(), }, }); @@ -256,7 +256,7 @@ test("returns false when navigator is unavailable", async () => { }); test("returns false when Clipboard API rejects", async () => { - const writeText = vi.fn().mockRejectedValue(new Error("denied")); + const writeText = rs.fn().mockRejectedValue(new Error("denied")); Object.defineProperty(globalThis, "navigator", { configurable: true, value: { @@ -271,14 +271,14 @@ test("returns false when Clipboard API rejects", async () => { test("installs a writeText fallback when Clipboard API is unavailable", async () => { const textarea = { - remove: vi.fn(), - select: vi.fn(), - setAttribute: vi.fn(), + remove: rs.fn(), + select: rs.fn(), + setAttribute: rs.fn(), style: {}, value: "", }; - const appendChild = vi.fn(); - const execCommand = vi.fn().mockReturnValue(true); + const appendChild = rs.fn(); + const execCommand = rs.fn().mockReturnValue(true); Object.defineProperty(globalThis, "navigator", { configurable: true, @@ -290,7 +290,7 @@ test("installs a writeText fallback when Clipboard API is unavailable", async () body: { appendChild, }, - createElement: vi.fn().mockReturnValue(textarea), + createElement: rs.fn().mockReturnValue(textarea), execCommand, }, }); @@ -333,12 +333,12 @@ test("installed writeText fallback converts thrown DOM failures to rejections", configurable: true, value: { body: { - appendChild: vi.fn(), + appendChild: rs.fn(), }, - createElement: vi.fn(() => { + createElement: rs.fn(() => { throw new Error("dom unavailable"); }), - execCommand: vi.fn(), + execCommand: rs.fn(), }, }); @@ -358,16 +358,16 @@ test("installed writeText fallback distinguishes copy command failure", async () configurable: true, value: { body: { - appendChild: vi.fn(), + appendChild: rs.fn(), }, - createElement: vi.fn().mockReturnValue({ - remove: vi.fn(), - select: vi.fn(), - setAttribute: vi.fn(), + createElement: rs.fn().mockReturnValue({ + remove: rs.fn(), + select: rs.fn(), + setAttribute: rs.fn(), style: {}, value: "", }), - execCommand: vi.fn().mockReturnValue(false), + execCommand: rs.fn().mockReturnValue(false), }, }); @@ -380,13 +380,13 @@ test("installed writeText fallback distinguishes copy command failure", async () test("installs a write fallback for ClipboardItem text/plain payloads", async () => { const textarea = { - remove: vi.fn(), - select: vi.fn(), - setAttribute: vi.fn(), + remove: rs.fn(), + select: rs.fn(), + setAttribute: rs.fn(), style: {}, value: "", }; - const execCommand = vi.fn().mockReturnValue(true); + const execCommand = rs.fn().mockReturnValue(true); Object.defineProperty(globalThis, "navigator", { configurable: true, @@ -396,9 +396,9 @@ test("installs a write fallback for ClipboardItem text/plain payloads", async () configurable: true, value: { body: { - appendChild: vi.fn(), + appendChild: rs.fn(), }, - createElement: vi.fn().mockReturnValue(textarea), + createElement: rs.fn().mockReturnValue(textarea), execCommand, }, }); @@ -418,7 +418,7 @@ test("installs a write fallback for ClipboardItem text/plain payloads", async () }); test("installed write fallback rejects when ClipboardItem lacks text/plain", async () => { - const execCommand = vi.fn().mockReturnValue(true); + const execCommand = rs.fn().mockReturnValue(true); Object.defineProperty(globalThis, "navigator", { configurable: true, @@ -428,12 +428,12 @@ test("installed write fallback rejects when ClipboardItem lacks text/plain", asy configurable: true, value: { body: { - appendChild: vi.fn(), + appendChild: rs.fn(), }, - createElement: vi.fn().mockReturnValue({ - remove: vi.fn(), - select: vi.fn(), - setAttribute: vi.fn(), + createElement: rs.fn().mockReturnValue({ + remove: rs.fn(), + select: rs.fn(), + setAttribute: rs.fn(), style: {}, value: "", }), @@ -454,7 +454,7 @@ test("installed write fallback rejects when ClipboardItem lacks text/plain", asy }); test("installed write fallback rejects when getType cannot provide text/plain", async () => { - const execCommand = vi.fn().mockReturnValue(true); + const execCommand = rs.fn().mockReturnValue(true); Object.defineProperty(globalThis, "navigator", { configurable: true, @@ -464,12 +464,12 @@ test("installed write fallback rejects when getType cannot provide text/plain", configurable: true, value: { body: { - appendChild: vi.fn(), + appendChild: rs.fn(), }, - createElement: vi.fn().mockReturnValue({ - remove: vi.fn(), - select: vi.fn(), - setAttribute: vi.fn(), + createElement: rs.fn().mockReturnValue({ + remove: rs.fn(), + select: rs.fn(), + setAttribute: rs.fn(), style: {}, value: "", }), @@ -482,7 +482,7 @@ test("installed write fallback rejects when getType cannot provide text/plain", await expect( globalThis.navigator.clipboard.write([ { - getType: vi.fn().mockRejectedValue(new Error("missing")), + getType: rs.fn().mockRejectedValue(new Error("missing")), types: ["text/plain"], } as unknown as ClipboardItem, ]), @@ -491,7 +491,7 @@ test("installed write fallback rejects when getType cannot provide text/plain", }); test("installed write fallback rejects before getType when item types exclude text/plain", async () => { - const getType = vi.fn().mockResolvedValue(new Blob(["ignored"])); + const getType = rs.fn().mockResolvedValue(new Blob(["ignored"])); Object.defineProperty(globalThis, "navigator", { configurable: true, value: {}, @@ -550,7 +550,7 @@ test("installed write fallback rejects when getType returns a non-Blob", async ( await expect( globalThis.navigator.clipboard.write([ { - getType: vi.fn().mockResolvedValue("plain text"), + getType: rs.fn().mockResolvedValue("plain text"), types: ["text/plain"], } as unknown as ClipboardItem, ]), @@ -558,7 +558,7 @@ test("installed write fallback rejects when getType returns a non-Blob", async ( }); test("installed write fallback preserves existing clipboard prototype methods", async () => { - const readText = vi.fn().mockResolvedValue("existing"); + const readText = rs.fn().mockResolvedValue("existing"); const clipboard = Object.create({ readText, }); @@ -588,8 +588,8 @@ test("installed write fallback preserves existing clipboard prototype methods", }); test("installClipboardFallback does not replace existing clipboard methods when only ClipboardItem is missing", async () => { - const write = vi.fn().mockResolvedValue(undefined); - const writeText = vi.fn().mockResolvedValue(undefined); + const write = rs.fn().mockResolvedValue(undefined); + const writeText = rs.fn().mockResolvedValue(undefined); const clipboard = { write, writeText, @@ -727,7 +727,7 @@ test("installClipboardFallback does not throw when ClipboardItem cannot be defin value: undefined, }); Reflect.deleteProperty(globalThis, "ClipboardItem"); - vi.spyOn(Object, "defineProperty").mockImplementation( + rs.spyOn(Object, "defineProperty").mockImplementation( (target, property, descriptor) => { if (target === globalThis && property === "ClipboardItem") { throw new Error("locked global"); @@ -746,8 +746,8 @@ test("installs ClipboardItem fallback when the global property exists but is unu configurable: true, value: { clipboard: { - write: vi.fn().mockResolvedValue(undefined), - writeText: vi.fn().mockResolvedValue(undefined), + write: rs.fn().mockResolvedValue(undefined), + writeText: rs.fn().mockResolvedValue(undefined), }, }, }); diff --git a/frontend/tests/unit/core/mcp/api.test.ts b/frontend/tests/unit/core/mcp/api.test.ts index fa7909d2c..813490d3c 100644 --- a/frontend/tests/unit/core/mcp/api.test.ts +++ b/frontend/tests/unit/core/mcp/api.test.ts @@ -13,13 +13,13 @@ * so the React Query hook's `error` branch can render a friendly empty * state (admin-required for 403) instead of crashing. */ -import { beforeEach, describe, expect, test, vi } from "vitest"; +import { beforeEach, describe, expect, test, rs } from "@rstest/core"; -vi.mock("@/core/api/fetcher", () => ({ - fetch: vi.fn(), +rs.mock("@/core/api/fetcher", () => ({ + fetch: rs.fn(), })); -vi.mock("@/core/config", () => ({ +rs.mock("@/core/config", () => ({ getBackendBaseURL: () => "", })); @@ -30,7 +30,7 @@ import { updateMCPConfig, } from "@/core/mcp/api"; -const mockedFetch = vi.mocked(fetcher); +const mockedFetch = rs.mocked(fetcher); function jsonResponse(status: number, body: unknown): Response { return new Response(JSON.stringify(body), { diff --git a/frontend/tests/unit/core/mcp/hooks.test.ts b/frontend/tests/unit/core/mcp/hooks.test.ts index 0dd9115ab..35e661103 100644 --- a/frontend/tests/unit/core/mcp/hooks.test.ts +++ b/frontend/tests/unit/core/mcp/hooks.test.ts @@ -1,14 +1,14 @@ +import { beforeEach, describe, expect, it, rs } from "@rstest/core"; import { QueryClient } from "@tanstack/react-query"; -import { beforeEach, describe, expect, it, vi } from "vitest"; -vi.mock("@/core/api/fetcher", () => ({ - fetch: vi.fn(), +rs.mock("@/core/api/fetcher", () => ({ + fetch: rs.fn(), })); import { fetch } from "@/core/api/fetcher"; import { MCPConfigRequestError, loadMCPConfig } from "@/core/mcp/api"; -const mockedFetch = vi.mocked(fetch); +const mockedFetch = rs.mocked(fetch); function makeClient() { return new QueryClient({ diff --git a/frontend/tests/unit/core/messages/usage-model.test.ts b/frontend/tests/unit/core/messages/usage-model.test.ts index ce6eda279..f9354268f 100644 --- a/frontend/tests/unit/core/messages/usage-model.test.ts +++ b/frontend/tests/unit/core/messages/usage-model.test.ts @@ -1,5 +1,5 @@ import type { Message } from "@langchain/langgraph-sdk"; -import { expect, test } from "vitest"; +import { expect, test } from "@rstest/core"; import { enUS } from "@/core/i18n"; import { diff --git a/frontend/tests/unit/core/messages/usage.test.ts b/frontend/tests/unit/core/messages/usage.test.ts index 6a4144f87..f400a7b98 100644 --- a/frontend/tests/unit/core/messages/usage.test.ts +++ b/frontend/tests/unit/core/messages/usage.test.ts @@ -1,5 +1,5 @@ import type { Message } from "@langchain/langgraph-sdk"; -import { expect, test } from "vitest"; +import { expect, test } from "@rstest/core"; import { accumulateUsage, selectHeaderTokenUsage } from "@/core/messages/usage"; import { diff --git a/frontend/tests/unit/core/messages/utils.test.ts b/frontend/tests/unit/core/messages/utils.test.ts index cba5f37fa..999b2d4b8 100644 --- a/frontend/tests/unit/core/messages/utils.test.ts +++ b/frontend/tests/unit/core/messages/utils.test.ts @@ -1,5 +1,5 @@ import type { Message } from "@langchain/langgraph-sdk"; -import { describe, expect, test } from "vitest"; +import { describe, expect, test } from "@rstest/core"; import { extractContentFromMessage, diff --git a/frontend/tests/unit/core/reasoning-trigger.test.ts b/frontend/tests/unit/core/reasoning-trigger.test.ts index 2fbe97977..20d87d327 100644 --- a/frontend/tests/unit/core/reasoning-trigger.test.ts +++ b/frontend/tests/unit/core/reasoning-trigger.test.ts @@ -1,8 +1,8 @@ +import { expect, test, rs } from "@rstest/core"; import { createElement } from "react"; import { renderToStaticMarkup } from "react-dom/server"; -import { expect, test, vi } from "vitest"; -vi.mock("streamdown", () => ({ +rs.mock("streamdown", () => ({ Streamdown: ({ children }: { children: string }) => createElement("div", null, children), })); diff --git a/frontend/tests/unit/core/settings/local.test.ts b/frontend/tests/unit/core/settings/local.test.ts index da9f9f8dc..b7c564fb2 100644 --- a/frontend/tests/unit/core/settings/local.test.ts +++ b/frontend/tests/unit/core/settings/local.test.ts @@ -1,4 +1,4 @@ -import { expect, test } from "vitest"; +import { expect, test } from "@rstest/core"; import { DEFAULT_LOCAL_SETTINGS } from "@/core/settings/local"; diff --git a/frontend/tests/unit/core/streamdown/mermaid.test.ts b/frontend/tests/unit/core/streamdown/mermaid.test.ts index 71f22e851..9074a0827 100644 --- a/frontend/tests/unit/core/streamdown/mermaid.test.ts +++ b/frontend/tests/unit/core/streamdown/mermaid.test.ts @@ -1,4 +1,4 @@ -import { expect, test } from "vitest"; +import { expect, test } from "@rstest/core"; import { normalizeMermaidMarkdown } from "@/core/streamdown/mermaid"; import { preprocessStreamdownMarkdown } from "@/core/streamdown/preprocess"; diff --git a/frontend/tests/unit/core/streamdown/plugins.test.ts b/frontend/tests/unit/core/streamdown/plugins.test.ts index efe0ab719..84754f7a1 100644 --- a/frontend/tests/unit/core/streamdown/plugins.test.ts +++ b/frontend/tests/unit/core/streamdown/plugins.test.ts @@ -1,5 +1,5 @@ +import { expect, test } from "@rstest/core"; import rehypeRaw from "rehype-raw"; -import { expect, test } from "vitest"; import { reasoningPlugins, streamdownPlugins } from "@/core/streamdown/plugins"; diff --git a/frontend/tests/unit/core/streamdown/preprocess.test.ts b/frontend/tests/unit/core/streamdown/preprocess.test.ts index 20adcefec..e8ff1033c 100644 --- a/frontend/tests/unit/core/streamdown/preprocess.test.ts +++ b/frontend/tests/unit/core/streamdown/preprocess.test.ts @@ -1,4 +1,4 @@ -import { expect, test } from "vitest"; +import { expect, test } from "@rstest/core"; import { capBlockquoteNesting, diff --git a/frontend/tests/unit/core/tasks/subtask-result.test.ts b/frontend/tests/unit/core/tasks/subtask-result.test.ts index d6e8bfbd4..1d676b49c 100644 --- a/frontend/tests/unit/core/tasks/subtask-result.test.ts +++ b/frontend/tests/unit/core/tasks/subtask-result.test.ts @@ -1,8 +1,8 @@ import { readFileSync } from "node:fs"; -import { fileURLToPath } from "node:url"; +import { resolve } from "node:path"; import type { Message } from "@langchain/langgraph-sdk"; -import { describe, expect, it } from "vitest"; +import { describe, expect, it } from "@rstest/core"; import { SUBAGENT_ERROR_KEY, @@ -24,13 +24,9 @@ interface ContractFile { cases: ContractCase[]; } -// The frontend package is ESM (`"type": "module"`), so `__dirname` is not -// defined. Resolve the cross-language fixture relative to this module URL. -const CONTRACT_PATH = fileURLToPath( - new URL( - "../../../../../contracts/subagent_status_contract.json", - import.meta.url, - ), +const CONTRACT_PATH = resolve( + __dirname, + "../../../../../contracts/subagent_status_contract.json", ); const CONTRACT: ContractFile = JSON.parse( readFileSync(CONTRACT_PATH, "utf-8"), diff --git a/frontend/tests/unit/core/threads/api.test.ts b/frontend/tests/unit/core/threads/api.test.ts index 4d1268694..bd2a6671f 100644 --- a/frontend/tests/unit/core/threads/api.test.ts +++ b/frontend/tests/unit/core/threads/api.test.ts @@ -1,8 +1,8 @@ -import { beforeEach, expect, test, vi } from "vitest"; +import { beforeEach, expect, test, rs } from "@rstest/core"; -const fetchWithAuth = vi.fn(); +const fetchWithAuth = rs.fn(); -vi.mock("@/core/api/fetcher", () => ({ +rs.mock("@/core/api/fetcher", () => ({ fetch: fetchWithAuth, })); diff --git a/frontend/tests/unit/core/threads/export.test.ts b/frontend/tests/unit/core/threads/export.test.ts index 58219f8a0..09f42217d 100644 --- a/frontend/tests/unit/core/threads/export.test.ts +++ b/frontend/tests/unit/core/threads/export.test.ts @@ -1,5 +1,5 @@ import type { Message } from "@langchain/langgraph-sdk"; -import { describe, expect, it } from "vitest"; +import { describe, expect, it } from "@rstest/core"; import { formatThreadAsJSON, diff --git a/frontend/tests/unit/core/threads/infinite.test.ts b/frontend/tests/unit/core/threads/infinite.test.ts index d040ff0a1..52bd6224f 100644 --- a/frontend/tests/unit/core/threads/infinite.test.ts +++ b/frontend/tests/unit/core/threads/infinite.test.ts @@ -1,5 +1,5 @@ +import { describe, expect, test } from "@rstest/core"; import { QueryClient, type InfiniteData } from "@tanstack/react-query"; -import { describe, expect, test } from "vitest"; import { filterInfiniteThreadsCache, diff --git a/frontend/tests/unit/core/threads/message-merge.test.ts b/frontend/tests/unit/core/threads/message-merge.test.ts index 51f4c4763..2eb5693dd 100644 --- a/frontend/tests/unit/core/threads/message-merge.test.ts +++ b/frontend/tests/unit/core/threads/message-merge.test.ts @@ -1,5 +1,5 @@ import type { Message, Run } from "@langchain/langgraph-sdk"; -import { expect, test } from "vitest"; +import { expect, test } from "@rstest/core"; import { buildRunMessagesUrl, diff --git a/frontend/tests/unit/core/threads/thread-search-query.test.ts b/frontend/tests/unit/core/threads/thread-search-query.test.ts index 34a008035..f6b35bfc2 100644 --- a/frontend/tests/unit/core/threads/thread-search-query.test.ts +++ b/frontend/tests/unit/core/threads/thread-search-query.test.ts @@ -1,4 +1,4 @@ -import { expect, test, vi } from "vitest"; +import { expect, test, rs } from "@rstest/core"; import { buildThreadsSearchQueryOptions, @@ -7,7 +7,7 @@ import { } from "@/core/threads/thread-search-query"; test("thread search query refreshes so IM-created sessions appear in the sidebar", () => { - const search = vi.fn(); + const search = rs.fn(); const options = buildThreadsSearchQueryOptions( { threads: { search } }, DEFAULT_THREAD_SEARCH_PARAMS, diff --git a/frontend/tests/unit/core/threads/token-usage.test.ts b/frontend/tests/unit/core/threads/token-usage.test.ts index c6ba0978e..6e73cca1b 100644 --- a/frontend/tests/unit/core/threads/token-usage.test.ts +++ b/frontend/tests/unit/core/threads/token-usage.test.ts @@ -1,4 +1,4 @@ -import { expect, test } from "vitest"; +import { expect, test } from "@rstest/core"; import { threadTokenUsageToTokenUsage } from "@/core/threads/token-usage"; import type { ThreadTokenUsageResponse } from "@/core/threads/types"; diff --git a/frontend/tests/unit/core/threads/utils.test.ts b/frontend/tests/unit/core/threads/utils.test.ts index bc5a684be..a55d47ca0 100644 --- a/frontend/tests/unit/core/threads/utils.test.ts +++ b/frontend/tests/unit/core/threads/utils.test.ts @@ -1,5 +1,5 @@ import type { Message } from "@langchain/langgraph-sdk"; -import { expect, test } from "vitest"; +import { expect, test } from "@rstest/core"; import { channelSourceOfThread, diff --git a/frontend/tests/unit/core/uploads/file-validation.test.ts b/frontend/tests/unit/core/uploads/file-validation.test.ts index 102ef990a..25cb1d4af 100644 --- a/frontend/tests/unit/core/uploads/file-validation.test.ts +++ b/frontend/tests/unit/core/uploads/file-validation.test.ts @@ -1,4 +1,4 @@ -import { expect, test } from "vitest"; +import { expect, test } from "@rstest/core"; import { MACOS_APP_BUNDLE_UPLOAD_MESSAGE, diff --git a/frontend/tests/unit/core/uploads/prompt-input-files.test.ts b/frontend/tests/unit/core/uploads/prompt-input-files.test.ts index 28c578bd3..aa16d413d 100644 --- a/frontend/tests/unit/core/uploads/prompt-input-files.test.ts +++ b/frontend/tests/unit/core/uploads/prompt-input-files.test.ts @@ -1,4 +1,4 @@ -import { afterEach, expect, test, vi } from "vitest"; +import { afterEach, expect, test, rs } from "@rstest/core"; import { type PromptInputFilePart, @@ -6,8 +6,8 @@ import { } from "@/core/uploads/prompt-input-files"; afterEach(() => { - vi.restoreAllMocks(); - vi.unstubAllGlobals(); + rs.restoreAllMocks(); + rs.unstubAllGlobals(); }); test("exports the prompt-input file conversion helper", () => { @@ -17,9 +17,9 @@ test("exports the prompt-input file conversion helper", () => { test("reuses the original File when a prompt attachment already has one", async () => { const file = new File(["hello"], "note.txt", { type: "text/plain" }); - vi.stubGlobal( + rs.stubGlobal( "fetch", - vi.fn(() => { + rs.fn(() => { throw new Error("fetch should not run when File is already present"); }), ); @@ -76,11 +76,11 @@ test("returns null when upload preparation is missing required data", async () = }); test("returns null when the URL fallback fetch fails", async () => { - const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => ({})); + const warnSpy = rs.spyOn(console, "warn").mockImplementation(() => ({})); - vi.stubGlobal( + rs.stubGlobal( "fetch", - vi.fn(async () => { + rs.fn(async () => { throw new Error("network down"); }), ); @@ -96,11 +96,11 @@ test("returns null when the URL fallback fetch fails", async () => { }); test("returns null when the URL fallback fetch response is non-ok", async () => { - const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => ({})); + const warnSpy = rs.spyOn(console, "warn").mockImplementation(() => ({})); - vi.stubGlobal( + rs.stubGlobal( "fetch", - vi.fn( + rs.fn( async () => new Response("missing", { status: 404, diff --git a/frontend/tests/unit/hooks/use-global-shortcuts.test.ts b/frontend/tests/unit/hooks/use-global-shortcuts.test.ts index 1c5028dfd..d370c94c7 100644 --- a/frontend/tests/unit/hooks/use-global-shortcuts.test.ts +++ b/frontend/tests/unit/hooks/use-global-shortcuts.test.ts @@ -1,4 +1,4 @@ -import { afterEach, describe, expect, test, vi } from "vitest"; +import { afterEach, describe, expect, test, rs } from "@rstest/core"; type KeydownHandler = (event: KeyboardEvent) => void; @@ -6,23 +6,23 @@ async function loadHookWithCapturedHandler() { let cleanup: (() => void) | undefined; let keydownHandler: KeydownHandler | undefined; - const addEventListener = vi.fn( + const addEventListener = rs.fn( (type: string, listener: EventListenerOrEventListenerObject) => { if (type === "keydown" && typeof listener === "function") { keydownHandler = listener as KeydownHandler; } }, ); - const removeEventListener = vi.fn(); + const removeEventListener = rs.fn(); - vi.resetModules(); - vi.doMock("react", () => ({ + rs.resetModules(); + rs.doMock("react", () => ({ useEffect: (effect: () => void | (() => void)) => { const result = effect(); cleanup = typeof result === "function" ? result : undefined; }, })); - vi.stubGlobal("window", { addEventListener, removeEventListener }); + rs.stubGlobal("window", { addEventListener, removeEventListener }); const { useGlobalShortcuts } = await import("@/hooks/use-global-shortcuts"); @@ -34,14 +34,14 @@ async function loadHookWithCapturedHandler() { } afterEach(() => { - vi.doUnmock("react"); - vi.unstubAllGlobals(); - vi.resetModules(); + rs.doUnmock("react"); + rs.unstubAllGlobals(); + rs.resetModules(); }); describe("useGlobalShortcuts", () => { test("ignores keydown events without a key", async () => { - const action = vi.fn(); + const action = rs.fn(); const { getKeydownHandler, useGlobalShortcuts } = await loadHookWithCapturedHandler(); diff --git a/frontend/vitest.config.ts b/frontend/vitest.config.ts deleted file mode 100644 index ec8b8de31..000000000 --- a/frontend/vitest.config.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { resolve } from "path"; - -import { defineConfig } from "vitest/config"; - -export default defineConfig({ - resolve: { - alias: { - "@": resolve(__dirname, "src"), - }, - }, - test: { - include: ["tests/unit/**/*.test.ts"], - }, -});