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,