From 7d0264996df6d5e94d9baf85d9ea44cade815588 Mon Sep 17 00:00:00 2001 From: Yeganathan S <63534555+skwowet@users.noreply.github.com> Date: Mon, 1 Jun 2026 10:52:50 +0530 Subject: [PATCH 1/5] test: introduce foundation for automated server tests - Add shared test infrastructure - Configure backend and microservice test setup Signed-off-by: Yeganathan S <63534555+skwowet@users.noreply.github.com> --- .env.test | 5 + .github/workflows/server-tests.yml | 71 ++ backend/package.json | 2 - package.json | 10 +- pnpm-lock.yaml | 694 +++++++++--------- scripts/cli | 29 +- scripts/prepare-test-template-db.sh | 53 ++ scripts/scaffold.test.yaml | 25 + .../src/bin/fix-member-attributes.ts | 4 +- services/libs/data-access-layer/package.json | 6 +- .../data-access-layer/src/auditLogs/index.ts | 4 +- .../data-access-layer/src/queryExecutor.ts | 62 +- .../libs/data-access-layer/vitest.config.ts | 13 - services/libs/database/src/connection.ts | 8 - services/libs/database/src/index.ts | 1 + services/libs/database/src/queryExecutor.ts | 63 ++ services/libs/test-kit/package.json | 23 + services/libs/test-kit/src/fixtures.ts | 23 + services/libs/test-kit/src/postgres.ts | 197 +++++ services/libs/test-kit/src/types.ts | 6 + services/libs/test-kit/tsconfig.json | 4 + vitest.config.ts | 28 + 22 files changed, 888 insertions(+), 443 deletions(-) create mode 100644 .env.test create mode 100644 .github/workflows/server-tests.yml create mode 100755 scripts/prepare-test-template-db.sh create mode 100644 scripts/scaffold.test.yaml delete mode 100644 services/libs/data-access-layer/vitest.config.ts create mode 100644 services/libs/database/src/queryExecutor.ts create mode 100644 services/libs/test-kit/package.json create mode 100644 services/libs/test-kit/src/fixtures.ts create mode 100644 services/libs/test-kit/src/postgres.ts create mode 100644 services/libs/test-kit/src/types.ts create mode 100644 services/libs/test-kit/tsconfig.json create mode 100644 vitest.config.ts diff --git a/.env.test b/.env.test new file mode 100644 index 0000000000..130207ecee --- /dev/null +++ b/.env.test @@ -0,0 +1,5 @@ +NODE_ENV=test +DB_HOST=localhost +DB_PORT=5434 +DB_USER=postgres +DB_PASSWORD=example diff --git a/.github/workflows/server-tests.yml b/.github/workflows/server-tests.yml new file mode 100644 index 0000000000..020e5c24e2 --- /dev/null +++ b/.github/workflows/server-tests.yml @@ -0,0 +1,71 @@ +name: Server Tests + +on: + pull_request: + # Only trigger when relevant files change to save CI minutes + paths-ignore: + - 'frontend/**' + - '**.md' + - '.gitignore' + - '.editorconfig' + - '**/.eslintrc*' + - '.prettierrc' + - '.prettierignore' + - 'LICENSE' + +# Automatically cancel in-progress runs if you push new code to the same PR +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + test: + runs-on: ubuntu-latest + timeout-minutes: 30 + + services: + postgres: + image: postgres:14-alpine + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: example + POSTGRES_DB: postgres + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U postgres" + --health-interval 2s + --health-timeout 5s + --health-retries 15 + postgres -c wal_level=logical + + env: + NODE_ENV: test + DB_HOST: localhost + DB_PORT: 5432 + DB_USER: postgres + DB_PASSWORD: example + + steps: + - name: Check out repository code + uses: actions/checkout@v4 + + - name: Setup Node and cache pnpm + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'pnpm' # Built-in caching for faster runs + + - name: Install dependencies + run: | + corepack enable + pnpm i --frozen-lockfile + + - name: Prepare test database + run: SEQUIN_BOOTSTRAP=1 ./scripts/prepare-test-template-db.sh + + - name: Run server tests + run: pnpm test:server diff --git a/backend/package.json b/backend/package.json index 94d27bb3d9..c1eec8fa5d 100644 --- a/backend/package.json +++ b/backend/package.json @@ -93,7 +93,6 @@ "dotenv": "8.2.0", "dotenv-expand": "^8.0.3", "emoji-dictionary": "^1.0.11", - "erlpack": "^0.1.4", "express": "4.17.1", "express-oauth2-jwt-bearer": "^1.7.4", "express-rate-limit": "6.5.1", @@ -132,7 +131,6 @@ "uuid": "^9.0.0", "validator": "^13.7.0", "verify-github-webhook": "^1.0.1", - "zlib-sync": "^0.1.8", "zod": "^4.3.6" }, "private": true, diff --git a/package.json b/package.json index c6179e1862..9de1a22101 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,18 @@ { "private": true, "scripts": { - "prepare": "husky" + "prepare": "husky", + "test": "vitest run", + "test:server": "vitest run --project server", + "test:changed": "vitest --changed", + "test:watch": "vitest" }, "devDependencies": { "@commitlint/cli": "^19.8.0", "@commitlint/config-conventional": "^19.8.0", - "husky": "^9.1.7" + "husky": "^9.1.7", + "vite": "6.3.5", + "vitest": "4.1.7" }, "packageManager": "pnpm@9.11.0+sha512.0a203ffaed5a3f63242cd064c8fb5892366c103e328079318f78062f24ea8c9d50bc6a47aa3567cabefd824d170e78fa2745ed1f16b132e16436146b7688f19b" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8b3408a593..d9ed0434f2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,12 @@ importers: husky: specifier: ^9.1.7 version: 9.1.7 + vite: + specifier: 6.3.5 + version: 6.3.5(@types/node@22.19.10)(jiti@2.4.2)(terser@5.43.1)(yaml@2.7.0) + vitest: + specifier: 4.1.7 + version: 4.1.7(@types/node@22.19.10)(vite@6.3.5(@types/node@22.19.10)(jiti@2.4.2)(terser@5.43.1)(yaml@2.7.0)) .github/actions/node: dependencies: @@ -222,9 +228,6 @@ importers: emoji-dictionary: specifier: ^1.0.11 version: 1.0.11 - erlpack: - specifier: ^0.1.4 - version: 0.1.4 express: specifier: 4.17.1 version: 4.17.1 @@ -339,9 +342,6 @@ importers: verify-github-webhook: specifier: ^1.0.1 version: 1.0.1 - zlib-sync: - specifier: ^0.1.8 - version: 0.1.9 zod: specifier: ^4.3.6 version: 4.3.6 @@ -2174,9 +2174,6 @@ importers: typescript: specifier: ^5.6.3 version: 5.6.3 - vitest: - specifier: ^3.2.4 - version: 3.2.4(@types/debug@4.1.12)(@types/node@18.19.31)(terser@5.43.1) services/libs/database: dependencies: @@ -2495,6 +2492,28 @@ importers: specifier: ^5.6.3 version: 5.6.3 + services/libs/test-kit: + dependencies: + '@crowd/common': + specifier: workspace:* + version: link:../common + '@crowd/database': + specifier: workspace:* + version: link:../database + pg-promise: + specifier: ^11.4.3 + version: 11.6.0 + devDependencies: + '@types/node': + specifier: ^20.0.0 + version: 20.12.7 + typescript: + specifier: ^5.6.3 + version: 5.6.3 + vitest: + specifier: 4.1.7 + version: 4.1.7(@types/node@20.12.7)(vite@6.3.5(@types/node@20.12.7)(jiti@2.4.2)(terser@5.43.1)(yaml@2.7.0)) + services/libs/types: devDependencies: '@types/node': @@ -3210,9 +3229,9 @@ packages: cpu: [ppc64] os: [aix] - '@esbuild/aix-ppc64@0.21.5': - resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} - engines: {node: '>=12'} + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + engines: {node: '>=18'} cpu: [ppc64] os: [aix] @@ -3222,9 +3241,9 @@ packages: cpu: [arm64] os: [android] - '@esbuild/android-arm64@0.21.5': - resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} - engines: {node: '>=12'} + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + engines: {node: '>=18'} cpu: [arm64] os: [android] @@ -3234,9 +3253,9 @@ packages: cpu: [arm] os: [android] - '@esbuild/android-arm@0.21.5': - resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} - engines: {node: '>=12'} + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + engines: {node: '>=18'} cpu: [arm] os: [android] @@ -3246,9 +3265,9 @@ packages: cpu: [x64] os: [android] - '@esbuild/android-x64@0.21.5': - resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} - engines: {node: '>=12'} + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + engines: {node: '>=18'} cpu: [x64] os: [android] @@ -3258,9 +3277,9 @@ packages: cpu: [arm64] os: [darwin] - '@esbuild/darwin-arm64@0.21.5': - resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} - engines: {node: '>=12'} + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + engines: {node: '>=18'} cpu: [arm64] os: [darwin] @@ -3270,9 +3289,9 @@ packages: cpu: [x64] os: [darwin] - '@esbuild/darwin-x64@0.21.5': - resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} - engines: {node: '>=12'} + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + engines: {node: '>=18'} cpu: [x64] os: [darwin] @@ -3282,9 +3301,9 @@ packages: cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-arm64@0.21.5': - resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} - engines: {node: '>=12'} + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + engines: {node: '>=18'} cpu: [arm64] os: [freebsd] @@ -3294,9 +3313,9 @@ packages: cpu: [x64] os: [freebsd] - '@esbuild/freebsd-x64@0.21.5': - resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} - engines: {node: '>=12'} + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + engines: {node: '>=18'} cpu: [x64] os: [freebsd] @@ -3306,9 +3325,9 @@ packages: cpu: [arm64] os: [linux] - '@esbuild/linux-arm64@0.21.5': - resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} - engines: {node: '>=12'} + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + engines: {node: '>=18'} cpu: [arm64] os: [linux] @@ -3318,9 +3337,9 @@ packages: cpu: [arm] os: [linux] - '@esbuild/linux-arm@0.21.5': - resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} - engines: {node: '>=12'} + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + engines: {node: '>=18'} cpu: [arm] os: [linux] @@ -3330,9 +3349,9 @@ packages: cpu: [ia32] os: [linux] - '@esbuild/linux-ia32@0.21.5': - resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} - engines: {node: '>=12'} + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + engines: {node: '>=18'} cpu: [ia32] os: [linux] @@ -3342,9 +3361,9 @@ packages: cpu: [loong64] os: [linux] - '@esbuild/linux-loong64@0.21.5': - resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} - engines: {node: '>=12'} + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + engines: {node: '>=18'} cpu: [loong64] os: [linux] @@ -3354,9 +3373,9 @@ packages: cpu: [mips64el] os: [linux] - '@esbuild/linux-mips64el@0.21.5': - resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} - engines: {node: '>=12'} + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + engines: {node: '>=18'} cpu: [mips64el] os: [linux] @@ -3366,9 +3385,9 @@ packages: cpu: [ppc64] os: [linux] - '@esbuild/linux-ppc64@0.21.5': - resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} - engines: {node: '>=12'} + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + engines: {node: '>=18'} cpu: [ppc64] os: [linux] @@ -3378,9 +3397,9 @@ packages: cpu: [riscv64] os: [linux] - '@esbuild/linux-riscv64@0.21.5': - resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} - engines: {node: '>=12'} + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + engines: {node: '>=18'} cpu: [riscv64] os: [linux] @@ -3390,9 +3409,9 @@ packages: cpu: [s390x] os: [linux] - '@esbuild/linux-s390x@0.21.5': - resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} - engines: {node: '>=12'} + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + engines: {node: '>=18'} cpu: [s390x] os: [linux] @@ -3402,45 +3421,63 @@ packages: cpu: [x64] os: [linux] - '@esbuild/linux-x64@0.21.5': - resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} - engines: {node: '>=12'} + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + engines: {node: '>=18'} cpu: [x64] os: [linux] + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + '@esbuild/netbsd-x64@0.19.12': resolution: {integrity: sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==} engines: {node: '>=12'} cpu: [x64] os: [netbsd] - '@esbuild/netbsd-x64@0.21.5': - resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} - engines: {node: '>=12'} + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + engines: {node: '>=18'} cpu: [x64] os: [netbsd] + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-x64@0.19.12': resolution: {integrity: sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==} engines: {node: '>=12'} cpu: [x64] os: [openbsd] - '@esbuild/openbsd-x64@0.21.5': - resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} - engines: {node: '>=12'} + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + engines: {node: '>=18'} cpu: [x64] os: [openbsd] + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + '@esbuild/sunos-x64@0.19.12': resolution: {integrity: sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==} engines: {node: '>=12'} cpu: [x64] os: [sunos] - '@esbuild/sunos-x64@0.21.5': - resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} - engines: {node: '>=12'} + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + engines: {node: '>=18'} cpu: [x64] os: [sunos] @@ -3450,9 +3487,9 @@ packages: cpu: [arm64] os: [win32] - '@esbuild/win32-arm64@0.21.5': - resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} - engines: {node: '>=12'} + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + engines: {node: '>=18'} cpu: [arm64] os: [win32] @@ -3462,9 +3499,9 @@ packages: cpu: [ia32] os: [win32] - '@esbuild/win32-ia32@0.21.5': - resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} - engines: {node: '>=12'} + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + engines: {node: '>=18'} cpu: [ia32] os: [win32] @@ -3474,9 +3511,9 @@ packages: cpu: [x64] os: [win32] - '@esbuild/win32-x64@0.21.5': - resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} - engines: {node: '>=12'} + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + engines: {node: '>=18'} cpu: [x64] os: [win32] @@ -4510,6 +4547,9 @@ packages: '@socket.io/component-emitter@3.1.2': resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@swc/core-darwin-arm64@1.4.17': resolution: {integrity: sha512-HVl+W4LezoqHBAYg2JCqR+s9ife9yPfgWSj37iIawLWzOmuuJ7jVdIB7Ee2B75bEisSEKyxRlTl6Y1Oq3owBgw==} engines: {node: '>=10'} @@ -4929,34 +4969,34 @@ packages: resolution: {integrity: sha512-IBBb+iI2NLu4VQn3Vwldyi2QwaXt5+hTyh58ggAMoCGE6DJmPvwL3KPBWcJl1m9LYPChBLE980Jw+CS4Wokqxw==} hasBin: true - '@vitest/expect@3.2.4': - resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} + '@vitest/expect@4.1.7': + resolution: {integrity: sha512-1R+tw0ortHEbZDGMymm+pN7/AFQ/RkFFdtd7EN+VBpynKmLbP8A3rpEXdshBJ7+8hQ9zBJh/i1s0yKNtxAnU7w==} - '@vitest/mocker@3.2.4': - resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==} + '@vitest/mocker@4.1.7': + resolution: {integrity: sha512-vY7nuamKgfvpA1Koa3oYIw/k7D6kZnpGyNMZW8loow2bsBYla1TFdqTaXncWdRn4pgwNs+90RhnXhJScDwQeJA==} peerDependencies: msw: ^2.4.9 - vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: msw: optional: true vite: optional: true - '@vitest/pretty-format@3.2.4': - resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} + '@vitest/pretty-format@4.1.7': + resolution: {integrity: sha512-umgCarTOYQWIaDMvGDRZij+6b9oVeLIyJzfN+AS88e0ZOU3QTgNNSTtjQOpcvWr3np1N0j4WgZj+sb3oYBDscw==} - '@vitest/runner@3.2.4': - resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==} + '@vitest/runner@4.1.7': + resolution: {integrity: sha512-BapjmAQ2aI78WdMEfeUWivnfVzB+VPGwWRQcJE0OUq7qEeEcBsCSf+0T5iREBNE5nBb4wA5Ya0W6IA+sghdEFw==} - '@vitest/snapshot@3.2.4': - resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==} + '@vitest/snapshot@4.1.7': + resolution: {integrity: sha512-ZacLzja+TmJeZ1h14xW2FB/WpeimUD3haBXQPyJqxvo8jQTmfeA8zv58mtjN2C7EHXZDYVcVYdYmAxjkWVvKCw==} - '@vitest/spy@3.2.4': - resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} + '@vitest/spy@4.1.7': + resolution: {integrity: sha512-kbkI5LMWakyuTIvs6fUJ5qdIVb1XVKsYJAT4OJ938cHMROYMSfmoQdZy0aaAnjbbc8F61vkoTqz/Az+/HiIu5Q==} - '@vitest/utils@3.2.4': - resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} + '@vitest/utils@4.1.7': + resolution: {integrity: sha512-T532WBu791cBxJlCl6SO+J14l81DQx6uQHm1bQbmCDY7nqlEIgkza/UFnSBNaUtSf41unldDFjdOBYEQC4b5Hw==} '@webassemblyjs/ast@1.14.1': resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} @@ -5334,9 +5374,6 @@ packages: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} - bindings@1.5.0: - resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} - bintrees@1.0.2: resolution: {integrity: sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==} @@ -5456,10 +5493,6 @@ packages: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} - cac@6.7.14: - resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} - engines: {node: '>=8'} - cacheable-request@6.1.0: resolution: {integrity: sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==} engines: {node: '>=8'} @@ -5502,8 +5535,8 @@ packages: resolution: {integrity: sha512-6F+UYzTaGB+awsTXg0uSJA1/b/B3DDJzpKVRu0UmyI7DmNeaAl2RFHuTGIN6fEgpadRxoXGb7gbC1xo4C3IdyA==} hasBin: true - chai@5.3.3: - resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} engines: {node: '>=18'} chalk@2.4.2: @@ -5525,10 +5558,6 @@ packages: charenc@0.0.2: resolution: {integrity: sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==} - check-error@2.1.3: - resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} - engines: {node: '>= 16'} - chokidar@3.6.0: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} @@ -5731,6 +5760,9 @@ packages: engines: {node: '>=16'} hasBin: true + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie-signature@1.0.6: resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} @@ -5913,15 +5945,6 @@ packages: supports-color: optional: true - debug@4.4.3: - resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - decamelize@1.2.0: resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} engines: {node: '>=0.10.0'} @@ -5930,10 +5953,6 @@ packages: resolution: {integrity: sha512-BzRPQuY1ip+qDonAOz42gRm/pg9F768C+npV/4JOsxRC2sq+Rlk+Q4ZCAsOhnIaMrgarILY+RMUIvMmmX1qAEA==} engines: {node: '>=4'} - deep-eql@5.0.2: - resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} - engines: {node: '>=6'} - deep-extend@0.6.0: resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} engines: {node: '>=4.0.0'} @@ -6191,9 +6210,6 @@ packages: resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} engines: {node: '>=18'} - erlpack@0.1.4: - resolution: {integrity: sha512-CJYbkEvsB5FqCCu2tLxF1eYKi28PvemC12oqzJ9oO6mDFrFO9G9G7nNJUHhiAyyL9zfXTOJx/tOcrQk+ncD65w==} - error-ex@1.3.2: resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} @@ -6216,6 +6232,9 @@ packages: es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + es-module-lexer@2.1.0: + resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==} + es-object-atoms@1.0.0: resolution: {integrity: sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==} engines: {node: '>= 0.4'} @@ -6257,9 +6276,9 @@ packages: engines: {node: '>=12'} hasBin: true - esbuild@0.21.5: - resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} - engines: {node: '>=12'} + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + engines: {node: '>=18'} hasBin: true escalade@3.1.2: @@ -6577,9 +6596,6 @@ packages: resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} engines: {node: ^10.12.0 || >=12.0.0} - file-uri-to-path@1.0.0: - resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} - fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} @@ -7435,9 +7451,6 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - js-tokens@9.0.1: - resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} - js-yaml@3.14.2: resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} hasBin: true @@ -7732,9 +7745,6 @@ packages: long@5.3.2: resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} - loupe@3.2.1: - resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} - lowercase-keys@1.0.1: resolution: {integrity: sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==} engines: {node: '>=0.10.0'} @@ -8212,6 +8222,9 @@ packages: resolution: {integrity: sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==} engines: {node: '>= 0.4'} + obug@2.1.1: + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + omit-deep-by-values@1.0.2: resolution: {integrity: sha512-+EptGOxiWlwhXyBJK9/UZu8k4LQfTmgCkFj/lrAyUBxAcyF174oFfYbkCk2hcZE1d8D+G4e0YTV2qYzCNCEnmw==} engines: {node: '>=0.10.0'} @@ -8497,10 +8510,6 @@ packages: pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} - pathval@2.0.1: - resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} - engines: {node: '>= 14.16'} - pause@0.0.1: resolution: {integrity: sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==} @@ -9276,8 +9285,8 @@ packages: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} - std-env@3.10.0: - resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + std-env@4.1.0: + resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} stream-events@1.0.5: resolution: {integrity: sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==} @@ -9369,9 +9378,6 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} - strip-literal@3.1.0: - resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} - strnum@1.0.5: resolution: {integrity: sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==} @@ -9511,20 +9517,16 @@ packages: tinyexec@0.3.2: resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + tinyexec@1.2.2: + resolution: {integrity: sha512-M/Q0B2cp4K7kynaT/vnED1j8TlLY+Pp7C6Wl2bl/7u/F0mUVwdyOpwomQb8JpYLitHUssAJRmLZdMCGsrx7i+g==} + engines: {node: '>=18'} + tinyglobby@0.2.15: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} - tinypool@1.1.1: - resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} - engines: {node: ^18.0.0 || >=20.0.0} - - tinyrainbow@2.0.0: - resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} - engines: {node: '>=14.0.0'} - - tinyspy@4.0.4: - resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} + tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} engines: {node: '>=14.0.0'} tldts-core@6.1.18: @@ -9822,27 +9824,27 @@ packages: verify-github-webhook@1.0.1: resolution: {integrity: sha512-c5Kv/wzbPBii5s2FyZssgarhMCaHCQAXexqKl2JX/BbCCbZ8oRICS+x9E82pdCNnV9az2sd9SOb+MUtdPNHyvA==} - vite-node@3.2.4: - resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} + vite@6.3.5: + resolution: {integrity: sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true - - vite@5.4.21: - resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==} - engines: {node: ^18.0.0 || >=20.0.0} - hasBin: true peerDependencies: - '@types/node': ^18.0.0 || >=20.0.0 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + jiti: '>=1.21.0' less: '*' lightningcss: ^1.21.0 sass: '*' sass-embedded: '*' stylus: '*' sugarss: '*' - terser: ^5.4.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 peerDependenciesMeta: '@types/node': optional: true + jiti: + optional: true less: optional: true lightningcss: @@ -9857,27 +9859,44 @@ packages: optional: true terser: optional: true + tsx: + optional: true + yaml: + optional: true - vitest@3.2.4: - resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==} - engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + vitest@4.1.7: + resolution: {integrity: sha512-flYyaFd2CgoCoU+0UKt3pxksgC+S02iTDN0n3LtqaMeXsI9SBcdNujc2k0DeFLzUn/0k538yNjOSdwgCqcrwJA==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' - '@types/debug': ^4.1.12 - '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 - '@vitest/browser': 3.2.4 - '@vitest/ui': 3.2.4 + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.7 + '@vitest/browser-preview': 4.1.7 + '@vitest/browser-webdriverio': 4.1.7 + '@vitest/coverage-istanbul': 4.1.7 + '@vitest/coverage-v8': 4.1.7 + '@vitest/ui': 4.1.7 happy-dom: '*' jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: '@edge-runtime/vm': optional: true - '@types/debug': + '@opentelemetry/api': optional: true '@types/node': optional: true - '@vitest/browser': + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': optional: true '@vitest/ui': optional: true @@ -10120,9 +10139,6 @@ packages: resolution: {integrity: sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==} engines: {node: '>=12.20'} - zlib-sync@0.1.9: - resolution: {integrity: sha512-DinB43xCjVwIBDpaIvQqHbmDsnYnSt6HJ/yiB2MZQGTqgPcwBSZqLkimXwK8BvdjQ/MaZysb5uEenImncqvCqQ==} - zod@4.3.6: resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} @@ -11885,139 +11901,148 @@ snapshots: '@esbuild/aix-ppc64@0.19.12': optional: true - '@esbuild/aix-ppc64@0.21.5': + '@esbuild/aix-ppc64@0.25.12': optional: true '@esbuild/android-arm64@0.19.12': optional: true - '@esbuild/android-arm64@0.21.5': + '@esbuild/android-arm64@0.25.12': optional: true '@esbuild/android-arm@0.19.12': optional: true - '@esbuild/android-arm@0.21.5': + '@esbuild/android-arm@0.25.12': optional: true '@esbuild/android-x64@0.19.12': optional: true - '@esbuild/android-x64@0.21.5': + '@esbuild/android-x64@0.25.12': optional: true '@esbuild/darwin-arm64@0.19.12': optional: true - '@esbuild/darwin-arm64@0.21.5': + '@esbuild/darwin-arm64@0.25.12': optional: true '@esbuild/darwin-x64@0.19.12': optional: true - '@esbuild/darwin-x64@0.21.5': + '@esbuild/darwin-x64@0.25.12': optional: true '@esbuild/freebsd-arm64@0.19.12': optional: true - '@esbuild/freebsd-arm64@0.21.5': + '@esbuild/freebsd-arm64@0.25.12': optional: true '@esbuild/freebsd-x64@0.19.12': optional: true - '@esbuild/freebsd-x64@0.21.5': + '@esbuild/freebsd-x64@0.25.12': optional: true '@esbuild/linux-arm64@0.19.12': optional: true - '@esbuild/linux-arm64@0.21.5': + '@esbuild/linux-arm64@0.25.12': optional: true '@esbuild/linux-arm@0.19.12': optional: true - '@esbuild/linux-arm@0.21.5': + '@esbuild/linux-arm@0.25.12': optional: true '@esbuild/linux-ia32@0.19.12': optional: true - '@esbuild/linux-ia32@0.21.5': + '@esbuild/linux-ia32@0.25.12': optional: true '@esbuild/linux-loong64@0.19.12': optional: true - '@esbuild/linux-loong64@0.21.5': + '@esbuild/linux-loong64@0.25.12': optional: true '@esbuild/linux-mips64el@0.19.12': optional: true - '@esbuild/linux-mips64el@0.21.5': + '@esbuild/linux-mips64el@0.25.12': optional: true '@esbuild/linux-ppc64@0.19.12': optional: true - '@esbuild/linux-ppc64@0.21.5': + '@esbuild/linux-ppc64@0.25.12': optional: true '@esbuild/linux-riscv64@0.19.12': optional: true - '@esbuild/linux-riscv64@0.21.5': + '@esbuild/linux-riscv64@0.25.12': optional: true '@esbuild/linux-s390x@0.19.12': optional: true - '@esbuild/linux-s390x@0.21.5': + '@esbuild/linux-s390x@0.25.12': optional: true '@esbuild/linux-x64@0.19.12': optional: true - '@esbuild/linux-x64@0.21.5': + '@esbuild/linux-x64@0.25.12': + optional: true + + '@esbuild/netbsd-arm64@0.25.12': optional: true '@esbuild/netbsd-x64@0.19.12': optional: true - '@esbuild/netbsd-x64@0.21.5': + '@esbuild/netbsd-x64@0.25.12': + optional: true + + '@esbuild/openbsd-arm64@0.25.12': optional: true '@esbuild/openbsd-x64@0.19.12': optional: true - '@esbuild/openbsd-x64@0.21.5': + '@esbuild/openbsd-x64@0.25.12': + optional: true + + '@esbuild/openharmony-arm64@0.25.12': optional: true '@esbuild/sunos-x64@0.19.12': optional: true - '@esbuild/sunos-x64@0.21.5': + '@esbuild/sunos-x64@0.25.12': optional: true '@esbuild/win32-arm64@0.19.12': optional: true - '@esbuild/win32-arm64@0.21.5': + '@esbuild/win32-arm64@0.25.12': optional: true '@esbuild/win32-ia32@0.19.12': optional: true - '@esbuild/win32-ia32@0.21.5': + '@esbuild/win32-ia32@0.25.12': optional: true '@esbuild/win32-x64@0.19.12': optional: true - '@esbuild/win32-x64@0.21.5': + '@esbuild/win32-x64@0.25.12': optional: true '@eslint-community/eslint-utils@4.4.0(eslint@8.57.0)': @@ -13454,6 +13479,8 @@ snapshots: '@socket.io/component-emitter@3.1.2': {} + '@standard-schema/spec@1.1.0': {} + '@swc/core-darwin-arm64@1.4.17': optional: true @@ -13966,47 +13993,54 @@ snapshots: '@vercel/ncc@0.38.1': {} - '@vitest/expect@3.2.4': + '@vitest/expect@4.1.7': dependencies: + '@standard-schema/spec': 1.1.0 '@types/chai': 5.2.3 - '@vitest/spy': 3.2.4 - '@vitest/utils': 3.2.4 - chai: 5.3.3 - tinyrainbow: 2.0.0 + '@vitest/spy': 4.1.7 + '@vitest/utils': 4.1.7 + chai: 6.2.2 + tinyrainbow: 3.1.0 - '@vitest/mocker@3.2.4(vite@5.4.21(@types/node@18.19.31)(terser@5.43.1))': + '@vitest/mocker@4.1.7(vite@6.3.5(@types/node@20.12.7)(jiti@2.4.2)(terser@5.43.1)(yaml@2.7.0))': dependencies: - '@vitest/spy': 3.2.4 + '@vitest/spy': 4.1.7 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 5.4.21(@types/node@18.19.31)(terser@5.43.1) + vite: 6.3.5(@types/node@20.12.7)(jiti@2.4.2)(terser@5.43.1)(yaml@2.7.0) - '@vitest/pretty-format@3.2.4': + '@vitest/mocker@4.1.7(vite@6.3.5(@types/node@22.19.10)(jiti@2.4.2)(terser@5.43.1)(yaml@2.7.0))': dependencies: - tinyrainbow: 2.0.0 + '@vitest/spy': 4.1.7 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 6.3.5(@types/node@22.19.10)(jiti@2.4.2)(terser@5.43.1)(yaml@2.7.0) - '@vitest/runner@3.2.4': + '@vitest/pretty-format@4.1.7': dependencies: - '@vitest/utils': 3.2.4 + tinyrainbow: 3.1.0 + + '@vitest/runner@4.1.7': + dependencies: + '@vitest/utils': 4.1.7 pathe: 2.0.3 - strip-literal: 3.1.0 - '@vitest/snapshot@3.2.4': + '@vitest/snapshot@4.1.7': dependencies: - '@vitest/pretty-format': 3.2.4 + '@vitest/pretty-format': 4.1.7 + '@vitest/utils': 4.1.7 magic-string: 0.30.21 pathe: 2.0.3 - '@vitest/spy@3.2.4': - dependencies: - tinyspy: 4.0.4 + '@vitest/spy@4.1.7': {} - '@vitest/utils@3.2.4': + '@vitest/utils@4.1.7': dependencies: - '@vitest/pretty-format': 3.2.4 - loupe: 3.2.1 - tinyrainbow: 2.0.0 + '@vitest/pretty-format': 4.1.7 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 '@webassemblyjs/ast@1.14.1': dependencies: @@ -14459,10 +14493,6 @@ snapshots: binary-extensions@2.3.0: {} - bindings@1.5.0: - dependencies: - file-uri-to-path: 1.0.0 - bintrees@1.0.2: {} bl@4.1.0: @@ -14635,8 +14665,6 @@ snapshots: bytes@3.1.2: {} - cac@6.7.14: {} - cacheable-request@6.1.0: dependencies: clone-response: 1.0.3 @@ -14679,13 +14707,7 @@ snapshots: cargo-cp-artifact@0.1.9: {} - chai@5.3.3: - dependencies: - assertion-error: 2.0.1 - check-error: 2.1.3 - deep-eql: 5.0.2 - loupe: 3.2.1 - pathval: 2.0.1 + chai@6.2.2: {} chalk@2.4.2: dependencies: @@ -14707,8 +14729,6 @@ snapshots: charenc@0.0.2: {} - check-error@2.1.3: {} - chokidar@3.6.0: dependencies: anymatch: 3.1.3 @@ -14937,6 +14957,8 @@ snapshots: meow: 12.1.1 split2: 4.2.0 + convert-source-map@2.0.0: {} + cookie-signature@1.0.6: {} cookie@0.4.0: {} @@ -15125,18 +15147,12 @@ snapshots: optionalDependencies: supports-color: 5.5.0 - debug@4.4.3: - dependencies: - ms: 2.1.3 - decamelize@1.2.0: {} decompress-response@3.3.0: dependencies: mimic-response: 1.0.1 - deep-eql@5.0.2: {} - deep-extend@0.6.0: {} deep-is@0.1.4: {} @@ -15394,11 +15410,6 @@ snapshots: environment@1.1.0: {} - erlpack@0.1.4: - dependencies: - bindings: 1.5.0 - nan: 2.19.0 - error-ex@1.3.2: dependencies: is-arrayish: 0.2.1 @@ -15462,6 +15473,8 @@ snapshots: es-module-lexer@1.7.0: {} + es-module-lexer@2.1.0: {} + es-object-atoms@1.0.0: dependencies: es-errors: 1.3.0 @@ -15540,31 +15553,34 @@ snapshots: '@esbuild/win32-ia32': 0.19.12 '@esbuild/win32-x64': 0.19.12 - esbuild@0.21.5: + esbuild@0.25.12: optionalDependencies: - '@esbuild/aix-ppc64': 0.21.5 - '@esbuild/android-arm': 0.21.5 - '@esbuild/android-arm64': 0.21.5 - '@esbuild/android-x64': 0.21.5 - '@esbuild/darwin-arm64': 0.21.5 - '@esbuild/darwin-x64': 0.21.5 - '@esbuild/freebsd-arm64': 0.21.5 - '@esbuild/freebsd-x64': 0.21.5 - '@esbuild/linux-arm': 0.21.5 - '@esbuild/linux-arm64': 0.21.5 - '@esbuild/linux-ia32': 0.21.5 - '@esbuild/linux-loong64': 0.21.5 - '@esbuild/linux-mips64el': 0.21.5 - '@esbuild/linux-ppc64': 0.21.5 - '@esbuild/linux-riscv64': 0.21.5 - '@esbuild/linux-s390x': 0.21.5 - '@esbuild/linux-x64': 0.21.5 - '@esbuild/netbsd-x64': 0.21.5 - '@esbuild/openbsd-x64': 0.21.5 - '@esbuild/sunos-x64': 0.21.5 - '@esbuild/win32-arm64': 0.21.5 - '@esbuild/win32-ia32': 0.21.5 - '@esbuild/win32-x64': 0.21.5 + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 escalade@3.1.2: {} @@ -15991,8 +16007,6 @@ snapshots: dependencies: flat-cache: 3.2.0 - file-uri-to-path@1.0.0: {} - fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 @@ -16920,8 +16934,6 @@ snapshots: js-tokens@4.0.0: {} - js-tokens@9.0.1: {} - js-yaml@3.14.2: dependencies: argparse: 1.0.10 @@ -17216,8 +17228,6 @@ snapshots: long@5.3.2: {} - loupe@3.2.1: {} - lowercase-keys@1.0.1: {} lowercase-keys@2.0.0: {} @@ -17418,7 +17428,8 @@ snapshots: object-assign: 4.1.1 thenify-all: 1.6.0 - nan@2.19.0: {} + nan@2.19.0: + optional: true nanoid@3.3.11: {} @@ -17707,6 +17718,8 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.0.0 + obug@2.1.1: {} + omit-deep-by-values@1.0.2: dependencies: lodash: 4.17.21 @@ -18000,8 +18013,6 @@ snapshots: pathe@2.0.3: {} - pathval@2.0.1: {} - pause@0.0.1: {} peberminta@0.9.0: {} @@ -18942,7 +18953,7 @@ snapshots: statuses@2.0.1: {} - std-env@3.10.0: {} + std-env@4.1.0: {} stream-events@1.0.5: dependencies: @@ -19038,10 +19049,6 @@ snapshots: strip-json-comments@3.1.1: {} - strip-literal@3.1.0: - dependencies: - js-tokens: 9.0.1 - strnum@1.0.5: {} strnum@2.1.2: {} @@ -19219,16 +19226,14 @@ snapshots: tinyexec@0.3.2: {} + tinyexec@1.2.2: {} + tinyglobby@0.2.15: dependencies: fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 - tinypool@1.1.1: {} - - tinyrainbow@2.0.0: {} - - tinyspy@4.0.4: {} + tinyrainbow@3.1.0: {} tldts-core@6.1.18: {} @@ -19525,72 +19530,89 @@ snapshots: verify-github-webhook@1.0.1: {} - vite-node@3.2.4(@types/node@18.19.31)(terser@5.43.1): + vite@6.3.5(@types/node@20.12.7)(jiti@2.4.2)(terser@5.43.1)(yaml@2.7.0): dependencies: - cac: 6.7.14 - debug: 4.4.3 - es-module-lexer: 1.7.0 - pathe: 2.0.3 - vite: 5.4.21(@types/node@18.19.31)(terser@5.43.1) - transitivePeerDependencies: - - '@types/node' - - less - - lightningcss - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser + esbuild: 0.25.12 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + postcss: 8.5.8 + rollup: 4.60.1 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 20.12.7 + fsevents: 2.3.3 + jiti: 2.4.2 + terser: 5.43.1 + yaml: 2.7.0 - vite@5.4.21(@types/node@18.19.31)(terser@5.43.1): + vite@6.3.5(@types/node@22.19.10)(jiti@2.4.2)(terser@5.43.1)(yaml@2.7.0): dependencies: - esbuild: 0.21.5 + esbuild: 0.25.12 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 postcss: 8.5.8 rollup: 4.60.1 + tinyglobby: 0.2.15 optionalDependencies: - '@types/node': 18.19.31 + '@types/node': 22.19.10 fsevents: 2.3.3 + jiti: 2.4.2 terser: 5.43.1 + yaml: 2.7.0 - vitest@3.2.4(@types/debug@4.1.12)(@types/node@18.19.31)(terser@5.43.1): + vitest@4.1.7(@types/node@20.12.7)(vite@6.3.5(@types/node@20.12.7)(jiti@2.4.2)(terser@5.43.1)(yaml@2.7.0)): dependencies: - '@types/chai': 5.2.3 - '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@5.4.21(@types/node@18.19.31)(terser@5.43.1)) - '@vitest/pretty-format': 3.2.4 - '@vitest/runner': 3.2.4 - '@vitest/snapshot': 3.2.4 - '@vitest/spy': 3.2.4 - '@vitest/utils': 3.2.4 - chai: 5.3.3 - debug: 4.4.3 + '@vitest/expect': 4.1.7 + '@vitest/mocker': 4.1.7(vite@6.3.5(@types/node@20.12.7)(jiti@2.4.2)(terser@5.43.1)(yaml@2.7.0)) + '@vitest/pretty-format': 4.1.7 + '@vitest/runner': 4.1.7 + '@vitest/snapshot': 4.1.7 + '@vitest/spy': 4.1.7 + '@vitest/utils': 4.1.7 + es-module-lexer: 2.1.0 expect-type: 1.3.0 magic-string: 0.30.21 + obug: 2.1.1 pathe: 2.0.3 picomatch: 4.0.4 - std-env: 3.10.0 + std-env: 4.1.0 tinybench: 2.9.0 - tinyexec: 0.3.2 + tinyexec: 1.2.2 tinyglobby: 0.2.15 - tinypool: 1.1.1 - tinyrainbow: 2.0.0 - vite: 5.4.21(@types/node@18.19.31)(terser@5.43.1) - vite-node: 3.2.4(@types/node@18.19.31)(terser@5.43.1) + tinyrainbow: 3.1.0 + vite: 6.3.5(@types/node@20.12.7)(jiti@2.4.2)(terser@5.43.1)(yaml@2.7.0) why-is-node-running: 2.3.0 optionalDependencies: - '@types/debug': 4.1.12 - '@types/node': 18.19.31 + '@types/node': 20.12.7 + transitivePeerDependencies: + - msw + + vitest@4.1.7(@types/node@22.19.10)(vite@6.3.5(@types/node@22.19.10)(jiti@2.4.2)(terser@5.43.1)(yaml@2.7.0)): + dependencies: + '@vitest/expect': 4.1.7 + '@vitest/mocker': 4.1.7(vite@6.3.5(@types/node@22.19.10)(jiti@2.4.2)(terser@5.43.1)(yaml@2.7.0)) + '@vitest/pretty-format': 4.1.7 + '@vitest/runner': 4.1.7 + '@vitest/snapshot': 4.1.7 + '@vitest/spy': 4.1.7 + '@vitest/utils': 4.1.7 + es-module-lexer: 2.1.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 4.1.0 + tinybench: 2.9.0 + tinyexec: 1.2.2 + tinyglobby: 0.2.15 + tinyrainbow: 3.1.0 + vite: 6.3.5(@types/node@22.19.10)(jiti@2.4.2)(terser@5.43.1)(yaml@2.7.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.19.10 transitivePeerDependencies: - - less - - lightningcss - msw - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser watchpack@2.4.4: dependencies: @@ -19875,8 +19897,4 @@ snapshots: yocto-queue@1.2.1: {} - zlib-sync@0.1.9: - dependencies: - nan: 2.19.0 - zod@4.3.6: {} diff --git a/scripts/cli b/scripts/cli index f2830b8274..c8d174e56f 100755 --- a/scripts/cli +++ b/scripts/cli @@ -412,6 +412,10 @@ function scaffold() { up_test_scaffold exit ;; + down-test) + down_test_scaffold + exit + ;; *) error "Invalid command '$1'" && say "$HELP" exit 1 @@ -723,23 +727,20 @@ function migrate_local() { function up_test_scaffold() { scaffold_set_up_network "${PROJECT_NAME}-bridge-test" $DOCKET_TEST_NETWORK_SUBNET $DOCKER_TEST_NETWORK_GATEWAY - $_DC -p "$PROJECT_NAME-test" -f $CLI_HOME/../backend/docker-compose.test.yaml down - $_DC -p "$PROJECT_NAME-test" -f $CLI_HOME/../backend/docker-compose.test.yaml up -d + $_DC -p "$PROJECT_NAME-test" -f $CLI_HOME/scaffold.test.yaml up -d --wait migrate_test } -function migrate_test() { - say "Building flyway migration image..." - docker build -t crowd_flyway -f $CLI_HOME/../backend/src/database/Dockerfile.flyway $CLI_HOME/../backend/src/database +function down_test_scaffold() { + $_DC -p "$PROJECT_NAME-test" -f $CLI_HOME/scaffold.test.yaml down +} - say "Applying database migrations!" - docker run --rm --network "${PROJECT_NAME}-bridge-test" \ - -e PGHOST=db-test \ - -e PGPORT=5432 \ - -e PGUSER=postgres \ - -e PGPASSWORD=example \ - -e PGDATABASE=crowd-web \ - crowd_flyway +function migrate_test() { + set -a + # shellcheck source=/dev/null + source "$CLI_HOME/../.env.test" + set +a + "$CLI_HOME/prepare-test-template-db.sh" } function source_edition() { @@ -952,7 +953,7 @@ function start() { SCRIPT_USAGE="${YELLOW}${PROJECT_NAME} CLI ${RESET}\n Usage: ./cli \n ${YELLOW}Scaffold:${RESET} - scaffold [up|down|destroy|reset|create-migration|create-product-migration|migrate-up|up-test]\n + scaffold [up|down|destroy|reset|create-migration|create-product-migration|migrate-up|up-test|down-test]\n ${YELLOW}Services:${RESET} service [up|down|restart|status|logs|id] start | start-dev | start-be | start-e2e diff --git a/scripts/prepare-test-template-db.sh b/scripts/prepare-test-template-db.sh new file mode 100755 index 0000000000..85887c5211 --- /dev/null +++ b/scripts/prepare-test-template-db.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +# Recreates test_template and applies Flyway migrations. +# Requires a running Postgres reachable at DB_HOST:DB_PORT (see .env.test for local). +# +# Optional: SEQUIN_BOOTSTRAP=1 — CI Postgres has no init SQL; local compose runs it on first boot. + +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +# shellcheck source=scripts/utils +source "${ROOT_DIR}/scripts/utils" + +: "${DB_HOST:?DB_HOST is required}" +: "${DB_PORT:?DB_PORT is required}" +: "${DB_USER:?DB_USER is required}" +: "${DB_PASSWORD:?DB_PASSWORD is required}" + +DATABASE_DIR="${ROOT_DIR}/backend/src/database" + +pg_psql() { + docker run --rm --network host \ + -e "PGPASSWORD=${DB_PASSWORD}" \ + postgres:14-alpine \ + psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d postgres -v ON_ERROR_STOP=1 "$@" +} + +if [[ "${SEQUIN_BOOTSTRAP:-0}" == "1" ]]; then + say "Applying Sequin bootstrap SQL..." + docker run --rm --network host \ + -v "${ROOT_DIR}/scripts/scaffold/sequin/postgres-docker-entrypoint-initdb.d/create-sequin-database.sql:/bootstrap.sql:ro" \ + -e "PGPASSWORD=${DB_PASSWORD}" \ + postgres:14-alpine \ + psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d postgres -v ON_ERROR_STOP=1 -f /bootstrap.sql +fi + +say "Recreating test_template..." +pg_psql \ + -c "DROP DATABASE IF EXISTS test_template;" \ + -c "CREATE DATABASE test_template;" + +say "Building flyway image..." +docker build -t crowd_flyway -f "${DATABASE_DIR}/Dockerfile.flyway" "${DATABASE_DIR}" + +say "Migrating test_template..." +docker run --rm --network host \ + -e "PGHOST=${DB_HOST}" \ + -e "PGPORT=${DB_PORT}" \ + -e "PGUSER=${DB_USER}" \ + -e "PGPASSWORD=${DB_PASSWORD}" \ + -e PGDATABASE=test_template \ + crowd_flyway + +say "Test template database ready." diff --git a/scripts/scaffold.test.yaml b/scripts/scaffold.test.yaml new file mode 100644 index 0000000000..c18ae2a038 --- /dev/null +++ b/scripts/scaffold.test.yaml @@ -0,0 +1,25 @@ +services: + test-db: + image: postgres:14-alpine + container_name: crowd-test-db + restart: unless-stopped + command: -c 'wal_level=logical' + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: example + POSTGRES_DB: postgres + ports: + - '5434:5432' + volumes: + - ./scaffold/sequin/postgres-docker-entrypoint-initdb.d/create-sequin-database.sql:/docker-entrypoint-initdb.d/create-sequin-database.sql + healthcheck: + test: ['CMD-SHELL', 'pg_isready -U postgres'] + interval: 2s + timeout: 5s + retries: 15 + networks: + - crowd-bridge-test + +networks: + crowd-bridge-test: + external: true diff --git a/services/apps/data_sink_worker/src/bin/fix-member-attributes.ts b/services/apps/data_sink_worker/src/bin/fix-member-attributes.ts index 302adbfc3f..8e414c5ae4 100644 --- a/services/apps/data_sink_worker/src/bin/fix-member-attributes.ts +++ b/services/apps/data_sink_worker/src/bin/fix-member-attributes.ts @@ -1,7 +1,7 @@ import isEqual from 'lodash.isequal' import mergeWith from 'lodash.mergewith' -import { connQx, updateMember } from '@crowd/data-access-layer' +import { pgpQx, updateMember } from '@crowd/data-access-layer' import { DbConnOrTx, DbStore, @@ -98,7 +98,7 @@ setImmediate(async () => { redisClient = await getRedisClient(REDIS_CONFIG()) log.info('Redis connection established') - const pgQx = connQx(dbClient) + const pgQx = pgpQx(dbClient) const mas = new MemberAttributeService(redisClient, new DbStore(log, dbClient), log) let totalProcessed = 0 diff --git a/services/libs/data-access-layer/package.json b/services/libs/data-access-layer/package.json index a373ea5142..8e2c473d68 100644 --- a/services/libs/data-access-layer/package.json +++ b/services/libs/data-access-layer/package.json @@ -3,9 +3,6 @@ "private": true, "main": "src/index.ts", "scripts": { - "test": "vitest run", - "test:affiliations": "vitest run src/affiliations", - "test:affiliations:watch": "vitest src/affiliations", "lint": "npx eslint --ext .ts src --max-warnings=0", "format": "npx prettier --write \"src/**/*.ts\"", "format-check": "npx prettier --check .", @@ -38,7 +35,6 @@ "devDependencies": { "@types/node": "^18.16.3", "sequelize": "6.37.8", - "typescript": "^5.6.3", - "vitest": "^3.2.4" + "typescript": "^5.6.3" } } diff --git a/services/libs/data-access-layer/src/auditLogs/index.ts b/services/libs/data-access-layer/src/auditLogs/index.ts index c6d690bed3..803ac3e362 100644 --- a/services/libs/data-access-layer/src/auditLogs/index.ts +++ b/services/libs/data-access-layer/src/auditLogs/index.ts @@ -2,7 +2,7 @@ import validator from 'validator' import { WRITE_DB_CONFIG, getDbConnection } from '@crowd/database' -import { QueryExecutor, connQx } from '../queryExecutor' +import { QueryExecutor, pgpQx } from '../queryExecutor' export enum ActorType { USER = 'user', @@ -75,7 +75,7 @@ let qx: QueryExecutor | undefined = undefined export async function addAuditAction(options: AuditLogRequestOptions, action: AuditLogAction) { if (!qx) { const conn = await getDbConnection(WRITE_DB_CONFIG()) - qx = connQx(conn) + qx = pgpQx(conn) } await qx.result( diff --git a/services/libs/data-access-layer/src/queryExecutor.ts b/services/libs/data-access-layer/src/queryExecutor.ts index 298b997507..ad9b3892ce 100644 --- a/services/libs/data-access-layer/src/queryExecutor.ts +++ b/services/libs/data-access-layer/src/queryExecutor.ts @@ -1,24 +1,13 @@ -import pgp from 'pg-promise' import { QueryTypes, Sequelize, Transaction } from 'sequelize' -import { DbConnOrTx, DbConnection, DbStore, DbTransaction, RepositoryBase } from '@crowd/database' +import { type QueryExecutor, formatQuery } from '@crowd/database' -/* eslint-disable @typescript-eslint/no-explicit-any */ - -export interface QueryExecutor { - select(query: string, params?: object): Promise - selectNone(query: string, params?: object): Promise - selectOneOrNone(query: string, params?: object): Promise - selectOne(query: string, params?: object): Promise - result(query: string, params?: object): Promise - - tx(fn: (tx: QueryExecutor) => Promise): Promise -} +export { PgPromiseQueryExecutor, dbStoreQx, formatQuery, pgpQx, repoQx } from '@crowd/database' +export type { QueryExecutor } from '@crowd/database' -export function formatQuery(query: string, params?: object): string { - return pgp.as.format(query, params) -} +/* eslint-disable @typescript-eslint/no-explicit-any */ +/** Sequelize-backed QueryExecutor for legacy backend repositories. */ export class SequelizeQueryExecutor implements QueryExecutor { constructor(private readonly sequelize: Sequelize) {} @@ -117,47 +106,6 @@ export class TransactionalSequelizeQueryExecutor extends SequelizeQueryExecutor } } -export class PgPromiseQueryExecutor implements QueryExecutor { - constructor(private readonly db: DbConnection | DbTransaction) {} - - select(query: string, params?: object): Promise { - return this.db.query(formatQuery(query, params)) - } - selectNone(query: string, params?: object): Promise { - return this.db.none(formatQuery(query, params)) - } - selectOneOrNone(query: string, params?: object): Promise { - return this.db.oneOrNone(formatQuery(query, params)) - } - selectOne(query: string, params?: object): Promise { - return this.db.one(formatQuery(query, params)) - } - async result(query: string, params?: object): Promise { - const result = await this.db.result(formatQuery(query, params)) - return result.rowCount - } - - tx(fn: (tx: QueryExecutor) => Promise): Promise { - return this.db.tx((tx) => fn(new PgPromiseQueryExecutor(tx))) - } -} - -export function pgpQx(db: DbConnOrTx): QueryExecutor { - return new PgPromiseQueryExecutor(db) -} - -export function dbStoreQx(dbStore: DbStore): QueryExecutor { - return pgpQx(dbStore.connection()) -} - -export function repoQx(repo: RepositoryBase): QueryExecutor { - return pgpQx(repo.db()) -} - -export function connQx(conn: DbConnOrTx): QueryExecutor { - return pgpQx(conn) -} - export function optionsQx(options: any): QueryExecutor { const seq = options.database.sequelize if (options.transaction) { diff --git a/services/libs/data-access-layer/vitest.config.ts b/services/libs/data-access-layer/vitest.config.ts deleted file mode 100644 index 530cc66032..0000000000 --- a/services/libs/data-access-layer/vitest.config.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { defineConfig } from 'vitest/config' - -export default defineConfig({ - test: { - environment: 'node', - include: ['src/**/*.test.ts'], - server: { - deps: { - inline: [/@crowd\//], - }, - }, - }, -}) diff --git a/services/libs/database/src/connection.ts b/services/libs/database/src/connection.ts index 6f1dd2d762..7f0c714bb8 100644 --- a/services/libs/database/src/connection.ts +++ b/services/libs/database/src/connection.ts @@ -87,14 +87,6 @@ export const getDbInstance = (): DbInstance => { return dbInstance } -export const formatQuery = (query: string, values: Record): string => { - if (!dbInstance) { - throw new Error('Database instance not initialized!') - } - - return dbInstance.as.format(query, values) -} - const dbConnection: Record = {} export const getDbConnection = async ( diff --git a/services/libs/database/src/index.ts b/services/libs/database/src/index.ts index 74a7067668..e8faf09cae 100644 --- a/services/libs/database/src/index.ts +++ b/services/libs/database/src/index.ts @@ -1,6 +1,7 @@ export * from './connection' export * from './dbStore' export * from './locking' +export * from './queryExecutor' export * from './repoBase' export * from './tinybirdClient' export * from './types' diff --git a/services/libs/database/src/queryExecutor.ts b/services/libs/database/src/queryExecutor.ts new file mode 100644 index 0000000000..582960534d --- /dev/null +++ b/services/libs/database/src/queryExecutor.ts @@ -0,0 +1,63 @@ +import pgp from 'pg-promise' + +import { DbStore } from './dbStore' +import { RepositoryBase } from './repoBase' +import type { DbConnOrTx, DbConnection, DbTransaction } from './types' + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +export interface QueryExecutor { + select(query: string, params?: object): Promise + selectNone(query: string, params?: object): Promise + selectOneOrNone(query: string, params?: object): Promise + selectOne(query: string, params?: object): Promise + result(query: string, params?: object): Promise + + tx(fn: (tx: QueryExecutor) => Promise): Promise +} + +export function formatQuery(query: string, params?: object): string { + return pgp.as.format(query, params) +} + +export class PgPromiseQueryExecutor implements QueryExecutor { + constructor(private readonly db: DbConnection | DbTransaction) {} + + select(query: string, params?: object): Promise { + return this.db.query(formatQuery(query, params)) + } + + selectNone(query: string, params?: object): Promise { + return this.db.none(formatQuery(query, params)) + } + + selectOneOrNone(query: string, params?: object): Promise { + return this.db.oneOrNone(formatQuery(query, params)) + } + + selectOne(query: string, params?: object): Promise { + return this.db.one(formatQuery(query, params)) + } + + async result(query: string, params?: object): Promise { + const result = await this.db.result(formatQuery(query, params)) + return result.rowCount + } + + tx(fn: (tx: QueryExecutor) => Promise): Promise { + return this.db.tx((tx) => fn(new PgPromiseQueryExecutor(tx))) + } +} + +export function pgpQx(db: DbConnOrTx): QueryExecutor { + return new PgPromiseQueryExecutor(db) +} + +export function dbStoreQx(dbStore: DbStore): QueryExecutor { + return pgpQx(dbStore.connection()) +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function repoQx(repo: RepositoryBase): QueryExecutor { + return pgpQx(repo.db()) +} diff --git a/services/libs/test-kit/package.json b/services/libs/test-kit/package.json new file mode 100644 index 0000000000..2ba5fe2ebf --- /dev/null +++ b/services/libs/test-kit/package.json @@ -0,0 +1,23 @@ +{ + "name": "@crowd/test-kit", + "private": true, + "exports": { + "./fixtures": "./src/fixtures.ts" + }, + "scripts": { + "lint": "npx eslint --ext .ts src --max-warnings=0", + "format": "npx prettier --write \"src/**/*.ts\"", + "format-check": "npx prettier --check .", + "tsc-check": "tsc --noEmit" + }, + "dependencies": { + "@crowd/common": "workspace:*", + "@crowd/database": "workspace:*", + "pg-promise": "^11.4.3" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "typescript": "^5.6.3", + "vitest": "4.1.7" + } +} diff --git a/services/libs/test-kit/src/fixtures.ts b/services/libs/test-kit/src/fixtures.ts new file mode 100644 index 0000000000..f893ec2424 --- /dev/null +++ b/services/libs/test-kit/src/fixtures.ts @@ -0,0 +1,23 @@ +import { test as baseTest } from 'vitest' + +import { pgpQx } from '@crowd/database' + +import { openTestWorkerDatabase, resetTestDatabase, seedTestBaseline } from './postgres' + +export function withQx(test: T) { + return ( + test + // eslint-disable-next-line no-empty-pattern + .extend('_db', { scope: 'file' }, async ({}, { onCleanup }) => { + const worker = await openTestWorkerDatabase() + onCleanup(() => worker.cleanup()) + return worker.db + }) + .extend('qx', async ({ _db }) => { + const executor = pgpQx(_db) + await resetTestDatabase(executor) + await seedTestBaseline(executor) + return executor + }) + ) +} diff --git a/services/libs/test-kit/src/postgres.ts b/services/libs/test-kit/src/postgres.ts new file mode 100644 index 0000000000..a714ab8156 --- /dev/null +++ b/services/libs/test-kit/src/postgres.ts @@ -0,0 +1,197 @@ +import pgPromise from 'pg-promise' + +import { IS_TEST_ENV } from '@crowd/common' +import { DEFAULT_TENANT_ID } from '@crowd/common/src/env' +import { type DbConnection, type DbInstance, type IDatabaseConfig } from '@crowd/database' +import type { QueryExecutor } from '@crowd/database' + +import type { TestPostgres } from './types' + +/** + * Creates an isolated, dynamic test database cloned from the template database + * for the current Vitest worker pool, and handles its lifecycle cleanup. + */ +export async function openTestWorkerDatabase(): Promise<{ + db: DbConnection + cleanup: () => Promise +}> { + const postgres = getTestPostgres() + + const poolId = process.env.VITEST_POOL_ID ?? '0' + const name = `test_${poolId.replace(/[^a-z0-9_]/gi, '_').toLowerCase()}` + + await withCatalogDb(postgres, async (catalog) => { + await catalog.result(`SELECT pg_advisory_lock(hashtext('cdp-test-template-clone'))`) + try { + await dropDatabase(catalog, name) + await catalog.result( + `SELECT pg_terminate_backend(pid) + FROM pg_stat_activity + WHERE datname = 'test_template' AND pid <> pg_backend_pid()`, + ) + await catalog.none(`CREATE DATABASE ${quoteIdent(name)} WITH TEMPLATE "test_template"`) + } finally { + await catalog.result(`SELECT pg_advisory_unlock(hashtext('cdp-test-template-clone'))`) + } + }) + + const db = connectTestDb({ + host: postgres.host, + port: postgres.port, + user: postgres.user, + password: postgres.password, + database: name, + }) + + return { + db, + async cleanup() { + await db.$pool.end() + await withCatalogDb(postgres, (catalog) => dropDatabase(catalog, name)) + }, + } +} + +/** + * Truncates all public tables in the worker database. + * @throws {Error} If executed against a non-test database database name. + */ +export async function resetTestDatabase(qx: QueryExecutor): Promise { + const { name } = await qx.selectOne('SELECT current_database() AS name') + + if (!/^test_[a-z0-9_]+$/.test(name)) { + throw new Error(`Not a worker test database: ${name}`) + } + + await qx.selectNone(` + DO $$ + DECLARE tables text; + BEGIN + SELECT string_agg(format('%I.%I', schemaname, tablename), ', ') + INTO tables + FROM pg_tables + WHERE schemaname = 'public'; + + IF tables IS NOT NULL THEN + EXECUTE 'TRUNCATE TABLE ' || tables || ' RESTART IDENTITY CASCADE'; + END IF; + END $$; + `) +} + +/** + * Seeds the default tenant into the database after a table reset. + */ +export async function seedTestBaseline(qx: QueryExecutor): Promise { + await qx.result( + ` + INSERT INTO tenants (id, name, url, plan, "createdAt", "updatedAt") + VALUES ($(id), 'Default', 'default', 'Essential', NOW(), NOW()) + ON CONFLICT (id) DO NOTHING + `, + { id: DEFAULT_TENANT_ID }, + ) +} + +/** + * Executes an operation scoped to a short-lived catalog connection (`postgres` DB), + * ensuring the connection pool is cleanly terminated afterward. + */ +async function withCatalogDb( + endpoint: TestPostgres, + fn: (catalog: DbConnection) => Promise, +): Promise { + const catalog = connectTestDb({ + host: endpoint.host, + port: endpoint.port, + user: endpoint.user, + password: endpoint.password, + database: 'postgres', + }) + try { + return await fn(catalog) + } finally { + await catalog.$pool.end() + } +} + +/** + * Forcefully drops a target database by terminating active connections first. + */ +async function dropDatabase(catalog: DbConnection, name: string): Promise { + if (name !== 'test_template' && !/^test_[a-z0-9_]+$/.test(name)) { + throw new Error(`Not a test database: ${name}`) + } + + await catalog.result( + `SELECT pg_terminate_backend(pid) + FROM pg_stat_activity + WHERE datname = $(database) AND pid <> pg_backend_pid()`, + { database: name }, + ) + await catalog.none(`DROP DATABASE IF EXISTS ${quoteIdent(name)}`) +} + +/** + * Resolves and validates local test database credentials from the environment. + */ +function getTestPostgres(): TestPostgres { + if (!IS_TEST_ENV) { + throw new Error(`NODE_ENV=test required (got ${process.env.NODE_ENV ?? 'unset'})`) + } + + const { DB_HOST: host, DB_PORT, DB_USER: user, DB_PASSWORD: password } = process.env + + if (!host || !DB_PORT || !user || !password) { + throw new Error( + 'DB_HOST, DB_PORT, DB_USER, and DB_PASSWORD are required — run ./scripts/cli scaffold up-test and use .env.test', + ) + } + + const port = Number(DB_PORT) + + if (!['localhost', '127.0.0.1', '::1'].includes(host)) { + throw new Error(`Security safeguard: DB_HOST must be localhost (got: ${host})`) + } + + return { host, port, user, password } +} + +let testPgInstance: DbInstance | undefined + +/** Test-scoped pg-promise main instance (no prod error/query hooks). */ +function createTestPgPromise(): DbInstance { + if (testPgInstance) { + return testPgInstance + } + + testPgInstance = pgPromise({}) + + // Keep in sync with getDbInstance() + testPgInstance.pg.types.setTypeParser(1114, (s) => s) + testPgInstance.pg.types.setTypeParser(1184, (s) => s) + testPgInstance.pg.types.setTypeParser(1700, (s) => parseFloat(s)) + testPgInstance.pg.types.setTypeParser(23, (s) => parseInt(s, 10)) + + return testPgInstance +} + +function connectTestDb(config: IDatabaseConfig): DbConnection { + return createTestPgPromise()({ + ...config, + ssl: false, + max: 5, + idleTimeoutMillis: 10_000, + application_name: 'cdp-test', + }) +} + +/** + * Standard PostgreSQL identifier wrapping for variable database names. + */ +function quoteIdent(name: string): string { + if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) { + throw new Error(`Invalid database name: ${name}`) + } + return `"${name}"` +} diff --git a/services/libs/test-kit/src/types.ts b/services/libs/test-kit/src/types.ts new file mode 100644 index 0000000000..902271f076 --- /dev/null +++ b/services/libs/test-kit/src/types.ts @@ -0,0 +1,6 @@ +export type TestPostgres = { + host: string + port: number + user: string + password: string +} diff --git a/services/libs/test-kit/tsconfig.json b/services/libs/test-kit/tsconfig.json new file mode 100644 index 0000000000..bf7f183850 --- /dev/null +++ b/services/libs/test-kit/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../base.tsconfig.json", + "include": ["src/**/*"] +} diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000000..ea4d664f63 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,28 @@ +import { loadEnv } from 'vite' +import { defineConfig } from 'vitest/config' + +const root = __dirname + +const testEnv = loadEnv('test', root, '') + +export default defineConfig({ + test: { + env: testEnv, + server: { + deps: { inline: [/@crowd\//] }, + }, + projects: [ + { + extends: true, + test: { + name: 'server', + include: ['backend/**/*.test.ts', 'services/**/*.test.ts'], + exclude: ['**/node_modules/**', '**/dist/**', 'services/cronjobs/**'], + pool: 'forks', + hookTimeout: 300_000, + testTimeout: 30_000, + }, + }, + ], + }, +}) From 35d79069d739f5348544850c6b2bdf1bb201544c Mon Sep 17 00:00:00 2001 From: Yeganathan S <63534555+skwowet@users.noreply.github.com> Date: Mon, 1 Jun 2026 14:19:33 +0530 Subject: [PATCH 2/5] fix: resolve pr review comments Signed-off-by: Yeganathan S <63534555+skwowet@users.noreply.github.com> --- .github/scripts/prepare-test-template-db.sh | 41 ++++++++++++++++ .github/workflows/server-tests.yml | 6 +-- scripts/cli | 20 ++++++-- scripts/prepare-test-template-db.sh | 53 --------------------- services/libs/test-kit/src/postgres.ts | 13 ++--- vitest.config.ts | 31 ++++++++++-- 6 files changed, 90 insertions(+), 74 deletions(-) create mode 100755 .github/scripts/prepare-test-template-db.sh delete mode 100755 scripts/prepare-test-template-db.sh diff --git a/.github/scripts/prepare-test-template-db.sh b/.github/scripts/prepare-test-template-db.sh new file mode 100755 index 0000000000..6acfcc2bf5 --- /dev/null +++ b/.github/scripts/prepare-test-template-db.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +# shellcheck source=scripts/utils +source "${REPO_ROOT}/scripts/utils" + +: "${DB_HOST:?DB_HOST is required}" +: "${DB_PORT:?DB_PORT is required}" +: "${DB_USER:?DB_USER is required}" +: "${DB_PASSWORD:?DB_PASSWORD is required}" + +say "Applying Sequin bootstrap SQL..." +docker run --rm --network host \ + -v "${REPO_ROOT}/scripts/scaffold/sequin/postgres-docker-entrypoint-initdb.d/create-sequin-database.sql:/bootstrap.sql:ro" \ + -e "PGPASSWORD=${DB_PASSWORD}" \ + postgres:14-alpine \ + psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d postgres -v ON_ERROR_STOP=1 -f /bootstrap.sql + +say "Recreating test_template..." +docker run --rm --network host \ + -e "PGPASSWORD=${DB_PASSWORD}" \ + postgres:14-alpine \ + psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d postgres -v ON_ERROR_STOP=1 \ + -c "DROP DATABASE IF EXISTS test_template;" \ + -c "CREATE DATABASE test_template;" + +say "Building flyway image..." +docker build -t crowd_flyway -f "${REPO_ROOT}/backend/src/database/Dockerfile.flyway" "${REPO_ROOT}/backend/src/database" + +say "Migrating test_template..." +docker run --rm --network host \ + -e "PGHOST=${DB_HOST}" \ + -e "PGPORT=${DB_PORT}" \ + -e "PGUSER=${DB_USER}" \ + -e "PGPASSWORD=${DB_PASSWORD}" \ + -e PGDATABASE=test_template \ + crowd_flyway + +say "Test template database ready." diff --git a/.github/workflows/server-tests.yml b/.github/workflows/server-tests.yml index 020e5c24e2..5474008a0a 100644 --- a/.github/workflows/server-tests.yml +++ b/.github/workflows/server-tests.yml @@ -29,6 +29,7 @@ jobs: services: postgres: image: postgres:14-alpine + command: postgres -c wal_level=logical env: POSTGRES_USER: postgres POSTGRES_PASSWORD: example @@ -40,7 +41,6 @@ jobs: --health-interval 2s --health-timeout 5s --health-retries 15 - postgres -c wal_level=logical env: NODE_ENV: test @@ -53,7 +53,7 @@ jobs: - name: Check out repository code uses: actions/checkout@v4 - - name: Setup Node and cache pnpm + - name: Setup Node uses: actions/setup-node@v4 with: node-version: 20 @@ -65,7 +65,7 @@ jobs: pnpm i --frozen-lockfile - name: Prepare test database - run: SEQUIN_BOOTSTRAP=1 ./scripts/prepare-test-template-db.sh + run: ./.github/scripts/prepare-test-template-db.sh - name: Run server tests run: pnpm test:server diff --git a/scripts/cli b/scripts/cli index f6c2623764..0dd5bc2ca4 100755 --- a/scripts/cli +++ b/scripts/cli @@ -787,11 +787,21 @@ function down_test_scaffold() { } function migrate_test() { - set -a - # shellcheck source=/dev/null - source "$CLI_HOME/../.env.test" - set +a - "$CLI_HOME/prepare-test-template-db.sh" + wait_for_postgres test-db crowd-test-db + say "Recreating test_template..." + docker exec crowd-test-db psql -U postgres -d postgres -v ON_ERROR_STOP=1 \ + -c "DROP DATABASE IF EXISTS test_template;" \ + -c "CREATE DATABASE test_template;" + say "Building crowd flyway migration image..." + docker build $DOCKER_PLATFORM_FLAGS -t crowd_flyway -f $CLI_HOME/../backend/src/database/Dockerfile.flyway $CLI_HOME/../backend/src/database --load + say "Migrating test_template..." + docker run --rm --network "${PROJECT_NAME}-bridge-test" \ + -e PGHOST=crowd-test-db \ + -e PGPORT=5432 \ + -e PGUSER=postgres \ + -e PGPASSWORD=example \ + -e PGDATABASE=test_template \ + crowd_flyway } function source_edition() { diff --git a/scripts/prepare-test-template-db.sh b/scripts/prepare-test-template-db.sh deleted file mode 100755 index 85887c5211..0000000000 --- a/scripts/prepare-test-template-db.sh +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env bash -# Recreates test_template and applies Flyway migrations. -# Requires a running Postgres reachable at DB_HOST:DB_PORT (see .env.test for local). -# -# Optional: SEQUIN_BOOTSTRAP=1 — CI Postgres has no init SQL; local compose runs it on first boot. - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -# shellcheck source=scripts/utils -source "${ROOT_DIR}/scripts/utils" - -: "${DB_HOST:?DB_HOST is required}" -: "${DB_PORT:?DB_PORT is required}" -: "${DB_USER:?DB_USER is required}" -: "${DB_PASSWORD:?DB_PASSWORD is required}" - -DATABASE_DIR="${ROOT_DIR}/backend/src/database" - -pg_psql() { - docker run --rm --network host \ - -e "PGPASSWORD=${DB_PASSWORD}" \ - postgres:14-alpine \ - psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d postgres -v ON_ERROR_STOP=1 "$@" -} - -if [[ "${SEQUIN_BOOTSTRAP:-0}" == "1" ]]; then - say "Applying Sequin bootstrap SQL..." - docker run --rm --network host \ - -v "${ROOT_DIR}/scripts/scaffold/sequin/postgres-docker-entrypoint-initdb.d/create-sequin-database.sql:/bootstrap.sql:ro" \ - -e "PGPASSWORD=${DB_PASSWORD}" \ - postgres:14-alpine \ - psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d postgres -v ON_ERROR_STOP=1 -f /bootstrap.sql -fi - -say "Recreating test_template..." -pg_psql \ - -c "DROP DATABASE IF EXISTS test_template;" \ - -c "CREATE DATABASE test_template;" - -say "Building flyway image..." -docker build -t crowd_flyway -f "${DATABASE_DIR}/Dockerfile.flyway" "${DATABASE_DIR}" - -say "Migrating test_template..." -docker run --rm --network host \ - -e "PGHOST=${DB_HOST}" \ - -e "PGPORT=${DB_PORT}" \ - -e "PGUSER=${DB_USER}" \ - -e "PGPASSWORD=${DB_PASSWORD}" \ - -e PGDATABASE=test_template \ - crowd_flyway - -say "Test template database ready." diff --git a/services/libs/test-kit/src/postgres.ts b/services/libs/test-kit/src/postgres.ts index a714ab8156..614e781ab2 100644 --- a/services/libs/test-kit/src/postgres.ts +++ b/services/libs/test-kit/src/postgres.ts @@ -1,7 +1,6 @@ import pgPromise from 'pg-promise' -import { IS_TEST_ENV } from '@crowd/common' -import { DEFAULT_TENANT_ID } from '@crowd/common/src/env' +import { DEFAULT_TENANT_ID, IS_TEST_ENV } from '@crowd/common' import { type DbConnection, type DbInstance, type IDatabaseConfig } from '@crowd/database' import type { QueryExecutor } from '@crowd/database' @@ -60,7 +59,7 @@ export async function resetTestDatabase(qx: QueryExecutor): Promise { const { name } = await qx.selectOne('SELECT current_database() AS name') if (!/^test_[a-z0-9_]+$/.test(name)) { - throw new Error(`Not a worker test database: ${name}`) + throw new Error(`Expected worker test database (got ${name})`) } await qx.selectNone(` @@ -137,21 +136,19 @@ async function dropDatabase(catalog: DbConnection, name: string): Promise */ function getTestPostgres(): TestPostgres { if (!IS_TEST_ENV) { - throw new Error(`NODE_ENV=test required (got ${process.env.NODE_ENV ?? 'unset'})`) + throw new Error(`Expected NODE_ENV=test (got ${process.env.NODE_ENV ?? 'unset'})`) } const { DB_HOST: host, DB_PORT, DB_USER: user, DB_PASSWORD: password } = process.env if (!host || !DB_PORT || !user || !password) { - throw new Error( - 'DB_HOST, DB_PORT, DB_USER, and DB_PASSWORD are required — run ./scripts/cli scaffold up-test and use .env.test', - ) + throw new Error('Missing required database environment variables') } const port = Number(DB_PORT) if (!['localhost', '127.0.0.1', '::1'].includes(host)) { - throw new Error(`Security safeguard: DB_HOST must be localhost (got: ${host})`) + throw new Error(`Expected local DB_HOST (got ${host})`) } return { host, port, user, password } diff --git a/vitest.config.ts b/vitest.config.ts index ea4d664f63..f50903040f 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -3,11 +3,25 @@ import { defineConfig } from 'vitest/config' const root = __dirname -const testEnv = loadEnv('test', root, '') +/** Load .env.${mode} and let existing process env vars override file values. */ +function resolveTestEnv(mode: string): Record { + const env = loadEnv(mode, root, '') + const resolved = { ...env } -export default defineConfig({ + for (const key in env) { + const value = process.env[key] + + if (value !== undefined) { + resolved[key] = value + } + } + + return resolved +} + +export default defineConfig(({ mode }) => ({ test: { - env: testEnv, + env: resolveTestEnv(mode), server: { deps: { inline: [/@crowd\//] }, }, @@ -17,7 +31,14 @@ export default defineConfig({ test: { name: 'server', include: ['backend/**/*.test.ts', 'services/**/*.test.ts'], - exclude: ['**/node_modules/**', '**/dist/**', 'services/cronjobs/**'], + exclude: [ + '**/node_modules/**', + '**/dist/**', + 'services/cronjobs/**', + // TODO: packages_worker has its own vitest config and packages-db; it was landed in another PR + // alongside the test foundation. excluding this for now, will refactor it later! + 'services/apps/packages_worker/**', + ], pool: 'forks', hookTimeout: 300_000, testTimeout: 30_000, @@ -25,4 +46,4 @@ export default defineConfig({ }, ], }, -}) +})) From cca6421578b553ff5f4c3fb4b96cf414fb86e652 Mon Sep 17 00:00:00 2001 From: Yeganathan S <63534555+skwowet@users.noreply.github.com> Date: Mon, 1 Jun 2026 14:26:48 +0530 Subject: [PATCH 3/5] fix: server tests ci workflow Signed-off-by: Yeganathan S <63534555+skwowet@users.noreply.github.com> --- .github/workflows/server-tests.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/server-tests.yml b/.github/workflows/server-tests.yml index 5474008a0a..2cb2fcfb5c 100644 --- a/.github/workflows/server-tests.yml +++ b/.github/workflows/server-tests.yml @@ -53,16 +53,17 @@ jobs: - name: Check out repository code uses: actions/checkout@v4 + - name: Setup pnpm + uses: pnpm/action-setup@v4 + - name: Setup Node uses: actions/setup-node@v4 with: node-version: 20 - cache: 'pnpm' # Built-in caching for faster runs + cache: 'pnpm' - name: Install dependencies - run: | - corepack enable - pnpm i --frozen-lockfile + run: pnpm i --frozen-lockfile - name: Prepare test database run: ./.github/scripts/prepare-test-template-db.sh From 7189fcbf0a9956cada7e2bd5baf0af055e1eff3c Mon Sep 17 00:00:00 2001 From: Yeganathan S <63534555+skwowet@users.noreply.github.com> Date: Mon, 1 Jun 2026 14:33:43 +0530 Subject: [PATCH 4/5] fix: resolve pr review comments Signed-off-by: Yeganathan S <63534555+skwowet@users.noreply.github.com> --- services/libs/test-kit/src/postgres.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/services/libs/test-kit/src/postgres.ts b/services/libs/test-kit/src/postgres.ts index 614e781ab2..c69878b86d 100644 --- a/services/libs/test-kit/src/postgres.ts +++ b/services/libs/test-kit/src/postgres.ts @@ -53,7 +53,7 @@ export async function openTestWorkerDatabase(): Promise<{ /** * Truncates all public tables in the worker database. - * @throws {Error} If executed against a non-test database database name. + * @throws {Error} If executed against a non-test database name. */ export async function resetTestDatabase(qx: QueryExecutor): Promise { const { name } = await qx.selectOne('SELECT current_database() AS name') @@ -147,6 +147,10 @@ function getTestPostgres(): TestPostgres { const port = Number(DB_PORT) + if (!Number.isInteger(port) || port <= 0 || port > 65535) { + throw new Error(`Expected valid DB_PORT (got ${DB_PORT})`) + } + if (!['localhost', '127.0.0.1', '::1'].includes(host)) { throw new Error(`Expected local DB_HOST (got ${host})`) } From 86947de3a3bb4cb29a41f8c956498e15dbd48f2e Mon Sep 17 00:00:00 2001 From: Yeganathan S <63534555+skwowet@users.noreply.github.com> Date: Mon, 1 Jun 2026 14:49:46 +0530 Subject: [PATCH 5/5] fix: resolve pr review comments Signed-off-by: Yeganathan S <63534555+skwowet@users.noreply.github.com> --- scripts/cli | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/cli b/scripts/cli index 0dd5bc2ca4..2a09510ebf 100755 --- a/scripts/cli +++ b/scripts/cli @@ -778,7 +778,7 @@ function migrate_local() { function up_test_scaffold() { scaffold_set_up_network "${PROJECT_NAME}-bridge-test" $DOCKET_TEST_NETWORK_SUBNET $DOCKER_TEST_NETWORK_GATEWAY - $_DC -p "$PROJECT_NAME-test" -f $CLI_HOME/scaffold.test.yaml up -d --wait + $_DC -p "$PROJECT_NAME-test" -f $CLI_HOME/scaffold.test.yaml up -d migrate_test }