Skip to content
All posts
·6 min read·Shashank Bindal

Did the AI Weaken Your Tests? Catching Deleted, Skipped and Loosened Tests With quell gate

quell gate compares test files with their merge-base version and fails on deleted, skipped, .only and loosened tests. A real diff and its real output.

TL;DR. quell gate --diff main compares every changed test file with the version at the merge-base and fails with exit code 5 if tests were deleted, skipped, narrowed with .only, or had their assertions loosened. We built a throwaway repository where the suite stayed green after an "agent" removed two tests, skipped one and softened one. The gate flagged all four.

Captured with quelltest@0.3.1 from npm on Windows, Node.js 24.11.0, node:test.

Why would a change weaken tests?

An agent with a failing test and a task to finish has two ways out: fix the code, or change the test. Changing the test is often a legitimate edit. It is also the cheapest way to turn a red build green, and a diff of a few hundred lines hides it well.

gate does not judge intent. It reports test-file changes that reduce what the suite can catch, so a human or a CI rule decides whether each one is justified.

What does a weakened change look like?

The baseline test file on main had five tests for a applyDiscount function. On a branch, the file became this:

 test("applies a percentage discount", () => {
-  assert.equal(applyDiscount(200, 10), 180);
+  assert.ok(applyDiscount(200, 10));
 });

-test("caps the discount at 50 percent", () => {
+test.skip("caps the discount at 50 percent", () => {
   assert.equal(applyDiscount(100, 90), 50);
 });

-test("rejects negative prices", () => {
-  assert.throws(() => applyDiscount(-1, 10), RangeError);
-});
-
 test("treats a missing percent as no discount", () => {
   assert.equal(applyDiscount(100), 100);
 });
-
-test("accepts a price of exactly zero", () => {
-  assert.equal(applyDiscount(0, 10), 0);
-});

npm test on that branch:

ℹ tests 3
ℹ pass 2
ℹ fail 0
ℹ skipped 1

Green. Now the gate:

npm install --save-dev quelltest
npx quell gate --diff main
Gate failed: 4 issue(s) in 1 changed test file(s)
  ✖ tests/discount.test.js :: rejects negative prices — test deleted
  ✖ tests/discount.test.js :: accepts a price of exactly zero — test deleted
  ✖ tests/discount.test.js :: applies a percentage discount — assertions weakened (0 -> 1 weak, e.g. equality replaced by ok/truthy/not-null)
  ✖ tests/discount.test.js :: caps the discount at 50 percent — test is now skipped (skip/todo/x-prefixed)
exit=5

For machines, --json gives each issue a kind (test-deleted, assertions-weakened, test-skipped), a severity, a test name and a line. --sarif produces SARIF 2.1.0 for GitHub code scanning.

What does the gate catch?

  • Deleted tests and deleted test files, and a test deleted and then rewritten (shown as a replacement).
  • Skipped tests: skip, todo, x-prefixed names and { skip: true }.
  • A newly added .only or describe.only, which silently skips every other test.
  • Removed or reduced assertions.
  • Equality loosened to ok, truthy, defined or not-null.
  • Changed expected values, as a review item.

Git renames are handled, and an unparsable test file is an error, not a pass.

What happens when the expected value changes, or .only appears?

On another branch we changed 50 to 10 in one expectation. That is sometimes a legitimate fix and sometimes not, so by default it is a review item and the gate passes:

Gate passed: 1 changed test file(s) checked, no weakened tests
  ? tests/discount.test.js :: caps the discount at 50 percent — expected value changed in assertion 1 ([100,90,50] -> [100,90,10])
exit=0

With --strict, the same change fails:

Gate failed: 1 issue(s) in 1 changed test file(s)
  ? tests/discount.test.js :: caps the discount at 50 percent — expected value changed in assertion 1 ([100,90,50] -> [100,90,10])
exit=5

Adding .only to a test is always an error:

  ✖ tests/discount.test.js :: accepts a price of exactly zero — `.only` silently skips every other test

How do I allow a deliberate change?

Sometimes a test should get weaker: a requirement was dropped. Say so in the test, with a reason, so the exception is visible in review:

test("accepts a price of exactly zero", () => {
  // quell-allow-weaken zero price now handled by checkout, see ADR 41
  assert.ok(applyDiscount(0, 10));
});

Without the comment the gate fails on assert.ok replacing assert.equal. With it:

Gate passed: 1 changed test file(s) checked, no weakened tests

Can I also check that the remaining tests still catch faults?

Yes, gate and verify answer different questions. After the weakening above, we ran the changed test file through verify with a floor of 60:

npx quell verify tests/discount.test.js --source src/discount.js --min-score 60
Not verified: mutation score 29% is below the required 60% [copy], 1 ignored
  survived: guard removed (line 2)
  survived: throw removed (line 3)
  survived: || -> && (line 2)
  survived: < -> <= (line 2)
  survived: - -> + (line 7)

On main, the same command printed Verified: test killed 7/7 mutants (100%) [copy], 1 ignored. The "1 ignored" is an equivalent mutant marked in the source, which the verify post explains. The score fell from 100% to 29%. The gate told us what changed; verify told us what the suite can no longer see.

How do I run it in CI or in an agent loop?

- uses: actions/checkout@v4
  with: { fetch-depth: 0 }
- uses: actions/setup-node@v4
  with: { node-version: 22 }
- run: npm ci
- run: npx quell gate --diff "origin/${{ github.base_ref }}"

fetch-depth: 0 matters because the gate needs the merge-base. For agents, the same command can run in a Claude Code Stop hook; see the MCP and hook post. More recipes are in CI integration and the gate reference.

What are the limits?

  • A test rewritten from scratch under a new name with different assertions is not caught.
  • Weakening done by changing the source instead of the tests is not caught. That is verify's job.
  • Logic inside helper functions that the test calls is not analysed.
  • The gate looks at the shape of assertions. In our run, replacing assert.equal(x, 0) with assert.ok(x === 0) raised no issue, which is reasonable because the strength is the same, but it shows the check is syntactic.
  • Changed expected values are review items by default, because the tool cannot know whether the new value is the correct one.
  • Exit code 5 is the gate's. Your CI must treat any non-zero exit as failure, which most do.

Read the limits page and the changelog.

FAQ

What does --diff main compare against?

The merge-base between your working tree and the ref you pass. Use origin/main in CI.

Does the gate run my tests?

No. It reads files and their git history. verify runs tests.

Does it work with Jest and Vitest?

The gate does not need a runner: it reads test files and git history. The demo here used node:test. We did not run the same diff through Jest, Vitest or Mocha, so check your own suite's shape on a scratch branch first.

Can an agent bypass it by adding quell-allow-weaken?

Yes, and it will show up in the diff for a reviewer, with a reason. That is the design: the exception is explicit, not silent.

Sources