# quell gate: detect weakened tests in a change

> quell gate compares changed test files with their merge-base version and fails on deleted, skipped, .only, removed or loosened assertions. Flags and SARIF.

Canonical page: https://quelltest.com/docs/npm/gate

`quell gate --diff REF` compares every test file changed since the merge-base with `REF` against its previous version and fails (exit code 5) when a change makes the suite weaker: tests deleted or skipped, `.only` added, assertions removed, or equality loosened to a truthy check. Use it in pull requests and as an agent hook, because an agent that writes both the code and the tests can turn a red check green by weakening a test.

```bash
npx quell gate --diff origin/main
```

```text
Gate failed: 3 issue(s) in 1 changed test file(s)
  ✖ tests/pay.test.js :: charge returns something — test deleted
  ✖ tests/pay.test.js :: charge subtracts the amount — assertions weakened (0 -> 1 weak, e.g. equality replaced by ok/truthy/not-null)
  ✖ tests/pay.test.js :: charge rejects zero — test is now skipped (skip/todo/x-prefixed)
```

## What does it compare?

The gate runs `git merge-base REF HEAD` and compares the **working tree** (committed, uncommitted and untracked changes) with the files at that merge-base. A test file is any `.js`, `.jsx`, `.mjs`, `.cjs`, `.ts` or `.tsx` file named `*.test.*` or `*.spec.*`, or inside a `test`, `tests`, `__tests__`, `__mocks__`, `e2e` or `cypress` directory. It parses both versions with Babel and matches tests by their full name (`describe > test`). Repeated names stay distinct (`name #2`). Newly added test files are not checked, since there is nothing to weaken.

You need the history: in CI use `fetch-depth: 0` and a base ref such as `origin/main` that exists locally. An invalid ref or a failing git command exits 2.

## What does it catch?

| Kind | Severity | Triggered by |
| --- | --- | --- |
| `test-deleted` | error | A test is gone and no similar test replaced it. |
| `test-file-deleted` | error | A test file was deleted. |
| `test-skipped` | error | A test is newly skipped: `.skip`, `.todo`, `xit`, `xtest`, `xdescribe`, or `{ skip: true }` / `{ todo: true }` options. |
| `test-only` | error | `.only` was added, including `describe.only`, which silently skips the rest of the file. |
| `assertions-removed` | error | The test has fewer assertions than before. |
| `assertions-weakened` | error | More weak assertions than before at the same count: equality replaced by `ok`, `toBeTruthy`, `toBeDefined`, a plain `assert(x)`, or a negated `toBeNull`/`toBeUndefined`/`toBeFalsy`/`toBeNaN`. |
| `unparsable` | error | The base or changed test file cannot be parsed, so it cannot be compared. |
| `test-replaced` | review | A test was deleted and another with the same subject (the functions its assertions call) was added. Compare the pair. |
| `expected-value-changed` | review | An assertion kept its method but its literals changed (`toBe(70)` to `toBe(71)`). That is where "make the test match the bug" hides. |

Errors make the gate fail. Review items are printed with a `?` and pass unless you use `--strict`:

```bash
npx quell gate --diff main --strict
```

```text
Gate failed: 1 issue(s) in 1 changed test file(s)
  ? tests/pay.test.js :: charge subtracts the amount — expected value changed in assertion 1 ([30,100,70] -> [30,100,71])
```

Renamed tests are paired first by their assertions, so renaming a test or moving a test file (git reports a rename) is not a deletion. `expect.assertions(n)`, `expect.any(...)`, asymmetric matchers and `t.mock`/`t.plan` are not counted as assertions.

## How do I allow an intentional change?

Put `// quell-allow-weaken <reason>` anywhere inside the test body (a comment on any line of the test). The test is exempt from the weakening checks (skip, `.only`, removed and loosened assertions, changed values). Use it when you deliberately replace an assertion, and write the reason for the reviewer.

```js
test('charge rejects zero', () => {
  // quell-allow-weaken: covered by the property test in charge.prop.test.js
  expect(() => charge(0, 100)).toThrow();
});
```

## What output formats exist?

| Flag | Output |
| --- | --- |
| (none) | Human-readable lines, as above. |
| `--json` | `{ "issues": [...], "ok": false, "checked": 1 }`. Each issue has `file`, `test`, `kind`, `severity` (`error` or `review`), `line` and `message`. |
| `--sarif` | SARIF 2.1.0 with tool name `quelltest-gate`; each issue kind is a rule, errors have level `error` and review items level `note`. |

Upload SARIF with `github/codeql-action/upload-sarif` for inline annotations; see the [CI guide](https://quelltest.com/docs/npm/ci.md).

## What does it not catch?

- A test rewritten from scratch under a new name with different, weaker assertions, unless it shares assertions or a subject with the deleted one.
- Weakening done by changing the source instead of the test.
- Logic hidden in helper functions that the test calls.
- Weakness in newly added test files: use [`quell verify`](https://quelltest.com/docs/npm/verify.md) for those.

The gate checks that a change did not make existing tests weaker. It does not say whether the remaining tests are good: that is what `verify` is for.

## How do I run it in a hook or CI?

```bash
npx quell gate --diff origin/main || exit 2
```

Exit code 5 means weakened tests; exit 2 means a usage or git error. For a Claude Code Stop hook, see [MCP server and hooks](https://quelltest.com/docs/npm/agents.md).
