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
This commit is contained in:
Jiahan Chen 2026-06-22 16:12:49 +08:00 committed by GitHub
parent 9535a4f1c2
commit 6fb22bb311
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
41 changed files with 500 additions and 439 deletions

View file

@ -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

View file

@ -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`.

View file

@ -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 |

View file

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

441
frontend/pnpm-lock.yaml generated
View file

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

19
frontend/rstest.config.ts Normal file
View file

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

View file

@ -1,4 +1,4 @@
import { describe, expect, it } from "vitest";
import { describe, expect, it } from "@rstest/core";
import {
OFFLINE_BANNER_AUTH_FAILURE_THRESHOLD,

View file

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

View file

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

View file

@ -1,4 +1,4 @@
import { expect, test } from "vitest";
import { expect, test } from "@rstest/core";
import { sanitizeRunStreamOptions } from "@/core/api/stream-mode";

View file

@ -1,4 +1,4 @@
import { expect, test } from "vitest";
import { expect, test } from "@rstest/core";
import {
appendHtmlPreviewBaseHref,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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<typeof vi.fn>;
close: ReturnType<typeof rs.fn>;
location: {
replace: ReturnType<typeof vi.fn>;
replace: ReturnType<typeof rs.fn>;
};
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", () => {

View file

@ -1,4 +1,4 @@
import { describe, expect, it } from "vitest";
import { describe, expect, it } from "@rstest/core";
import {
providerCanConnect,

View file

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

View file

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

View file

@ -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({

View file

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

View file

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

View file

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

View file

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

View file

@ -1,4 +1,4 @@
import { expect, test } from "vitest";
import { expect, test } from "@rstest/core";
import { DEFAULT_LOCAL_SETTINGS } from "@/core/settings/local";

View file

@ -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";

View file

@ -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";

View file

@ -1,4 +1,4 @@
import { expect, test } from "vitest";
import { expect, test } from "@rstest/core";
import {
capBlockquoteNesting,

View file

@ -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"),

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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";

View file

@ -1,5 +1,5 @@
import type { Message } from "@langchain/langgraph-sdk";
import { expect, test } from "vitest";
import { expect, test } from "@rstest/core";
import {
channelSourceOfThread,

View file

@ -1,4 +1,4 @@
import { expect, test } from "vitest";
import { expect, test } from "@rstest/core";
import {
MACOS_APP_BUNDLE_UPLOAD_MESSAGE,

View file

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

View file

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

View file

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