{"id":"l-tj-5-3","courseId":"testing-javascript","lang":"en","kind":"lesson","summary":"Explain how mutation testing grades a test suite, run StrykerJS with Vitest on core logic, and turn survived mutants into the specific tests that were missing.","html":"<p>The last exercise left you with an uncomfortable fact: coverage said 100%, and the suite caught nothing. You need a measurement that asks the real question, <em>would these tests notice if the code were wrong?</em> Mutation testing answers it by trying. You’ve already been doing it by hand: every exercise in this course ran your tests against “mutants”. A tool does the same thing systematically, for every line.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">How it works</h2>\n<ol>\n<li>The tool makes a <strong>mutant</strong>: a copy of your code with one small change, the kind of slip a person makes. <code dir=\"ltr\">&lt;</code> becomes <code dir=\"ltr\">&lt;=</code>, <code dir=\"ltr\">+</code> becomes <code dir=\"ltr\">-</code>, <code dir=\"ltr\">Math.min</code> becomes <code dir=\"ltr\">Math.max</code>, a condition becomes <code dir=\"ltr\">true</code>, a block is emptied.</li>\n<li>It runs the tests that cover that line against the mutant.</li>\n<li>If any test fails, the mutant is <strong>killed</strong>: your tests would catch that bug. If every test passes, it <strong>survived</strong>: that bug would ship.</li>\n</ol>\n<p>It repeats this for hundreds or thousands of mutants. The <strong>mutation score</strong> is the percentage killed.</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 680 230\" role=\"img\" aria-labelledby=\"l-tj-5-3-t1\"><defs><marker id=\"arrow-l-tj-5-3-1\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\"><path d=\"M0 0 L10 5 L0 10 z\" class=\"d-arrowhead\"/></marker></defs>\n  <title id=\"l-tj-5-3-t1\">Source code produces three mutants. The test suite runs against each: two fail and the mutants are killed, one passes and the mutant survives, pointing at a missing test.</title>\n  <rect class=\"d-box\" x=\"10\" y=\"85\" width=\"120\" height=\"60\" rx=\"12\"/>\n  <text class=\"d-code\" x=\"70\" y=\"120\" text-anchor=\"middle\">settleUp</text>\n  <path class=\"d-arrow\" d=\"M130 105 L198 45\" marker-end=\"url(#arrow-l-tj-5-3-1)\"/>\n  <path class=\"d-arrow\" d=\"M130 115 L198 115\" marker-end=\"url(#arrow-l-tj-5-3-1)\"/>\n  <path class=\"d-arrow\" d=\"M130 125 L198 185\" marker-end=\"url(#arrow-l-tj-5-3-1)\"/>\n  <rect class=\"d-box-accent\" x=\"200\" y=\"25\" width=\"190\" height=\"40\" rx=\"8\"/>\n  <text class=\"d-code\" x=\"295\" y=\"50\" text-anchor=\"middle\">min → max</text>\n  <rect class=\"d-box-accent\" x=\"200\" y=\"95\" width=\"190\" height=\"40\" rx=\"8\"/>\n  <text class=\"d-code\" x=\"295\" y=\"120\" text-anchor=\"middle\">-cents → +cents</text>\n  <rect class=\"d-box-accent\" x=\"200\" y=\"165\" width=\"190\" height=\"40\" rx=\"8\"/>\n  <text class=\"d-code\" x=\"295\" y=\"190\" text-anchor=\"middle\">sum !== 0 → false</text>\n  <path class=\"d-arrow\" d=\"M390 45 L478 45\" marker-end=\"url(#arrow-l-tj-5-3-1)\"/>\n  <path class=\"d-arrow\" d=\"M390 115 L478 115\" marker-end=\"url(#arrow-l-tj-5-3-1)\"/>\n  <path class=\"d-arrow\" d=\"M390 185 L478 185\" marker-end=\"url(#arrow-l-tj-5-3-1)\"/>\n  <rect class=\"d-box-success\" x=\"480\" y=\"25\" width=\"190\" height=\"40\" rx=\"8\"/>\n  <text class=\"d-label\" x=\"575\" y=\"50\" text-anchor=\"middle\">a test fails: killed</text>\n  <rect class=\"d-box-success\" x=\"480\" y=\"95\" width=\"190\" height=\"40\" rx=\"8\"/>\n  <text class=\"d-label\" x=\"575\" y=\"120\" text-anchor=\"middle\">a test fails: killed</text>\n  <rect class=\"d-box-warn\" x=\"480\" y=\"165\" width=\"190\" height=\"40\" rx=\"8\"/>\n  <text class=\"d-label\" x=\"575\" y=\"190\" text-anchor=\"middle\">all pass: survived</text>\n</svg><figcaption>Each mutant is one small deliberate bug; tests either kill it or let it survive</figcaption></figure>\n<p>Coverage asks “did this line run?” Mutation testing asks “if this line were wrong, would anyone notice?” The second question is the one you care about.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">Running StrykerJS with Vitest</h2>\n<p>StrykerJS is the established mutation testing tool for JavaScript and TypeScript, with a runner plugin for Vitest:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"bash\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">Terminal</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">npm</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> install</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> --save-dev</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> @stryker-mutator/core</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> @stryker-mutator/vitest-runner</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">npx</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> stryker</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> init</span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">   # interactive; or write the config yourself</span></span></code></pre></div>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"json\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">stryker.config.json</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">{</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  \"$schema\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"./node_modules/@stryker-mutator/core/schema/stryker-schema.json\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  \"testRunner\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"vitest\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  \"plugins\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"@stryker-mutator/vitest-runner\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">],</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  \"mutate\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"src/money/**/*.js\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"!src/**/*.test.js\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">],</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  \"reporters\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"html\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"clear-text\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"progress\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">],</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  \"thresholds\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: { </span><span style=\"color:#116329;--shiki-dark:#7EE787\">\"high\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">80</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#116329;--shiki-dark:#7EE787\">\"low\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">60</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#116329;--shiki-dark:#7EE787\">\"break\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">50</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> },</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  \"incremental\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">true</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p><code dir=\"ltr\">npx stryker run</code> first runs your suite once to see which tests cover which lines, then tests every mutant. The HTML report shows your source with each mutant marked killed or survived. <code dir=\"ltr\">thresholds.break</code> fails the run below that score, and <code dir=\"ltr\">incremental</code> reuses results for code and tests that didn’t change since the last run, which matters a lot for speed.</p>\n<h2 id=\"sec-3\" tabindex=\"-1\">Reading the survivors</h2>\n<p>The statuses you’ll see most:</p>\n<ul>\n<li><strong>Killed</strong>: a test failed. Good.</li>\n<li><strong>Survived</strong>: every covering test passed. A missing or weak assertion.</li>\n<li><strong>No coverage</strong>: no test even ran that line. Coverage would have told you this too.</li>\n<li><strong>Timeout</strong>: the mutant caused an infinite loop and the tests timed out. Counted as detected.</li>\n</ul>\n<p>Survivors are the gold. For <code dir=\"ltr\">settleUp</code>, a report might say:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"text\" data-index=\"2\"><div class=\"code-head\"><span class=\"code-title\">Text</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code>[Survived] MethodExpression   src/money/settle-up.js:21\n-   const amountCents = Math.min(debtors[d].cents, creditors[c].cents);\n+   const amountCents = Math.max(debtors[d].cents, creditors[c].cents);\n\n[Survived] ConditionalExpression   src/money/settle-up.js:4\n-   if (sum !== 0) throw new Error('Balances must add up to zero');\n+   if (false) throw new Error('Balances must add up to zero');</code></pre></div>\n<p>Each one is a precise instruction. The first says no test uses amounts where the debtor owes a different sum than the creditor is owed, so <code dir=\"ltr\">min</code> and <code dir=\"ltr\">max</code> give the same answer in every test. The second says no test ever passes unbalanced input. Write those two tests and both mutants die.</p>\n<aside class=\"callout callout-mistake\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\"/><path d=\"M12 9v4\"/><path d=\"M12 17h.01\"/></svg><span><span class=\"callout-kind\">Common mistake</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Chasing 100% mutation score</span></p>\n<p>Some mutants are <strong>equivalent</strong>: the change doesn’t alter behaviour any caller could observe. Flipping the sort direction in <code dir=\"ltr\">settleUp</code> often still produces a valid settlement; a mutant that changes <code dir=\"ltr\">cents &lt; 0</code> to <code dir=\"ltr\">cents &lt;= 0</code> may only affect people with zero balance, who are never reached. Writing tests to kill those means asserting on implementation details. Accept them, or disable the specific mutation on that line with a comment such as <code dir=\"ltr\">// Stryker disable next-line all: equivalent, order doesn't matter</code>.</p>\n</aside>\n<h2 id=\"sec-4\" tabindex=\"-1\">Hand-picked versus generated mutants</h2>\n<p>The mutants in this course’s exercises were hand-picked: each one imitates a bug a person really writes, like a swapped argument or a forgotten <code dir=\"ltr\">await</code>. Stryker’s are generated systematically from a fixed set of mutators, typically dozens per function, so it finds gaps you would never think to look for, and also produces some noise. Both are useful. When you review a teammate’s tests, the hand-picked habit is the quick version: ask “what is the most likely bug here, and which test would fail?” When you want a measurement across a whole module, let the tool generate the list.</p>\n<h2 id=\"sec-5\" tabindex=\"-1\">Where it fits</h2>\n<p>Mutation testing is slow: hundreds of mutants, each running part of your suite. A few habits keep it practical:</p>\n<ul>\n<li><strong>Scope it</strong> with <code dir=\"ltr\">mutate</code> to the code where bugs are expensive: money, permissions, parsing. Not UI glue.</li>\n<li><strong>Run it incrementally</strong> locally, and on a schedule (nightly) or on changed files in CI, rather than on every push of the whole repo.</li>\n<li><strong>Use the report in review</strong>: a pull request that adds logic and survivors deserves a question.</li>\n</ul>\n<p>In the exercise you get a weak <code dir=\"ltr\">settleUp</code> suite and a list of survivors, and you write the tests that kill them.</p>\n","toc":[{"id":"sec-1","text":"How it works","level":2},{"id":"sec-2","text":"Running StrykerJS with Vitest","level":2},{"id":"sec-3","text":"Reading the survivors","level":2},{"id":"sec-4","text":"Hand-picked versus generated mutants","level":2},{"id":"sec-5","text":"Where it fits","level":2}],"takeaways":["A mutation testing tool makes small deliberate bugs (mutants) in your code and reruns the tests; a mutant that makes a test fail is killed, one that doesn’t has survived.","The mutation score (killed mutants as a share of all mutants) measures how many plausible bugs your tests would catch, which coverage can’t.","Each survived mutant points at a missing or weak assertion; read it, then write the test that kills it.","Some mutants are equivalent (they don’t change behaviour) and can’t be killed; ignore them rather than contorting tests.","Mutation testing is slow, so run it on core logic, incrementally or on a schedule, not on every file in every commit."],"further":[{"title":"Vitest — Coverage guide","url":"https://vitest.dev/guide/coverage"},{"title":"StrykerJS on GitHub","url":"https://github.com/stryker-mutator/stryker-js"}],"quiz":[{"q":"<p>Stryker reports: <code dir=\"ltr\">ConditionalExpression: if (sum !== 0) → if (false)</code> — Survived. What does that tell you?</p>","options":[{"text":"The check is unnecessary and can be deleted.","why":"It tells you no test notices when the check is removed, not that the check is useless; unbalanced input would silently produce wrong transfers."},{"text":"No test passes unbalanced balances and expects <code dir=\"ltr\">settleUp</code> to throw.","why":"Correct. Add a test like <code dir=\"ltr\">expect(() =&gt; settleUp({ Ana: 500, Ben: -400 })).toThrow()</code> and the mutant is killed."},{"text":"Stryker failed to run the tests for that line.","why":"When tests can’t run, the status is a runtime or compile error, or “no coverage”; “Survived” means the tests ran and all passed."}],"answer":1},{"q":"<p>Your suite has 100% line coverage and a mutation score of 55%. Which is the better summary?</p>","options":[{"text":"Every line runs, but nearly half of the plausible bugs Stryker introduced would go unnoticed.","why":"Correct. Coverage shows execution; the mutation score shows how much of that execution is actually checked."},{"text":"The suite is excellent; mutation scores above 50% are rare.","why":"There’s no universal bar, but 45% surviving mutants on core logic usually means weak assertions worth fixing."},{"text":"The two numbers contradict each other, so one of the tools is misconfigured.","why":"They measure different things; high coverage with a low mutation score is the classic pattern of assertion-light tests."}],"answer":0},{"q":"<p>A mutant changes <code dir=\"ltr\">b.cents - a.cents</code> to <code dir=\"ltr\">a.cents - b.cents</code> in the sort comparator, and every valid input still produces a correct settlement. What should you do?</p>","options":[{"text":"Write a test that asserts the exact order of transfers for a large group, so the mutant dies.","why":"If several orders are valid, pinning one couples the test to the implementation, the exact mistake from section 2."},{"text":"Lower the mutation score threshold to zero.","why":"One equivalent mutant isn’t a reason to throw away the signal from all the others."},{"text":"Treat it as a likely equivalent mutant and ignore it (or disable that mutation on the line with a comment).","why":"Correct. If no caller could observe the difference, there is nothing to test; Stryker lets you mark such lines so the report stays meaningful."}],"answer":2}],"exercise":{"kind":"web","mode":"test","subject":"// Returns the transfers that settle a group: [{ from, to, amountCents }].\n// balances: { name: cents }, positive = is owed, negative = owes.\n// Throws if the balances don't add up to zero.\nfunction settleUp(balances) {\n  const entries = Object.entries(balances);\n  const sum = entries.reduce((total, [, cents]) => total + cents, 0);\n  if (sum !== 0) throw new Error('Balances must add up to zero');\n\n  const debtors = [];\n  const creditors = [];\n  for (const [name, cents] of entries) {\n    if (cents < 0) debtors.push({ name, cents: -cents });\n    else if (cents > 0) creditors.push({ name, cents });\n  }\n  const largestFirst = (a, b) => b.cents - a.cents || a.name.localeCompare(b.name);\n  debtors.sort(largestFirst);\n  creditors.sort(largestFirst);\n\n  const transfers = [];\n  let d = 0;\n  let c = 0;\n  while (d < debtors.length && c < creditors.length) {\n    const amountCents = Math.min(debtors[d].cents, creditors[c].cents);\n    transfers.push({ from: debtors[d].name, to: creditors[c].name, amountCents });\n    debtors[d].cents -= amountCents;\n    creditors[c].cents -= amountCents;\n    if (debtors[d].cents === 0) d += 1;\n    if (creditors[c].cents === 0) c += 1;\n  }\n  return transfers;\n}\n","mutants":[{"id":"min-to-max","code":"// Returns the transfers that settle a group: [{ from, to, amountCents }].\n// balances: { name: cents }, positive = is owed, negative = owes.\n// Throws if the balances don't add up to zero.\nfunction settleUp(balances) {\n  const entries = Object.entries(balances);\n  const sum = entries.reduce((total, [, cents]) => total + cents, 0);\n  if (sum !== 0) throw new Error('Balances must add up to zero');\n\n  const debtors = [];\n  const creditors = [];\n  for (const [name, cents] of entries) {\n    if (cents < 0) debtors.push({ name, cents: -cents });\n    else if (cents > 0) creditors.push({ name, cents });\n  }\n  const largestFirst = (a, b) => b.cents - a.cents || a.name.localeCompare(b.name);\n  debtors.sort(largestFirst);\n  creditors.sort(largestFirst);\n\n  const transfers = [];\n  let d = 0;\n  let c = 0;\n  while (d < debtors.length && c < creditors.length) {\n    const amountCents = Math.max(debtors[d].cents, creditors[c].cents);\n    transfers.push({ from: debtors[d].name, to: creditors[c].name, amountCents });\n    debtors[d].cents -= amountCents;\n    creditors[c].cents -= amountCents;\n    if (debtors[d].cents === 0) d += 1;\n    if (creditors[c].cents === 0) c += 1;\n  }\n  return transfers;\n}\n"},{"id":"no-balance-check","code":"// Returns the transfers that settle a group: [{ from, to, amountCents }].\n// balances: { name: cents }, positive = is owed, negative = owes.\n// Throws if the balances don't add up to zero.\nfunction settleUp(balances) {\n  const entries = Object.entries(balances);\n  const sum = entries.reduce((total, [, cents]) => total + cents, 0);\n  if (false) throw new Error('Balances must add up to zero');\n\n  const debtors = [];\n  const creditors = [];\n  for (const [name, cents] of entries) {\n    if (cents < 0) debtors.push({ name, cents: -cents });\n    else if (cents > 0) creditors.push({ name, cents });\n  }\n  const largestFirst = (a, b) => b.cents - a.cents || a.name.localeCompare(b.name);\n  debtors.sort(largestFirst);\n  creditors.sort(largestFirst);\n\n  const transfers = [];\n  let d = 0;\n  let c = 0;\n  while (d < debtors.length && c < creditors.length) {\n    const amountCents = Math.min(debtors[d].cents, creditors[c].cents);\n    transfers.push({ from: debtors[d].name, to: creditors[c].name, amountCents });\n    debtors[d].cents -= amountCents;\n    creditors[c].cents -= amountCents;\n    if (debtors[d].cents === 0) d += 1;\n    if (creditors[c].cents === 0) c += 1;\n  }\n  return transfers;\n}\n"},{"id":"unary-sign","code":"// Returns the transfers that settle a group: [{ from, to, amountCents }].\n// balances: { name: cents }, positive = is owed, negative = owes.\n// Throws if the balances don't add up to zero.\nfunction settleUp(balances) {\n  const entries = Object.entries(balances);\n  const sum = entries.reduce((total, [, cents]) => total + cents, 0);\n  if (sum !== 0) throw new Error('Balances must add up to zero');\n\n  const debtors = [];\n  const creditors = [];\n  for (const [name, cents] of entries) {\n    if (cents < 0) debtors.push({ name, cents: +cents });\n    else if (cents > 0) creditors.push({ name, cents });\n  }\n  const largestFirst = (a, b) => b.cents - a.cents || a.name.localeCompare(b.name);\n  debtors.sort(largestFirst);\n  creditors.sort(largestFirst);\n\n  const transfers = [];\n  let d = 0;\n  let c = 0;\n  while (d < debtors.length && c < creditors.length) {\n    const amountCents = Math.min(debtors[d].cents, creditors[c].cents);\n    transfers.push({ from: debtors[d].name, to: creditors[c].name, amountCents });\n    debtors[d].cents -= amountCents;\n    creditors[c].cents -= amountCents;\n    if (debtors[d].cents === 0) d += 1;\n    if (creditors[c].cents === 0) c += 1;\n  }\n  return transfers;\n}\n"},{"id":"swapped-direction","code":"// Returns the transfers that settle a group: [{ from, to, amountCents }].\n// balances: { name: cents }, positive = is owed, negative = owes.\n// Throws if the balances don't add up to zero.\nfunction settleUp(balances) {\n  const entries = Object.entries(balances);\n  const sum = entries.reduce((total, [, cents]) => total + cents, 0);\n  if (sum !== 0) throw new Error('Balances must add up to zero');\n\n  const debtors = [];\n  const creditors = [];\n  for (const [name, cents] of entries) {\n    if (cents < 0) debtors.push({ name, cents: -cents });\n    else if (cents > 0) creditors.push({ name, cents });\n  }\n  const largestFirst = (a, b) => b.cents - a.cents || a.name.localeCompare(b.name);\n  debtors.sort(largestFirst);\n  creditors.sort(largestFirst);\n\n  const transfers = [];\n  let d = 0;\n  let c = 0;\n  while (d < debtors.length && c < creditors.length) {\n    const amountCents = Math.min(debtors[d].cents, creditors[c].cents);\n    transfers.push({ from: creditors[c].name, to: debtors[d].name, amountCents });\n    debtors[d].cents -= amountCents;\n    creditors[c].cents -= amountCents;\n    if (debtors[d].cents === 0) d += 1;\n    if (creditors[c].cents === 0) c += 1;\n  }\n  return transfers;\n}\n"}],"starter":{"js":"// Stryker's report on this suite:\n//   [Survived] MethodExpression       Math.min(...) -> Math.max(...)\n//   [Survived] ConditionalExpression  if (sum !== 0) -> if (false)\n//   [Survived] UnaryOperator          cents: -cents -> cents: +cents\n// and a bug from last month's code review:\n//   [Survived] from/to swapped in each transfer\ndescribe('settleUp', () => {\n  test('returns no transfers when everyone is settled', () => {\n    expect(settleUp({ Ana: 0, Ben: 0 })).toEqual([]);\n  });\n\n  test('returns one transfer for two people', () => {\n    expect(settleUp({ Ana: 500, Ben: -500 })).toHaveLength(1);\n  });\n});\n"},"solution":{"js":"const sum = (xs) => xs.reduce((a, b) => a + b, 0);\n\ndescribe('settleUp', () => {\n  test('returns no transfers when everyone is settled', () => {\n    expect(settleUp({ Ana: 0, Ben: 0 })).toEqual([]);\n  });\n\n  test('the debtor pays the creditor the full amount', () => {\n    expect(settleUp({ Ana: 500, Ben: -500 })).toEqual([\n      { from: 'Ben', to: 'Ana', amountCents: 500 },\n    ]);\n  });\n\n  test('one debtor pays two creditors exactly what each is owed', () => {\n    const transfers = settleUp({ Ana: -1000, Ben: 600, Cai: 400 });\n\n    expect(transfers).toEqual([\n      { from: 'Ana', to: 'Ben', amountCents: 600 },\n      { from: 'Ana', to: 'Cai', amountCents: 400 },\n    ]);\n    expect(sum(transfers.map((t) => t.amountCents))).toBe(1000);\n  });\n\n  test('refuses balances that do not add up to zero', () => {\n    expect(() => settleUp({ Ana: 500, Ben: -400 })).toThrow('add up to zero');\n  });\n});\n"},"checks":[{"id":"passes","suite":"subject"},{"id":"catches-min-to-max","suite":"min-to-max"},{"id":"catches-no-balance-check","suite":"no-balance-check"},{"id":"catches-unary-sign","suite":"unary-sign"},{"id":"catches-swapped-direction","suite":"swapped-direction"}],"title":"Kill the surviving mutants","prompt":"<p>This <code dir=\"ltr\">settleUp</code> suite passes, but Stryker’s report (copied into the starter’s comment) lists three mutants that <strong>survived</strong> it: <code dir=\"ltr\">Math.min</code> changed to <code dir=\"ltr\">Math.max</code>, the zero-sum check disabled, and the minus sign dropped when recording what a debtor owes. Last month’s code review found a fourth bug the suite also misses: <code dir=\"ltr\">from</code>/<code dir=\"ltr\">to</code> swapped in each transfer.</p>\n<p>Write the tests that kill all four, without breaking the suite on the real code. The subject is the <code dir=\"ltr\">settleUp</code> you built in the TDD lesson.</p>\n","hints":["<code dir=\"ltr\">toHaveLength(1)</code> can’t tell a correct transfer from a wrong one. Assert the whole transfer with <code dir=\"ltr\">toEqual([{ from: 'Ben', to: 'Ana', amountCents: 500 }])</code>; that kills two mutants at once.","<code dir=\"ltr\">Math.min</code> and <code dir=\"ltr\">Math.max</code> agree whenever the debtor owes exactly what the creditor is owed. Use one debtor and two creditors with different amounts, such as <code dir=\"ltr\">{ Ana: -1000, Ben: 600, Cai: 400 }</code>.","For the disabled check, pass balances that don’t add up to zero and expect a throw: <code dir=\"ltr\">expect(() =&gt; settleUp({ Ana: 500, Ben: -400 })).toThrow()</code>."],"explanation":"<p>Each survivor maps to one missing assertion. The swapped direction and the dropped minus sign both survived because <code dir=\"ltr\">toHaveLength(1)</code> never looked inside the transfer; asserting the exact transfer kills both (one produces Ana paying Ben, the other a transfer of -500). <code dir=\"ltr\">Math.max</code> survived because in every test the debtor owed exactly what the creditor was owed, so <code dir=\"ltr\">min</code> and <code dir=\"ltr\">max</code> agreed; with one debtor owing 1000 and creditors owed 600 and 400, <code dir=\"ltr\">max</code> pays Ben 1000 and stops, so the exact list fails. The zero-sum check survived because no test ever sent unbalanced input. Notice what you <strong>didn’t</strong> need: a test of the sort order for large groups. That mutant would be equivalent in many cases, and pinning it would test the implementation, not the behaviour.</p>\n","checkLabels":{"passes":"All your tests pass against the real settleUp","catches-min-to-max":"Kills the Math.min → Math.max mutant","catches-no-balance-check":"Kills the mutant with the zero-sum check disabled","catches-unary-sign":"Kills the mutant that drops the minus sign for debtors","catches-swapped-direction":"Kills the mutant that swaps from and to"}},"words":688,"readMinutes":3}