From 3788b8d67e3ea7e10da794cbcb159fd0e491ebc5 Mon Sep 17 00:00:00 2001 From: Baptiste LAFOURCADE Date: Fri, 11 Sep 2026 05:54:09 +0200 Subject: [PATCH] test(cli): count a mutant as killed only when a test failed Stryker scores a timed-out mutant as detected. `stryker.conf.json` set no `timeoutMS`, so a test run that was merely slow crossed Stryker's default limit and was scored as a kill. The mutants concerned were static ones (a string or object literal in a tool profile), which cannot loop. With `timeoutMS` at 20000, tools-cursor and tools-copilot went from 7 and 17 timeouts to none, and 7 and 12 of those turned out to be survivors. `pruneIncremental` also carried a timed-out mutant forward like a kill, so a higher limit would never have re-tested one. It now carries only kills, as its own doc comment already said. The survivors this uncovered are declarations nothing asserted: an agent format, an MCP file's format and entry section, a marketplace probe path, Copilot's CLI verbs. The claude, copilot and cursor profile tests now assert them. Fixes #837 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_011x4ms5qcGuZgYhCxfdHMUb AIDD-Session-Id: 4acc9a1c-19bc-4468-b8b6-e86644bcba60 --- cli/scripts/run-mutation.mjs | 4 +- cli/stryker.conf.json | 1 + .../mutation-covers-source.arch.test.ts | 2 +- .../tools/domain/profiles/claude.unit.test.ts | 15 +++++++ .../domain/profiles/copilot.unit.test.ts | 39 +++++++++++++++++++ .../tools/domain/profiles/cursor.unit.test.ts | 26 +++++++++++++ 6 files changed, 83 insertions(+), 4 deletions(-) diff --git a/cli/scripts/run-mutation.mjs b/cli/scripts/run-mutation.mjs index 3e178e051..0c6f7e984 100644 --- a/cli/scripts/run-mutation.mjs +++ b/cli/scripts/run-mutation.mjs @@ -56,9 +56,7 @@ export function pruneIncremental(report) { for (const [name, file] of Object.entries(report.files ?? {})) { files[name] = { ...file, - mutants: file.mutants.filter( - (mutant) => !mutant.static && (mutant.status === "Killed" || mutant.status === "Timeout") - ), + mutants: file.mutants.filter((mutant) => !mutant.static && mutant.status === "Killed"), }; } return { ...report, files }; diff --git a/cli/stryker.conf.json b/cli/stryker.conf.json index 252767561..c4cd509f2 100644 --- a/cli/stryker.conf.json +++ b/cli/stryker.conf.json @@ -5,6 +5,7 @@ "plugins": ["@stryker-mutator/vitest-runner"], "ignorePatterns": ["reports"], "coverageAnalysis": "perTest", + "timeoutMS": 20000, "thresholds": { "high": 80, "low": 60, diff --git a/cli/tests/architecture/mutation-covers-source.arch.test.ts b/cli/tests/architecture/mutation-covers-source.arch.test.ts index d2d1df59b..682406f06 100644 --- a/cli/tests/architecture/mutation-covers-source.arch.test.ts +++ b/cli/tests/architecture/mutation-covers-source.arch.test.ts @@ -220,7 +220,7 @@ describe("the guard itself", () => { }, }, }); - expect(pruned.files?.["a.ts"]?.mutants).toEqual([{ status: "Killed" }, { status: "Timeout" }]); + expect(pruned.files?.["a.ts"]?.mutants).toEqual([{ status: "Killed" }]); }); it("fails a score under the floor and passes one on it", () => { diff --git a/cli/tests/contexts/tools/domain/profiles/claude.unit.test.ts b/cli/tests/contexts/tools/domain/profiles/claude.unit.test.ts index fc9d6cc55..246749a38 100644 --- a/cli/tests/contexts/tools/domain/profiles/claude.unit.test.ts +++ b/cli/tests/contexts/tools/domain/profiles/claude.unit.test.ts @@ -3,11 +3,26 @@ import { describe, expect, it } from "vitest"; import { claude } from "../../../../../src/contexts/tools/domain/profiles/claude/profile.js"; describe("claude", () => { + it("has .claude/ directory", () => { + expect(claude.directory).toBe(".claude/"); + }); + + it("writes agents as markdown", () => { + expect(claude.capabilities.agents.params.format).toBe("markdown"); + }); + describe("capabilities.mcp", () => { it("outputs to .mcp.json", () => { expect(claude.capabilities.mcp.params.outputPath).toBe(".mcp.json"); }); + it("writes its servers as JSON under `mcpServers`", () => { + expect(claude.capabilities.mcp.params).toMatchObject({ + format: "json", + entrySection: "mcpServers", + }); + }); + it("consumes the mcp config name", () => { expect(claude.capabilities.mcp.consumes).toContain("mcp"); }); diff --git a/cli/tests/contexts/tools/domain/profiles/copilot.unit.test.ts b/cli/tests/contexts/tools/domain/profiles/copilot.unit.test.ts index 2a15d8eeb..53a7ce5ca 100644 --- a/cli/tests/contexts/tools/domain/profiles/copilot.unit.test.ts +++ b/cli/tests/contexts/tools/domain/profiles/copilot.unit.test.ts @@ -1,6 +1,8 @@ import { join } from "node:path"; import { describe, expect, it } from "vitest"; +import { CONFIG_MCP } from "../../../../../src/contexts/tools/domain/capabilities/config-refs.js"; import { copilot } from "../../../../../src/contexts/tools/domain/profiles/copilot/profile.js"; +import { nativeActivationOf } from "../../../../../src/contexts/tools/domain/registry.js"; describe("copilot", () => { describe("capabilities.rules.convertFrontmatter()", () => { @@ -490,3 +492,40 @@ describe("a reference to another framework file, installed for Copilot", () => { }); }); }); + +describe("copilot declarations the rest of the CLI reads", () => { + it("probes a plugin manifest in the three places Copilot reads one, in its order", () => { + expect(copilot.distributionProbes?.manifest).toStrictEqual([ + ".plugin/plugin.json", + ".github/plugin/plugin.json", + "plugin.json", + ]); + }); + + it("writes agents as markdown", () => { + expect(copilot.capabilities.agents.params.format).toBe("markdown"); + }); + + it("writes its MCP servers as JSON under `servers`", () => { + expect(copilot.capabilities.mcp.params).toMatchObject({ + format: "json", + entrySection: "servers", + }); + expect(copilot.capabilities.mcp.consumes).toStrictEqual([CONFIG_MCP]); + }); + + it("translates plugins for its own marketplace", () => { + expect(copilot.capabilities.plugins.translationMode).toBe("marketplace"); + }); + + it("drives its own CLI with the verbs it answers to", () => { + expect(nativeActivationOf("copilot")).toMatchObject({ + binary: "copilot", + upgradeVerb: "update", + enableVerb: "install", + disableVerb: "uninstall", + sourceCheckVerb: "update", + forceRemoveArgs: ["--force"], + }); + }); +}); diff --git a/cli/tests/contexts/tools/domain/profiles/cursor.unit.test.ts b/cli/tests/contexts/tools/domain/profiles/cursor.unit.test.ts index c97ef2758..aa92cb0cb 100644 --- a/cli/tests/contexts/tools/domain/profiles/cursor.unit.test.ts +++ b/cli/tests/contexts/tools/domain/profiles/cursor.unit.test.ts @@ -1,5 +1,6 @@ import { join } from "node:path"; import { describe, expect, it } from "vitest"; +import { CONFIG_MCP } from "../../../../../src/contexts/tools/domain/capabilities/config-refs.js"; import { cursor } from "../../../../../src/contexts/tools/domain/profiles/cursor/profile.js"; describe("cursor", () => { @@ -170,3 +171,28 @@ describe("cursor", () => { }); }); }); + +describe("cursor declarations the rest of the CLI reads", () => { + it("has .cursor/ directory", () => { + expect(cursor.directory).toBe(".cursor/"); + }); + + it("probes a marketplace catalog at .cursor-plugin/marketplace.json", () => { + expect(cursor.distributionProbes?.marketplace).toStrictEqual([ + ".cursor-plugin/marketplace.json", + ]); + }); + + it("writes agents as markdown", () => { + expect(cursor.capabilities.agents.params.format).toBe("markdown"); + }); + + it("writes its MCP servers as JSON under `mcpServers`, in .cursor/mcp.json", () => { + expect(cursor.capabilities.mcp.params).toMatchObject({ + outputPath: ".cursor/mcp.json", + format: "json", + entrySection: "mcpServers", + }); + expect(cursor.capabilities.mcp.consumes).toStrictEqual([CONFIG_MCP]); + }); +});