diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml
index 69f18e5..8388dd3 100644
--- a/.github/workflows/validate.yml
+++ b/.github/workflows/validate.yml
@@ -68,7 +68,7 @@ jobs:
test:
runs-on: ubuntu-latest
- name: ✅ Bun Tests
+ name: ✅ Unit Tests
if: >-
github.event_name != 'pull_request' || github.event.pull_request.draft ==
false
@@ -84,8 +84,8 @@ jobs:
- name: 📥 Install Dependencies
run: bun install --frozen-lockfile
- - name: ✅ Run Bun Tests
- run: bun test ./server ./mock-servers
+ - name: ✅ Run Unit Tests
+ run: bun run test:unit
e2e:
runs-on: ubuntu-latest
diff --git a/bun.lock b/bun.lock
index df77071..95245db 100644
--- a/bun.lock
+++ b/bun.lock
@@ -28,6 +28,7 @@
"prettier": "^3.8.1",
"set-cookie-parser": "^3.0.1",
"typescript": "^5.9.3",
+ "vitest": "^4.1.2",
"wrangler": "^4.72.0",
},
},
@@ -258,6 +259,8 @@
"@oven/bun-windows-x64-baseline": ["@oven/bun-windows-x64-baseline@1.3.8", "", { "os": "win32", "cpu": "x64" }, "sha512-K6qBUKAZLXsjAwFxGTG87dsWlDjyDl2fqjJr7+x7lmv2m+aSEzmLOK+Z5pSvGkpjBp3LXV35UUgj8G0UTd0pPg=="],
+ "@oxc-project/types": ["@oxc-project/types@0.122.0", "", {}, "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA=="],
+
"@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.55.0", "", { "os": "android", "cpu": "arm" }, "sha512-NhvgAhncTSOhRahQSCnkK/4YIGPjTmhPurQQ2dwt2IvwCMTvZRW5vF2K10UBOxFve4GZDMw6LtXZdC2qeuYIVQ=="],
"@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.55.0", "", { "os": "android", "cpu": "arm64" }, "sha512-P9iWRh+Ugqhg+D7rkc7boHX8o3H2h7YPcZHQIgvVBgnua5tk4LR2L+IBlreZs58/95cd2x3/004p5VsQM9z4SA=="],
@@ -372,6 +375,38 @@
"@remix-run/tar-parser": ["@remix-run/tar-parser@0.7.0", "", {}, "sha512-PW8JxEUzaGcnqxC5hBI8L9lK/Qz3oad6IGKZ+NExI3L7urVJUux+yCBrsme79DMBgS6hL+lgd/5LPFA5fSwF9A=="],
+ "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.12", "", { "os": "android", "cpu": "arm64" }, "sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA=="],
+
+ "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-rc.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg=="],
+
+ "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-rc.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw=="],
+
+ "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-rc.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q=="],
+
+ "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12", "", { "os": "linux", "cpu": "arm" }, "sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q=="],
+
+ "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg=="],
+
+ "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-rc.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw=="],
+
+ "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g=="],
+
+ "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og=="],
+
+ "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-rc.12", "", { "os": "linux", "cpu": "x64" }, "sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg=="],
+
+ "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-rc.12", "", { "os": "linux", "cpu": "x64" }, "sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig=="],
+
+ "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-rc.12", "", { "os": "none", "cpu": "arm64" }, "sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA=="],
+
+ "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-rc.12", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.1.1" }, "cpu": "none" }, "sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg=="],
+
+ "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q=="],
+
+ "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-rc.12", "", { "os": "win32", "cpu": "x64" }, "sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw=="],
+
+ "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.12", "", {}, "sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw=="],
+
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.57.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg=="],
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.57.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w=="],
@@ -396,6 +431,10 @@
"@types/bun": ["@types/bun@1.3.10", "", { "dependencies": { "bun-types": "1.3.10" } }, "sha512-0+rlrUrOrTSskibryHbvQkDOWRJwJZqZlxrUs1u4oOoTln8+WIXBPmAuCF35SWB2z4Zl3E84Nl/D0P7803nigQ=="],
+ "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="],
+
+ "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="],
+
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
@@ -468,6 +507,20 @@
"@vitest/eslint-plugin": ["@vitest/eslint-plugin@1.6.6", "", { "dependencies": { "@typescript-eslint/scope-manager": "^8.51.0", "@typescript-eslint/utils": "^8.51.0" }, "peerDependencies": { "eslint": ">=8.57.0", "typescript": ">=5.0.0", "vitest": "*" }, "optionalPeers": ["typescript", "vitest"] }, "sha512-bwgQxQWRtnTVzsUHK824tBmHzjV0iTx3tZaiQIYDjX3SA7TsQS8CuDVqxXrRY3FaOUMgbGavesCxI9MOfFLm7Q=="],
+ "@vitest/expect": ["@vitest/expect@4.1.2", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.2", "@vitest/utils": "4.1.2", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-gbu+7B0YgUJ2nkdsRJrFFW6X7NTP44WlhiclHniUhxADQJH5Szt9mZ9hWnJPJ8YwOK5zUOSSlSvyzRf0u1DSBQ=="],
+
+ "@vitest/mocker": ["@vitest/mocker@4.1.2", "", { "dependencies": { "@vitest/spy": "4.1.2", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-Ize4iQtEALHDttPRCmN+FKqOl2vxTiNUhzobQFFt/BM1lRUTG7zRCLOykG/6Vo4E4hnUdfVLo5/eqKPukcWW7Q=="],
+
+ "@vitest/pretty-format": ["@vitest/pretty-format@4.1.2", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-dwQga8aejqeuB+TvXCMzSQemvV9hNEtDDpgUKDzOmNQayl2OG241PSWeJwKRH3CiC+sESrmoFd49rfnq7T4RnA=="],
+
+ "@vitest/runner": ["@vitest/runner@4.1.2", "", { "dependencies": { "@vitest/utils": "4.1.2", "pathe": "^2.0.3" } }, "sha512-Gr+FQan34CdiYAwpGJmQG8PgkyFVmARK8/xSijia3eTFgVfpcpztWLuP6FttGNfPLJhaZVP/euvujeNYar36OQ=="],
+
+ "@vitest/snapshot": ["@vitest/snapshot@4.1.2", "", { "dependencies": { "@vitest/pretty-format": "4.1.2", "@vitest/utils": "4.1.2", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-g7yfUmxYS4mNxk31qbOYsSt2F4m1E02LFqO53Xpzg3zKMhLAPZAjjfyl9e6z7HrW6LvUdTwAQR3HHfLjpko16A=="],
+
+ "@vitest/spy": ["@vitest/spy@4.1.2", "", {}, "sha512-DU4fBnbVCJGNBwVA6xSToNXrkZNSiw59H8tcuUspVMsBDBST4nfvsPsEHDHGtWRRnqBERBQu7TrTKskmjqTXKA=="],
+
+ "@vitest/utils": ["@vitest/utils@4.1.2", "", { "dependencies": { "@vitest/pretty-format": "4.1.2", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-xw2/TiX82lQHA06cgbqRKFb5lCAy3axQ4H4SoUFhUsg+wztiet+co86IAMDtF6Vm1hc7J6j09oh/rgDn+JdKIQ=="],
+
"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
@@ -502,6 +555,8 @@
"arraybuffer.prototype.slice": ["arraybuffer.prototype.slice@1.0.4", "", { "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "is-array-buffer": "^3.0.4" } }, "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ=="],
+ "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="],
+
"async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="],
"available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="],
@@ -528,6 +583,8 @@
"callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
+ "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="],
+
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
"cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
@@ -546,6 +603,8 @@
"content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
+ "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
+
"cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
"cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
@@ -598,6 +657,8 @@
"es-iterator-helpers": ["es-iterator-helpers@1.2.2", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.1", "es-errors": "^1.3.0", "es-set-tostringtag": "^2.1.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.3.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "iterator.prototype": "^1.1.5", "safe-array-concat": "^1.1.3" } }, "sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w=="],
+ "es-module-lexer": ["es-module-lexer@2.0.0", "", {}, "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw=="],
+
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
"es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
@@ -642,6 +703,8 @@
"estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
+ "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
+
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
"etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
@@ -652,6 +715,8 @@
"eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="],
+ "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="],
+
"express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="],
"express-rate-limit": ["express-rate-limit@8.2.1", "", { "dependencies": { "ip-address": "10.0.1" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g=="],
@@ -834,6 +899,30 @@
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
+ "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
+
+ "lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
+
+ "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="],
+
+ "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="],
+
+ "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="],
+
+ "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="],
+
+ "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="],
+
+ "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="],
+
+ "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="],
+
+ "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="],
+
+ "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="],
+
+ "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
+
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
"lodash": ["lodash@4.17.23", "", {}, "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w=="],
@@ -842,6 +931,8 @@
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
+ "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
+
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
"media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
@@ -884,6 +975,8 @@
"object.values": ["object.values@1.2.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA=="],
+ "obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="],
+
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
@@ -916,6 +1009,8 @@
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
+ "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
+
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
"pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="],
@@ -926,6 +1021,8 @@
"possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="],
+ "postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="],
+
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
"prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="],
@@ -966,6 +1063,8 @@
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
+ "rolldown": ["rolldown@1.0.0-rc.12", "", { "dependencies": { "@oxc-project/types": "=0.122.0", "@rolldown/pluginutils": "1.0.0-rc.12" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.12", "@rolldown/binding-darwin-arm64": "1.0.0-rc.12", "@rolldown/binding-darwin-x64": "1.0.0-rc.12", "@rolldown/binding-freebsd-x64": "1.0.0-rc.12", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.12", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.12", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.12", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.12", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.12", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.12", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.12", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.12", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.12", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.12", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.12" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A=="],
+
"router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
"rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="],
@@ -1010,10 +1109,18 @@
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
+ "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="],
+
+ "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
+
"stable-hash-x": ["stable-hash-x@0.2.0", "", {}, "sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ=="],
+ "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="],
+
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
+ "std-env": ["std-env@4.0.0", "", {}, "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ=="],
+
"stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="],
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
@@ -1040,8 +1147,14 @@
"throttleit": ["throttleit@2.1.0", "", {}, "sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw=="],
+ "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="],
+
+ "tinyexec": ["tinyexec@1.0.4", "", {}, "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw=="],
+
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
+ "tinyrainbow": ["tinyrainbow@3.1.0", "", {}, "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw=="],
+
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
"tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="],
@@ -1084,6 +1197,10 @@
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
+ "vite": ["vite@8.0.3", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.8", "rolldown": "1.0.0-rc.12", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.0", "esbuild": "^0.27.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ=="],
+
+ "vitest": ["vitest@4.1.2", "", { "dependencies": { "@vitest/expect": "4.1.2", "@vitest/mocker": "4.1.2", "@vitest/pretty-format": "4.1.2", "@vitest/runner": "4.1.2", "@vitest/snapshot": "4.1.2", "@vitest/spy": "4.1.2", "@vitest/utils": "4.1.2", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.2", "@vitest/browser-preview": "4.1.2", "@vitest/browser-webdriverio": "4.1.2", "@vitest/ui": "4.1.2", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-xjR1dMTVHlFLh98JE3i/f/WePqJsah4A0FK9cc8Ehp9Udk0AZk6ccpIZhh1qJ/yxVWRZ+Q54ocnD8TXmkhspGg=="],
+
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
"which-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="],
@@ -1094,6 +1211,8 @@
"which-typed-array": ["which-typed-array@1.1.20", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg=="],
+ "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="],
+
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
"workerd": ["workerd@1.20260310.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260310.1", "@cloudflare/workerd-darwin-arm64": "1.20260310.1", "@cloudflare/workerd-linux-64": "1.20260310.1", "@cloudflare/workerd-linux-arm64": "1.20260310.1", "@cloudflare/workerd-windows-64": "1.20260310.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-yawXhypXXHtArikJj15HOMknNGikpBbSg2ZDe6lddUbqZnJXuCVSkgc/0ArUeVMG1jbbGvpst+REFtKwILvRTQ=="],
@@ -1142,6 +1261,8 @@
"@poppinss/dumper/supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="],
+ "@rolldown/binding-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.2", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw=="],
+
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
@@ -1164,8 +1285,12 @@
"playwright/fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="],
+ "postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
+
"router/path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="],
+ "vite/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
+
"wrangler/esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="],
"youch/cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
diff --git a/client/app-session-refresh.test.ts b/client/app-session-refresh.test.ts
index 99bfb36..46cd460 100644
--- a/client/app-session-refresh.test.ts
+++ b/client/app-session-refresh.test.ts
@@ -1,32 +1,34 @@
///
-import { expect, mock, test } from 'bun:test'
+import { expect, test, vi } from 'vitest'
import { type Handle } from 'remix/component'
type QueueTask = Parameters[0]
const navigationListeners: Array<() => void> = []
const queuedSessionResponses: Array<{ email: string } | null> = []
-const fetchSessionInfoMock = mock(async () => {
+const fetchSessionInfoMock = vi.fn(async () => {
return queuedSessionResponses.shift() ?? null
})
-mock.module('./client-router.tsx', () => ({
- routerEvents: new EventTarget(),
- listenToRouterNavigation: (_handle: Handle, listener: () => void) => {
- navigationListeners.push(listener)
- },
- getPathname: () => '/',
- navigate: () => {
- return
- },
- Router: () => () => null,
-}))
-
-mock.module('./session.ts', () => ({
- fetchSessionInfo: fetchSessionInfoMock,
-}))
-
-const { App } = await import('./app.tsx')
+async function loadApp() {
+ vi.resetModules()
+ vi.doMock('./client-router.tsx', () => ({
+ routerEvents: new EventTarget(),
+ listenToRouterNavigation: (_handle: Handle, listener: () => void) => {
+ navigationListeners.push(listener)
+ },
+ getPathname: () => '/',
+ navigate: () => {
+ return
+ },
+ Router: () => () => null,
+ }))
+ vi.doMock('./session.ts', () => ({
+ fetchSessionInfo: fetchSessionInfoMock,
+ }))
+ const { App } = await import('./app.tsx')
+ return App
+}
async function runNextTask(tasks: Array, aborted: boolean) {
const task = tasks.shift()
@@ -40,6 +42,7 @@ test('aborted refresh does not erase a ready authenticated session', async () =>
navigationListeners.length = 0
queuedSessionResponses.length = 0
queuedSessionResponses.push({ email: 'signed-in@example.com' }, null)
+ fetchSessionInfoMock.mockClear()
const queuedTasks: Array = []
const handle = {
@@ -54,6 +57,7 @@ test('aborted refresh does not erase a ready authenticated session', async () =>
},
} as unknown as Handle
+ const App = await loadApp()
const render = App(handle)
expect(navigationListeners).toHaveLength(1)
@@ -61,17 +65,48 @@ test('aborted refresh does not erase a ready authenticated session', async () =>
await runNextTask(queuedTasks, false)
await runNextTask(queuedTasks, false)
- const authenticatedUi = Bun.inspect(render())
- expect(authenticatedUi).toContain('signed-in@example.com')
- expect(authenticatedUi).toContain('Log out')
+ const authenticatedUi = render()
+ const navChildren = (authenticatedUi.props as { children: Array })
+ .children[0] as {
+ props: { children: Array }
+ }
+ const navItems = navChildren.props.children as Array
+ const accountLink = navItems[2] as { props?: { children?: Array } }
+ const accountEntry = (accountLink.props?.children ?? [])[1] as {
+ props?: { children?: string }
+ }
+ const logoutEntry = (accountLink.props?.children ?? [])[2] as {
+ props?: { children?: { props?: { children?: string } } }
+ }
+ expect(accountEntry.props?.children).toBe('signed-in@example.com')
+ expect(logoutEntry.props?.children?.props?.children).toBe('Log out')
// Re-run refresh via navigation, then abort in-flight fetch.
navigationListeners[0]!()
await runNextTask(queuedTasks, true)
- const uiAfterAbort = Bun.inspect(render())
- expect(uiAfterAbort).toContain('signed-in@example.com')
- expect(uiAfterAbort).toContain('Log out')
- expect(uiAfterAbort).not.toContain('>Login<')
- expect(uiAfterAbort).not.toContain('>Signup<')
+ const uiAfterAbort = render()
+ const navAfterAbort = (uiAfterAbort.props as { children: Array })
+ .children[0] as {
+ props: { children: Array }
+ }
+ const navAfterAbortItems = navAfterAbort.props.children as Array
+ expect(navAfterAbortItems).toHaveLength(3)
+ const accountLinkAfterAbort = navAfterAbortItems[2] as {
+ props?: { children?: Array }
+ }
+ const accountEntryAfterAbort = (accountLinkAfterAbort.props?.children ?? [])[1] as {
+ props?: { children?: string }
+ }
+ const logoutEntryAfterAbort = (accountLinkAfterAbort.props?.children ?? [])[2] as {
+ props?: { children?: { props?: { children?: string } } }
+ }
+ expect(accountEntryAfterAbort.props?.children).toBe('signed-in@example.com')
+ expect(logoutEntryAfterAbort.props?.children?.props?.children).toBe('Log out')
+ const navAfterAbortText = JSON.stringify(navAfterAbort)
+ expect(navAfterAbortText).not.toContain('Login')
+ expect(navAfterAbortText).not.toContain('Signup')
+
+ vi.doUnmock('./client-router.tsx')
+ vi.doUnmock('./session.ts')
})
diff --git a/client/mcp-apps/widget-host-bridge.test.ts b/client/mcp-apps/widget-host-bridge.test.ts
index 36e935a..1339563 100644
--- a/client/mcp-apps/widget-host-bridge.test.ts
+++ b/client/mcp-apps/widget-host-bridge.test.ts
@@ -1,4 +1,4 @@
-import { afterEach, expect, test } from 'bun:test'
+import { afterEach, expect, test } from 'vitest'
import { createWidgetHostBridge } from './widget-host-bridge.ts'
type HostRequestMessage = {
diff --git a/docs/agents/testing-principles.md b/docs/agents/testing-principles.md
index 3899e2a..ce46801 100644
--- a/docs/agents/testing-principles.md
+++ b/docs/agents/testing-principles.md
@@ -17,7 +17,7 @@ magic.
- Write tests so they could run offline if necessary: avoid relying on the
public internet and third-party services; prefer local fakes/fixtures.
- Prefer fast unit tests for server logic; keep e2e tests focused on journeys.
-- Run server/unit tests with `bun test ./server ./mock-servers` to avoid
+- Run server/unit tests with `bun run test:unit` to avoid
Playwright spec discovery and accidental matches like `mcp-server-e2e`.
## Examples
@@ -25,7 +25,7 @@ magic.
### `Symbol.dispose` with `using`
```ts
-import { test, expect } from 'bun:test'
+import { test, expect } from 'vitest'
const createTempFile = () => {
const path = `/tmp/test-${crypto.randomUUID()}.txt`
@@ -53,7 +53,7 @@ test('reads a temp file', () => {
### `Symbol.asyncDispose` with `await using`
```ts
-import { test, expect } from 'bun:test'
+import { test, expect } from 'vitest'
const createDisposableServer = async () => {
const server = Bun.serve({
diff --git a/mcp/context.test.ts b/mcp/context.test.ts
index 1191194..d8834fd 100644
--- a/mcp/context.test.ts
+++ b/mcp/context.test.ts
@@ -1,5 +1,5 @@
///
-import { expect, test } from 'bun:test'
+import { expect, test } from 'vitest'
import { createMcpCallerContext, parseMcpCallerContext } from './context.ts'
test('createMcpCallerContext normalizes missing user to null', () => {
diff --git a/mcp/mcp-server-e2e.test.ts b/mcp/mcp-server-e2e.test.ts
index 9164467..680b407 100644
--- a/mcp/mcp-server-e2e.test.ts
+++ b/mcp/mcp-server-e2e.test.ts
@@ -1,4 +1,4 @@
-import { expect, test } from 'bun:test'
+import { expect, test } from 'vitest'
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
import {
@@ -14,14 +14,23 @@ import {
type ContentBlock,
} from '@modelcontextprotocol/sdk/types.js'
import getPort from 'get-port'
+import { spawn } from 'node:child_process'
import { mkdtemp, readdir, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
+import {
+ captureOutput,
+ createExitPromise,
+ formatOutput,
+ stopProcess,
+ type TrackedProcess,
+} from '#test-support/process-utils.ts'
const projectRoot = fileURLToPath(new URL('..', import.meta.url))
const migrationsDir = join(projectRoot, 'migrations')
-const bunBin = process.execPath
+const nodeBin = process.execPath
+const wranglerCli = join(projectRoot, 'node_modules', 'wrangler', 'wrangler-dist', 'cli.js')
const defaultTimeoutMs = 60_000
const calculatorUiResourceUri = 'ui://calculator-app/entry-point.html'
@@ -69,19 +78,27 @@ function escapeSql(value: string) {
}
async function runWrangler(args: Array) {
- const proc = Bun.spawn({
- cmd: [bunBin, 'x', 'wrangler', ...args],
- cwd: projectRoot,
- stdout: 'pipe',
- stderr: 'pipe',
- })
+ const proc = spawn(
+ nodeBin,
+ ['--no-warnings', '--experimental-vm-modules', wranglerCli, ...args],
+ {
+ cwd: projectRoot,
+ stdio: ['ignore', 'pipe', 'pipe'],
+ env: {
+ ...process.env,
+ CLOUDFLARE_ENV: 'test',
+ NODE_OPTIONS: '',
+ },
+ },
+ )
+ const exitPromise = createExitPromise(proc)
const stdoutPromise = proc.stdout
- ? new Response(proc.stdout).text()
+ ? streamToText(proc.stdout)
: Promise.resolve('')
const stderrPromise = proc.stderr
- ? new Response(proc.stderr).text()
+ ? streamToText(proc.stderr)
: Promise.resolve('')
- const exitCode = await proc.exited
+ const exitCode = await exitPromise
const [stdout, stderr] = await Promise.all([stdoutPromise, stderrPromise])
if (exitCode !== 0) {
throw new Error(
@@ -158,53 +175,30 @@ async function listMigrationFiles() {
.sort((left, right) => left.localeCompare(right))
}
-function captureOutput(stream: ReadableStream | null) {
- let output = ''
- if (!stream) {
- return () => output
- }
-
- const reader = stream.getReader()
- const decoder = new TextDecoder()
-
- const read = async () => {
- try {
- while (true) {
- const { value, done } = await reader.read()
- if (done) break
- if (value) {
- output += decoder.decode(value)
- }
- }
- } catch {
- // Ignore stream errors while capturing output.
- }
- }
-
- void read()
- return () => output
-}
-
-function formatOutput(stdout: string, stderr: string) {
- const snippets: Array = []
- if (stdout.trim()) {
- snippets.push(`stdout: ${stdout.trim().slice(-2000)}`)
- }
- if (stderr.trim()) {
- snippets.push(`stderr: ${stderr.trim().slice(-2000)}`)
- }
- return snippets.length > 0 ? ` Output:\n${snippets.join('\n')}` : ''
+function streamToText(
+ stream: NodeJS.ReadableStream | null | undefined,
+): Promise {
+ if (!stream) return Promise.resolve('')
+ return new Promise((resolve, reject) => {
+ let output = ''
+ stream.setEncoding('utf8')
+ stream.on('data', (chunk) => {
+ output += chunk
+ })
+ stream.on('end', () => resolve(output))
+ stream.on('error', reject)
+ })
}
async function waitForServer(
origin: string,
- proc: ReturnType,
+ process: TrackedProcess,
getStdout: () => string,
getStderr: () => string,
) {
let exited = false
let exitCode: number | null = null
- void proc.exited
+ void process.exitPromise
.then((code) => {
exited = true
exitCode = code
@@ -244,19 +238,6 @@ async function waitForServer(
)
}
-async function stopProcess(proc: ReturnType) {
- let exited = false
- void proc.exited.then(() => {
- exited = true
- })
- proc.kill('SIGINT')
- await Promise.race([proc.exited, delay(5_000)])
- if (!exited) {
- proc.kill('SIGKILL')
- await proc.exited
- }
-}
-
async function startDevServer(persistDir: string) {
const port = await getPort({ host: '127.0.0.1' })
const inspectorPortBase =
@@ -269,11 +250,12 @@ async function startDevServer(persistDir: string) {
).filter((candidate) => candidate > 0 && candidate <= 65_535),
})
const origin = `http://127.0.0.1:${port}`
- const proc = Bun.spawn({
- cmd: [
- bunBin,
- 'x',
- 'wrangler',
+ const proc = spawn(
+ nodeBin,
+ [
+ '--no-warnings',
+ '--experimental-vm-modules',
+ wranglerCli,
'dev',
'--local',
'--env',
@@ -290,24 +272,30 @@ async function startDevServer(persistDir: string) {
'--log-level',
'error',
],
- cwd: projectRoot,
- stdout: 'pipe',
- stderr: 'pipe',
- env: {
- ...process.env,
- CLOUDFLARE_ENV: 'test',
+ {
+ cwd: projectRoot,
+ stdio: ['ignore', 'pipe', 'pipe'],
+ env: {
+ ...process.env,
+ CLOUDFLARE_ENV: 'test',
+ NODE_OPTIONS: '',
+ },
},
- })
+ )
+ const trackedProcess: TrackedProcess = {
+ proc,
+ exitPromise: createExitPromise(proc),
+ }
const getStdout = captureOutput(proc.stdout)
const getStderr = captureOutput(proc.stderr)
- await waitForServer(origin, proc, getStdout, getStderr)
+ await waitForServer(origin, trackedProcess, getStdout, getStderr)
return {
origin,
[Symbol.asyncDispose]: async () => {
- await stopProcess(proc)
+ await stopProcess(trackedProcess)
},
}
}
@@ -481,6 +469,7 @@ async function createMcpClient(
test(
'mcp server lists tools after interactive oauth authorize flow',
+ { timeout: defaultTimeoutMs },
async () => {
await using database = await createTestDatabase()
await using server = await startDevServer(database.persistDir)
@@ -496,12 +485,12 @@ test(
const toolNames = result.tools.map((tool) => tool.name)
expect(toolNames.sort()).toEqual(['do_math', 'open_calculator_ui'])
- },
- { timeout: defaultTimeoutMs },
+ }
)
test(
'mcp server lists tools after oauth flow',
+ { timeout: defaultTimeoutMs },
async () => {
await using database = await createTestDatabase()
await using server = await startDevServer(database.persistDir)
@@ -521,12 +510,12 @@ test(
)
expect(resourceUris).toContain(calculatorUiResourceUri)
- },
- { timeout: defaultTimeoutMs },
+ }
)
test(
'mcp server executes do_math tool',
+ { timeout: defaultTimeoutMs },
async () => {
await using database = await createTestDatabase()
await using server = await startDevServer(database.persistDir)
@@ -553,12 +542,12 @@ test(
)?.text ?? ''
expect(textOutput).toContain('12')
- },
- { timeout: defaultTimeoutMs },
+ }
)
test(
'mcp server executes calculator ui tool and serves resource entry point',
+ { timeout: defaultTimeoutMs },
async () => {
await using database = await createTestDatabase()
await using server = await startDevServer(database.persistDir)
@@ -640,6 +629,5 @@ test(
expect(calculatorResourceMeta?.ui?.csp?.resourceDomains).toContain(
server.origin,
)
- },
- { timeout: defaultTimeoutMs },
+ }
)
diff --git a/mock-servers/resend/resend-mock.test.ts b/mock-servers/resend/resend-mock.test.ts
index 0b90fec..62ee3c9 100644
--- a/mock-servers/resend/resend-mock.test.ts
+++ b/mock-servers/resend/resend-mock.test.ts
@@ -1,61 +1,37 @@
///
-import { expect, test } from 'bun:test'
+import { expect, test } from 'vitest'
import getPort from 'get-port'
+import { spawn, spawnSync } from 'node:child_process'
import { setTimeout as delay } from 'node:timers/promises'
+import { join } from 'node:path'
import { createTemporaryDirectory } from '#tools/temp-directory.ts'
+import {
+ captureOutput,
+ createExitPromise,
+ formatOutput,
+ stopProcess,
+ type TrackedProcess,
+} from '#test-support/process-utils.ts'
const workerConfig = 'mock-servers/resend/wrangler.jsonc'
-const bunBin = process.execPath
const projectRoot = process.cwd()
+const nodeBin = process.execPath
+const wranglerCli = join(projectRoot, 'node_modules', 'wrangler', 'wrangler-dist', 'cli.js')
const defaultTimeoutMs = 60_000
-function captureOutput(stream: ReadableStream | null) {
- let output = ''
- if (!stream) {
- return () => output
- }
-
- const reader = stream.getReader()
- const decoder = new TextDecoder()
-
- const read = async () => {
- try {
- while (true) {
- const { value, done } = await reader.read()
- if (done) break
- if (value) {
- output += decoder.decode(value)
- }
- }
- } catch {
- // Ignore stream errors while capturing output.
- }
- }
-
- void read()
- return () => output
-}
-
-function formatOutput(stdout: string, stderr: string) {
- const snippets: Array = []
- if (stdout.trim()) {
- snippets.push(`stdout: ${stdout.trim().slice(-2000)}`)
- }
- if (stderr.trim()) {
- snippets.push(`stderr: ${stderr.trim().slice(-2000)}`)
- }
- return snippets.length > 0 ? ` Output:\n${snippets.join('\n')}` : ''
+function bufferToText(buffer: Uint8Array | null | undefined) {
+ return buffer ? Buffer.from(buffer).toString() : ''
}
async function waitForMockServer(
origin: string,
- proc: ReturnType,
+ process: TrackedProcess,
getStdout: () => string,
getStderr: () => string,
) {
let exited = false
let exitCode: number | null = null
- void proc.exited
+ void process.exitPromise
.then((code) => {
exited = true
exitCode = code
@@ -96,11 +72,12 @@ async function waitForMockServer(
}
function applyResendMockMigrations(persistDir: string) {
- const proc = Bun.spawnSync({
- cmd: [
- bunBin,
- 'x',
- 'wrangler',
+ const proc = spawnSync(
+ nodeBin,
+ [
+ '--no-warnings',
+ '--experimental-vm-modules',
+ wranglerCli,
'd1',
'migrations',
'apply',
@@ -113,29 +90,22 @@ function applyResendMockMigrations(persistDir: string) {
'--persist-to',
persistDir,
],
- cwd: projectRoot,
- stdout: 'pipe',
- stderr: 'pipe',
- })
- if (proc.exitCode !== 0) {
- const err = proc.stderr?.toString() ?? proc.stdout?.toString() ?? ''
+ {
+ cwd: projectRoot,
+ stdio: ['ignore', 'pipe', 'pipe'],
+ env: {
+ ...process.env,
+ CLOUDFLARE_ENV: 'test',
+ NODE_OPTIONS: '',
+ },
+ },
+ )
+ if (proc.status !== 0) {
+ const err = bufferToText(proc.stderr) || bufferToText(proc.stdout)
throw new Error(`Resend mock D1 migrations failed: ${err}`)
}
}
-async function stopProcess(proc: ReturnType) {
- let exited = false
- void proc.exited.then(() => {
- exited = true
- })
- proc.kill('SIGINT')
- await Promise.race([proc.exited, delay(5_000)])
- if (!exited) {
- proc.kill('SIGKILL')
- await proc.exited
- }
-}
-
async function startMockResendWorker(persistDir: string, token: string) {
const port = await getPort({ host: '127.0.0.1' })
const inspectorPortBase =
@@ -149,11 +119,12 @@ async function startMockResendWorker(persistDir: string, token: string) {
})
const origin = `http://127.0.0.1:${port}`
applyResendMockMigrations(persistDir)
- const proc = Bun.spawn({
- cmd: [
- bunBin,
- 'x',
- 'wrangler',
+ const proc = spawn(
+ nodeBin,
+ [
+ '--no-warnings',
+ '--experimental-vm-modules',
+ wranglerCli,
'dev',
'--local',
'--env',
@@ -174,30 +145,37 @@ async function startMockResendWorker(persistDir: string, token: string) {
'--log-level',
'error',
],
- cwd: projectRoot,
- stdout: 'pipe',
- stderr: 'pipe',
- env: {
- ...process.env,
- CLOUDFLARE_ENV: 'test',
+ {
+ cwd: projectRoot,
+ stdio: ['ignore', 'pipe', 'pipe'],
+ env: {
+ ...process.env,
+ CLOUDFLARE_ENV: 'test',
+ NODE_OPTIONS: '',
+ },
},
- })
+ )
+ const trackedProcess: TrackedProcess = {
+ proc,
+ exitPromise: createExitPromise(proc),
+ }
const getStdout = captureOutput(proc.stdout)
const getStderr = captureOutput(proc.stderr)
- await waitForMockServer(origin, proc, getStdout, getStderr)
+ await waitForMockServer(origin, trackedProcess, getStdout, getStderr)
return {
origin,
[Symbol.asyncDispose]: async () => {
- await stopProcess(proc)
+ await stopProcess(trackedProcess)
},
}
}
test(
'resend mock stores messages in D1 and exposes a count',
+ { timeout: defaultTimeoutMs },
async () => {
await using tempDir = await createTemporaryDirectory('resend-mock-d1-')
const token = 'test-mock-token'
@@ -262,11 +240,11 @@ test(
email,
)
},
- { timeout: defaultTimeoutMs },
)
test(
'resend mock rejects unauthenticated requests when a token is configured',
+ { timeout: defaultTimeoutMs },
async () => {
await using tempDir = await createTemporaryDirectory('resend-mock-auth-')
const token = 'test-mock-token'
@@ -284,11 +262,11 @@ test(
})
expect(createResp.status).toBe(401)
},
- { timeout: defaultTimeoutMs },
)
test(
'resend mock dashboard keeps token in endpoint links',
+ { timeout: defaultTimeoutMs },
async () => {
await using tempDir = await createTemporaryDirectory(
'resend-mock-dashboard-',
@@ -314,5 +292,4 @@ test(
expect(dashboardHtml).toContain(`href="/__mocks/meta?token=${token}"`)
expect(dashboardHtml).toContain(`href="/__mocks/messages?token=${token}"`)
},
- { timeout: defaultTimeoutMs },
)
diff --git a/package.json b/package.json
index ff7d1dd..318e13c 100644
--- a/package.json
+++ b/package.json
@@ -36,10 +36,11 @@
"typecheck": "tsc -b --noEmit",
"inspect": "bunx -p @mcpjam/inspector inspector",
"validate": "concurrently --kill-others-on-fail -n format,lint,build,typecheck,test,test:mcp -c green,yellow,blue,magenta,cyan,red \"bun run format:check\" \"bun run lint:fix\" \"bun run build\" \"bun run typecheck\" \"bun run test:e2e\" \"bun run test:mcp\"",
+ "test:unit": "bunx vitest run --config vitest-unit.config.ts",
"test:e2e": "bun tools/prepare-e2e-env.ts && playwright test",
"test:e2e:ui": "bun tools/prepare-e2e-env.ts && playwright test --ui",
"test:e2e:install": "playwright install chromium --with-deps",
- "test:mcp": "bun run build:mcp-apps && bun test mcp/mcp-server-e2e.test.ts"
+ "test:mcp": "bun run build:mcp-apps && bunx vitest run mcp/mcp-server-e2e.test.ts"
},
"devDependencies": {
"@epic-web/config": "^1.24.1",
@@ -52,6 +53,7 @@
"prettier": "^3.8.1",
"set-cookie-parser": "^3.0.1",
"typescript": "^5.9.3",
+ "vitest": "^4.1.2",
"wrangler": "^4.72.0"
},
"prettier": "@epic-web/config/prettier",
diff --git a/server/handlers/auth-handler.test.ts b/server/handlers/auth-handler.test.ts
index 5bb7e7e..f56946c 100644
--- a/server/handlers/auth-handler.test.ts
+++ b/server/handlers/auth-handler.test.ts
@@ -1,5 +1,5 @@
///
-import { beforeAll, expect, test } from 'bun:test'
+import { beforeAll, expect, test } from 'vitest'
import { RequestContext } from 'remix/fetch-router'
import { setAuthSessionSecret } from '#server/auth-session.ts'
import { createPasswordHash } from '#server/password-hash.ts'
diff --git a/server/handlers/health-handler.test.ts b/server/handlers/health-handler.test.ts
index aed5db6..37b6d7a 100644
--- a/server/handlers/health-handler.test.ts
+++ b/server/handlers/health-handler.test.ts
@@ -1,5 +1,5 @@
///
-import { expect, test } from 'bun:test'
+import { expect, test } from 'vitest'
import { RequestContext } from 'remix/fetch-router'
import { createHealthHandler } from './health.ts'
diff --git a/server/handlers/session-handler.test.ts b/server/handlers/session-handler.test.ts
index 3b8408f..99c9b98 100644
--- a/server/handlers/session-handler.test.ts
+++ b/server/handlers/session-handler.test.ts
@@ -1,5 +1,5 @@
///
-import { beforeAll, expect, test } from 'bun:test'
+import { beforeAll, expect, test } from 'vitest'
import { RequestContext } from 'remix/fetch-router'
import {
createAuthCookie,
diff --git a/shared/ai-env-validation.test.ts b/shared/ai-env-validation.test.ts
index 126248d..f039f14 100644
--- a/shared/ai-env-validation.test.ts
+++ b/shared/ai-env-validation.test.ts
@@ -1,4 +1,4 @@
-import { expect, test } from 'bun:test'
+import { expect, test } from 'vitest'
import {
getRemoteAiLocalDevCredentialsError,
getRemoteAiLocalDevStartupError,
diff --git a/shared/mock-ai.test.ts b/shared/mock-ai.test.ts
index 83c225e..00616d3 100644
--- a/shared/mock-ai.test.ts
+++ b/shared/mock-ai.test.ts
@@ -1,4 +1,4 @@
-import { expect, test } from 'bun:test'
+import { expect, test } from 'vitest'
import { parseMockToolCommand } from './mock-ai.ts'
test('parseMockToolCommand returns null for non-tool messages', () => {
diff --git a/shared/password-hash.test.ts b/shared/password-hash.test.ts
index b279ae7..2a4f787 100644
--- a/shared/password-hash.test.ts
+++ b/shared/password-hash.test.ts
@@ -1,4 +1,4 @@
-import { expect, test } from 'bun:test'
+import { expect, test } from 'vitest'
import { createPasswordHash, verifyPassword } from './password-hash.ts'
diff --git a/test-support/cloudflare-workers-shim.ts b/test-support/cloudflare-workers-shim.ts
new file mode 100644
index 0000000..aa50391
--- /dev/null
+++ b/test-support/cloudflare-workers-shim.ts
@@ -0,0 +1,9 @@
+export class WorkerEntrypoint {
+ ctx?: unknown
+ env?: Env
+
+ constructor(ctx?: unknown, env?: Env) {
+ this.ctx = ctx
+ this.env = env
+ }
+}
diff --git a/test-support/process-utils.ts b/test-support/process-utils.ts
new file mode 100644
index 0000000..c750753
--- /dev/null
+++ b/test-support/process-utils.ts
@@ -0,0 +1,62 @@
+import { setTimeout as delay } from 'node:timers/promises'
+import { type ChildProcess } from 'node:child_process'
+
+export type TrackedProcess = {
+ proc: ChildProcess
+ exitPromise: Promise
+}
+
+export function captureOutput(stream: NodeJS.ReadableStream | null | undefined) {
+ let output = ''
+ if (!stream) {
+ return () => output
+ }
+ stream.setEncoding('utf8')
+ stream.on('data', (chunk) => {
+ output += chunk
+ })
+ stream.on('error', () => {
+ // Ignore stream errors while capturing output.
+ })
+ return () => output
+}
+
+export function formatOutput(stdout: string, stderr: string) {
+ const snippets: Array = []
+ if (stdout.trim()) {
+ snippets.push(`stdout: ${stdout.trim().slice(-2000)}`)
+ }
+ if (stderr.trim()) {
+ snippets.push(`stderr: ${stderr.trim().slice(-2000)}`)
+ }
+ return snippets.length > 0 ? ` Output:\n${snippets.join('\n')}` : ''
+}
+
+export function createExitPromise(proc: ChildProcess): Promise {
+ if (proc.exitCode !== null || proc.signalCode !== null) {
+ return Promise.resolve(proc.exitCode)
+ }
+ return new Promise((resolve) => {
+ const finalize = (code: number | null) => {
+ proc.off('error', onError)
+ proc.off('exit', onExit)
+ resolve(code)
+ }
+ const onError = () => finalize(null)
+ const onExit = (code: number | null) => finalize(code)
+ proc.once('error', onError)
+ proc.once('exit', onExit)
+ })
+}
+
+export async function stopProcess({ proc, exitPromise }: TrackedProcess) {
+ proc.kill('SIGINT')
+ const exited = await Promise.race([
+ exitPromise.then(() => true),
+ delay(5_000).then(() => false),
+ ])
+ if (!exited) {
+ proc.kill('SIGKILL')
+ await exitPromise
+ }
+}
diff --git a/tools/seed-test-data.test.ts b/tools/seed-test-data.test.ts
index 1d87025..9dbe1fd 100644
--- a/tools/seed-test-data.test.ts
+++ b/tools/seed-test-data.test.ts
@@ -1,4 +1,4 @@
-import { expect, test } from 'bun:test'
+import { expect, test } from 'vitest'
import { parseArgs, resolveWranglerEnv } from './seed-test-data.ts'
diff --git a/types/tsconfig-tools.json b/types/tsconfig-tools.json
index a8f1217..06231b5 100644
--- a/types/tsconfig-tools.json
+++ b/types/tsconfig-tools.json
@@ -34,6 +34,7 @@
"../cli.ts",
"../docs/post-download.ts",
"../mcp/mcp-server-e2e.test.ts",
+ "../test-support/**/*.ts",
"../tools/**/*.ts",
"../shared/**/*.ts"
]
diff --git a/vitest-unit.config.ts b/vitest-unit.config.ts
new file mode 100644
index 0000000..702bc52
--- /dev/null
+++ b/vitest-unit.config.ts
@@ -0,0 +1,9 @@
+import { defineConfig } from 'vitest/config'
+
+export default defineConfig({
+ test: {
+ environment: 'node',
+ globals: false,
+ include: ['server/**/*.test.ts', 'mock-servers/**/*.test.ts'],
+ },
+})
diff --git a/vitest.config.ts b/vitest.config.ts
new file mode 100644
index 0000000..226d9b0
--- /dev/null
+++ b/vitest.config.ts
@@ -0,0 +1,25 @@
+import { fileURLToPath } from 'node:url'
+import { defineConfig } from 'vitest/config'
+
+export default defineConfig({
+ resolve: {
+ alias: {
+ 'cloudflare:workers': fileURLToPath(
+ new URL('./test-support/cloudflare-workers-shim.ts', import.meta.url),
+ ),
+ },
+ },
+ test: {
+ environment: 'node',
+ globals: false,
+ include: [
+ 'client/**/*.test.ts',
+ 'mcp/**/*.test.ts',
+ 'mock-servers/**/*.test.ts',
+ 'server/**/*.test.ts',
+ 'shared/**/*.test.ts',
+ 'tools/**/*.test.ts',
+ 'worker/**/*.test.ts',
+ ],
+ },
+})
diff --git a/worker/ai-runtime.test.ts b/worker/ai-runtime.test.ts
index f0cf469..9a27be7 100644
--- a/worker/ai-runtime.test.ts
+++ b/worker/ai-runtime.test.ts
@@ -1,31 +1,54 @@
///
-import { expect, mock, test } from 'bun:test'
-
-async function loadCreateAiRuntime(cacheKey = crypto.randomUUID()) {
- const module = await import(`./ai-runtime.ts?test=${cacheKey}`)
+import { createServer } from 'node:http'
+import { type AddressInfo } from 'node:net'
+import { expect, test, vi } from 'vitest'
+
+async function loadCreateAiRuntime(
+ setupMocks?: () => void,
+) {
+ vi.resetModules()
+ setupMocks?.()
+ const module = await import('./ai-runtime.ts')
return module.createAiRuntime
}
async function createMockServer() {
- const server = Bun.serve({
- port: 0,
- fetch(request) {
- const url = new URL(request.url)
- if (url.pathname !== '/chat') {
- return new Response('Not Found', { status: 404 })
- }
- return Response.json({
+ const server = createServer((request, response) => {
+ const url = new URL(request.url ?? '', 'http://127.0.0.1')
+ if (url.pathname !== '/chat') {
+ response.statusCode = 404
+ response.end('Not Found')
+ return
+ }
+
+ response.statusCode = 200
+ response.setHeader('Content-Type', 'application/json')
+ response.end(
+ JSON.stringify({
kind: 'text',
text: 'hello from mock runtime',
chunks: ['hello ', 'from ', 'mock runtime'],
- })
- },
+ }),
+ )
})
+ await new Promise((resolve) => {
+ server.listen(0, '127.0.0.1', resolve)
+ })
+ const address = server.address() as AddressInfo
+
return {
- baseUrl: `http://127.0.0.1:${server.port}`,
+ baseUrl: `http://127.0.0.1:${address.port}`,
[Symbol.asyncDispose]: async () => {
- await server.stop()
+ await new Promise((resolve, reject) => {
+ server.close((error) => {
+ if (error) {
+ reject(error)
+ return
+ }
+ resolve()
+ })
+ })
},
}
}
@@ -95,27 +118,27 @@ test('createAiRuntime configures remote streaming to continue after tool calls',
const streamTextCalls: Array> = []
const stopWhenCalls: Array = []
- mock.module('ai', () => ({
- convertToModelMessages: async (messages: Array) => messages,
- stepCountIs: (stepCount: number) => {
- stopWhenCalls.push(stepCount)
- return { kind: 'stop-condition', stepCount }
- },
- streamText: (options: Record) => {
- streamTextCalls.push(options)
- return {
- toUIMessageStreamResponse: () =>
- new Response('ok', {
- headers: { 'Content-Type': 'text/plain; charset=utf-8' },
- }),
- }
- },
- }))
- mock.module('workers-ai-provider', () => ({
- createWorkersAI: () => (model: string) => ({ provider: 'workers-ai', model }),
- }))
-
- const createAiRuntime = await loadCreateAiRuntime('remote-tool-loop')
+ const createAiRuntime = await loadCreateAiRuntime(() => {
+ vi.doMock('ai', () => ({
+ convertToModelMessages: async (messages: Array) => messages,
+ stepCountIs: (stepCount: number) => {
+ stopWhenCalls.push(stepCount)
+ return { kind: 'stop-condition', stepCount }
+ },
+ streamText: (options: Record) => {
+ streamTextCalls.push(options)
+ return {
+ toUIMessageStreamResponse: () =>
+ new Response('ok', {
+ headers: { 'Content-Type': 'text/plain; charset=utf-8' },
+ }),
+ }
+ },
+ }))
+ vi.doMock('workers-ai-provider', () => ({
+ createWorkersAI: () => (model: string) => ({ provider: 'workers-ai', model }),
+ }))
+ })
const runtime = createAiRuntime({
AI_MODE: 'remote',
AI_GATEWAY_ID: 'gateway-id',
@@ -140,4 +163,7 @@ test('createAiRuntime configures remote streaming to continue after tool calls',
onFinish,
stopWhen: { kind: 'stop-condition', stepCount: 5 },
})
+
+ vi.doUnmock('ai')
+ vi.doUnmock('workers-ai-provider')
})
diff --git a/worker/mcp-auth.test.ts b/worker/mcp-auth.test.ts
index 1fd5df1..8d1fb10 100644
--- a/worker/mcp-auth.test.ts
+++ b/worker/mcp-auth.test.ts
@@ -1,9 +1,6 @@
///
-import { expect, test } from 'bun:test'
-import {
- type OAuthHelpers,
- type TokenSummary,
-} from '@cloudflare/workers-oauth-provider'
+import { expect, test } from 'vitest'
+import { type OAuthHelpers, type TokenSummary } from '@cloudflare/workers-oauth-provider'
import {
buildProtectedResourceMetadata,
handleMcpRequest,
diff --git a/worker/oauth-handlers.test.ts b/worker/oauth-handlers.test.ts
index 9b79319..974ea37 100644
--- a/worker/oauth-handlers.test.ts
+++ b/worker/oauth-handlers.test.ts
@@ -1,5 +1,5 @@
///
-import { expect, test } from 'bun:test'
+import { expect, test } from 'vitest'
import {
type AuthRequest,
type ClientInfo,
diff --git a/worker/oauth-handlers.ts b/worker/oauth-handlers.ts
index cd150a7..650ff66 100644
--- a/worker/oauth-handlers.ts
+++ b/worker/oauth-handlers.ts
@@ -1,7 +1,4 @@
-import {
- type AuthRequest,
- type OAuthHelpers,
-} from '@cloudflare/workers-oauth-provider'
+import { type AuthRequest, type OAuthHelpers } from '@cloudflare/workers-oauth-provider'
import { getRequestIp, logAuditEvent } from '#server/audit-log.ts'
import {
readAuthSessionResult,