plugins/unslop-tests/skills/unslop-tests/SKILL.md
Cuts AI tells from test code: tests that pass without proving anything, tautological assertions, mock-only tests, hardcoded waits, coverage theater, vague names, swallowed errors, retries used as fixes. Use whenever test code is written, changed, or reviewed, including tests produced as a side effect of a feature task, and when the request mentions "review these tests", "are these tests any good", "this test always passes", "this suite is flaky", or "clean up these tests". Must always apply to test code.
npx skillsauth add jaktestowac/awesome-copilot-for-testers unslop-testsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Cut AI tells from test code.
A generated test suite reads well, runs green, and often proves nothing. That combination is the whole problem: the suite looks like coverage, gets reviewed like coverage, and fails to notice the bug it was written for. This skill names the tells so they can be caught by sight, then proves the important ones by running code.
Style slop wastes a reader's time. Test slop hides defects. Treat it accordingly.
Use the files or diff the caller gives you. Otherwise use the working diff against the base branch, default main, including uncommitted changes.
When auditing a whole suite rather than a diff, say how much of it you actually read. A sampled audit reported as a full one is its own kind of slop.
Not all tells are equal. Rank every finding.
Report tiers in that order. A page of naming nits above one silently passing test buries the thing that matters.
Examples use neutral test / expect pseudocode. ./resources/framework-spellings.md has how each tell is spelled in Vitest, Jest, Playwright, and Mocha.
expect(total(items)).toBe(items.reduce((s, i) => s + i.price, 0)). Use a literal from an independent source: the spec, a worked example, a number a human calculated. If you cannot write the literal without running the code, you do not yet know what the function should return.expect(result).toBeDefined(), toBeTruthy(), not.toBeNull(), expect(list.length).toBeGreaterThanOrEqual(0), expect(true).toBe(true). These pass for almost any return value. Assert the value.expect(typeof id).toBe('string'), expect(Array.isArray(rows)).toBe(true). The type system already promises this. Assert what the string is and what is in the array.expect(fn).toThrow() also passes when fn throws because the function name is misspelled. Assert the error type and the load-bearing part of the message.catch with nothing forcing the try to throw. The day the call stops throwing, the test goes green. Assert on the rejection directly, or declare the expected assertion count.await on the act or on a promise-returning assertion. The test function returns, the runner reports a pass, and the assertion runs after nobody is listening.for (const c of cases) expect(fn(c.in)).toBe(c.out). The failure never says which input broke. Use the runner's parameterized API so each row reports as its own test.if (result.items.length) expect(...). A branch that can skip the assertion is a test that can pass by accident. Assert the precondition, then assert the behavior.waitForTimeout(2000), sleep(500), a bare setTimeout in a test body. Too slow when the machine is fast, flaky when CI is loaded. Wait for the condition, not the clock.new Date() inside the expectation, or Date.now() compared against another Date.now(). Freeze time or inject a clock. Anything asserting on "today" breaks at midnight in some timezone.Math.random(), a data faker with no seed, a generated UUID inside the assertion. Seed it or inject the generator, and record the seed in the failure output.retries: 3, a per-test retry, a raised timeout on something that flakes. Containment, not a fix. The race is still in the product and the suite now reports it as a pass. Reproduce, diagnose, fix the cause.expect(save).toHaveBeenCalledWith(order) as the whole test. It proves a call happened, not that anything is now true. Assert what a caller can observe.calls getUserById, works correctly, should handle edge cases, test 1, happy path. State the behavior and the condition: returns null when the user is soft-deleted. A reader should know what broke from the failure line alone.// check that the total is 100 above expect(total).toBe(100). Delete it. Keep a comment only when it says why 100 is the right number.// Arrange, // Act, // Assert labelling three one-line sections. Blank lines already show the structure. Keep the labels only where a section is long enough to need them.expect(items).toHaveLength(7) says nothing about why seven is right. Name it, or derive it visibly from the input.skip, a commented-out test, or a deleted assertion with no issue link and no name attached. Coverage quietly removed.only left in (T1). One focused test silently disables the rest of the file, and CI reports green.nth(0), generated class names, or text that a copy edit will change. Use a role, label, or test id.slow() or a raised timeout as a flake fix (T1). Same as pattern 20, wearing different clothes.expect(page.url()).toContain('/checkout') proves navigation, not that checkout works. Assert something on the page.Removing tells is half the job. A test stripped of every pattern above can still be pointless. The tests worth keeping share these traits.
A test-quality writeup that sounds right is worthless. It reads as convincing whether or not it is true. So do not hand back the reading. For every Tier 1 finding, get as far down this ladder as is cheap and say where you stopped.
file:line.Rung 4 is the whole point and it is usually one edit and one command. Any Tier 1 finding you did not get to rung 4, mark unproven and do not report it as settled. Do not round up.
./resources/mutation-check.md has the per-runner commands, how to choose which behavior to break, and what to do when the whole file is too slow to iterate on.
Two more checks worth running on anything you changed:
Lead with what is broken, not with what you read.
file:line, what you broke, the command, the output showing it stayed green, and the fix.file:line and the tell by name../resources/audit-report-template.md has the shape filled in.
Do not reshape production code to make a test honest without saying so. Hard-to-test code is a finding, and fixing it is its own change.
./resources/test-slop-before-after.md - worked before/after pairs for every Tier 1 pattern./resources/mutation-check.md - how to run the break-it check per runner, and how to pick what to break./resources/framework-spellings.md - how each tell is written in Vitest, Jest, Playwright, and Mocha, with the fix./resources/audit-report-template.md - the report shapewriting-unit-tests - the authoring workflow. Use it to write the tests, use this skill to judge them afterwardswriting-unit-tests-quick - the compact authoring path for routine teststest-driven-development - when the test should come first, which removes several of these patterns by constructiondesigning-test-data - when the fixtures and boundary values need real designcode-review-advanced - the broader review; this skill is its test lensstatic-code-analysis-typescript - when the production code, not the test, is the real problemThis skill is complete when:
file:linetesting
Tests the customization assets themselves - skills, prompts, custom agents, instructions - the way a product is tested: activation cases that check an asset fires when it should and stays quiet when it should not, output-contract cases, safety cases, collision cases between assets competing for the same trigger, a weighted rubric scored blind, and a baseline-versus-candidate gate before an edit ships. Use when a skill is edited and nobody knows whether behaviour changed, when two skills fight over the same request, when a description is being tuned for discoverability, when a collection has grown past manual spot-checking, or when the request mentions skill evals, prompt regression, or "does this skill actually work".
development
Shapes QA output for the person who has to act on it: result and blocker in the first two lines, one decision per report, findings ordered by what they cost, the long artifact in a file and the decisions in the message, and magnitude stated in units the reader can count. Use when a report is accurate but nobody acts on it, when a finding set is too long to read under time pressure, when the same findings must be retold for a developer, a release manager, and an on-call engineer, or when the request mentions "too long", "make this readable", "just tell me what to do", "so what", or "summarize this for stakeholders". Pairs with unslop-answers, which makes the same report honest.
testing
Verifies that the lines and branches a change actually touched are executed by tests, using LCOV or Cobertura diff coverage instead of whole-repo percentages, and escalates uncovered high-risk changes into a blocking finding. Use when a pull request needs a coverage gate that unrelated tests cannot satisfy, when total coverage looks healthy but the diff is untested, when wiring diff coverage into CI, or when someone claims a change is covered because the suite is green.
development
Cuts AI tells from test code: tests that pass without proving anything, tautological assertions, mock-only tests, hardcoded waits, coverage theater, vague names, swallowed errors, retries used as fixes. Use whenever test code is written, changed, or reviewed, including tests produced as a side effect of a feature task, and when the request mentions "review these tests", "are these tests any good", "this test always passes", "this suite is flaky", or "clean up these tests". Must always apply to test code.