From 7fbc01824a48105ae4b7b17605f00ef3b8e73043 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=90=BE=20smolpaws?= Date: Thu, 30 Jul 2026 08:39:49 +0200 Subject: [PATCH] test: add Stryker mutation testing (#16184) Co-authored-by: Ai Vong Co-authored-by: Engel Nyst --- .gitignore | 2 + __tests__/scripts/stryker-diff.test.ts | 318 ++++++++ __tests__/scripts/stryker-setup.test.ts | 64 ++ docs/DEVELOPMENT.md | 27 + package-lock.json | 979 ++++++++++++++++++++++++ package.json | 5 + scripts/stryker-diff.mjs | 137 ++++ stryker.config.mjs | 22 + 8 files changed, 1554 insertions(+) create mode 100644 __tests__/scripts/stryker-diff.test.ts create mode 100644 __tests__/scripts/stryker-setup.test.ts create mode 100644 scripts/stryker-diff.mjs create mode 100644 stryker.config.mjs diff --git a/.gitignore b/.gitignore index a66c0a039b..caffed67f2 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,8 @@ __pycache__ /playwright-report-mock-llm-docker/ /blob-report/ /playwright/.cache/ +/.stryker-tmp/ +/reports/ .react-router/ ralph/ .vercel diff --git a/__tests__/scripts/stryker-diff.test.ts b/__tests__/scripts/stryker-diff.test.ts new file mode 100644 index 0000000000..4385c3503f --- /dev/null +++ b/__tests__/scripts/stryker-diff.test.ts @@ -0,0 +1,318 @@ +// @vitest-environment node +import { spawnSync } from "node:child_process"; +import type { SpawnSyncOptions } from "node:child_process"; +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { describe, expect, it } from "vitest"; + +import { main } from "../../scripts/stryker-diff.mjs"; + +const scriptPath = fileURLToPath( + new URL("../../scripts/stryker-diff.mjs", import.meta.url), +); + +interface SpawnResult { + error?: Error; + status: number | null; + stderr: string; + stdout: string | null; +} + +interface ProcessCall { + args: readonly string[]; + command: string; + options?: SpawnSyncOptions; +} + +function executeWithResults( + argv: string[], + results: SpawnResult[], +): { calls: ProcessCall[]; errors: string; output: string; status: number } { + const calls: ProcessCall[] = []; + const output: string[] = []; + const errors: string[] = []; + + const status = main(argv, { + spawn( + command: string, + args: readonly string[], + options?: SpawnSyncOptions, + ) { + calls.push({ command, args, options }); + const result = results[calls.length - 1]; + if (!result) { + throw new Error( + `Unexpected process call: ${command} ${args.join(" ")}`, + ); + } + return result; + }, + writeError(message: string) { + errors.push(message); + }, + writeOutput(message: string) { + output.push(message); + }, + }); + + return { + calls, + errors: errors.join(""), + output: output.join(""), + status, + }; +} + +function successfulProcess(stdout = ""): SpawnResult { + return { status: 0, stderr: "", stdout }; +} + +describe("Stryker diff runner", () => { + it("defaults to main and exits successfully when nothing changed", () => { + const result = executeWithResults([], [successfulProcess()]); + + expect(result.calls).toHaveLength(1); + expect(result.calls[0]).toMatchObject({ + command: "git", + args: ["diff", "--name-only", "--diff-filter=ACMRTUXB", "main...HEAD"], + options: { encoding: "utf8", windowsHide: true }, + }); + expect(result.output).toBe( + "No changed production files to mutate against main.\n", + ); + expect(result.status).toBe(0); + }); + + it("uses the requested base ref and mutates changed TypeScript production files", () => { + const result = executeWithResults( + ["origin/main"], + [successfulProcess("src/a.ts\nsrc/ui/card.tsx\n"), successfulProcess()], + ); + + expect(result.calls[0]?.args).toContain("origin/main...HEAD"); + expect(result.calls[1]?.args.slice(1)).toEqual([ + "run", + "--incremental", + "--force", + "--mutate", + "src/a.ts,src/ui/card.tsx", + ]); + expect(result.calls[1]?.args[0]).toMatch( + /node_modules\/@stryker-mutator\/core\/bin\/stryker\.js$/, + ); + expect(result.calls[1]?.options).toEqual({ + stdio: "inherit", + windowsHide: true, + }); + expect(result.status).toBe(0); + }); + + it("filters non-production and non-TypeScript paths before mutation", () => { + const changedFiles = [ + "src/keep.ts", + "src/keep.tsx", + "src/a.test.ts.helper.ts", + "src\\windows\\keep.ts", + "src/keep.js", + "src/looks-like.ts.backup", + "nested/src/not-rooted.ts", + "scripts/tool.mjs", + "tools/canvas_ui_tool.py", + "src/a.test.ts", + "src/a.spec.tsx", + "src/types/a.d.ts", + "src/fixtures/a.ts", + "src/mocks/a.ts", + "src/__tests__/a.ts", + "src/test/a.ts", + "src/x/__snapshots__/a.ts", + "src/generated/a.ts", + "src/a.gen.ts", + "src/a.generated.tsx", + "build/a.ts", + ].join("\n"); + + const result = executeWithResults( + [], + [successfulProcess(`${changedFiles}\n`), successfulProcess()], + ); + + expect(result.calls[1]?.args.at(-1)).toBe( + "src/keep.ts,src/keep.tsx,src/a.test.ts.helper.ts,src\\windows\\keep.ts", + ); + }); + + it("passes paths with spaces as one mutation argument", () => { + const result = executeWithResults( + [], + [ + successfulProcess("src/path with spaces/a.ts\nsrc/b.tsx\n"), + successfulProcess(), + ], + ); + + const mutationArgs = result.calls[1]?.args ?? []; + expect(mutationArgs.at(-2)).toBe("--mutate"); + expect(mutationArgs.at(-1)).toBe("src/path with spaces/a.ts,src/b.tsx"); + }); + + it("does not run Stryker when every changed file is filtered out", () => { + const result = executeWithResults( + [], + [ + successfulProcess( + "README.md\nsrc/a.test.ts\ntools/canvas_ui_tool.py\nscripts/a.mjs\n", + ), + ], + ); + + expect(result.calls).toHaveLength(1); + expect(result.output).toContain("No changed production files"); + expect(result.status).toBe(0); + }); + + it("propagates Stryker's failing exit status", () => { + const result = executeWithResults( + [], + [ + successfulProcess("src/a.ts\n"), + { status: 23, stdout: "", stderr: "mutation failed" }, + ], + ); + + expect(result.status).toBe(23); + }); + + it("reports git diff failures without starting Stryker", () => { + const result = executeWithResults( + ["missing-ref"], + [ + { + status: 128, + stdout: "", + stderr: " fatal: ambiguous argument ", + }, + ], + ); + + expect(result.calls).toHaveLength(1); + expect(result.errors).toBe( + "Unable to determine changed files against missing-ref: fatal: ambiguous argument\n", + ); + expect(result.status).toBe(128); + }); + + it("reports an unknown git error when no diagnostic is available", () => { + const result = executeWithResults( + [], + [{ status: 128, stdout: "", stderr: "" }], + ); + + expect(result.errors).toBe( + "Unable to determine changed files against main: unknown error\n", + ); + expect(result.status).toBe(128); + }); + + it("reports git process errors when no exit status is available", () => { + const result = executeWithResults( + [], + [ + { + error: new Error("git executable not found"), + status: null, + stdout: "", + stderr: "", + }, + ], + ); + + expect(result.calls).toHaveLength(1); + expect(result.errors).toBe( + "Unable to determine changed files against main: git executable not found\n", + ); + expect(result.status).toBe(1); + }); + + it("reports Stryker process errors", () => { + const result = executeWithResults( + [], + [ + successfulProcess("src/a.ts\n"), + { + error: new Error("Stryker executable failed to start"), + status: null, + stdout: "", + stderr: "", + }, + ], + ); + + expect(result.errors).toBe( + "Unable to start Stryker: Stryker executable failed to start\n", + ); + expect(result.status).toBe(1); + }); + + it("fails when Stryker exits without a status", () => { + const result = executeWithResults( + [], + [ + successfulProcess("src/a.ts\n"), + { status: null, stdout: "", stderr: "" }, + ], + ); + + expect(result.status).toBe(1); + }); + + it("runs as a CLI in a clean git repository without invoking Stryker", () => { + const repo = mkdtempSync(path.join(tmpdir(), "stryker-diff-")); + + try { + expect( + spawnSync("git", ["init", "-b", "main"], { + cwd: repo, + encoding: "utf8", + }).status, + ).toBe(0); + writeFileSync(path.join(repo, "README.md"), "# fixture\n"); + expect( + spawnSync("git", ["add", "README.md"], { + cwd: repo, + encoding: "utf8", + }).status, + ).toBe(0); + expect( + spawnSync( + "git", + [ + "-c", + "user.name=Stryker Test", + "-c", + "user.email=stryker@example.test", + "commit", + "-m", + "fixture", + ], + { cwd: repo, encoding: "utf8" }, + ).status, + ).toBe(0); + + const result = spawnSync(process.execPath, [scriptPath], { + cwd: repo, + encoding: "utf8", + }); + + expect(result.stderr).toBe(""); + expect(result.stdout).toBe( + "No changed production files to mutate against main.\n", + ); + expect(result.status).toBe(0); + } finally { + rmSync(repo, { force: true, recursive: true }); + } + }); +}); diff --git a/__tests__/scripts/stryker-setup.test.ts b/__tests__/scripts/stryker-setup.test.ts new file mode 100644 index 0000000000..28ab5428a8 --- /dev/null +++ b/__tests__/scripts/stryker-setup.test.ts @@ -0,0 +1,64 @@ +// @vitest-environment node +import { readFileSync } from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { describe, expect, it } from "vitest"; + +const repoRoot = path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + "../..", +); + +describe("Stryker mutation testing setup", () => { + it("uses the Vitest runner for first-party TypeScript source", async () => { + const config = ( + (await import("../../stryker.config.mjs")) as { + default: { + mutate: string[]; + testRunner: string; + vitest: { configFile: string; related: boolean }; + }; + } + ).default; + + expect(config).toMatchObject({ + testRunner: "vitest", + vitest: { + configFile: "vite.config.ts", + related: true, + }, + }); + expect(config.mutate).toContain("src/**/*.{ts,tsx}"); + expect(config.mutate).toContain("!src/**/*.{test,spec}.{ts,tsx}"); + expect(config.mutate).toContain("!src/**/*.d.ts"); + expect(config.mutate).toContain("!src/{fixtures,mocks,dev}/**"); + }); + + it("provides full, incremental, and branch-diff commands", () => { + const packageJson = JSON.parse( + readFileSync(path.join(repoRoot, "package.json"), "utf8"), + ) as { + devDependencies: Record; + scripts: Record; + }; + + expect(packageJson.scripts).toMatchObject({ + "test:mutation": "npm run make-i18n && stryker run", + "test:mutation:diff": + "npm run make-i18n && node scripts/stryker-diff.mjs", + "test:mutation:incremental": + "npm run make-i18n && stryker run --incremental", + }); + expect(packageJson.devDependencies).toMatchObject({ + "@stryker-mutator/core": "9.6.1", + "@stryker-mutator/vitest-runner": "9.6.1", + }); + }); + + it("ignores local mutation sandboxes and reports", () => { + const gitignore = readFileSync(path.join(repoRoot, ".gitignore"), "utf8"); + + expect(gitignore).toContain(".stryker-tmp/"); + expect(gitignore).toContain("reports/"); + }); +}); diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 77882451eb..d75c79ce4e 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -122,6 +122,33 @@ Useful targeted verification for the isolated dev launcher: npm run test -- __tests__/api/agent-server-config.test.ts __tests__/scripts/dev-safe.test.ts ``` +### Mutation testing + +Stryker checks whether the Vitest suite detects deliberate changes to the +first-party TypeScript source under `src/`. The default configuration excludes +tests, declarations, generated files, fixtures, mocks, and development seeds. + +```sh +# Full mutation run (expensive for the whole frontend) +npm run test:mutation + +# Reuse results from the previous run +npm run test:mutation:incremental + +# Mutate only production files changed from the local main branch +npm run test:mutation:diff + +# Compare with another base ref, such as the latest remote main +npm run test:mutation:diff -- origin/main +``` + +The HTML report is written to `reports/mutation.html`. Mutation scores are +report-only initially; establish a stable baseline before adding a failing +threshold. + +Stryker does not cover the small Python surface in this repository; mutating it +would need a Python test harness and Python-specific mutation tool. + ## CSS isolation and host-app customization The standalone app and the exported provider/root wrapper now scope all bundled CSS under a dedicated shell element with the `data-agent-server-ui` attribute. That means Tailwind utilities, HeroUI component styles, xterm styles, and local CSS only apply inside the OpenHands UI subtree instead of leaking into a host app. diff --git a/package-lock.json b/package-lock.json index a8f4e7a4e1..8e1fc00796 100644 --- a/package-lock.json +++ b/package-lock.json @@ -69,6 +69,8 @@ "@mswjs/socket.io-binding": "0.2.0", "@playwright/test": "1.59.1", "@react-router/dev": "7.17.0", + "@stryker-mutator/core": "9.6.1", + "@stryker-mutator/vitest-runner": "9.6.1", "@tailwindcss/typography": "0.5.19", "@tanstack/eslint-plugin-query": "5.100.9", "@testing-library/dom": "10.4.1", @@ -564,6 +566,40 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/plugin-proposal-decorators": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.29.7.tgz", + "integrity": "sha512-EtU0Hi3GvrTqD56xKmZvV/uCXK2ZbwVNPNLAquVItcAZpUhkXwWlo3Fmj0c2LxgSf2I8IDULeAepwNP1OefLXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-syntax-decorators": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-decorators": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.29.7.tgz", + "integrity": "sha512-9MTTLbF39X6sqM92JPEsoI7++26hjZvzkxKZy64aMhWLH2mPkJ/Q3AV4QLmls3R14FpSpkOwQQfUh962JGQxxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-syntax-jsx": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.29.7.tgz", @@ -596,6 +632,40 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.29.7.tgz", + "integrity": "sha512-iPX8aD6H9zV5s7ZsqTdNocPN/MGQ5sSMnElKrktxjJRMnB2jN/1p2+R7GkfD6CAYoVFqy5A4XnSIUeGgJzIWpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-explicit-resource-management": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.29.7.tgz", + "integrity": "sha512-Rstj7coNz8sE+7Ju7ihpHLI564lsK5pUpNNlvptCIC/16E/S5hbl6n3kESPKdNRmqEWlpn5xpS5Q2dvXBsySLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-transform-destructuring": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-modules-commonjs": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.29.7.tgz", @@ -3434,6 +3504,30 @@ "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" } }, + "node_modules/@inquirer/checkbox": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-5.2.1.tgz", + "integrity": "sha512-b6xmA/VlTe0ZgDQHDui+Nav470u7u49nRd8/iuhOcQPO9Ch7lGuogydhi2VOmNlZ+zXcM8IcPuNSwQcdJaF/kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.7", + "@inquirer/core": "^11.2.1", + "@inquirer/figures": "^2.0.7", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, "node_modules/@inquirer/confirm": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-6.1.1.tgz", @@ -3483,6 +3577,90 @@ } } }, + "node_modules/@inquirer/editor": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-5.2.2.tgz", + "integrity": "sha512-ZRVd/oD+sYsUd5zVm0NflqEzlqfYCyHNsqkHl2oWXEUHs12tCbcSFi+wVFEvD8+LGRaMUsVrE7qeo6lSG/S1Vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.2.1", + "@inquirer/external-editor": "^3.0.3", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-5.1.1.tgz", + "integrity": "sha512-YmQpenjbFSHAK3sOd44puHh3V1KXXr+JiNpUztoSQ4drLh2rTVzTap/YtlAVu/5xavifIlBfNEzJ/neZJ1a/1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.2.1", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-3.0.3.tgz", + "integrity": "sha512-6thf5I8q7lZwzGLAxPaaGEREEkZ3nyePPDQ1oyobblxmEE8mqTLguScP7pDjUTAibiyb4hfXl+qjUEJ+di/aNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.2" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor/node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/@inquirer/figures": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-2.0.7.tgz", @@ -3493,6 +3671,172 @@ "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" } }, + "node_modules/@inquirer/input": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-5.1.2.tgz", + "integrity": "sha512-9K/DDBSQpOyZSkt6sOVP9Vo0TR7atX2kuILsUu0x3wVcVbe97lJwIJKMLdMw25tDYuXl/qp6erT0Xs1rfmcfZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.2.1", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-4.1.1.tgz", + "integrity": "sha512-XF4IXAbPnGPgw0wsbC/i2tPcyfdZgDpUlhsqU0SfT4IRIGWha6Xm9VRgN5yYxJq+jnyXlfXI/nQ3ulfk0iEICA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.2.1", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-5.1.1.tgz", + "integrity": "sha512-3XBfF7DAsp5qeDsvN5Rd1HmbNokVvEQoUM0QLrRcybC9nX96w3Pbmu7qUsb3IT3J3jBvs2+mTXaKHOUsgHMLzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.7", + "@inquirer/core": "^11.2.1", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-8.5.2.tgz", + "integrity": "sha512-IYR/3C/paEVVQYQvdDlFZVjRCJVYHHON0XXMH91KO9GSxs0TdKYWlUdvfQl2EfAHDxUaN3IBffkE/BDTh5nJ6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^5.2.1", + "@inquirer/confirm": "^6.1.1", + "@inquirer/editor": "^5.2.2", + "@inquirer/expand": "^5.1.1", + "@inquirer/input": "^5.1.2", + "@inquirer/number": "^4.1.1", + "@inquirer/password": "^5.1.1", + "@inquirer/rawlist": "^5.3.1", + "@inquirer/search": "^4.2.1", + "@inquirer/select": "^5.2.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-5.3.1.tgz", + "integrity": "sha512-QqdTqQddL3qPX/PPrjobpsO25NZ4dWXgTLenrR445L2ptLEYE6Z+PD5c5CNDJNx4ugRgELAIpSIJxZaO2jJ2Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.2.1", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-4.2.1.tgz", + "integrity": "sha512-xJj8QWKRSrfKoBIITLZK61dD3zwo0Rz11fgDImku30/Oe81zMdIdGgrLY2h6RkJ+KZ/GhNYIRMKnH/62qBTA5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.2.1", + "@inquirer/figures": "^2.0.7", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-5.2.1.tgz", + "integrity": "sha512-FlDndEUww8m7BfukO2nJa25vhD+H5jxxCv4oGioKqzyWz3nPHhhw4LKdYRSlXuAx7DsdWia7iyaBPKKS95Evfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.7", + "@inquirer/core": "^11.2.1", + "@inquirer/figures": "^2.0.7", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, "node_modules/@inquirer/type": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.0.7.tgz", @@ -6536,6 +6880,13 @@ "win32" ] }, + "node_modules/@sec-ant/readable-stream": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", + "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", + "dev": true, + "license": "MIT" + }, "node_modules/@sindresorhus/is": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", @@ -6549,6 +6900,19 @@ "url": "https://github.com/sindresorhus/is?sponsor=1" } }, + "node_modules/@sindresorhus/merge-streams": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", + "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@so-ric/colorspace": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/@so-ric/colorspace/-/colorspace-1.1.6.tgz", @@ -6649,6 +7013,240 @@ "dev": true, "license": "MIT" }, + "node_modules/@stryker-mutator/api": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@stryker-mutator/api/-/api-9.6.1.tgz", + "integrity": "sha512-g8VNoFWQWbx0pdal3Vt8jVCZW+v3sc3gi94iI0GVtVgUGTqphAjJF6EAruPTx0lqvtonsaAxn5TD36hcG1d6Wg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "mutation-testing-metrics": "3.7.3", + "mutation-testing-report-schema": "3.7.3", + "tslib": "~2.8.0", + "typed-inject": "~5.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@stryker-mutator/core": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@stryker-mutator/core/-/core-9.6.1.tgz", + "integrity": "sha512-WMgnvf+Wyh/yiruhNZwc8w8DlzmmjXhPjSn5MR8RhAXzlnWji8TQrUYgBUkHk9bEgSaIlB3KZHm37iiU5Q2cLQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@inquirer/prompts": "^8.0.0", + "@stryker-mutator/api": "9.6.1", + "@stryker-mutator/instrumenter": "9.6.1", + "@stryker-mutator/util": "9.6.1", + "ajv": "~8.18.0", + "chalk": "~5.6.0", + "commander": "~14.0.0", + "diff-match-patch": "1.0.5", + "emoji-regex": "~10.6.0", + "execa": "~9.6.0", + "json-rpc-2.0": "^1.7.0", + "lodash.groupby": "~4.6.0", + "minimatch": "~10.2.4", + "mutation-server-protocol": "~0.4.0", + "mutation-testing-elements": "3.7.3", + "mutation-testing-metrics": "3.7.3", + "mutation-testing-report-schema": "3.7.3", + "npm-run-path": "~6.0.0", + "progress": "~2.0.3", + "rxjs": "~7.8.1", + "semver": "^7.6.3", + "source-map": "~0.7.4", + "tree-kill": "~1.2.2", + "tslib": "2.8.1", + "typed-inject": "~5.0.0", + "typed-rest-client": "~2.3.0" + }, + "bin": { + "stryker": "bin/stryker.js" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@stryker-mutator/core/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@stryker-mutator/core/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@stryker-mutator/core/node_modules/brace-expansion": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz", + "integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@stryker-mutator/core/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@stryker-mutator/core/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@stryker-mutator/core/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/@stryker-mutator/core/node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@stryker-mutator/core/node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/@stryker-mutator/instrumenter": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@stryker-mutator/instrumenter/-/instrumenter-9.6.1.tgz", + "integrity": "sha512-5K8wH4Pthly25c2uKKik4Dfcoeou7sbJdFS6u3QIYHlulgFVDJwtEMWTZGkZfs7IiUEXIDNa0keRACq5jn5AvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@babel/core": "~7.29.0", + "@babel/generator": "~7.29.0", + "@babel/parser": "~7.29.0", + "@babel/plugin-proposal-decorators": "~7.29.0", + "@babel/plugin-transform-explicit-resource-management": "^7.28.0", + "@babel/preset-typescript": "~7.28.0", + "@stryker-mutator/api": "9.6.1", + "@stryker-mutator/util": "9.6.1", + "angular-html-parser": "~10.4.0", + "semver": "~7.7.0", + "tslib": "2.8.1", + "weapon-regex": "~1.3.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@stryker-mutator/instrumenter/node_modules/@babel/preset-typescript": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.28.5.tgz", + "integrity": "sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-transform-modules-commonjs": "^7.27.1", + "@babel/plugin-transform-typescript": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@stryker-mutator/instrumenter/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@stryker-mutator/util": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@stryker-mutator/util/-/util-9.6.1.tgz", + "integrity": "sha512-Lk/ALVctJjFv1vvwR+CFoKzDCWvsBlq7flDUnmnpuwTrGbm156EdZD1Jjq4o8KdOap0ezUZqQNE9OAI1m2+pUQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@stryker-mutator/vitest-runner": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@stryker-mutator/vitest-runner/-/vitest-runner-9.6.1.tgz", + "integrity": "sha512-eyUHTCf3Ui+SUn/tpFJwzw6MV391kyBLZk/cDHFUfKFELqKMLbvd7e81axArlApKqO6cOnLfrxlwED+2SRN0ow==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@stryker-mutator/api": "9.6.1", + "@stryker-mutator/util": "9.6.1", + "semver": "^7.7.4", + "tslib": "~2.8.0" + }, + "engines": { + "node": ">=14.18.0" + }, + "peerDependencies": { + "@stryker-mutator/core": "9.6.1", + "vitest": ">=2.0.0" + } + }, "node_modules/@svgr/babel-plugin-add-jsx-attribute": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz", @@ -8787,6 +9385,16 @@ "ajv": "^6.9.1" } }, + "node_modules/angular-html-parser": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/angular-html-parser/-/angular-html-parser-10.4.0.tgz", + "integrity": "sha512-++nLNyZwRfHqFh7akH5Gw/JYizoFlMRz0KRigfwfsLqV8ZqlcVRb1LkPEWdYvEKDnbktknM2J4BXaYUGrQZPww==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/ansi-escapes": { "version": "7.3.0", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", @@ -9904,6 +10512,13 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/chardet": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.2.0.tgz", + "integrity": "sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA==", + "dev": true, + "license": "MIT" + }, "node_modules/chokidar": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", @@ -10727,6 +11342,17 @@ "node": ">=6" } }, + "node_modules/des.js": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.1.0.tgz", + "integrity": "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, "node_modules/destroy": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", @@ -10767,6 +11393,13 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/diff-match-patch": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz", + "integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/dir-compare": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-4.2.0.tgz", @@ -12095,6 +12728,63 @@ "dev": true, "license": "MIT" }, + "node_modules/execa": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-9.6.1.tgz", + "integrity": "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^4.0.0", + "cross-spawn": "^7.0.6", + "figures": "^6.1.0", + "get-stream": "^9.0.0", + "human-signals": "^8.0.1", + "is-plain-obj": "^4.1.0", + "is-stream": "^4.0.1", + "npm-run-path": "^6.0.0", + "pretty-ms": "^9.2.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^4.0.0", + "yoctocolors": "^2.1.1" + }, + "engines": { + "node": "^18.19.0 || >=20.5.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/get-stream": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/execa/node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/exit-hook": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-2.2.1.tgz", @@ -12395,6 +13085,22 @@ "integrity": "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==", "license": "MIT" }, + "node_modules/figures": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", + "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -13472,6 +14178,16 @@ "integrity": "sha512-SMS9V6Sn7VWaS11lYhoAr0ceoaiolTWf4jYdJn0NJhCdKMu9R2H9Fh0LBDWBHQF6HRLI1PmaePYsjanSpE5PEw==", "license": "MIT" }, + "node_modules/human-signals": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz", + "integrity": "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, "node_modules/husky": { "version": "9.1.7", "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", @@ -14246,6 +14962,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-weakmap": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", @@ -14412,6 +15141,13 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/js-md4": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/js-md4/-/js-md4-0.3.2.tgz", + "integrity": "sha512-/GDnfQYsltsjRswQhN9fhv3EMw2sCpUdrdxyWDOUK7eyD++r3gRhzgiQgc/x4MAv2i1iuQ4lxO5mvqM3vj4bwA==", + "dev": true, + "license": "MIT" + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -14519,6 +15255,13 @@ "dev": true, "license": "MIT" }, + "node_modules/json-rpc-2.0": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/json-rpc-2.0/-/json-rpc-2.0-1.7.1.tgz", + "integrity": "sha512-JqZjhjAanbpkXIzFE7u8mE/iFblawwlXtONaCvRqI+pyABVz7B4M1EUNpyVW+dZjqgQ2L5HFmZCmOCgUKm00hg==", + "dev": true, + "license": "MIT" + }, "node_modules/json-schema-to-ts": { "version": "1.6.4", "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-1.6.4.tgz", @@ -14996,6 +15739,13 @@ "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", "license": "MIT" }, + "node_modules/lodash.groupby": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.groupby/-/lodash.groupby-4.6.0.tgz", + "integrity": "sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -16247,6 +16997,13 @@ "node": ">=4" } }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true, + "license": "ISC" + }, "node_modules/minimatch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", @@ -16497,6 +17254,43 @@ "dev": true, "license": "MIT" }, + "node_modules/mutation-server-protocol": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/mutation-server-protocol/-/mutation-server-protocol-0.4.1.tgz", + "integrity": "sha512-SBGK0j8hLDne7bktgThKI8kGvGTx3rY3LAeQTmOKZ5bVnL/7TorLMvcVF7dIPJCu5RNUWhkkuF53kurygYVt3g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "zod": "^4.1.12" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/mutation-testing-elements": { + "version": "3.7.3", + "resolved": "https://registry.npmjs.org/mutation-testing-elements/-/mutation-testing-elements-3.7.3.tgz", + "integrity": "sha512-SMeIPxngJpfjfNYctFpYQQtlBlZaVO0aoB3FKdwrI8Ee/2bkyUuCZzAOCLv1U9fnmfA37dPFq0Owduoxs2XgGQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/mutation-testing-metrics": { + "version": "3.7.3", + "resolved": "https://registry.npmjs.org/mutation-testing-metrics/-/mutation-testing-metrics-3.7.3.tgz", + "integrity": "sha512-B8QrP0ZomErzTPNlhrzKWPNBln+3afwBZPHv0Q7N8wZZTYxMptzb/Gdm3ExXVmioVYrtZAtsDs7W/T/b2AixOQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "mutation-testing-report-schema": "3.7.3" + } + }, + "node_modules/mutation-testing-report-schema": { + "version": "3.7.3", + "resolved": "https://registry.npmjs.org/mutation-testing-report-schema/-/mutation-testing-report-schema-3.7.3.tgz", + "integrity": "sha512-BHm3MYq+ckO+t5CtlG8zpqxc75rdJCkxVlE+fGuGJM3F7tNCQ/OW2N+TQVHN3BHsYa84+BFc6g3AwDYkUsw2MA==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/mute-stream": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-3.0.0.tgz", @@ -16738,6 +17532,36 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/npm-run-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", + "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -17078,6 +17902,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse-ms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", + "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/parse5": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", @@ -17470,6 +18307,22 @@ "dev": true, "license": "MIT" }, + "node_modules/pretty-ms": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz", + "integrity": "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse-ms": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/prism-react-renderer": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/prism-react-renderer/-/prism-react-renderer-2.4.1.tgz", @@ -18579,6 +19432,16 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/sade": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", @@ -19488,6 +20351,19 @@ "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, + "node_modules/strip-final-newline": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz", + "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/strip-indent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", @@ -19919,6 +20795,16 @@ "node": ">=20" } }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, "node_modules/trim-lines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", @@ -19995,6 +20881,16 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -20115,6 +21011,49 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/typed-inject": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/typed-inject/-/typed-inject-5.0.0.tgz", + "integrity": "sha512-0Ql2ORqBORLMdAW89TQKZsb1PQkFGImFfVmncXWe7a+AA3+7dh7Se9exxZowH4kbnlvKEFkMxUYdHUpjYWFJaA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/typed-rest-client": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-2.3.1.tgz", + "integrity": "sha512-k4kX5Up6qA68D0Cby2AK+6+vM5k3qTxe+/3FqhnHRExjY5cfbOnzjQZbP/LXleF8hVoDvDqxlgk9KK83HoBZlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "des.js": "^1.1.0", + "js-md4": "^0.3.2", + "qs": "6.15.1", + "tunnel": "0.0.6", + "underscore": "^1.13.8" + }, + "engines": { + "node": ">= 16.0.0" + } + }, + "node_modules/typed-rest-client/node_modules/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/typescript": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", @@ -20148,6 +21087,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/underscore": { + "version": "1.13.8", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz", + "integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==", + "dev": true, + "license": "MIT" + }, "node_modules/undici": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/undici/-/undici-7.27.1.tgz", @@ -20164,6 +21110,19 @@ "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", "license": "MIT" }, + "node_modules/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/unified": { "version": "11.0.5", "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", @@ -20881,6 +21840,13 @@ "node": ">=18" } }, + "node_modules/weapon-regex": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/weapon-regex/-/weapon-regex-1.3.6.tgz", + "integrity": "sha512-wsf1m1jmMrso5nhwVFJJHSubEBf3+pereGd7+nBKtYJ18KoB/PWJOHS3WRkwS04VrOU0iJr2bZU+l1QaTJ+9nA==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/web-namespaces": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", @@ -21368,6 +22334,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/yoctocolors": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.2.0.tgz", + "integrity": "sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/zod": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", diff --git a/package.json b/package.json index 1f72ba1efb..1f7a7e679f 100644 --- a/package.json +++ b/package.json @@ -87,6 +87,9 @@ "test:e2e:mock-llm": "playwright test --config=playwright.mock-llm.config.ts", "test:e2e:mock-llm:docker": "playwright test --config=playwright.mock-llm-docker.config.ts", "test:coverage": "npm run make-i18n && vitest run --coverage", + "test:mutation": "npm run make-i18n && stryker run", + "test:mutation:diff": "npm run make-i18n && node scripts/stryker-diff.mjs", + "test:mutation:incremental": "npm run make-i18n && stryker run --incremental", "dev_wsl": "VITE_WATCH_USE_POLLING=true vite", "preview": "vite preview", "make-i18n": "node scripts/make-i18n-translations.cjs", @@ -125,6 +128,8 @@ "@mswjs/socket.io-binding": "0.2.0", "@playwright/test": "1.59.1", "@react-router/dev": "7.17.0", + "@stryker-mutator/core": "9.6.1", + "@stryker-mutator/vitest-runner": "9.6.1", "@tailwindcss/typography": "0.5.19", "@tanstack/eslint-plugin-query": "5.100.9", "@testing-library/dom": "10.4.1", diff --git a/scripts/stryker-diff.mjs b/scripts/stryker-diff.mjs new file mode 100644 index 0000000000..194c1b2662 --- /dev/null +++ b/scripts/stryker-diff.mjs @@ -0,0 +1,137 @@ +#!/usr/bin/env node + +import { spawnSync } from "node:child_process"; +import process from "node:process"; +import { fileURLToPath, pathToFileURL } from "node:url"; + +const EXCLUDED_DIRECTORY = + /(^|\/)(__tests__|tests?|fixtures|__fixtures__|snapshots|__snapshots__|generated|mocks|dev)(\/|$)/; +const EXCLUDED_FILE = /\.(test|spec|d|gen|generated)\.(ts|tsx)$/; + +function isProductionTypeScript(file) { + const normalized = file.replaceAll("\\", "/"); + return ( + /^src\/.+\.(ts|tsx)$/.test(normalized) && + !EXCLUDED_DIRECTORY.test(normalized) && + !EXCLUDED_FILE.test(normalized) + ); +} + +/** + * @typedef {object} ProcessResult + * @property {Error | undefined} [error] + * @property {number | null} status + * @property {string | null} stderr + * @property {string | null} stdout + */ + +/** + * @typedef {object} Dependencies + * @property {(command: string, args: readonly string[], options?: import("node:child_process").SpawnSyncOptions) => ProcessResult} spawn + * @property {(message: string) => void} writeError + * @property {(message: string) => void} writeOutput + */ + +// Stryker disable all: the real process adapter is verified by the CLI +// subprocess smoke test, which Vitest cannot attribute to the parent process. +/** @type {Dependencies} */ +const defaultDependencies = { + spawn(command, args, options) { + const result = spawnSync(command, [...args], options); + return { + error: result.error, + status: result.status, + stderr: result.stderr?.toString() ?? null, + stdout: result.stdout?.toString() ?? null, + }; + }, + writeError(message) { + process.stderr.write(message); + }, + writeOutput(message) { + process.stdout.write(message); + }, +}; +// Stryker restore all + +/** + * Run mutation testing against production TypeScript files changed from a base + * git ref. The dependency parameter keeps the process boundary observable in + * tests while the command-line behavior remains the public API. + * + * @param {string[]} argv + * @param {Partial} [overrides] + * @returns {number} + */ +export function main(argv, overrides = {}) { + const dependencies = { ...defaultDependencies, ...overrides }; + const baseRef = argv[0] ?? "main"; + const gitArgs = [ + "diff", + "--name-only", + "--diff-filter=ACMRTUXB", + `${baseRef}...HEAD`, + ]; + const gitResult = dependencies.spawn("git", gitArgs, { + encoding: "utf8", + windowsHide: true, + }); + + if (gitResult.error || gitResult.status !== 0) { + const detail = + gitResult.stderr || gitResult.error?.message || "unknown error"; + dependencies.writeError( + `Unable to determine changed files against ${baseRef}: ${detail.trim()}\n`, + ); + return gitResult.status ?? 1; + } + + const mutationTargets = gitResult.stdout + .split(/\r?\n/u) + .filter(isProductionTypeScript); + + if (mutationTargets.length === 0) { + dependencies.writeOutput( + `No changed production files to mutate against ${baseRef}.\n`, + ); + return 0; + } + + const strykerCliPath = fileURLToPath( + new URL( + "../node_modules/@stryker-mutator/core/bin/stryker.js", + import.meta.url, + ), + ); + const strykerResult = dependencies.spawn( + process.execPath, + [ + strykerCliPath, + "run", + "--incremental", + "--force", + "--mutate", + mutationTargets.join(","), + ], + { stdio: "inherit", windowsHide: true }, + ); + + if (strykerResult.error) { + dependencies.writeError( + `Unable to start Stryker: ${strykerResult.error.message}\n`, + ); + return 1; + } + + return strykerResult.status ?? 1; +} + +// Stryker disable all: the CLI guard is verified by a real subprocess test, +// which the Vitest runner cannot attribute to the mutated parent process. +const isMainModule = + process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href; + +if (isMainModule) { + process.exitCode = main(process.argv.slice(2)); +} +// Stryker restore all diff --git a/stryker.config.mjs b/stryker.config.mjs new file mode 100644 index 0000000000..ac2b815e30 --- /dev/null +++ b/stryker.config.mjs @@ -0,0 +1,22 @@ +// @ts-check + +/** @type {import("@stryker-mutator/api/core").PartialStrykerOptions} */ +const config = { + testRunner: "vitest", + mutate: [ + "src/**/*.{ts,tsx}", + "!src/**/*.{test,spec}.{ts,tsx}", + "!src/**/__tests__/**", + "!src/**/*.d.ts", + "!src/**/*.types.ts", + "!src/**/*.{gen,generated}.{ts,tsx}", + "!src/i18n/declaration.ts", + "!src/{fixtures,mocks,dev}/**", + ], + vitest: { + configFile: "vite.config.ts", + related: true, + }, +}; + +export default config;