From 0a3890a4721520da6abc99b57c230053acca101e Mon Sep 17 00:00:00 2001 From: Roomote Date: Tue, 1 Sep 2026 05:30:35 +0000 Subject: [PATCH 1/4] feat(ci): add changed-code mutation gate --- .github/workflows/mutation-testing.yml | 68 ++ .gitignore | 2 + package.json | 3 + pnpm-lock.yaml | 932 +++++++++++++++++++++++++ scripts/stryker-diff.mjs | 509 ++++++++++++++ scripts/stryker-diff.test.mjs | 249 +++++++ stryker.config.mjs | 34 + 7 files changed, 1797 insertions(+) create mode 100644 .github/workflows/mutation-testing.yml create mode 100644 scripts/stryker-diff.mjs create mode 100644 scripts/stryker-diff.test.mjs create mode 100644 stryker.config.mjs diff --git a/.github/workflows/mutation-testing.yml b/.github/workflows/mutation-testing.yml new file mode 100644 index 0000000000..b4f9f47b55 --- /dev/null +++ b/.github/workflows/mutation-testing.yml @@ -0,0 +1,68 @@ +name: Changed-code mutation testing + +on: + pull_request: + types: [edited, opened, reopened, ready_for_review, synchronize] + merge_group: + types: [checks_requested] + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + mutation-diff: + name: mutation-diff + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Record merge-queue enforcement + if: github.event_name == 'merge_group' + run: | + echo "## Changed-code mutation testing" >> "$GITHUB_STEP_SUMMARY" + echo "Mutation testing was enforced on each pull request before it entered the merge queue." >> "$GITHUB_STEP_SUMMARY" + + - name: Checkout pull request head + if: github.event_name == 'pull_request' + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + repository: ${{ github.event.pull_request.head.repo.full_name }} + ref: ${{ github.event.pull_request.head.sha }} + fetch-depth: 0 + persist-credentials: false + + - name: Fetch pull request base + if: github.event_name == 'pull_request' + env: + BASE_REPOSITORY_URL: ${{ github.server_url }}/${{ github.repository }}.git + BASE_SHA: ${{ github.event.pull_request.base.sha }} + run: git fetch --no-tags "$BASE_REPOSITORY_URL" "$BASE_SHA" + + - name: Setup Node.js and pnpm + if: github.event_name == 'pull_request' + uses: ./.github/actions/setup-node-pnpm + with: + install-args: "--frozen-lockfile" + + - name: Test mutation gate logic + if: github.event_name == 'pull_request' + run: pnpm test:mutation-ci + + - name: Mutate changed executable lines + if: github.event_name == 'pull_request' + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + run: node scripts/stryker-diff.mjs ci --base "$BASE_SHA" --head "$HEAD_SHA" + + - name: Upload mutation reports + if: always() && github.event_name == 'pull_request' + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: changed-code-mutation-report + path: reports/mutation/ + if-no-files-found: ignore + retention-days: 7 diff --git a/.gitignore b/.gitignore index e16f2cfe1d..3961778d5e 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,8 @@ out-* node_modules package-lock.json coverage/ +reports/mutation/ +.stryker-tmp/ mock/ .DS_Store diff --git a/package.json b/package.json index 2b90356386..7cdb2ec42c 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "lint": "turbo lint --log-order grouped --output-logs new-only", "check-types": "turbo check-types --log-order grouped --output-logs new-only", "test": "turbo test --log-order grouped --output-logs new-only", + "test:mutation-ci": "node --test scripts/stryker-diff.test.mjs", "test:coverage": "turbo test:coverage --log-order grouped --output-logs new-only", "format": "turbo format --log-order grouped --output-logs new-only", "build": "turbo build --log-order grouped --output-logs new-only", @@ -27,6 +28,8 @@ "devDependencies": { "@changesets/cli": "2.31.0", "@roo-code/config-typescript": "workspace:^", + "@stryker-mutator/core": "10.0.0", + "@stryker-mutator/vitest-runner": "10.0.0", "@types/node": "22.20.1", "@vscode/vsce": "3.9.2", "esbuild": "0.28.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 556a8e107d..1bd786520f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -28,6 +28,12 @@ importers: '@roo-code/config-typescript': specifier: workspace:^ version: link:packages/config-typescript + '@stryker-mutator/core': + specifier: 10.0.0 + version: 10.0.0(@types/node@22.20.1) + '@stryker-mutator/vitest-runner': + specifier: 10.0.0 + version: 10.0.0(@stryker-mutator/core@10.0.0(@types/node@22.20.1))(vitest@4.1.9) '@types/node': specifier: 22.20.1 version: 22.20.1 @@ -1171,61 +1177,204 @@ packages: resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} engines: {node: '>=6.9.0'} + '@babel/code-frame@8.0.0': + resolution: {integrity: sha512-dYYg153EyN2Ekbqw2zAsbd6/JR+9N2SEoC7YV2GyyqMM7x9bLDTjBD6XBhSMLH0wtIVyJj03jWNriQhaN+eoCw==} + engines: {node: ^22.18.0 || >=24.11.0} + '@babel/compat-data@7.29.3': resolution: {integrity: sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==} engines: {node: '>=6.9.0'} + '@babel/compat-data@8.0.0': + resolution: {integrity: sha512-DOjnob/cXOUgDOozCDeq/aK2p5y8dUIVdf6tNhEV1HQRd6I8aQ4f4fbtHRVEvb6lP3BGomrKHiS8ICAASSVQSw==} + engines: {node: ^22.18.0 || >=24.11.0} + '@babel/core@7.29.0': resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} engines: {node: '>=6.9.0'} + '@babel/core@8.0.1': + resolution: {integrity: sha512-5FgxM4dLQpMJHSiVATk8foW263dVHQHBVpXYiimNECVWG01f4nFyEbQixeT6Mwvg7TayREJ2gpKl3o2RoMdnqw==} + engines: {node: ^22.18.0 || >=24.11.0} + '@babel/generator@7.29.1': resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} engines: {node: '>=6.9.0'} + '@babel/generator@8.0.0': + resolution: {integrity: sha512-NT9NrVwJsbSV6Y2FSstWa71EETOnzrjkL5/wX3D2mYHtKM+qvqB1DvR4D0Setb/gDBsHzRICifwEWMO8CnTF6g==} + engines: {node: ^22.18.0 || >=24.11.0} + + '@babel/helper-annotate-as-pure@8.0.0': + resolution: {integrity: sha512-NSpMkMsvvZqzThJ0p1B02cbtA2ObEyfBvq950bmNkyxsxvcxwhvvCB036rKhlEnuBBo30bOrk13u3FzlKSoRrw==} + engines: {node: ^22.18.0 || >=24.11.0} + '@babel/helper-compilation-targets@7.28.6': resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} engines: {node: '>=6.9.0'} + '@babel/helper-compilation-targets@8.0.0': + resolution: {integrity: sha512-JwculLABZvyPvyLBpwU/E/IbH2uM3mnxNtIJpxnIfb24y1PrdVxK5Dqjle4DpgqpGRnwgC7G8IkzPdSXZrO1Ew==} + engines: {node: ^22.18.0 || >=24.11.0} + + '@babel/helper-create-class-features-plugin@8.0.1': + resolution: {integrity: sha512-++t3ZktzlLmASAxIlxeXQK9Z2YwUafYGYcvGBFevqOqt16HozVHStUoQvWD09fzAZOb/uJGpUTBuGK41AJAuOA==} + engines: {node: ^22.18.0 || >=24.11.0} + peerDependencies: + '@babel/core': ^8.0.0 + '@babel/helper-globals@7.28.0': resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} engines: {node: '>=6.9.0'} + '@babel/helper-globals@8.0.0': + resolution: {integrity: sha512-lLozHOM6sWWlxNo8CYqHy4MBZeTvHXNgVPBfPOGsjPKUzHC2Az9QwB6gxdQmpwHl6GlQtbGgS+lj5887guDiLw==} + engines: {node: ^22.18.0 || >=24.11.0} + + '@babel/helper-member-expression-to-functions@8.0.0': + resolution: {integrity: sha512-xkXrMbtk87Gk7+oKBVmBc6EORg/Qwx++AHESldmHkpvG8wgccdhJJFwrzqlF382Fk8wfXhJHWE/g/43QvEGNPQ==} + engines: {node: ^22.18.0 || >=24.11.0} + '@babel/helper-module-imports@7.28.6': resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} engines: {node: '>=6.9.0'} + '@babel/helper-module-imports@8.0.0': + resolution: {integrity: sha512-NZ7mSS93o4ndX4KrbD7W8Sf3QT8Qe24PrnFyUcuOPDzK6faqDFKjY9RG7he7+I7FdiQ4llpnosFqzrXa+Vy3Ew==} + engines: {node: ^22.18.0 || >=24.11.0} + '@babel/helper-module-transforms@7.28.6': resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 + '@babel/helper-module-transforms@8.0.1': + resolution: {integrity: sha512-UgAhl1kqiW5ciE0yCXqqvnb4H2n3IELJ7lIIQRezwDPilPEZX5i+Rvbja9MFTkwUn2biEiSMeV31aUzR4Lwakw==} + engines: {node: ^22.18.0 || >=24.11.0} + peerDependencies: + '@babel/core': ^8.0.0 + + '@babel/helper-optimise-call-expression@8.0.0': + resolution: {integrity: sha512-3W6satvtPuCUkUx63S2jMoW9EQNYkADgs1HTfufmL7gCmAulHMKupA/12WNz4A0GMMFn/YnWWwqOT9IZrJHQjg==} + engines: {node: ^22.18.0 || >=24.11.0} + '@babel/helper-plugin-utils@7.27.1': resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==} engines: {node: '>=6.9.0'} + '@babel/helper-plugin-utils@8.0.1': + resolution: {integrity: sha512-3PKFgjTyPlhFhorfP+SjKQxLViIL++zWjFOO4hGriYU+Bsm983DxEM1JmDRJVWXV0O9npu+xXRqz7Pbd3mh70g==} + engines: {node: ^22.18.0 || >=24.11.0} + peerDependencies: + '@babel/core': ^8.0.0 + + '@babel/helper-replace-supers@8.0.1': + resolution: {integrity: sha512-B1SZADIcy3tmH8CmWvj4SHi/oAPom4UL3uknTc2QRNsPVLFk/sPnZvQL/8kj7Y5omvjMqie0vklvs6XM4OLW5Q==} + engines: {node: ^22.18.0 || >=24.11.0} + peerDependencies: + '@babel/core': ^8.0.0 + + '@babel/helper-skip-transparent-expression-wrappers@8.0.0': + resolution: {integrity: sha512-xmCA9kP3IhySsqhzwIdWGlDN/1A4cCKNBO/uwZx/3YzmDoMePwno2Q5/Bq0q+tYaKbeF940YiKV/kaW8Mzvpjw==} + engines: {node: ^22.18.0 || >=24.11.0} + '@babel/helper-string-parser@7.27.1': resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} engines: {node: '>=6.9.0'} + '@babel/helper-string-parser@8.0.0': + resolution: {integrity: sha512-6mJgmFFFIIO82vvoLt9XtRC7/TkzXfts1t/SpRX4IHSzMgqoPYCWesVu1udUPUWioAE/2fcG6WuI8zrkE1gwrg==} + engines: {node: ^22.18.0 || >=24.11.0} + '@babel/helper-validator-identifier@7.29.7': resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@8.0.4': + resolution: {integrity: sha512-4wFaiLd0bVo4cIoTXI3zKI038NIWE/cr3jvBjejOVYVxV/m8Ltav1USiGzG1fmS5J2RhgEOgXNNK46cRPnRsrg==} + engines: {node: ^22.18.0 || >=24.11.0} + '@babel/helper-validator-option@7.27.1': resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} engines: {node: '>=6.9.0'} + '@babel/helper-validator-option@8.0.0': + resolution: {integrity: sha512-U4Dybxh4WESWHt5XhBeExi4DrY0/DNK1aHpQbsrQXCUbFHuMweT0TpLEWKvaraV2Y6fS+ZXunsZ8zIuZIgvF2Q==} + engines: {node: ^22.18.0 || >=24.11.0} + '@babel/helpers@7.29.2': resolution: {integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==} engines: {node: '>=6.9.0'} + '@babel/helpers@8.0.0': + resolution: {integrity: sha512-wfbi91pM3py96oIiJEz7qIpyXDytgr9zQC1HEWwlGNVRAEmItuU/0a41ZUKu1sJGyhhOIpc4t5vk4PYzt8wpsg==} + engines: {node: ^22.18.0 || >=24.11.0} + '@babel/parser@7.29.3': resolution: {integrity: sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==} engines: {node: '>=6.0.0'} hasBin: true + '@babel/parser@8.0.4': + resolution: {integrity: sha512-srpptsAkEbbNIC/q8nT7o+m6CQe8CJUTV/t7MYc9NnWlgYVtHOb7JH6SorxMhN0kuRJjVqXbKClG6xSbPtzz+g==} + engines: {node: ^22.18.0 || >=24.11.0} + hasBin: true + + '@babel/plugin-proposal-decorators@8.0.2': + resolution: {integrity: sha512-+C6O6KKXU7BBq1GNaIkFJxrALUVGRcr+WeWm4OcuRl3h+l/CmNfcTLMrT2Lm3uvGBimBH/8pEBRrXJFLoO67Gg==} + engines: {node: ^22.18.0 || >=24.11.0} + peerDependencies: + '@babel/core': ^8.0.0 + + '@babel/plugin-syntax-decorators@8.0.1': + resolution: {integrity: sha512-NI+0S/6MvR6GlcQFwjDZ+WIc2qvG6TXN534lYs9llNldwW4b7Dh6KTtk030FA0xWdYGs4t1lWo+OEWN8wGB+Nw==} + engines: {node: ^22.18.0 || >=24.11.0} + peerDependencies: + '@babel/core': ^8.0.0 + + '@babel/plugin-syntax-jsx@8.0.1': + resolution: {integrity: sha512-n0jtCOxEovhU7METqSQjcZO9pX53nu9uNIjMS+hEt+Nt9jA7oOZoBIgbCxhhASmF6T6rPDGge5UAvh6Z4eFz/g==} + engines: {node: ^22.18.0 || >=24.11.0} + peerDependencies: + '@babel/core': ^8.0.0 + + '@babel/plugin-syntax-typescript@8.0.3': + resolution: {integrity: sha512-jmTPwps7oSQSZaV1SxkQ3C12UWyufGysGc5OzDpZzvPAIX4mO7dJT3hoqkWVrSImvkcMiknir1iLN1SNV/CZzg==} + engines: {node: ^22.18.0 || >=24.11.0} + peerDependencies: + '@babel/core': ^8.0.0 + + '@babel/plugin-transform-destructuring@8.0.1': + resolution: {integrity: sha512-RtR8uLDl0QcCmqMNIkM8gmDeYZ3rS0ZH+sa+I6sfc09yFoqfp9AEPgBstq9KyfVb0lFCVSRFfJXCI70FIl5ccw==} + engines: {node: ^22.18.0 || >=24.11.0} + peerDependencies: + '@babel/core': ^8.0.0 + + '@babel/plugin-transform-explicit-resource-management@8.0.1': + resolution: {integrity: sha512-VzDIYwBlLCpV6mJfloRdJm8HmYnMqs7O+bGha8yfg2kP7jAdxeCw6yZBVBeaKKQUThtSU52iy+3lB7DhYsbOBA==} + engines: {node: ^22.18.0 || >=24.11.0} + peerDependencies: + '@babel/core': ^8.0.0 + + '@babel/plugin-transform-modules-commonjs@8.0.1': + resolution: {integrity: sha512-PMuzulWrrzFNmY3lXSk/tV9NRb7y0eZZLJY4UEo2TKszroxvUZHAPPi+T9FDyrQhod+TQA+t+8/QYaaMpiEuhA==} + engines: {node: ^22.18.0 || >=24.11.0} + peerDependencies: + '@babel/core': ^8.0.0 + + '@babel/plugin-transform-react-display-name@8.0.1': + resolution: {integrity: sha512-soLishXlkyu6jcICPyO3HEP7A3GCzKEnn7XfvYrImuWEOwFAz93qShmWSYPf5ww0ZkO4By0zsN2bVIDF54fSdA==} + engines: {node: ^22.18.0 || >=24.11.0} + peerDependencies: + '@babel/core': ^8.0.0 + + '@babel/plugin-transform-react-jsx-development@8.0.1': + resolution: {integrity: sha512-Hb+HUZpV9KFHjm+F+P3aLDMi8QXU9l3ROCQv20z18Me2sGyW5nNNR5YTevNlgHvCpFek3BnAwhDGq/BRndXViw==} + engines: {node: ^22.18.0 || >=24.11.0} + peerDependencies: + '@babel/core': ^8.0.0 + '@babel/plugin-transform-react-jsx-self@7.27.1': resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} engines: {node: '>=6.9.0'} @@ -1238,6 +1387,36 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-react-jsx@8.0.1': + resolution: {integrity: sha512-NgkoF7Uq+30TmOPDdNUimT0Nta02uVjqJRFNlVWKrbOCu/CkzfHa4aMnIs0lMpkMmZmWA1e42Va+F04i/pY1zw==} + engines: {node: ^22.18.0 || >=24.11.0} + peerDependencies: + '@babel/core': ^8.0.0 + + '@babel/plugin-transform-react-pure-annotations@8.0.1': + resolution: {integrity: sha512-7/8UwU8hoPBurXa9tUiTTC8aACTRy5tCqLUtqikHp2eGiWoEB57AduOdbQ71OOMTEvawKrGhv3WfzkDpI+/oSg==} + engines: {node: ^22.18.0 || >=24.11.0} + peerDependencies: + '@babel/core': ^8.0.0 + + '@babel/plugin-transform-typescript@8.0.1': + resolution: {integrity: sha512-0Svqp3413Eg0GElldykF/T7SNsxQO5YVGD70fZyAdZTnX8WRgcopmbiU7GTa5xY5ZnJcEpNbfns8/GjX+/1yeA==} + engines: {node: ^22.18.0 || >=24.11.0} + peerDependencies: + '@babel/core': ^8.0.0 + + '@babel/preset-react@8.0.1': + resolution: {integrity: sha512-jrFuPp/pTddFZbtmWhdLNAYc6UMcpboeUPnw0BBrm4nOmcAko/1TRcFi1PzWCeOFRU+VaSiKmat87W1HvR7mIg==} + engines: {node: ^22.18.0 || >=24.11.0} + peerDependencies: + '@babel/core': ^8.0.0 + + '@babel/preset-typescript@8.0.1': + resolution: {integrity: sha512-qrPhQIN1NLrPmzgazF9XKQqXrOcp/WJly+K+6ReFonn24FZqRJO7clxOJo6Ni75L+2vAqI3cHVU2OJLBxoPp5A==} + engines: {node: ^22.18.0 || >=24.11.0} + peerDependencies: + '@babel/core': ^8.0.0 + '@babel/runtime@7.29.7': resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} engines: {node: '>=6.9.0'} @@ -1246,14 +1425,26 @@ packages: resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} engines: {node: '>=6.9.0'} + '@babel/template@8.0.0': + resolution: {integrity: sha512-eAD0QW/AlbamBbw0FeGiwasbCVPq5ncW0HNVyLP3B9czqLyh4gvw+5JTSNt6le9+ziAU7mqDZsKTHf3jTb4chQ==} + engines: {node: ^22.18.0 || >=24.11.0} + '@babel/traverse@7.29.0': resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} engines: {node: '>=6.9.0'} + '@babel/traverse@8.0.4': + resolution: {integrity: sha512-bZnmqzGG8UZneG1lLxBoWIH0G6Gr1D846Yu4/3XnY6FhCndMR49u26nTY08u/dAxWmLWF9vGQOuC+84FfIUoeg==} + engines: {node: ^22.18.0 || >=24.11.0} + '@babel/types@7.29.0': resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} engines: {node: '>=6.9.0'} + '@babel/types@8.0.4': + resolution: {integrity: sha512-eY+Yn3dCqTGmyiq2QRU66lA5FL8lqqqvecHt0fF3uHONIa7ToYsaCiWV8lOKqAs0Rb2SjixiKFROngnulPtt2g==} + engines: {node: ^22.18.0 || >=24.11.0} + '@bcoe/v8-coverage@1.0.2': resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} engines: {node: '>=18'} @@ -1643,6 +1834,55 @@ packages: peerDependencies: ink: '>=5' + '@inquirer/ansi@2.0.7': + resolution: {integrity: sha512-3eTuUO1vH2cZm2ZKHeQxnOqlTi9EfZDGgIe3BL3I4u+rJHocr9Fz86M4fjYABPvFnQG/gGK551HqDiIcETwU6Q==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + + '@inquirer/checkbox@5.2.3': + resolution: {integrity: sha512-XEYX2WA8SBkLPczL6/yXPHLPCvDoptmh9v56Cy05BSV1Smk1vWy19bTC4qJBuIffw7+6l4CcaYYzGqG60RfW1g==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/confirm@6.3.0': + resolution: {integrity: sha512-pZHXJImFtERmSNMBHcjwuz8Ck5vEFEYNUZnwbb8aJpjHv/TwGuFErNxF2Hp8+V+pNJs2EYPMlyWscvFEqO9jOQ==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/core@12.0.1': + resolution: {integrity: sha512-JMD5Jy/ScL5TZE18m83Nw25HjqGFLoWXwnEkW7IdwwAhZpB9Bus55/WU7zn3UqR1MOCjjTQOIYpiD4vjWA3LPw==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/editor@5.3.1': + resolution: {integrity: sha512-y43COoyVUjPWIobn2Qep/uI1drPS78aaZZZ9kVi94Tyu/GuW2N8d8Q4rifJXGAXCEAXCPTTMjD8gC1HyvM5ukA==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/expand@5.1.3': + resolution: {integrity: sha512-3NQJiXNJ/aj9wiAsr7pECdp5Qe9J0X9YUJCKsaFXS+ddOxfL6J4AIl3w3T4Gq3kK0WsQY5GMoDokK5X94m6lHw==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@inquirer/external-editor@1.0.2': resolution: {integrity: sha512-yy9cOoBnx58TlsPrIxauKIFQTiyH+0MK4e97y4sV9ERbI+zDxw7i2hxHLCIEGIE/8PPvDxGhgzIOTSOWcs6/MQ==} engines: {node: '>=18'} @@ -1652,6 +1892,91 @@ packages: '@types/node': optional: true + '@inquirer/external-editor@3.0.4': + resolution: {integrity: sha512-tZbbaK2ovq6vlrRBNQvjrypmrED/p5x2ncIHQ79cD55tei3dD96v5glMMA+6tiq7K104i/25DVYKWVPJuV6ptA==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/figures@2.0.8': + resolution: {integrity: sha512-tApbon79GM9ry56ja/Ud3SY2CL4TQsao9fIwDQbgTeNY55025GdMzQ2+UdegV/lx51VNGUB59M0v0nMpybYY4Q==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + + '@inquirer/input@5.1.4': + resolution: {integrity: sha512-3xQkQrOvgOzpSN2ciTVdRDlg1FWMCA8l+0KfB6SNlILoTCGzJTzO/gc0Rwjcb3usuGyKdaGtI6OiyMdeMeLWkg==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/number@4.2.1': + resolution: {integrity: sha512-5KaqwZNLRpUuWcoCrYghPP9TMaXL5v2Sk4xqePM7RCVegcJStoXdWibio60YIC1bec+z1fCyb67N6XPJIkZtGA==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/password@5.2.0': + resolution: {integrity: sha512-CvVcW09emkBESEOW+4R8CjLNkP3fB3XrjeL8CDvfpjgrJN+V9oerXmJAXXM3l+4xqYPD5Yaujzy/Ph0PLOdDuA==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/prompts@8.7.0': + resolution: {integrity: sha512-yQwBMYvpJ6jqrXtKiOwRD5XezjJoyt3VQvIyjsr5Arqb519nfIohOQymWVJ8/vEgg8xtZerrCsqlSaqt/LPC9A==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/rawlist@5.3.3': + resolution: {integrity: sha512-Mu7WrtmDLaXBDEyrRLS70SZgX9ZSm4Up1w0ZxiH8C1OOp9oaVCn2k8q3QGgmlnhsKYUhuaU3zFWhAP6wxkVIMA==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/search@4.3.1': + resolution: {integrity: sha512-0VWOvsHWI0rPj6CG70MoP4oXNCB6adcyN8bVFZXnh11eLDdPIK2f2XCmva78acPPDfJisfab7qNakwBh7hdBXw==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/select@5.2.3': + resolution: {integrity: sha512-KuRTodDa6xBXX2noIpjuitpX/QT7Sfav7dIZ/OfUY54Hxg95nrGoshSzxx6Ey7qqLbImKdiGkSDt7KjPXjQgmA==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/type@4.1.0': + resolution: {integrity: sha512-FMiJpuHUG3Dk0ex+UIXkre7i+i4OcwHWk9YdcVtZHFwb/r2rnrU2ipTCNAB7A+QOP0ryzIcqOfy76fRyyvOEAw==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -2779,6 +3104,29 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@stryker-mutator/api@10.0.0': + resolution: {integrity: sha512-ZtAJ0ZT3MVRCWJTBE2h90XB/6E+4lifHYtcTyNG6nU2nLekPgTo4gD5esjX6Okxo1b/JB4jJzyxYB54fwKAoJw==} + engines: {node: '>=22.0.0'} + + '@stryker-mutator/core@10.0.0': + resolution: {integrity: sha512-ZvMsRyaXQQ5e6Thcid9pkuODv6Fn9E3nrBQJUap+hcJuGJ4unm26afo3m6YKSjn8kinyxJ/3TXf0cTWRDaTxVw==} + engines: {node: '>=22.0.0'} + hasBin: true + + '@stryker-mutator/instrumenter@10.0.0': + resolution: {integrity: sha512-B7Wmn1KlEWyFeOz6D6oGvQGRfi5Xw3VemG6dEKvFQp4qLvxD9Mf4kcZghfxffgnYwXd3bFgqXsJ+ZGlhdfIOrQ==} + engines: {node: '>=22.0.0'} + + '@stryker-mutator/util@10.0.0': + resolution: {integrity: sha512-LzOpHiJaCp2ABQgnPMlrQQcsK43bd5Vo/2FGL78aN62yDoeRQ+4j3tzeuXxK5OAHdC3fUz6TDoy4IsoAuLAd3w==} + + '@stryker-mutator/vitest-runner@10.0.0': + resolution: {integrity: sha512-SHK2/vfvRUpiz7jXPnQMBnr6zLdm69DK03Mo5mPhaZWcRSygrKUqYsPqWsXsK+5ySHzlMTfCyFK5NQ/X9sJFFw==} + engines: {node: '>=22.0.0'} + peerDependencies: + '@stryker-mutator/core': 10.0.0 + vitest: '>=2.0.0' + '@tailwindcss/node@4.3.2': resolution: {integrity: sha512-yWP/sqEcBLaD8JuA6zNwxoYKr75qxTioYwlRwekj5Jr/I5GXnoJfjetH/psLUIv74cYTH2lBUEzBkinthoYcBg==} @@ -3099,6 +3447,9 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/gensync@1.0.5': + resolution: {integrity: sha512-MbsRCT7mTikHwKZ0X+LVUTLRrZZRLipTuXEO9qOYO+zmjMVk81axyClMROf6uoPD9MRVu46bx8zoR0Ad9q3NAg==} + '@types/geojson@7946.0.16': resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} @@ -3120,6 +3471,9 @@ packages: '@types/js-cookie@3.0.6': resolution: {integrity: sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==} + '@types/jsesc@2.5.1': + resolution: {integrity: sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw==} + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -3527,6 +3881,10 @@ packages: ajv@8.20.0: resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + angular-html-parser@10.11.0: + resolution: {integrity: sha512-3vERzJ65UFDr3C7uozLJwsNcQS3FS784dSh583oDgDTTZMgXe3/pdyXgKndxiP5R2lvYRfW6gSl145Hnyf2OFA==} + engines: {node: '>= 14'} + ansi-colors@4.1.3: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} engines: {node: '>=6'} @@ -3880,6 +4238,9 @@ packages: chardet@2.1.0: resolution: {integrity: sha512-bNFETTG/pM5ryzQ9Ad0lJOTa6HWD/YsScAR3EnCPZRPlQh77JocYktSHOUHelyhm8IARL+o4c4F1bP5KVOjiRA==} + chardet@2.2.0: + resolution: {integrity: sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA==} + cheerio-select@2.1.0: resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==} @@ -3925,6 +4286,10 @@ packages: resolution: {integrity: sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==} engines: {node: '>=20'} + cli-width@4.1.0: + resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} + engines: {node: '>= 12'} + cliui@7.0.4: resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} @@ -4386,6 +4751,9 @@ packages: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} + des.js@1.1.0: + resolution: {integrity: sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==} + destroy@1.2.0: resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} @@ -4407,6 +4775,9 @@ packages: devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + diff-match-patch@1.0.5: + resolution: {integrity: sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==} + diff-sequences@29.6.3: resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -4507,12 +4878,19 @@ packages: emoji-regex@10.4.0: resolution: {integrity: sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==} + emoji-regex@10.6.0: + resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} + emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + empathic@2.0.1: + resolution: {integrity: sha512-YGRs8knHhKHVShLkFET/rWAU8kmHbOV5LwN938RHI0pljAJ1Gf6SzXsSmRaEzcXTtOOmVqJ5+WtQPL5uigY50Q==} + engines: {node: '>=14'} + encodeurl@2.0.0: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} @@ -4796,9 +5174,18 @@ packages: fast-shallow-equal@1.0.0: resolution: {integrity: sha512-HPtaa38cPgWvaCFmRNhlc6NG7pv6NUHqjPgVAkWGoB9mQMwYB27/K0CvOM5Czy+qpT3e8XJ6Q4aPAnzpNpzNaw==} + fast-string-truncated-width@3.0.3: + resolution: {integrity: sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==} + + fast-string-width@3.0.2: + resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==} + fast-uri@3.1.0: resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + fast-wrap-ansi@0.2.2: + resolution: {integrity: sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==} + fastest-levenshtein@1.0.16: resolution: {integrity: sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==} engines: {node: '>= 4.9.1'} @@ -5244,6 +5631,10 @@ packages: resolution: {integrity: sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==} engines: {node: '>=0.10.0'} + iconv-lite@0.7.3: + resolution: {integrity: sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==} + engines: {node: '>=0.10.0'} + ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} @@ -5635,6 +6026,9 @@ packages: js-cookie@3.0.8: resolution: {integrity: sha512-yeJd4aNAdYZQjaon2bpD/Gb0B/omw7HQOsynXXcOiWVCacbBcPlgn8S/d1X6blFSaHao7ozqtW7NZW19xpCtIw==} + js-md4@0.3.2: + resolution: {integrity: sha512-/GDnfQYsltsjRswQhN9fhv3EMw2sCpUdrdxyWDOUK7eyD++r3gRhzgiQgc/x4MAv2i1iuQ4lxO5mvqM3vj4bwA==} + js-message@1.0.7: resolution: {integrity: sha512-efJLHhLjIyKRewNS9EGZ4UpI8NguuL6fKkhRxVuMmrGV2xN/0APGdQYwLFky5w9naebSZ0OwAGp0G6/2Cg90rA==} engines: {node: '>=0.6.0'} @@ -5677,6 +6071,9 @@ packages: json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + json-rpc-2.0@1.8.0: + resolution: {integrity: sha512-4nw+XlJbk5XokA7BtqHGu+0PEiUPfAkRzXXd1Cgjy1c3F2UI/3Gy7yr76wyJEv3X1F1SRGGF8xPAPPhelBiGmA==} + json-schema-to-ts@3.1.1: resolution: {integrity: sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==} engines: {node: '>=16'} @@ -6299,6 +6696,9 @@ packages: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} + minimalistic-assert@1.0.1: + resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} + minimatch@10.2.5: resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} engines: {node: 18 || 20 || >=22} @@ -6374,9 +6774,26 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + mutation-server-protocol@0.4.1: + resolution: {integrity: sha512-SBGK0j8hLDne7bktgThKI8kGvGTx3rY3LAeQTmOKZ5bVnL/7TorLMvcVF7dIPJCu5RNUWhkkuF53kurygYVt3g==} + engines: {node: '>=18'} + + mutation-testing-elements@3.8.4: + resolution: {integrity: sha512-5CF1SNa7at5ZH33vEr+21wNebTSrtNIVvnzaUlxortHajOrIPaSLczIWvg6sI/fsExekQ7jookwf2cHftkckqQ==} + + mutation-testing-metrics@3.8.4: + resolution: {integrity: sha512-DZcmndJBH6nrNs3tpiB3OcMVq9KkG2cHCpJSnDxSxPwi9qrafRmoec40xjhkbzOoX1n7/4UkDqg5tIj4A6nvCw==} + + mutation-testing-report-schema@3.8.4: + resolution: {integrity: sha512-s4G71R6Lt/PpZ0cqeglIcgyBdzLM8E+SeCHZAPg1wkSsPtRBa4XfPzAozYKdiJk/TLbNEEb7En9t0/bveuPuxA==} + mute-stream@0.0.8: resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} + mute-stream@3.0.0: + resolution: {integrity: sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==} + engines: {node: ^20.17.0 || >=22.9.0} + mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} @@ -6859,6 +7276,10 @@ packages: process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + progress@2.0.3: + resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} + engines: {node: '>=0.4.0'} + prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} @@ -6903,6 +7324,10 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + qs@6.15.1: + resolution: {integrity: sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==} + engines: {node: '>=0.6'} + qs@6.15.2: resolution: {integrity: sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==} engines: {node: '>=0.6'} @@ -7238,6 +7663,9 @@ packages: rw@1.3.3: resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==} + rxjs@7.8.2: + resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + safe-array-concat@1.1.3: resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} engines: {node: '>=0.4'} @@ -7316,6 +7744,11 @@ packages: engines: {node: '>=10'} hasBin: true + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + send@1.2.0: resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==} engines: {node: '>= 18'} @@ -7937,9 +8370,17 @@ packages: resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} engines: {node: '>= 0.4'} + typed-inject@5.0.0: + resolution: {integrity: sha512-0Ql2ORqBORLMdAW89TQKZsb1PQkFGImFfVmncXWe7a+AA3+7dh7Se9exxZowH4kbnlvKEFkMxUYdHUpjYWFJaA==} + engines: {node: '>=18'} + typed-rest-client@1.8.11: resolution: {integrity: sha512-5UvfMpd1oelmUPRbbaVnq+rHP7ng2cE4qoQkQeAqxRL6PklkxsM0g32/HL0yfvruK6ojQ5x8EE+HF4YV6DtuCA==} + typed-rest-client@2.3.1: + resolution: {integrity: sha512-k4kX5Up6qA68D0Cby2AK+6+vM5k3qTxe+/3FqhnHRExjY5cfbOnzjQZbP/LXleF8hVoDvDqxlgk9KK83HoBZlQ==} + engines: {node: '>= 16.0.0'} + typescript-eslint@8.32.1: resolution: {integrity: sha512-D7el+eaDHAmXvrZBy1zpzSNIRqnCOrkwTgZxTu3MUqRWk8k0q9m9Ho4+vPf7iHtgUfrK/o8IZaEApsxPlHTFCg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -8266,6 +8707,9 @@ packages: resolution: {integrity: sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==} engines: {node: 20 || >=22} + weapon-regex@2.0.4: + resolution: {integrity: sha512-ubuhY5Lo4phWcMsJqe8j62m9uhsuo/VpfK5XsSgYGRGDSt10hwVwOBTriPRd0dac+KYbGNXOrfXjM6xCp2NUKg==} + web-namespaces@1.1.4: resolution: {integrity: sha512-wYxSGajtmoP4WxfejAPIr4l0fVh+jeMXZb08wNc0tMg6xsfZXj3cECqIK0G7ZAqUq0PP8WlMDtaOGVBTAWztNw==} @@ -8896,8 +9340,15 @@ snapshots: js-tokens: 4.0.0 picocolors: 1.1.1 + '@babel/code-frame@8.0.0': + dependencies: + '@babel/helper-validator-identifier': 8.0.4 + js-tokens: 10.0.0 + '@babel/compat-data@7.29.3': {} + '@babel/compat-data@8.0.0': {} + '@babel/core@7.29.0': dependencies: '@babel/code-frame': 7.29.7 @@ -8918,6 +9369,25 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/core@8.0.1': + dependencies: + '@babel/code-frame': 8.0.0 + '@babel/generator': 8.0.0 + '@babel/helper-compilation-targets': 8.0.0 + '@babel/helpers': 8.0.0 + '@babel/parser': 8.0.4 + '@babel/template': 8.0.0 + '@babel/traverse': 8.0.4 + '@babel/types': 8.0.4 + '@types/gensync': 1.0.5 + convert-source-map: 2.0.0 + empathic: 2.0.1 + gensync: 1.0.0-beta.2 + import-meta-resolve: 4.2.0 + json5: 2.2.3 + obug: 2.1.2 + semver: 7.8.5 + '@babel/generator@7.29.1': dependencies: '@babel/parser': 7.29.3 @@ -8926,6 +9396,19 @@ snapshots: '@jridgewell/trace-mapping': 0.3.31 jsesc: 3.1.0 + '@babel/generator@8.0.0': + dependencies: + '@babel/parser': 8.0.4 + '@babel/types': 8.0.4 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + '@types/jsesc': 2.5.1 + jsesc: 3.1.0 + + '@babel/helper-annotate-as-pure@8.0.0': + dependencies: + '@babel/types': 8.0.4 + '@babel/helper-compilation-targets@7.28.6': dependencies: '@babel/compat-data': 7.29.3 @@ -8934,8 +9417,34 @@ snapshots: lru-cache: 5.1.1 semver: 6.3.1 + '@babel/helper-compilation-targets@8.0.0': + dependencies: + '@babel/compat-data': 8.0.0 + '@babel/helper-validator-option': 8.0.0 + browserslist: 4.24.5 + lru-cache: 11.2.2 + semver: 7.8.5 + + '@babel/helper-create-class-features-plugin@8.0.1(@babel/core@8.0.1)': + dependencies: + '@babel/core': 8.0.1 + '@babel/helper-annotate-as-pure': 8.0.0 + '@babel/helper-member-expression-to-functions': 8.0.0 + '@babel/helper-optimise-call-expression': 8.0.0 + '@babel/helper-replace-supers': 8.0.1(@babel/core@8.0.1) + '@babel/helper-skip-transparent-expression-wrappers': 8.0.0 + '@babel/traverse': 8.0.4 + semver: 7.8.5 + '@babel/helper-globals@7.28.0': {} + '@babel/helper-globals@8.0.0': {} + + '@babel/helper-member-expression-to-functions@8.0.0': + dependencies: + '@babel/traverse': 8.0.4 + '@babel/types': 8.0.4 + '@babel/helper-module-imports@7.28.6': dependencies: '@babel/traverse': 7.29.0 @@ -8943,6 +9452,11 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/helper-module-imports@8.0.0': + dependencies: + '@babel/traverse': 8.0.4 + '@babel/types': 8.0.4 + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 @@ -8952,23 +9466,114 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/helper-module-transforms@8.0.1(@babel/core@8.0.1)': + dependencies: + '@babel/core': 8.0.1 + '@babel/helper-module-imports': 8.0.0 + '@babel/helper-validator-identifier': 8.0.4 + '@babel/traverse': 8.0.4 + + '@babel/helper-optimise-call-expression@8.0.0': + dependencies: + '@babel/types': 8.0.4 + '@babel/helper-plugin-utils@7.27.1': {} + '@babel/helper-plugin-utils@8.0.1(@babel/core@8.0.1)': + dependencies: + '@babel/core': 8.0.1 + + '@babel/helper-replace-supers@8.0.1(@babel/core@8.0.1)': + dependencies: + '@babel/core': 8.0.1 + '@babel/helper-member-expression-to-functions': 8.0.0 + '@babel/helper-optimise-call-expression': 8.0.0 + '@babel/traverse': 8.0.4 + + '@babel/helper-skip-transparent-expression-wrappers@8.0.0': + dependencies: + '@babel/traverse': 8.0.4 + '@babel/types': 8.0.4 + '@babel/helper-string-parser@7.27.1': {} + '@babel/helper-string-parser@8.0.0': {} + '@babel/helper-validator-identifier@7.29.7': {} + '@babel/helper-validator-identifier@8.0.4': {} + '@babel/helper-validator-option@7.27.1': {} + '@babel/helper-validator-option@8.0.0': {} + '@babel/helpers@7.29.2': dependencies: '@babel/template': 7.28.6 '@babel/types': 7.29.0 + '@babel/helpers@8.0.0': + dependencies: + '@babel/template': 8.0.0 + '@babel/types': 8.0.4 + '@babel/parser@7.29.3': dependencies: '@babel/types': 7.29.0 + '@babel/parser@8.0.4': + dependencies: + '@babel/types': 8.0.4 + + '@babel/plugin-proposal-decorators@8.0.2(@babel/core@8.0.1)': + dependencies: + '@babel/core': 8.0.1 + '@babel/helper-create-class-features-plugin': 8.0.1(@babel/core@8.0.1) + '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.1) + '@babel/plugin-syntax-decorators': 8.0.1(@babel/core@8.0.1) + + '@babel/plugin-syntax-decorators@8.0.1(@babel/core@8.0.1)': + dependencies: + '@babel/core': 8.0.1 + '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.1) + + '@babel/plugin-syntax-jsx@8.0.1(@babel/core@8.0.1)': + dependencies: + '@babel/core': 8.0.1 + '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.1) + + '@babel/plugin-syntax-typescript@8.0.3(@babel/core@8.0.1)': + dependencies: + '@babel/core': 8.0.1 + '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.1) + + '@babel/plugin-transform-destructuring@8.0.1(@babel/core@8.0.1)': + dependencies: + '@babel/core': 8.0.1 + '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.1) + + '@babel/plugin-transform-explicit-resource-management@8.0.1(@babel/core@8.0.1)': + dependencies: + '@babel/core': 8.0.1 + '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.1) + '@babel/plugin-transform-destructuring': 8.0.1(@babel/core@8.0.1) + + '@babel/plugin-transform-modules-commonjs@8.0.1(@babel/core@8.0.1)': + dependencies: + '@babel/core': 8.0.1 + '@babel/helper-module-transforms': 8.0.1(@babel/core@8.0.1) + '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.1) + + '@babel/plugin-transform-react-display-name@8.0.1(@babel/core@8.0.1)': + dependencies: + '@babel/core': 8.0.1 + '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.1) + + '@babel/plugin-transform-react-jsx-development@8.0.1(@babel/core@8.0.1)': + dependencies: + '@babel/core': 8.0.1 + '@babel/plugin-transform-react-jsx': 8.0.1(@babel/core@8.0.1) + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 @@ -8979,6 +9584,48 @@ snapshots: '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-react-jsx@8.0.1(@babel/core@8.0.1)': + dependencies: + '@babel/core': 8.0.1 + '@babel/helper-annotate-as-pure': 8.0.0 + '@babel/helper-module-imports': 8.0.0 + '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.1) + '@babel/plugin-syntax-jsx': 8.0.1(@babel/core@8.0.1) + '@babel/types': 8.0.4 + + '@babel/plugin-transform-react-pure-annotations@8.0.1(@babel/core@8.0.1)': + dependencies: + '@babel/core': 8.0.1 + '@babel/helper-annotate-as-pure': 8.0.0 + '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.1) + + '@babel/plugin-transform-typescript@8.0.1(@babel/core@8.0.1)': + dependencies: + '@babel/core': 8.0.1 + '@babel/helper-annotate-as-pure': 8.0.0 + '@babel/helper-create-class-features-plugin': 8.0.1(@babel/core@8.0.1) + '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.1) + '@babel/helper-skip-transparent-expression-wrappers': 8.0.0 + '@babel/plugin-syntax-typescript': 8.0.3(@babel/core@8.0.1) + + '@babel/preset-react@8.0.1(@babel/core@8.0.1)': + dependencies: + '@babel/core': 8.0.1 + '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.1) + '@babel/helper-validator-option': 8.0.0 + '@babel/plugin-transform-react-display-name': 8.0.1(@babel/core@8.0.1) + '@babel/plugin-transform-react-jsx': 8.0.1(@babel/core@8.0.1) + '@babel/plugin-transform-react-jsx-development': 8.0.1(@babel/core@8.0.1) + '@babel/plugin-transform-react-pure-annotations': 8.0.1(@babel/core@8.0.1) + + '@babel/preset-typescript@8.0.1(@babel/core@8.0.1)': + dependencies: + '@babel/core': 8.0.1 + '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.1) + '@babel/helper-validator-option': 8.0.0 + '@babel/plugin-transform-modules-commonjs': 8.0.1(@babel/core@8.0.1) + '@babel/plugin-transform-typescript': 8.0.1(@babel/core@8.0.1) + '@babel/runtime@7.29.7': {} '@babel/template@7.28.6': @@ -8987,6 +9634,12 @@ snapshots: '@babel/parser': 7.29.3 '@babel/types': 7.29.0 + '@babel/template@8.0.0': + dependencies: + '@babel/code-frame': 8.0.0 + '@babel/parser': 8.0.4 + '@babel/types': 8.0.4 + '@babel/traverse@7.29.0': dependencies: '@babel/code-frame': 7.29.7 @@ -8999,11 +9652,26 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/traverse@8.0.4': + dependencies: + '@babel/code-frame': 8.0.0 + '@babel/generator': 8.0.0 + '@babel/helper-globals': 8.0.0 + '@babel/parser': 8.0.4 + '@babel/template': 8.0.0 + '@babel/types': 8.0.4 + obug: 2.1.2 + '@babel/types@7.29.0': dependencies: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.29.7 + '@babel/types@8.0.4': + dependencies: + '@babel/helper-string-parser': 8.0.0 + '@babel/helper-validator-identifier': 8.0.4 + '@bcoe/v8-coverage@1.0.2': {} '@braintree/sanitize-url@7.1.2': {} @@ -9416,6 +10084,51 @@ snapshots: figures: 6.1.0 ink: 6.6.0(@types/react@18.3.31)(react@19.2.7) + '@inquirer/ansi@2.0.7': {} + + '@inquirer/checkbox@5.2.3(@types/node@22.20.1)': + dependencies: + '@inquirer/ansi': 2.0.7 + '@inquirer/core': 12.0.1(@types/node@22.20.1) + '@inquirer/figures': 2.0.8 + '@inquirer/type': 4.1.0(@types/node@22.20.1) + optionalDependencies: + '@types/node': 22.20.1 + + '@inquirer/confirm@6.3.0(@types/node@22.20.1)': + dependencies: + '@inquirer/core': 12.0.1(@types/node@22.20.1) + '@inquirer/type': 4.1.0(@types/node@22.20.1) + optionalDependencies: + '@types/node': 22.20.1 + + '@inquirer/core@12.0.1(@types/node@22.20.1)': + dependencies: + '@inquirer/ansi': 2.0.7 + '@inquirer/figures': 2.0.8 + '@inquirer/type': 4.1.0(@types/node@22.20.1) + cli-width: 4.1.0 + fast-wrap-ansi: 0.2.2 + mute-stream: 3.0.0 + signal-exit: 4.1.0 + optionalDependencies: + '@types/node': 22.20.1 + + '@inquirer/editor@5.3.1(@types/node@22.20.1)': + dependencies: + '@inquirer/core': 12.0.1(@types/node@22.20.1) + '@inquirer/external-editor': 3.0.4(@types/node@22.20.1) + '@inquirer/type': 4.1.0(@types/node@22.20.1) + optionalDependencies: + '@types/node': 22.20.1 + + '@inquirer/expand@5.1.3(@types/node@22.20.1)': + dependencies: + '@inquirer/core': 12.0.1(@types/node@22.20.1) + '@inquirer/type': 4.1.0(@types/node@22.20.1) + optionalDependencies: + '@types/node': 22.20.1 + '@inquirer/external-editor@1.0.2(@types/node@22.20.1)': dependencies: chardet: 2.1.0 @@ -9423,6 +10136,80 @@ snapshots: optionalDependencies: '@types/node': 22.20.1 + '@inquirer/external-editor@3.0.4(@types/node@22.20.1)': + dependencies: + chardet: 2.2.0 + iconv-lite: 0.7.3 + optionalDependencies: + '@types/node': 22.20.1 + + '@inquirer/figures@2.0.8': {} + + '@inquirer/input@5.1.4(@types/node@22.20.1)': + dependencies: + '@inquirer/core': 12.0.1(@types/node@22.20.1) + '@inquirer/type': 4.1.0(@types/node@22.20.1) + optionalDependencies: + '@types/node': 22.20.1 + + '@inquirer/number@4.2.1(@types/node@22.20.1)': + dependencies: + '@inquirer/core': 12.0.1(@types/node@22.20.1) + '@inquirer/type': 4.1.0(@types/node@22.20.1) + optionalDependencies: + '@types/node': 22.20.1 + + '@inquirer/password@5.2.0(@types/node@22.20.1)': + dependencies: + '@inquirer/ansi': 2.0.7 + '@inquirer/core': 12.0.1(@types/node@22.20.1) + '@inquirer/type': 4.1.0(@types/node@22.20.1) + optionalDependencies: + '@types/node': 22.20.1 + + '@inquirer/prompts@8.7.0(@types/node@22.20.1)': + dependencies: + '@inquirer/checkbox': 5.2.3(@types/node@22.20.1) + '@inquirer/confirm': 6.3.0(@types/node@22.20.1) + '@inquirer/editor': 5.3.1(@types/node@22.20.1) + '@inquirer/expand': 5.1.3(@types/node@22.20.1) + '@inquirer/input': 5.1.4(@types/node@22.20.1) + '@inquirer/number': 4.2.1(@types/node@22.20.1) + '@inquirer/password': 5.2.0(@types/node@22.20.1) + '@inquirer/rawlist': 5.3.3(@types/node@22.20.1) + '@inquirer/search': 4.3.1(@types/node@22.20.1) + '@inquirer/select': 5.2.3(@types/node@22.20.1) + optionalDependencies: + '@types/node': 22.20.1 + + '@inquirer/rawlist@5.3.3(@types/node@22.20.1)': + dependencies: + '@inquirer/core': 12.0.1(@types/node@22.20.1) + '@inquirer/type': 4.1.0(@types/node@22.20.1) + optionalDependencies: + '@types/node': 22.20.1 + + '@inquirer/search@4.3.1(@types/node@22.20.1)': + dependencies: + '@inquirer/core': 12.0.1(@types/node@22.20.1) + '@inquirer/figures': 2.0.8 + '@inquirer/type': 4.1.0(@types/node@22.20.1) + optionalDependencies: + '@types/node': 22.20.1 + + '@inquirer/select@5.2.3(@types/node@22.20.1)': + dependencies: + '@inquirer/ansi': 2.0.7 + '@inquirer/core': 12.0.1(@types/node@22.20.1) + '@inquirer/figures': 2.0.8 + '@inquirer/type': 4.1.0(@types/node@22.20.1) + optionalDependencies: + '@types/node': 22.20.1 + + '@inquirer/type@4.1.0(@types/node@22.20.1)': + optionalDependencies: + '@types/node': 22.20.1 + '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -10524,6 +11311,72 @@ snapshots: '@standard-schema/spec@1.1.0': {} + '@stryker-mutator/api@10.0.0': + dependencies: + mutation-testing-metrics: 3.8.4 + mutation-testing-report-schema: 3.8.4 + tslib: 2.8.1 + typed-inject: 5.0.0 + + '@stryker-mutator/core@10.0.0(@types/node@22.20.1)': + dependencies: + '@inquirer/prompts': 8.7.0(@types/node@22.20.1) + '@stryker-mutator/api': 10.0.0 + '@stryker-mutator/instrumenter': 10.0.0 + '@stryker-mutator/util': 10.0.0 + ajv: 8.20.0 + chalk: 5.6.2 + commander: 14.0.3 + diff-match-patch: 1.0.5 + emoji-regex: 10.6.0 + execa: 9.6.1 + json-rpc-2.0: 1.8.0 + lodash.groupby: 4.6.0 + minimatch: 10.2.5 + mutation-server-protocol: 0.4.1 + mutation-testing-elements: 3.8.4 + mutation-testing-metrics: 3.8.4 + mutation-testing-report-schema: 3.8.4 + npm-run-path: 6.0.0 + progress: 2.0.3 + rxjs: 7.8.2 + semver: 7.7.3 + source-map: 0.7.6 + tree-kill: 1.2.2 + tslib: 2.8.1 + typed-inject: 5.0.0 + typed-rest-client: 2.3.1 + transitivePeerDependencies: + - '@types/node' + + '@stryker-mutator/instrumenter@10.0.0': + dependencies: + '@babel/core': 8.0.1 + '@babel/generator': 8.0.0 + '@babel/parser': 8.0.4 + '@babel/plugin-proposal-decorators': 8.0.2(@babel/core@8.0.1) + '@babel/plugin-transform-explicit-resource-management': 8.0.1(@babel/core@8.0.1) + '@babel/preset-react': 8.0.1(@babel/core@8.0.1) + '@babel/preset-typescript': 8.0.1(@babel/core@8.0.1) + '@babel/traverse': 8.0.4 + '@stryker-mutator/api': 10.0.0 + '@stryker-mutator/util': 10.0.0 + angular-html-parser: 10.11.0 + semver: 7.8.5 + tslib: 2.8.1 + weapon-regex: 2.0.4 + + '@stryker-mutator/util@10.0.0': {} + + '@stryker-mutator/vitest-runner@10.0.0(@stryker-mutator/core@10.0.0(@types/node@22.20.1))(vitest@4.1.9)': + dependencies: + '@stryker-mutator/api': 10.0.0 + '@stryker-mutator/core': 10.0.0(@types/node@22.20.1) + '@stryker-mutator/util': 10.0.0 + semver: 7.8.5 + tslib: 2.8.1 + vitest: 4.1.9(@opentelemetry/api@1.9.0)(@types/node@22.20.1)(@vitest/coverage-v8@4.1.9)(@vitest/ui@4.1.9)(jsdom@26.1.0)(vite@8.1.0(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) + '@tailwindcss/node@4.3.2': dependencies: '@jridgewell/remapping': 2.3.5 @@ -10860,6 +11713,8 @@ snapshots: '@types/estree@1.0.8': {} + '@types/gensync@1.0.5': {} + '@types/geojson@7946.0.16': {} '@types/hast@3.0.4': @@ -10883,6 +11738,8 @@ snapshots: '@types/js-cookie@3.0.6': {} + '@types/jsesc@2.5.1': {} + '@types/json-schema@7.0.15': {} '@types/katex@0.16.7': {} @@ -11363,6 +12220,8 @@ snapshots: json-schema-traverse: 1.0.0 require-from-string: 2.0.2 + angular-html-parser@10.11.0: {} + ansi-colors@4.1.3: {} ansi-escapes@7.3.0: @@ -11735,6 +12594,8 @@ snapshots: chardet@2.1.0: {} + chardet@2.2.0: {} + cheerio-select@2.1.0: dependencies: boolbase: 1.0.0 @@ -11789,6 +12650,8 @@ snapshots: slice-ansi: 8.0.0 string-width: 8.2.1 + cli-width@4.1.0: {} + cliui@7.0.4: dependencies: string-width: 4.2.3 @@ -12255,6 +13118,11 @@ snapshots: dequal@2.0.3: {} + des.js@1.1.0: + dependencies: + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + destroy@1.2.0: {} detect-indent@6.1.0: {} @@ -12269,6 +13137,8 @@ snapshots: dependencies: dequal: 2.0.3 + diff-match-patch@1.0.5: {} + diff-sequences@29.6.3: {} diff@5.2.2: {} @@ -12362,10 +13232,14 @@ snapshots: emoji-regex@10.4.0: {} + emoji-regex@10.6.0: {} + emoji-regex@8.0.0: {} emoji-regex@9.2.2: {} + empathic@2.0.1: {} + encodeurl@2.0.0: {} encoding-sniffer@0.2.0: @@ -12815,8 +13689,18 @@ snapshots: fast-shallow-equal@1.0.0: {} + fast-string-truncated-width@3.0.3: {} + + fast-string-width@3.0.2: + dependencies: + fast-string-truncated-width: 3.0.3 + fast-uri@3.1.0: {} + fast-wrap-ansi@0.2.2: + dependencies: + fast-string-width: 3.0.2 + fastest-levenshtein@1.0.16: {} fastest-stable-stringify@2.0.2: {} @@ -13349,6 +14233,10 @@ snapshots: dependencies: safer-buffer: 2.1.2 + iconv-lite@0.7.3: + dependencies: + safer-buffer: 2.1.2 + ieee754@1.2.1: {} ignore@5.3.2: {} @@ -13716,6 +14604,8 @@ snapshots: js-cookie@3.0.8: {} + js-md4@0.3.2: {} + js-message@1.0.7: {} js-queue@2.0.2: @@ -13770,6 +14660,8 @@ snapshots: json-buffer@3.0.1: {} + json-rpc-2.0@1.8.0: {} + json-schema-to-ts@3.1.1: dependencies: '@babel/runtime': 7.29.7 @@ -14647,6 +15539,8 @@ snapshots: min-indent@1.0.1: {} + minimalistic-assert@1.0.1: {} + minimatch@10.2.5: dependencies: brace-expansion: 5.0.5 @@ -14747,8 +15641,22 @@ snapshots: ms@2.1.3: {} + mutation-server-protocol@0.4.1: + dependencies: + zod: 3.25.76 + + mutation-testing-elements@3.8.4: {} + + mutation-testing-metrics@3.8.4: + dependencies: + mutation-testing-report-schema: 3.8.4 + + mutation-testing-report-schema@3.8.4: {} + mute-stream@0.0.8: {} + mute-stream@3.0.0: {} + mz@2.7.0: dependencies: any-promise: 1.3.0 @@ -15264,6 +16172,8 @@ snapshots: process-nextick-args@2.0.1: {} + progress@2.0.3: {} + prop-types@15.8.1: dependencies: loose-envify: 1.4.0 @@ -15319,6 +16229,10 @@ snapshots: punycode@2.3.1: {} + qs@6.15.1: + dependencies: + side-channel: 1.1.0 + qs@6.15.2: dependencies: side-channel: 1.1.0 @@ -15787,6 +16701,10 @@ snapshots: rw@1.3.3: {} + rxjs@7.8.2: + dependencies: + tslib: 2.8.1 + safe-array-concat@1.1.3: dependencies: call-bind: 1.0.8 @@ -15865,6 +16783,8 @@ snapshots: semver@7.7.3: {} + semver@7.8.5: {} + send@1.2.0: dependencies: debug: 4.4.3(supports-color@8.1.1) @@ -16553,12 +17473,22 @@ snapshots: possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 + typed-inject@5.0.0: {} + typed-rest-client@1.8.11: dependencies: qs: 6.15.2 tunnel: 0.0.6 underscore: 1.13.8 + typed-rest-client@2.3.1: + dependencies: + des.js: 1.1.0 + js-md4: 0.3.2 + qs: 6.15.1 + tunnel: 0.0.6 + underscore: 1.13.8 + typescript-eslint@8.32.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3): dependencies: '@typescript-eslint/eslint-plugin': 8.32.1(@typescript-eslint/parser@8.32.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) @@ -16855,6 +17785,8 @@ snapshots: walk-up-path@4.0.0: {} + weapon-regex@2.0.4: {} + web-namespaces@1.1.4: {} web-namespaces@2.0.1: {} diff --git a/scripts/stryker-diff.mjs b/scripts/stryker-diff.mjs new file mode 100644 index 0000000000..aeef167db0 --- /dev/null +++ b/scripts/stryker-diff.mjs @@ -0,0 +1,509 @@ +#!/usr/bin/env node + +import { execFileSync, spawnSync } from "node:child_process" +import fs from "node:fs" +import path from "node:path" +import process from "node:process" +import { fileURLToPath } from "node:url" + +import ts from "typescript" + +export const MAX_CHANGED_LINES = 500 +export const MAX_MUTANTS = 400 + +export const PACKAGE_CONFIGS = [ + { + id: "core", + root: "packages/core", + sourceRoot: "packages/core/src/", + vitestConfig: "vitest.unit.config.ts", + }, + { + id: "cloud", + root: "packages/cloud", + sourceRoot: "packages/cloud/src/", + vitestConfig: "vitest.config.ts", + }, + { + id: "telemetry", + root: "packages/telemetry", + sourceRoot: "packages/telemetry/src/", + vitestConfig: "vitest.config.ts", + }, + { + id: "vscode-shim", + root: "packages/vscode-shim", + sourceRoot: "packages/vscode-shim/src/", + vitestConfig: "vitest.config.ts", + }, +] + +const VALID_MUTANT_STATUSES = new Set(["Killed", "Timeout", "Survived", "NoCoverage"]) +const BLOCKING_MUTANT_STATUSES = new Set(["Survived", "NoCoverage"]) +const SOURCE_EXTENSIONS = new Set([".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx"]) +const EXCLUDED_PATH_SEGMENTS = ["/__mocks__/", "/__tests__/", "/fixtures/"] +const DISABLE_DIRECTIVE = + /^\s*\/\/\s*Stryker disable next-line ([A-Za-z][A-Za-z0-9]*(?:,[A-Za-z][A-Za-z0-9]*)*)\s*:\s*(\S.*)$/ + +export function parseNameStatus(output) { + const tokens = output.split("\0") + const entries = [] + + for (let index = 0; index < tokens.length; ) { + const statusToken = tokens[index++] + if (!statusToken) break + + const status = statusToken[0] + if (status === "R" || status === "C") { + const oldPath = tokens[index++] + const newPath = tokens[index++] + if (!oldPath || !newPath) throw new Error(`Malformed git name-status entry: ${statusToken}`) + entries.push({ status, oldPath, path: newPath }) + } else { + const filePath = tokens[index++] + if (!filePath) throw new Error(`Malformed git name-status entry: ${statusToken}`) + entries.push({ status, path: filePath }) + } + } + + return entries +} + +export function parseChangedLines(diff) { + const lines = new Set() + const hunkPattern = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/ + + for (const line of diff.split("\n")) { + const match = hunkPattern.exec(line) + if (!match) continue + + const start = Number(match[1]) + const count = match[2] === undefined ? 1 : Number(match[2]) + for (let offset = 0; offset < count; offset++) lines.add(start + offset) + } + + return lines +} + +function isTypeOnlyNode(node) { + return ( + ts.isImportDeclaration(node) || + ts.isImportEqualsDeclaration(node) || + ts.isInterfaceDeclaration(node) || + ts.isTypeAliasDeclaration(node) || + ts.isTypeNode(node) || + ts.isExportDeclaration(node) || + (ts.isImportSpecifier(node) && node.isTypeOnly) || + (ts.isExportSpecifier(node) && node.isTypeOnly) + ) +} + +export function executableChangedLines(source, changedLines, filePath = "source.ts") { + const scriptKind = filePath.endsWith("x") ? ts.ScriptKind.TSX : ts.ScriptKind.TS + const sourceFile = ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true, scriptKind) + const executable = new Set() + + function mark(node, fullSpan) { + const start = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1 + const end = fullSpan ? sourceFile.getLineAndCharacterOfPosition(node.getEnd()).line + 1 : start + for (let line = start; line <= end; line++) { + if (changedLines.has(line)) executable.add(line) + } + } + + function visit(node) { + if (isTypeOnlyNode(node)) return + + if (ts.isExpression(node)) { + mark(node, true) + } else if (ts.isStatement(node) && !ts.isBlock(node) && !ts.isEmptyStatement(node)) { + mark(node, false) + } + + ts.forEachChild(node, visit) + } + + visit(sourceFile) + return executable +} + +export function validateDisableDirectives(source, changedLines, filePath) { + const lines = source.split(/\r?\n/) + for (const lineNumber of changedLines) { + const line = lines[lineNumber - 1] ?? "" + if (!line.includes("Stryker disable")) continue + + const match = DISABLE_DIRECTIVE.exec(line) + if (!match || match[1].split(",").some((mutator) => mutator.toLowerCase() === "all")) { + throw new Error( + `${filePath}:${lineNumber}: mutation exclusions must use ` + + '"// Stryker disable next-line MutatorName: concrete reason"; broad or unreasoned exclusions are not allowed.', + ) + } + } +} + +export function toRanges(lines) { + const sorted = [...lines].sort((left, right) => left - right) + const ranges = [] + + for (const line of sorted) { + const previous = ranges.at(-1) + if (previous && line === previous.end + 1) { + previous.end = line + } else { + ranges.push({ start: line, end: line }) + } + } + + return ranges +} + +export function packageForPath(filePath) { + if ( + !SOURCE_EXTENSIONS.has(path.posix.extname(filePath)) || + EXCLUDED_PATH_SEGMENTS.some((segment) => filePath.includes(segment)) || + /(?:^|\/)[^/]+\.(?:test|spec)(?:\.[^.]+)?\.[cm]?[jt]sx?$/.test(filePath) || + filePath.endsWith(".d.ts") || + path.posix.basename(filePath).startsWith("vitest.config.") + ) { + return undefined + } + + return PACKAGE_CONFIGS.find((candidate) => filePath.startsWith(candidate.sourceRoot)) +} + +export function buildManifest(entries, readSource, diffForPath) { + const packages = new Map() + + for (const entry of entries) { + if (!new Set(["A", "M", "R"]).has(entry.status)) continue + const packageConfig = packageForPath(entry.path) + if (!packageConfig) continue + + const source = readSource(entry.path) + const sourceLineCount = source.split(/\r?\n/).length + const changedLines = + entry.status === "A" + ? new Set(Array.from({ length: sourceLineCount }, (_, index) => index + 1)) + : parseChangedLines(diffForPath(entry.path)) + + validateDisableDirectives(source, new Set(source.split(/\r?\n/).map((_, index) => index + 1)), entry.path) + const executableLines = executableChangedLines(source, changedLines, entry.path) + if (executableLines.size === 0) continue + + const relativePath = path.posix.relative(packageConfig.root, entry.path) + const selectors = toRanges(executableLines).map(({ start, end }) => `${relativePath}:${start}-${end}`) + + const packageEntry = packages.get(packageConfig.id) ?? { + ...packageConfig, + changedExecutableLines: 0, + files: [], + selectors: [], + } + packageEntry.changedExecutableLines += executableLines.size + packageEntry.files.push({ + path: entry.path, + status: entry.status, + executableLines: [...executableLines].sort((a, b) => a - b), + }) + packageEntry.selectors.push(...selectors) + packages.set(packageConfig.id, packageEntry) + } + + for (const packageEntry of packages.values()) { + if (packageEntry.changedExecutableLines > MAX_CHANGED_LINES) { + throw new Error( + `${packageEntry.id} has ${packageEntry.changedExecutableLines} changed executable lines (limit ${MAX_CHANGED_LINES}). ` + + "Split the PR or obtain a maintainer-reviewed narrow exclusion.", + ) + } + } + + return { packages: [...packages.values()] } +} + +function validateSha(value, name) { + if (!/^[0-9a-f]{40}$/i.test(value)) throw new Error(`${name} must be a full 40-character commit SHA`) +} + +function git(repoRoot, args) { + return execFileSync("git", args, { cwd: repoRoot, encoding: "utf8", maxBuffer: 20 * 1024 * 1024 }) +} + +export function selectFromGit(repoRoot, baseSha, headSha) { + validateSha(baseSha, "base SHA") + validateSha(headSha, "head SHA") + const mergeBase = git(repoRoot, ["merge-base", baseSha, headSha]).trim() + const nameStatus = git(repoRoot, ["diff", "--name-status", "-z", "--find-renames", `${mergeBase}...${headSha}`]) + const entries = parseNameStatus(nameStatus) + const manifest = buildManifest( + entries, + (filePath) => fs.readFileSync(path.join(repoRoot, filePath), "utf8"), + (filePath) => + git(repoRoot, [ + "diff", + "--unified=0", + "--no-color", + "--no-ext-diff", + `${mergeBase}...${headSha}`, + "--", + filePath, + ]), + ) + + return { baseSha, headSha, mergeBase, ...manifest } +} + +function stripAnsi(value) { + return value.replace(/\u001B\[[0-?]*[ -/]*[@-~]/g, "") +} + +function runStryker(repoRoot, packageEntry, reportRoot, dryRunOnly) { + const packageRoot = path.join(repoRoot, packageEntry.root) + const reportDirectory = path.join(reportRoot, packageEntry.id) + fs.mkdirSync(reportDirectory, { recursive: true }) + + const args = [ + "run", + path.join(repoRoot, "stryker.config.mjs"), + "--force", + "--mutate", + packageEntry.selectors.join(","), + ] + if (dryRunOnly) args.push("--dryRunOnly", "--reporters", "clear-text", "--logLevel", "info") + + const result = spawnSync(path.join(repoRoot, "node_modules/.bin/stryker"), args, { + cwd: packageRoot, + encoding: "utf8", + timeout: 12 * 60 * 1_000, + maxBuffer: 50 * 1024 * 1024, + env: { + ...process.env, + STRYKER_VITEST_CONFIG: path.join(packageRoot, packageEntry.vitestConfig), + STRYKER_REPORT_DIR: reportDirectory, + }, + }) + + const output = `${result.stdout ?? ""}${result.stderr ?? ""}` + if (result.error?.code === "ETIMEDOUT") { + throw new Error( + `${packageEntry.id} mutation run exceeded 12 minutes. Split the PR or obtain a maintainer-reviewed narrow exclusion.`, + ) + } + if (result.status !== 0) { + throw new Error( + `${packageEntry.id} Stryker ${dryRunOnly ? "preflight" : "run"} failed:\n${stripAnsi(output).trim()}`, + ) + } + + return output +} + +export function mutantCounts(report) { + const counts = { valid: 0, killed: 0, timeout: 0, survived: 0, noCoverage: 0, ignored: 0, blocking: [] } + + for (const [filePath, file] of Object.entries(report.files ?? {})) { + for (const mutant of file.mutants ?? []) { + if (VALID_MUTANT_STATUSES.has(mutant.status)) counts.valid++ + if (mutant.status === "Killed") counts.killed++ + if (mutant.status === "Timeout") counts.timeout++ + if (mutant.status === "Survived") counts.survived++ + if (mutant.status === "NoCoverage") counts.noCoverage++ + if (mutant.status === "Ignored") counts.ignored++ + if (BLOCKING_MUTANT_STATUSES.has(mutant.status)) counts.blocking.push({ filePath, ...mutant }) + } + } + + return counts +} + +function escapeWorkflowValue(value) { + return String(value) + .replaceAll("%", "%25") + .replaceAll("\r", "%0D") + .replaceAll("\n", "%0A") + .replaceAll(":", "%3A") + .replaceAll(",", "%2C") +} + +export function formatAnnotations(blockingMutants, packageRoot) { + const perFile = new Map() + const annotations = [] + + for (const mutant of blockingMutants.sort((left, right) => { + const pathOrder = left.filePath.localeCompare(right.filePath) + return pathOrder || left.location.start.line - right.location.start.line + })) { + const repositoryPath = path.posix.join(packageRoot, mutant.filePath.replaceAll("\\", "/")) + const key = `${repositoryPath}:${mutant.location.start.line}` + const fileCount = perFile.get(repositoryPath) ?? 0 + if (annotations.some((annotation) => annotation.key === key) || fileCount >= 7 || annotations.length >= 20) + continue + + const replacement = String(mutant.replacement ?? "") + .replace(/\s+/g, " ") + .trim() + .slice(0, 160) + annotations.push({ + key, + file: repositoryPath, + line: mutant.location.start.line, + message: + `${mutant.status} ${mutant.mutatorName} mutant${replacement ? ` (replacement: ${replacement})` : ""}. ` + + "Add or strengthen a focused test that fails under this mutation, or add a maintainer-approved targeted exclusion with a reason.", + }) + perFile.set(repositoryPath, fileCount + 1) + } + + return annotations +} + +function appendSummary(rows, failures) { + if (!process.env.GITHUB_STEP_SUMMARY) return + + const lines = [ + "## Changed-code mutation testing", + "", + "| Package | Changed executable lines | Valid | Killed | Timeout | Survived | No coverage | Result |", + "| --- | ---: | ---: | ---: | ---: | ---: | ---: | --- |", + ] + for (const row of rows) { + lines.push( + `| ${row.id} | ${row.changedLines} | ${row.valid} | ${row.killed} | ${row.timeout} | ${row.survived} | ${row.noCoverage} | ${row.result} |`, + ) + } + if (rows.length === 0) lines.push("| — | 0 | 0 | 0 | 0 | 0 | 0 | Not applicable |") + if (failures.length > 0) lines.push("", ...failures.map((failure) => `- ${failure}`)) + fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, `${lines.join("\n")}\n`) +} + +export function evaluateReport(report, packageEntry) { + const counts = mutantCounts(report) + if (counts.valid > MAX_MUTANTS) { + throw new Error( + `${packageEntry.id} generated ${counts.valid} valid mutants (limit ${MAX_MUTANTS}). ` + + "Split the PR or obtain a maintainer-reviewed narrow exclusion.", + ) + } + if (counts.timeout > 10 || (counts.valid > 0 && counts.timeout / counts.valid > 0.15)) { + throw new Error( + `${packageEntry.id} timed out ${counts.timeout} of ${counts.valid} valid mutants. ` + + "The result is inconclusive; fix flaky or slow tests, or reduce the changed scope before merge.", + ) + } + if (counts.blocking.length > 0) { + throw new Error( + `${packageEntry.id} has ${counts.survived} surviving and ${counts.noCoverage} uncovered changed-code mutants. ` + + "Add or strengthen focused tests before merge.", + ) + } + return counts +} + +export function runManifest(repoRoot, manifest, reportRoot) { + const rows = [] + const failures = [] + + for (const packageEntry of manifest.packages) { + let counts + try { + const preflightOutput = stripAnsi(runStryker(repoRoot, packageEntry, reportRoot, true)) + const mutantMatch = /Instrumented \d+ source file\(s\) with (\d+) mutant\(s\)/.exec(preflightOutput) + if (!mutantMatch) throw new Error(`${packageEntry.id} preflight did not report a mutant count`) + const generatedMutants = Number(mutantMatch[1]) + if (generatedMutants > MAX_MUTANTS) { + throw new Error( + `${packageEntry.id} generated ${generatedMutants} mutants in preflight (limit ${MAX_MUTANTS}). ` + + "Split the PR or obtain a maintainer-reviewed narrow exclusion.", + ) + } + + if (generatedMutants === 0) { + rows.push({ + id: packageEntry.id, + changedLines: packageEntry.changedExecutableLines, + valid: 0, + killed: 0, + timeout: 0, + survived: 0, + noCoverage: 0, + result: "No mutants generated", + }) + continue + } + + runStryker(repoRoot, packageEntry, reportRoot, false) + const reportPath = path.join(reportRoot, packageEntry.id, "mutation.json") + const report = JSON.parse(fs.readFileSync(reportPath, "utf8")) + counts = mutantCounts(report) + for (const annotation of formatAnnotations(counts.blocking, packageEntry.root)) { + console.log( + `::error file=${escapeWorkflowValue(annotation.file)},line=${annotation.line},title=Mutation test gap::${escapeWorkflowValue(annotation.message)}`, + ) + } + evaluateReport(report, packageEntry) + rows.push({ + id: packageEntry.id, + changedLines: packageEntry.changedExecutableLines, + ...counts, + result: "Passed", + }) + } catch (error) { + failures.push(error.message) + rows.push({ + id: packageEntry.id, + changedLines: packageEntry.changedExecutableLines, + valid: counts?.valid ?? 0, + killed: counts?.killed ?? 0, + timeout: counts?.timeout ?? 0, + survived: counts?.survived ?? 0, + noCoverage: counts?.noCoverage ?? 0, + result: "Failed", + }) + } + } + + appendSummary(rows, failures) + if (failures.length > 0) throw new Error(failures.join("\n")) + return rows +} + +function argument(name) { + const index = process.argv.indexOf(name) + return index === -1 ? undefined : process.argv[index + 1] +} + +function main() { + const command = process.argv[2] + if (command !== "ci") + throw new Error("Usage: node scripts/stryker-diff.mjs ci --base --head [--reports ]") + + const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..") + const baseSha = argument("--base") + const headSha = argument("--head") + if (!baseSha || !headSha) throw new Error("--base and --head are required") + + const reportRoot = path.resolve(repoRoot, argument("--reports") ?? "reports/mutation") + const manifest = selectFromGit(repoRoot, baseSha, headSha) + if (manifest.packages.length === 0) { + appendSummary([], []) + console.log("No changed executable lines in mutation-tested packages; mutation testing is not applicable.") + return + } + + console.log( + `Mutation-testing ${manifest.packages.length} package(s) from merge base ${manifest.mergeBase.slice(0, 12)}: ` + + manifest.packages.map((entry) => `${entry.id} (${entry.changedExecutableLines} lines)`).join(", "), + ) + runManifest(repoRoot, manifest, reportRoot) +} + +if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { + try { + main() + } catch (error) { + console.error(`Mutation gate failed: ${error.message}`) + process.exitCode = 1 + } +} diff --git a/scripts/stryker-diff.test.mjs b/scripts/stryker-diff.test.mjs new file mode 100644 index 0000000000..69f9f4ff89 --- /dev/null +++ b/scripts/stryker-diff.test.mjs @@ -0,0 +1,249 @@ +import assert from "node:assert/strict" +import { execFileSync } from "node:child_process" +import fs from "node:fs" +import os from "node:os" +import path from "node:path" +import { describe, it } from "node:test" + +import { + MAX_CHANGED_LINES, + MAX_MUTANTS, + buildManifest, + evaluateReport, + executableChangedLines, + formatAnnotations, + mutantCounts, + parseChangedLines, + parseNameStatus, + selectFromGit, + validateDisableDirectives, +} from "./stryker-diff.mjs" + +describe("parseNameStatus", () => { + it("parses added, modified, and renamed paths", () => { + assert.deepEqual( + parseNameStatus( + "A\0packages/core/src/new.ts\0M\0packages/cloud/src/a.ts\0R095\0old.ts\0packages/telemetry/src/new.ts\0", + ), + [ + { status: "A", path: "packages/core/src/new.ts" }, + { status: "M", path: "packages/cloud/src/a.ts" }, + { status: "R", oldPath: "old.ts", path: "packages/telemetry/src/new.ts" }, + ], + ) + }) +}) + +describe("parseChangedLines", () => { + it("uses destination-side hunk ranges and ignores deletion-only hunks", () => { + const diff = ["@@ -2,0 +3,2 @@", "@@ -10,2 +12 @@", "@@ -20,3 +21,0 @@"].join("\n") + assert.deepEqual([...parseChangedLines(diff)], [3, 4, 12]) + }) +}) + +describe("executableChangedLines", () => { + it("excludes imports, interfaces, types, and comments while retaining runtime statements", () => { + const source = [ + 'import type { User } from "./types"', + "interface State { value: string }", + "// behavior starts below", + "export function value(input: boolean) {", + ' return input ? "yes" : "no"', + "}", + ].join("\n") + const changed = new Set([1, 2, 3, 4, 5, 6]) + assert.deepEqual([...executableChangedLines(source, changed, "source.ts")], [4, 5]) + }) +}) + +describe("buildManifest", () => { + it("builds explicit executable ranges for modified and new files", () => { + const sources = { + "packages/core/src/changed.ts": "export function changed(value: boolean) {\n\treturn value ? 1 : 2\n}\n", + "packages/cloud/src/new.ts": "export const enabled = true\n", + } + const diffs = { + "packages/core/src/changed.ts": "@@ -1,2 +1,2 @@\n", + } + const manifest = buildManifest( + [ + { status: "M", path: "packages/core/src/changed.ts" }, + { status: "A", path: "packages/cloud/src/new.ts" }, + ], + (filePath) => sources[filePath], + (filePath) => diffs[filePath] ?? "", + ) + + assert.deepEqual( + manifest.packages.map(({ id, selectors }) => ({ id, selectors })), + [ + { id: "core", selectors: ["src/changed.ts:1-2"] }, + { id: "cloud", selectors: ["src/new.ts:1-1"] }, + ], + ) + }) + + it("returns no packages for tests, barrels, unsupported packages, and type-only changes", () => { + const manifest = buildManifest( + [ + { status: "M", path: "packages/core/src/index.ts" }, + { status: "M", path: "packages/core/src/value.spec.ts" }, + { status: "M", path: "webview-ui/src/value.ts" }, + { status: "M", path: "packages/cloud/src/types.ts" }, + ], + (filePath) => { + if (filePath.endsWith("index.ts")) return 'export * from "./value.js"\n' + if (filePath.endsWith("types.ts")) return "export interface Value { id: string }\n" + return "export const value = true\n" + }, + () => "@@ -1 +1 @@\n", + ) + + assert.deepEqual(manifest, { packages: [] }) + }) + + it("fails rather than skipping a package over the changed-line cap", () => { + const source = Array.from({ length: MAX_CHANGED_LINES + 1 }, (_, index) => `call(${index})`).join("\n") + assert.throws( + () => + buildManifest( + [{ status: "A", path: "packages/telemetry/src/large.ts" }], + () => source, + () => "", + ), + /split the PR or obtain a maintainer-reviewed narrow exclusion/i, + ) + }) +}) + +describe("selectFromGit", () => { + it("derives changed executable ranges from the base/head merge base", () => { + const repo = fs.mkdtempSync(path.join(os.tmpdir(), "stryker-diff-")) + const runGit = (...args) => execFileSync("git", args, { cwd: repo, encoding: "utf8" }).trim() + + try { + runGit("init", "--initial-branch=main") + runGit("config", "user.name", "Mutation Test") + runGit("config", "user.email", "mutation@example.com") + fs.mkdirSync(path.join(repo, "packages/core/src"), { recursive: true }) + fs.writeFileSync( + path.join(repo, "packages/core/src/value.ts"), + "export function value(input: boolean) {\n\treturn input ? 1 : 2\n}\n", + ) + runGit("add", ".") + runGit("commit", "-m", "base") + const baseSha = runGit("rev-parse", "HEAD") + runGit("checkout", "-b", "feature") + fs.writeFileSync( + path.join(repo, "packages/core/src/value.ts"), + "export function value(input: boolean) {\n\treturn input ? 1 : 3\n}\n", + ) + runGit("add", ".") + runGit("commit", "-m", "change behavior") + const headSha = runGit("rev-parse", "HEAD") + + const manifest = selectFromGit(repo, baseSha, headSha) + assert.equal(manifest.mergeBase, baseSha) + assert.deepEqual( + manifest.packages.map(({ id, selectors }) => ({ id, selectors })), + [{ id: "core", selectors: ["src/value.ts:2-2"] }], + ) + } finally { + fs.rmSync(repo, { recursive: true, force: true }) + } + }) +}) + +describe("mutation exclusions", () => { + it("allows a targeted mutator exclusion with a reason", () => { + assert.doesNotThrow(() => + validateDisableDirectives( + "// Stryker disable next-line EqualityOperator: equivalent for normalized input\nreturn value <= limit\n", + new Set([1]), + "source.ts", + ), + ) + }) + + it("rejects broad or unreasoned exclusions", () => { + assert.throws( + () => + validateDisableDirectives( + "// Stryker disable next-line all: noisy\nreturn value\n", + new Set([1]), + "source.ts", + ), + /broad or unreasoned exclusions are not allowed/, + ) + assert.throws( + () => + validateDisableDirectives( + "// Stryker disable next-line EqualityOperator\nreturn value\n", + new Set([1]), + "source.ts", + ), + /broad or unreasoned exclusions are not allowed/, + ) + }) +}) + +describe("report evaluation", () => { + const packageEntry = { id: "core", root: "packages/core" } + + it("fails on surviving and uncovered changed-code mutants", () => { + const report = { + files: { + "src/value.ts": { + mutants: [ + { + status: "Survived", + mutatorName: "EqualityOperator", + replacement: ">=", + location: { start: { line: 4 } }, + }, + { + status: "NoCoverage", + mutatorName: "BooleanLiteral", + replacement: "false", + location: { start: { line: 8 } }, + }, + ], + }, + }, + } + + assert.throws(() => evaluateReport(report, packageEntry), /1 surviving and 1 uncovered/) + assert.equal(formatAnnotations(mutantCounts(report).blocking, packageEntry.root).length, 2) + }) + + it("passes only killed or timed-out mutants within the cap", () => { + const mutants = Array.from({ length: MAX_MUTANTS }, (_, index) => ({ + status: index === 0 ? "Timeout" : "Killed", + location: { start: { line: index + 1 } }, + })) + const counts = evaluateReport({ files: { "src/value.ts": { mutants } } }, packageEntry) + assert.equal(counts.valid, MAX_MUTANTS) + }) + + it("fails when valid mutants exceed the cap", () => { + const mutants = Array.from({ length: MAX_MUTANTS + 1 }, (_, index) => ({ + status: "Killed", + location: { start: { line: index + 1 } }, + })) + assert.throws( + () => evaluateReport({ files: { "src/value.ts": { mutants } } }, packageEntry), + /split the PR or obtain a maintainer-reviewed narrow exclusion/i, + ) + }) + + it("fails when timeouts could create false confidence", () => { + const mutants = Array.from({ length: 10 }, (_, index) => ({ + status: index < 2 ? "Timeout" : "Killed", + location: { start: { line: index + 1 } }, + })) + assert.throws( + () => evaluateReport({ files: { "src/value.ts": { mutants } } }, packageEntry), + /result is inconclusive/, + ) + }) +}) diff --git a/stryker.config.mjs b/stryker.config.mjs new file mode 100644 index 0000000000..dcd26422af --- /dev/null +++ b/stryker.config.mjs @@ -0,0 +1,34 @@ +const vitestConfig = process.env.STRYKER_VITEST_CONFIG +const reportDirectory = process.env.STRYKER_REPORT_DIR + +if (!vitestConfig || !reportDirectory) { + throw new Error("STRYKER_VITEST_CONFIG and STRYKER_REPORT_DIR are required") +} + +/** @type {import("@stryker-mutator/api/core").PartialStrykerOptions} */ +export default { + plugins: ["@stryker-mutator/vitest-runner"], + testRunner: "vitest", + vitest: { + configFile: vitestConfig, + related: true, + }, + incremental: false, + concurrency: 2, + timeoutMS: 5_000, + timeoutFactor: 1.5, + cleanTempDir: "always", + logLevel: "warn", + reporters: ["json", "html"], + jsonReporter: { + fileName: `${reportDirectory}/mutation.json`, + }, + htmlReporter: { + fileName: `${reportDirectory}/mutation.html`, + }, + thresholds: { + high: 100, + low: 100, + break: null, + }, +} From 67245f8da87f63e868d54d751ed9154e18a298bb Mon Sep 17 00:00:00 2001 From: Roomote Date: Tue, 1 Sep 2026 12:52:55 +0000 Subject: [PATCH 2/4] feat(ci): expand mutation gate coverage --- package.json | 1 + pnpm-lock.yaml | 11 +-- scripts/stryker-diff.mjs | 108 +++++++++++++++++++++++++--- scripts/stryker-diff.test.mjs | 64 ++++++++++++++++- stryker.config.mjs | 14 ++-- webview-ui/vitest.stryker.config.ts | 14 ++++ 6 files changed, 186 insertions(+), 26 deletions(-) create mode 100644 webview-ui/vitest.stryker.config.ts diff --git a/package.json b/package.json index 7cdb2ec42c..ce2437164a 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ "form-data": ">=4.0.4", "bluebird": ">=3.7.2", "glob": "11.1.0", + "qs": "6.15.2", "@types/react": "18.3.31", "@types/react-dom": "18.3.7", "csstype": "3.2.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1bd786520f..183a5e02d1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,6 +13,7 @@ overrides: form-data: '>=4.0.4' bluebird: '>=3.7.2' glob: 11.1.0 + qs: 6.15.2 '@types/react': 18.3.31 '@types/react-dom': 18.3.7 csstype: 3.2.3 @@ -7324,10 +7325,6 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} - qs@6.15.1: - resolution: {integrity: sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==} - engines: {node: '>=0.6'} - qs@6.15.2: resolution: {integrity: sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==} engines: {node: '>=0.6'} @@ -16229,10 +16226,6 @@ snapshots: punycode@2.3.1: {} - qs@6.15.1: - dependencies: - side-channel: 1.1.0 - qs@6.15.2: dependencies: side-channel: 1.1.0 @@ -17485,7 +17478,7 @@ snapshots: dependencies: des.js: 1.1.0 js-md4: 0.3.2 - qs: 6.15.1 + qs: 6.15.2 tunnel: 0.0.6 underscore: 1.13.8 diff --git a/scripts/stryker-diff.mjs b/scripts/stryker-diff.mjs index aeef167db0..ede7d9defe 100644 --- a/scripts/stryker-diff.mjs +++ b/scripts/stryker-diff.mjs @@ -36,12 +36,31 @@ export const PACKAGE_CONFIGS = [ sourceRoot: "packages/vscode-shim/src/", vitestConfig: "vitest.config.ts", }, + { + id: "webview", + root: "webview-ui", + runRoot: ".", + sourceRoot: "webview-ui/src/", + vitestConfig: "vitest.stryker.config.ts", + vitestRelated: false, + discoverRelatedTests: true, + excludedPaths: ["webview-ui/src/main.tsx"], + }, + { + id: "extension", + root: "src", + sourceRoot: "src/", + vitestConfig: "vitest.config.ts", + vitestRelated: false, + discoverRelatedTests: true, + excludedPaths: ["src/esbuild.mjs", "src/eslint.config.mjs", "src/utils/vitest-verbosity.ts"], + }, ] const VALID_MUTANT_STATUSES = new Set(["Killed", "Timeout", "Survived", "NoCoverage"]) const BLOCKING_MUTANT_STATUSES = new Set(["Survived", "NoCoverage"]) const SOURCE_EXTENSIONS = new Set([".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx"]) -const EXCLUDED_PATH_SEGMENTS = ["/__mocks__/", "/__tests__/", "/fixtures/"] +const EXCLUDED_PATH_SEGMENTS = ["/__mocks__/", "/__tests__/", "/fixtures/", "/test-utils/"] const DISABLE_DIRECTIVE = /^\s*\/\/\s*Stryker disable next-line ([A-Za-z][A-Za-z0-9]*(?:,[A-Za-z][A-Za-z0-9]*)*)\s*:\s*(\S.*)$/ @@ -163,14 +182,18 @@ export function packageForPath(filePath) { if ( !SOURCE_EXTENSIONS.has(path.posix.extname(filePath)) || EXCLUDED_PATH_SEGMENTS.some((segment) => filePath.includes(segment)) || - /(?:^|\/)[^/]+\.(?:test|spec)(?:\.[^.]+)?\.[cm]?[jt]sx?$/.test(filePath) || + /(?:^|\/)[^/]+\.(?:test|spec|visual)(?:\.[^.]+)?\.[cm]?[jt]sx?$/.test(filePath) || filePath.endsWith(".d.ts") || - path.posix.basename(filePath).startsWith("vitest.config.") + path.posix.basename(filePath).startsWith("vitest.config.") || + path.posix.basename(filePath).startsWith("vite.config.") || + path.posix.basename(filePath).startsWith("vitest.setup.") || + path.posix.basename(filePath).startsWith("test-utils.") ) { return undefined } - return PACKAGE_CONFIGS.find((candidate) => filePath.startsWith(candidate.sourceRoot)) + const packageConfig = PACKAGE_CONFIGS.find((candidate) => filePath.startsWith(candidate.sourceRoot)) + return packageConfig?.excludedPaths?.includes(filePath) ? undefined : packageConfig } export function buildManifest(entries, readSource, diffForPath) { @@ -192,7 +215,7 @@ export function buildManifest(entries, readSource, diffForPath) { const executableLines = executableChangedLines(source, changedLines, entry.path) if (executableLines.size === 0) continue - const relativePath = path.posix.relative(packageConfig.root, entry.path) + const relativePath = path.posix.relative(packageConfig.runRoot ?? packageConfig.root, entry.path) const selectors = toRanges(executableLines).map(({ start, end }) => `${relativePath}:${start}-${end}`) const packageEntry = packages.get(packageConfig.id) ?? { @@ -259,8 +282,67 @@ function stripAnsi(value) { return value.replace(/\u001B\[[0-?]*[ -/]*[@-~]/g, "") } +function selectorFile(selector) { + return selector.replace(/:\d+(?::\d+)?-\d+(?::\d+)?$/, "") +} + +export function parseVitestTestFiles(report, runRoot) { + return [ + ...new Set((report.testResults ?? []).map(({ name }) => path.relative(runRoot, name).replaceAll("\\", "/"))), + ] +} + +export function preferDirectTestFiles(testFiles, sourceFiles) { + const sourceNames = sourceFiles.map((sourceFile) => path.posix.basename(sourceFile, path.posix.extname(sourceFile))) + const direct = testFiles.filter((testFile) => { + const testName = path.posix.basename(testFile) + return sourceNames.some( + (sourceName) => + testName.startsWith(`${sourceName}.`) && /\.(?:test|spec)(?:\.[^.]+)?\.[cm]?[jt]sx?$/.test(testName), + ) + }) + return direct.length > 0 ? direct : testFiles +} + +function discoverRelatedTestFiles(repoRoot, packageEntry, reportDirectory) { + const packageRoot = path.join(repoRoot, packageEntry.root) + const runRoot = path.join(repoRoot, packageEntry.runRoot ?? packageEntry.root) + const outputFile = path.join(reportDirectory, "vitest-related.json") + const configFile = path.relative(runRoot, path.join(packageRoot, packageEntry.vitestConfig)).replaceAll("\\", "/") + const sourceFiles = [...new Set(packageEntry.selectors.map(selectorFile))] + const result = spawnSync( + path.join(repoRoot, "node_modules/.bin/vitest"), + ["related", ...sourceFiles, "--run", "--config", configFile, "--reporter=json", `--outputFile=${outputFile}`], + { + cwd: runRoot, + encoding: "utf8", + timeout: 5 * 60 * 1_000, + maxBuffer: 50 * 1024 * 1024, + env: process.env, + }, + ) + + if (result.error?.code === "ETIMEDOUT") { + throw new Error(`${packageEntry.id} related-test discovery exceeded 5 minutes`) + } + if (result.status !== 0) { + throw new Error( + `${packageEntry.id} related-test discovery failed:\n${stripAnsi(`${result.stdout ?? ""}${result.stderr ?? ""}`).trim()}`, + ) + } + + const testFiles = preferDirectTestFiles( + parseVitestTestFiles(JSON.parse(fs.readFileSync(outputFile, "utf8")), runRoot), + sourceFiles, + ) + if (testFiles.length === 0) + throw new Error(`${packageEntry.id} has no tests related to the changed executable lines`) + return testFiles +} + function runStryker(repoRoot, packageEntry, reportRoot, dryRunOnly) { const packageRoot = path.join(repoRoot, packageEntry.root) + const runRoot = path.join(repoRoot, packageEntry.runRoot ?? packageEntry.root) const reportDirectory = path.join(reportRoot, packageEntry.id) fs.mkdirSync(reportDirectory, { recursive: true }) @@ -274,14 +356,19 @@ function runStryker(repoRoot, packageEntry, reportRoot, dryRunOnly) { if (dryRunOnly) args.push("--dryRunOnly", "--reporters", "clear-text", "--logLevel", "info") const result = spawnSync(path.join(repoRoot, "node_modules/.bin/stryker"), args, { - cwd: packageRoot, + cwd: runRoot, encoding: "utf8", timeout: 12 * 60 * 1_000, maxBuffer: 50 * 1024 * 1024, env: { ...process.env, - STRYKER_VITEST_CONFIG: path.join(packageRoot, packageEntry.vitestConfig), + STRYKER_VITEST_CONFIG: path + .relative(runRoot, path.join(packageRoot, packageEntry.vitestConfig)) + .replaceAll("\\", "/"), STRYKER_REPORT_DIR: reportDirectory, + STRYKER_IN_PLACE: "false", + STRYKER_VITEST_RELATED: packageEntry.vitestRelated === false ? "false" : "true", + STRYKER_TEST_FILES: JSON.stringify(packageEntry.testFiles ?? []), }, }) @@ -408,6 +495,11 @@ export function runManifest(repoRoot, manifest, reportRoot) { for (const packageEntry of manifest.packages) { let counts try { + const reportDirectory = path.join(reportRoot, packageEntry.id) + fs.mkdirSync(reportDirectory, { recursive: true }) + if (packageEntry.discoverRelatedTests) { + packageEntry.testFiles = discoverRelatedTestFiles(repoRoot, packageEntry, reportDirectory) + } const preflightOutput = stripAnsi(runStryker(repoRoot, packageEntry, reportRoot, true)) const mutantMatch = /Instrumented \d+ source file\(s\) with (\d+) mutant\(s\)/.exec(preflightOutput) if (!mutantMatch) throw new Error(`${packageEntry.id} preflight did not report a mutant count`) @@ -437,7 +529,7 @@ export function runManifest(repoRoot, manifest, reportRoot) { const reportPath = path.join(reportRoot, packageEntry.id, "mutation.json") const report = JSON.parse(fs.readFileSync(reportPath, "utf8")) counts = mutantCounts(report) - for (const annotation of formatAnnotations(counts.blocking, packageEntry.root)) { + for (const annotation of formatAnnotations(counts.blocking, packageEntry.runRoot ?? packageEntry.root)) { console.log( `::error file=${escapeWorkflowValue(annotation.file)},line=${annotation.line},title=Mutation test gap::${escapeWorkflowValue(annotation.message)}`, ) diff --git a/scripts/stryker-diff.test.mjs b/scripts/stryker-diff.test.mjs index 69f9f4ff89..af1687ec68 100644 --- a/scripts/stryker-diff.test.mjs +++ b/scripts/stryker-diff.test.mjs @@ -15,6 +15,9 @@ import { mutantCounts, parseChangedLines, parseNameStatus, + parseVitestTestFiles, + preferDirectTestFiles, + packageForPath, selectFromGit, validateDisableDirectives, } from "./stryker-diff.mjs" @@ -61,14 +64,20 @@ describe("buildManifest", () => { const sources = { "packages/core/src/changed.ts": "export function changed(value: boolean) {\n\treturn value ? 1 : 2\n}\n", "packages/cloud/src/new.ts": "export const enabled = true\n", + "webview-ui/src/utils/changed.ts": "export const changed = (value: boolean) => (value ? 1 : 2)\n", + "src/utils/changed.ts": "export const changed = (value: boolean) => (value ? 1 : 2)\n", } const diffs = { "packages/core/src/changed.ts": "@@ -1,2 +1,2 @@\n", + "webview-ui/src/utils/changed.ts": "@@ -1 +1 @@\n", + "src/utils/changed.ts": "@@ -1 +1 @@\n", } const manifest = buildManifest( [ { status: "M", path: "packages/core/src/changed.ts" }, { status: "A", path: "packages/cloud/src/new.ts" }, + { status: "M", path: "webview-ui/src/utils/changed.ts" }, + { status: "M", path: "src/utils/changed.ts" }, ], (filePath) => sources[filePath], (filePath) => diffs[filePath] ?? "", @@ -79,8 +88,17 @@ describe("buildManifest", () => { [ { id: "core", selectors: ["src/changed.ts:1-2"] }, { id: "cloud", selectors: ["src/new.ts:1-1"] }, + { id: "webview", selectors: ["webview-ui/src/utils/changed.ts:1-1"] }, + { id: "extension", selectors: ["utils/changed.ts:1-1"] }, ], ) + const webview = manifest.packages.find(({ id }) => id === "webview") + const extension = manifest.packages.find(({ id }) => id === "extension") + assert.equal(webview.runRoot, ".") + assert.equal(webview.discoverRelatedTests, true) + assert.equal(webview.vitestRelated, false) + assert.equal(extension.discoverRelatedTests, true) + assert.equal(extension.vitestRelated, false) }) it("returns no packages for tests, barrels, unsupported packages, and type-only changes", () => { @@ -88,7 +106,10 @@ describe("buildManifest", () => { [ { status: "M", path: "packages/core/src/index.ts" }, { status: "M", path: "packages/core/src/value.spec.ts" }, - { status: "M", path: "webview-ui/src/value.ts" }, + { status: "M", path: "webview-ui/src/value.visual.tsx" }, + { status: "M", path: "webview-ui/src/main.tsx" }, + { status: "M", path: "src/utils/vitest-verbosity.ts" }, + { status: "M", path: "apps/cli/src/value.ts" }, { status: "M", path: "packages/cloud/src/types.ts" }, ], (filePath) => { @@ -116,6 +137,47 @@ describe("buildManifest", () => { }) }) +describe("packageForPath", () => { + it("routes webview and extension production code while excluding test infrastructure", () => { + assert.equal(packageForPath("webview-ui/src/utils/path-mentions.ts").id, "webview") + assert.equal(packageForPath("src/utils/tool-id.ts").id, "extension") + assert.equal(packageForPath("webview-ui/src/utils/test-utils.ts"), undefined) + assert.equal(packageForPath("src/__mocks__/vscode.js"), undefined) + assert.equal(packageForPath("apps/vscode-e2e/src/example.ts"), undefined) + }) +}) + +describe("parseVitestTestFiles", () => { + it("normalizes and deduplicates Vitest related-test results", () => { + assert.deepEqual( + parseVitestTestFiles( + { + testResults: [ + { name: "/repo/webview-ui/src/utils/__tests__/value.test.ts" }, + { name: "/repo/webview-ui/src/utils/__tests__/value.test.ts" }, + ], + }, + "/repo", + ), + ["webview-ui/src/utils/__tests__/value.test.ts"], + ) + }) +}) + +describe("preferDirectTestFiles", () => { + it("uses matching focused specs and falls back to all related tests", () => { + const related = [ + "webview-ui/src/__tests__/App.spec.tsx", + "webview-ui/src/utils/__tests__/path-mentions.test.ts", + "webview-ui/src/components/chat/__tests__/ChatView.spec.tsx", + ] + assert.deepEqual(preferDirectTestFiles(related, ["webview-ui/src/utils/path-mentions.ts"]), [ + "webview-ui/src/utils/__tests__/path-mentions.test.ts", + ]) + assert.deepEqual(preferDirectTestFiles(related, ["webview-ui/src/utils/unmatched.ts"]), related) + }) +}) + describe("selectFromGit", () => { it("derives changed executable ranges from the base/head merge base", () => { const repo = fs.mkdtempSync(path.join(os.tmpdir(), "stryker-diff-")) diff --git a/stryker.config.mjs b/stryker.config.mjs index dcd26422af..c1a652e7c1 100644 --- a/stryker.config.mjs +++ b/stryker.config.mjs @@ -1,19 +1,17 @@ -const vitestConfig = process.env.STRYKER_VITEST_CONFIG -const reportDirectory = process.env.STRYKER_REPORT_DIR +const vitestConfig = process.env.STRYKER_VITEST_CONFIG ?? "vitest.config.ts" +const reportDirectory = process.env.STRYKER_REPORT_DIR ?? "reports/mutation" +const testFiles = JSON.parse(process.env.STRYKER_TEST_FILES ?? "[]") -if (!vitestConfig || !reportDirectory) { - throw new Error("STRYKER_VITEST_CONFIG and STRYKER_REPORT_DIR are required") -} - -/** @type {import("@stryker-mutator/api/core").PartialStrykerOptions} */ export default { plugins: ["@stryker-mutator/vitest-runner"], testRunner: "vitest", vitest: { configFile: vitestConfig, - related: true, + related: process.env.STRYKER_VITEST_RELATED !== "false", }, + testFiles, incremental: false, + inPlace: process.env.STRYKER_IN_PLACE === "true", concurrency: 2, timeoutMS: 5_000, timeoutFactor: 1.5, diff --git a/webview-ui/vitest.stryker.config.ts b/webview-ui/vitest.stryker.config.ts new file mode 100644 index 0000000000..2e57465b6a --- /dev/null +++ b/webview-ui/vitest.stryker.config.ts @@ -0,0 +1,14 @@ +import path from "path" +import { defineConfig } from "vitest/config" + +import config from "./vitest.config" + +export default defineConfig({ + ...config, + root: path.resolve(__dirname, ".."), + test: { + ...config.test, + setupFiles: ["webview-ui/vitest.setup.ts"], + include: ["webview-ui/src/**/*.spec.{ts,tsx}", "webview-ui/src/**/*.test.{ts,tsx}"], + }, +}) From 07819aa04fde60a1e4586d95ba4066467a07ca2e Mon Sep 17 00:00:00 2001 From: Roomote Date: Tue, 1 Sep 2026 13:39:47 +0000 Subject: [PATCH 3/4] test(ci): run Stryker integration smoke --- .github/workflows/mutation-testing.yml | 4 ++ package.json | 1 + scripts/fixtures/stryker-smoke/smoke.test.ts | 9 +++ scripts/fixtures/stryker-smoke/smoke.ts | 7 ++ .../fixtures/stryker-smoke/vitest.config.ts | 9 +++ scripts/stryker-diff.test.mjs | 1 + scripts/stryker-smoke.mjs | 67 +++++++++++++++++++ 7 files changed, 98 insertions(+) create mode 100644 scripts/fixtures/stryker-smoke/smoke.test.ts create mode 100644 scripts/fixtures/stryker-smoke/smoke.ts create mode 100644 scripts/fixtures/stryker-smoke/vitest.config.ts create mode 100644 scripts/stryker-smoke.mjs diff --git a/.github/workflows/mutation-testing.yml b/.github/workflows/mutation-testing.yml index b4f9f47b55..ebd65abefe 100644 --- a/.github/workflows/mutation-testing.yml +++ b/.github/workflows/mutation-testing.yml @@ -51,6 +51,10 @@ jobs: if: github.event_name == 'pull_request' run: pnpm test:mutation-ci + - name: Run Stryker integration smoke + if: github.event_name == 'pull_request' + run: pnpm test:mutation-smoke + - name: Mutate changed executable lines if: github.event_name == 'pull_request' env: diff --git a/package.json b/package.json index ce2437164a..0f39203f37 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "check-types": "turbo check-types --log-order grouped --output-logs new-only", "test": "turbo test --log-order grouped --output-logs new-only", "test:mutation-ci": "node --test scripts/stryker-diff.test.mjs", + "test:mutation-smoke": "node scripts/stryker-smoke.mjs", "test:coverage": "turbo test:coverage --log-order grouped --output-logs new-only", "format": "turbo format --log-order grouped --output-logs new-only", "build": "turbo build --log-order grouped --output-logs new-only", diff --git a/scripts/fixtures/stryker-smoke/smoke.test.ts b/scripts/fixtures/stryker-smoke/smoke.test.ts new file mode 100644 index 0000000000..5cc4666ed9 --- /dev/null +++ b/scripts/fixtures/stryker-smoke/smoke.test.ts @@ -0,0 +1,9 @@ +import { isPositive, smokeLabel } from "./smoke" + +describe("Stryker integration smoke", () => { + it("kills a changed comparison while leaving the intentional label mutant alive", () => { + expect(isPositive(1)).toBe(true) + expect(isPositive(0)).toBe(false) + expect(typeof smokeLabel()).toBe("string") + }) +}) diff --git a/scripts/fixtures/stryker-smoke/smoke.ts b/scripts/fixtures/stryker-smoke/smoke.ts new file mode 100644 index 0000000000..a9e1941fab --- /dev/null +++ b/scripts/fixtures/stryker-smoke/smoke.ts @@ -0,0 +1,7 @@ +export function isPositive(value: number): boolean { + return value > 0 +} + +export function smokeLabel(): string { + return "smoke" +} diff --git a/scripts/fixtures/stryker-smoke/vitest.config.ts b/scripts/fixtures/stryker-smoke/vitest.config.ts new file mode 100644 index 0000000000..82845525af --- /dev/null +++ b/scripts/fixtures/stryker-smoke/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from "vitest/config" + +export default defineConfig({ + test: { + globals: true, + watch: false, + include: ["smoke.test.ts"], + }, +}) diff --git a/scripts/stryker-diff.test.mjs b/scripts/stryker-diff.test.mjs index af1687ec68..0d551676d0 100644 --- a/scripts/stryker-diff.test.mjs +++ b/scripts/stryker-diff.test.mjs @@ -144,6 +144,7 @@ describe("packageForPath", () => { assert.equal(packageForPath("webview-ui/src/utils/test-utils.ts"), undefined) assert.equal(packageForPath("src/__mocks__/vscode.js"), undefined) assert.equal(packageForPath("apps/vscode-e2e/src/example.ts"), undefined) + assert.equal(packageForPath("scripts/fixtures/stryker-smoke/smoke.ts"), undefined) }) }) diff --git a/scripts/stryker-smoke.mjs b/scripts/stryker-smoke.mjs new file mode 100644 index 0000000000..0bf782cd0c --- /dev/null +++ b/scripts/stryker-smoke.mjs @@ -0,0 +1,67 @@ +#!/usr/bin/env node + +import { spawnSync } from "node:child_process" +import fs from "node:fs" +import path from "node:path" +import process from "node:process" +import { fileURLToPath } from "node:url" + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..") +const fixtureRoot = path.join(repoRoot, "scripts/fixtures/stryker-smoke") +const reportDirectory = path.join(repoRoot, "reports/mutation/smoke") +const reportPath = path.join(reportDirectory, "mutation.json") + +fs.rmSync(reportDirectory, { recursive: true, force: true }) + +const result = spawnSync( + path.join(repoRoot, "node_modules/.bin/stryker"), + ["run", path.join(repoRoot, "stryker.config.mjs"), "--force", "--mutate", "smoke.ts"], + { + cwd: fixtureRoot, + encoding: "utf8", + timeout: 2 * 60 * 1_000, + maxBuffer: 10 * 1024 * 1024, + env: { + ...process.env, + STRYKER_VITEST_CONFIG: "vitest.config.ts", + STRYKER_VITEST_RELATED: "true", + STRYKER_TEST_FILES: "[]", + STRYKER_REPORT_DIR: reportDirectory, + }, + }, +) + +if (result.error?.code === "ETIMEDOUT") { + throw new Error("Stryker integration smoke exceeded two minutes") +} +if (result.status !== 0) { + throw new Error(`Stryker integration smoke failed:\n${`${result.stdout ?? ""}${result.stderr ?? ""}`.trim()}`) +} +if (!fs.existsSync(reportPath)) { + throw new Error("Stryker integration smoke did not write mutation.json") +} + +const report = JSON.parse(fs.readFileSync(reportPath, "utf8")) +const mutants = Object.values(report.files ?? {}).flatMap((file) => file.mutants ?? []) +const killedComparison = mutants.find( + (mutant) => + mutant.mutatorName === "EqualityOperator" && mutant.replacement.includes(">=") && mutant.status === "Killed", +) +const survivingLabel = mutants.find( + (mutant) => mutant.mutatorName === "StringLiteral" && mutant.replacement === '""' && mutant.status === "Survived", +) +const uncovered = mutants.filter((mutant) => mutant.status === "NoCoverage") + +if (!killedComparison) { + throw new Error("Stryker smoke report did not contain the expected killed EqualityOperator mutant") +} +if (!survivingLabel) { + throw new Error("Stryker smoke report did not contain the expected surviving StringLiteral mutant") +} +if (uncovered.length > 0) { + throw new Error(`Stryker smoke report unexpectedly contained ${uncovered.length} uncovered mutant(s)`) +} + +console.log( + `Stryker smoke passed: ${mutants.length} mutants; expected EqualityOperator killed, StringLiteral survived, 0 uncovered.`, +) From dcc19bb9f2abbc36d418e1be2cfe4b237451e256 Mon Sep 17 00:00:00 2001 From: Roomote Date: Tue, 1 Sep 2026 14:54:48 +0000 Subject: [PATCH 4/4] test(ci): remove Stryker smoke fixture --- .github/workflows/mutation-testing.yml | 4 -- package.json | 1 - scripts/fixtures/stryker-smoke/smoke.test.ts | 9 --- scripts/fixtures/stryker-smoke/smoke.ts | 7 -- .../fixtures/stryker-smoke/vitest.config.ts | 9 --- scripts/stryker-diff.test.mjs | 1 - scripts/stryker-smoke.mjs | 67 ------------------- 7 files changed, 98 deletions(-) delete mode 100644 scripts/fixtures/stryker-smoke/smoke.test.ts delete mode 100644 scripts/fixtures/stryker-smoke/smoke.ts delete mode 100644 scripts/fixtures/stryker-smoke/vitest.config.ts delete mode 100644 scripts/stryker-smoke.mjs diff --git a/.github/workflows/mutation-testing.yml b/.github/workflows/mutation-testing.yml index ebd65abefe..b4f9f47b55 100644 --- a/.github/workflows/mutation-testing.yml +++ b/.github/workflows/mutation-testing.yml @@ -51,10 +51,6 @@ jobs: if: github.event_name == 'pull_request' run: pnpm test:mutation-ci - - name: Run Stryker integration smoke - if: github.event_name == 'pull_request' - run: pnpm test:mutation-smoke - - name: Mutate changed executable lines if: github.event_name == 'pull_request' env: diff --git a/package.json b/package.json index 0f39203f37..ce2437164a 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,6 @@ "check-types": "turbo check-types --log-order grouped --output-logs new-only", "test": "turbo test --log-order grouped --output-logs new-only", "test:mutation-ci": "node --test scripts/stryker-diff.test.mjs", - "test:mutation-smoke": "node scripts/stryker-smoke.mjs", "test:coverage": "turbo test:coverage --log-order grouped --output-logs new-only", "format": "turbo format --log-order grouped --output-logs new-only", "build": "turbo build --log-order grouped --output-logs new-only", diff --git a/scripts/fixtures/stryker-smoke/smoke.test.ts b/scripts/fixtures/stryker-smoke/smoke.test.ts deleted file mode 100644 index 5cc4666ed9..0000000000 --- a/scripts/fixtures/stryker-smoke/smoke.test.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { isPositive, smokeLabel } from "./smoke" - -describe("Stryker integration smoke", () => { - it("kills a changed comparison while leaving the intentional label mutant alive", () => { - expect(isPositive(1)).toBe(true) - expect(isPositive(0)).toBe(false) - expect(typeof smokeLabel()).toBe("string") - }) -}) diff --git a/scripts/fixtures/stryker-smoke/smoke.ts b/scripts/fixtures/stryker-smoke/smoke.ts deleted file mode 100644 index a9e1941fab..0000000000 --- a/scripts/fixtures/stryker-smoke/smoke.ts +++ /dev/null @@ -1,7 +0,0 @@ -export function isPositive(value: number): boolean { - return value > 0 -} - -export function smokeLabel(): string { - return "smoke" -} diff --git a/scripts/fixtures/stryker-smoke/vitest.config.ts b/scripts/fixtures/stryker-smoke/vitest.config.ts deleted file mode 100644 index 82845525af..0000000000 --- a/scripts/fixtures/stryker-smoke/vitest.config.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { defineConfig } from "vitest/config" - -export default defineConfig({ - test: { - globals: true, - watch: false, - include: ["smoke.test.ts"], - }, -}) diff --git a/scripts/stryker-diff.test.mjs b/scripts/stryker-diff.test.mjs index 0d551676d0..af1687ec68 100644 --- a/scripts/stryker-diff.test.mjs +++ b/scripts/stryker-diff.test.mjs @@ -144,7 +144,6 @@ describe("packageForPath", () => { assert.equal(packageForPath("webview-ui/src/utils/test-utils.ts"), undefined) assert.equal(packageForPath("src/__mocks__/vscode.js"), undefined) assert.equal(packageForPath("apps/vscode-e2e/src/example.ts"), undefined) - assert.equal(packageForPath("scripts/fixtures/stryker-smoke/smoke.ts"), undefined) }) }) diff --git a/scripts/stryker-smoke.mjs b/scripts/stryker-smoke.mjs deleted file mode 100644 index 0bf782cd0c..0000000000 --- a/scripts/stryker-smoke.mjs +++ /dev/null @@ -1,67 +0,0 @@ -#!/usr/bin/env node - -import { spawnSync } from "node:child_process" -import fs from "node:fs" -import path from "node:path" -import process from "node:process" -import { fileURLToPath } from "node:url" - -const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..") -const fixtureRoot = path.join(repoRoot, "scripts/fixtures/stryker-smoke") -const reportDirectory = path.join(repoRoot, "reports/mutation/smoke") -const reportPath = path.join(reportDirectory, "mutation.json") - -fs.rmSync(reportDirectory, { recursive: true, force: true }) - -const result = spawnSync( - path.join(repoRoot, "node_modules/.bin/stryker"), - ["run", path.join(repoRoot, "stryker.config.mjs"), "--force", "--mutate", "smoke.ts"], - { - cwd: fixtureRoot, - encoding: "utf8", - timeout: 2 * 60 * 1_000, - maxBuffer: 10 * 1024 * 1024, - env: { - ...process.env, - STRYKER_VITEST_CONFIG: "vitest.config.ts", - STRYKER_VITEST_RELATED: "true", - STRYKER_TEST_FILES: "[]", - STRYKER_REPORT_DIR: reportDirectory, - }, - }, -) - -if (result.error?.code === "ETIMEDOUT") { - throw new Error("Stryker integration smoke exceeded two minutes") -} -if (result.status !== 0) { - throw new Error(`Stryker integration smoke failed:\n${`${result.stdout ?? ""}${result.stderr ?? ""}`.trim()}`) -} -if (!fs.existsSync(reportPath)) { - throw new Error("Stryker integration smoke did not write mutation.json") -} - -const report = JSON.parse(fs.readFileSync(reportPath, "utf8")) -const mutants = Object.values(report.files ?? {}).flatMap((file) => file.mutants ?? []) -const killedComparison = mutants.find( - (mutant) => - mutant.mutatorName === "EqualityOperator" && mutant.replacement.includes(">=") && mutant.status === "Killed", -) -const survivingLabel = mutants.find( - (mutant) => mutant.mutatorName === "StringLiteral" && mutant.replacement === '""' && mutant.status === "Survived", -) -const uncovered = mutants.filter((mutant) => mutant.status === "NoCoverage") - -if (!killedComparison) { - throw new Error("Stryker smoke report did not contain the expected killed EqualityOperator mutant") -} -if (!survivingLabel) { - throw new Error("Stryker smoke report did not contain the expected surviving StringLiteral mutant") -} -if (uncovered.length > 0) { - throw new Error(`Stryker smoke report unexpectedly contained ${uncovered.length} uncovered mutant(s)`) -} - -console.log( - `Stryker smoke passed: ${mutants.length} mutants; expected EqualityOperator killed, StringLiteral survived, 0 uncovered.`, -)