by vinicq
LLM skill that finds false-green tests across Python/TS/JS/Robot using the J1-J6 protocol (S-codes + the static catalog superset). Claude Code, Codex, Antigravity CLI, Cursor, CLI.
# Add to your Claude Code skills
git clone https://github.com/vinicq/falsegreen-skillGuides for using mcp servers skills like falsegreen-skill.
falsegreen-skill is an open-source mcp servers skill for AI coding assistants such as Claude Code, Codex CLI, and ChatGPT, built by vinicq. LLM skill that finds false-green tests across Python/TS/JS/Robot using the J1-J6 protocol (S-codes + the static catalog superset). Claude Code, Codex, Antigravity CLI, Cursor, CLI. It has 2 GitHub stars.
falsegreen-skill's catalog security scan is still queued. You can run an instant dependency and prompt-injection check now with the "Scan for vulnerabilities" button above.
Clone the repository with "git clone https://github.com/vinicq/falsegreen-skill" and add it to your Claude Code skills directory (see the Installation section above). falsegreen-skill ships a SKILL.md manifest, so compatible agents can discover and load it automatically.
falsegreen-skill is primarily written in JavaScript. It is open-source under vinicq on GitHub, so you can review or fork the full source.
Yes. SkillsLLM lists many other MCP Servers skills you can browse and compare side by side. Open the MCP Servers category from the badge at the top of this page, or use the Related Skills and comparison links further down to weigh falsegreen-skill against similar tools.
No comments yet. Be the first to share your thoughts!
Top skills in this category by stars
Unlocks once the catalog security scan passes (runs nightly).
The deep catalog scan for this skill is still queued. Run an instant dependency check now instead.
LLM-based semantic analysis for false-positive test detection. This skill judges whether a test genuinely verifies correct behavior, across Python, TypeScript, JavaScript, and Robot Framework - and catches semantic patterns no static tool can see.
For Python, this skill applies the complete falsegreen catalog directly — all structural and semantic patterns — without requiring the static scanner to run first. The companion falsegreen scanner is a faster batch alternative for Python; results must be consistent with this skill. For TypeScript and JavaScript, this skill is the primary detection tool.
Invoke by attaching a test file or pasting a test snippet and asking for false-positive analysis.
A test is useful only if it fails when the code breaks. Every pattern this skill looks for is a variation on tests that do not fail: tests that pass while the code is wrong, tests that check the wrong thing, or tests that borrow correctness from elsewhere.
This is one skill with one source of truth (the J1-J6 judgments and the catalog
in reference.md), used in three directions. Pick the intent from what the user asks:
Generation does not have its own rules. The test you write must pass the same J1-J6 it would be judged by, so it is born non-false-green. The developer chooses the language and the pyramid level; the catalog is the guard either way. The fix path reuses that same loop: a proposed fix is just an authored test that has to survive its own review before the host runs the gate.
Work through these steps in order. Do not skip steps.
If the user supplies a conventions: block, incorporate it before applying any judgments.
This block declares project-specific context that affects the look-alike rules.
Example:
conventions:
custom_assertion_helpers:
- conftest.assert_model_valid() # wraps assert + validation logic
- helpers.assert_valid_uuid()
test_layer_overrides:
- tests/integration/ is web-layer # apply C6 HTTP exemption here
excluded_codes:
- C8 # project uses Decimal, not float
The conventions block does NOT disable severity levels. It only extends the look-alike exemptions in reference.md. HIGH findings that survive after exemptions are still reported as HIGH.
If no conventions block is provided, proceed directly to Step 1.
Identify:
.robot / .resource, keyword-driven) · Tavern (*.tavern.yaml, API) ·
Gherkin/BDD (.feature: Cucumber.js / behave / pytest-bdd)How to tell the level. Read it from the signals; do not guess. Apply this precedence (strongest signal wins), in order:
responses/respx/requests-mock/vcr/moto/fakeredis/mongomock/pyfakefs/
unittest.mock/monkeypatch; JS/TS msw/nock/fetch-mock/axios-mock-adapter/
jest.mock/vi.mock/aws-sdk-client-mock - the test is unit/component even when a
real client (requests, axios, boto3, SQLAlchemy) is imported. The mock IS the boundary.| Signal | Unit (and component) | Integration | E2E |
|---|---|---|---|
| Path / marker | tests/unit/, none, co-located *.test.ts |
tests/integration/, *.int.test.ts, @pytest.mark.integration/functional/db/django_db |
tests/e2e/, cypress/e2e/, *.e2e-spec.ts, *.cy.ts, @pytest.mark.e2e/acceptance, playwright.config |
| Doubles | mock/patch or intercept the boundary (rule 1) | real client/driver, no double | no double at all |
| Component render | RTL / Vue Test Utils / Angular TestBed / cy.mount / Storybook with mocked network, jsdom/happy-dom - counts as unit for the oracle |
— | full app in a real browser |
| API | none, or HTTP intercepted | in-process test client (FastAPI/Starlette TestClient, Flask/Django/DRF client, supertest request(app), Nest TestingModule) or a real client to a live URL; gRPC/GraphQL/WebSocket; RequestsLibrary/RESTinstance; asserts status/body |
full app behind the browser |
| Database / store | repository or in-memory fake, mocked client | real ORM/driver (SQLAlchemy, Django ORM, Prisma, TypeORM, Drizzle, Knex, psycopg, asyncpg, mongoose, pymongo, redis, ioredis), testcontainers, session/transaction; sqlite :memory: and *-memory-server lean integration |
DB reached through the UI |
| Other I/O | mocked (moto / @mock_aws, aws-sdk-client-mock) |
real queue (Kafka, RabbitMQ, SQS, bullmq), object storage (real S3, or a LocalStack / testcontainers emulator - a real service over the wire, not moto), email/SMTP, subprocess, cache | — |
| UI / browser | none | none | Playwright page./expect(page), Cypress cy.visit, WebdriverIO browser., Puppeteer, Selenium driver., Robot SeleniumLibrary/Browser/AppiumLibrary; selectors, navigation |
A conventions: block (Step 0) with test_layer_overrides wins over all of this. Markers like
smoke/slow/asyncio/anyio are level-neutral: do not read a layer from them. When an
explicit marker/path says one level but the test mocks the whole boundary, trust the reality
(rule 1) and note the mismatch. When no signal is present, treat it as unit and say so.
Robot Framework: the level is dominated by the imported Library in *** Settings *** -
SeleniumLibrary / Browser / AppiumLibrary → E2E; RequestsLibrary / RESTinstance / RPA.HTTP →
API integration; DatabaseLibrary → DB; SSH/FTP/Imap/Process → system integration. Robot suites
are rarely unit. The full per-framework cue list lives in reference.md.
Why the level matters: in E2E/UI tests the presence of a response, page, or element IS the assertion at that layer - do not flag it as a weak check (affects C6 and C14). The level itself can be the smell: a real API or database call inside a test that presents as a unit test is a mystery-guest / over-mocking-inverted finding (J3/J6), not a valid integration test.
See reference.md for framework- and level-detection cues.
If the language is Python, scan the file against all falsegreen patterns
before proceeding to the semantic judgments. These patterns are organized in
reference.md under "Python" by family. Apply them in order:
| Family | Codes | What to look for |
|---|---|---|
| A — never checks | C1, C2, C2b, C2c, C3, C4, C4b, C20, C21, C38, C39, C43, C45, C49, C50, C51, C59, CC | assertion unreachable, missing, swallowed, uncollected, name-shadowed, returned-not-asserted, discarded-comparison statement, empty/multi-call raises-warns context, captured log/output never asserted, skipped mid-test, empty parametrize |
| B — weak/always-true | C5, C6, C6b, C6c, C7, C8, C8b, C9, C11a, C13, C13b, C14, C16, C18, C25, C34, C42, C44, C52, C55, C56, C57 | tautology, truthiness-only, self-compare, broad exception, string repr, generator/lambda always truthy, numeric tautology, membership self-confirmation, mock-rooted compare, never-awaited coroutine, unconfigured Mock attribute |
| C — checks own setup | C19, C28, C29, C48 | pytest.raises wraps too much, binding unread, env mutation, test-mode flag flipped then asserted |
| D — external state | C17, C23, C24, C27, C30, C31, C32, C35 | skip-on-failure, hard path, shared mutable, try/pass, flaky |
| E — wrong thing | C33, C36, C37, C41 | metric not asserted, fail without reason, duplicate case, assert on a None-returning mutator |
| Optional / diagnostic (opt-in) | C22, D1, D3, D4, D5, D6, D7, D8, M2 | apply only when user requests diagnostic pass |
Report each structural finding with its code number and confidence level before proceeding to Steps 3-6.
Relationship with the falsegreen scanner: if the user has already run
falsegreen <file> and provides its output, use that as the structural pass
result and proceed directly to Step 3 for findings the scanner marked as
needing semantic review. If no scanner output is provided, apply Step 2 fully.
For TypeScript and JavaScript, apply the TS/JS catalog below, then proceed to Step 3.
The companion static scanner for these languages is falsegreen-js. It shares the C-codes where the smell is the same concept and adds JS-specific codes. Apply the catalog, then proceed to Step 3 for the semantic judgments no static pass can make.
Load reference.md first (mandatory for non-Python). The table below is a
summary. The full JS-series, the Robot R-codes, and the PL config-audit codes are
defined only in reference.md. Read the matching language section in full before
judging any TypeScript, JavaScript, or Robot Framework test - do not rely on this
summary table alone. For Python, the structural catalog in Step 2 above is
complete on its own.
The S-series is separate, and it applies to every language including Python.
S1-S18 and S21 sit in reference.md, in the section
## Patterns only the semantic pass can catch (AI-only), which sits above the
per-language sections, so loading a language section alone skips all of them. Load
that section together with the "Look-alikes - do NOT flag" paragraph that closes
it. When the host budget cannot hold both the semantic
section and a language section, load the two floor fragments instead of the prose:
fragments/semantic-cases-compact.md (a row per S-code) plus
fragments/semantic-exemptions.md (the same look-alike exemptions, so the floor
needs no reference.md read at all). That is about 7.5 KiB against about 13 KiB.
The language section is what you defer and pull per finding, never the semantic
floor.
| Family | Codes | What to look for |
|---|---|---|
| A - never checks | C2, C2b, C21, CC, JS2, JS4, JS6, JS9, JS11 | empty test, no assertion, every assertion conditional, commented-out assertion, expect() without a matcher, skipped (it.skip/xit), empty describe, dead literal branch, swallowed try/catch |
| B - weak / always-true | C5, C7, C8, C9, C16, C18, JS3 | tautology, self-compare, exact float equality, toThrow() with no error type, time/randomness, stringified equality, snapshot-only |
| C - focus / async | JS1, JS5, JS7 | focused test (it.only/fit), async query/event not awaited, assertion in a non-awaited callback |
| D - duplicate | C37 | duplicate it.each/test.each case |
| F - query without assert | JS13 | getBy*/queryBy* query as a loose statement, result never asserted |
| Optional / diagnostic (opt-in) | D1, D3, D4, D6, D7, M2 | maintainability; apply only when the user requests a diagnostic pass |
If the user has run falsegreen-js <file> and provides its JSON output, use that as
the structural pass and proceed to Step 3 for findings that need semantic review.
Full TS/JS pattern detail and look-alikes: see reference.md.
For Robot Framework, Gherkin/BDD, and Tavern there is no summary table here: the
full catalog (the Robot R-codes and the shared C-codes, plus the Gherkin/Tavern
secondary passes) lives only in reference.md. Load it and apply the matching
language section before Step 3, together with the language-agnostic S-series
described in Step 2b. The companion Robot scanner is
robotframework-falsegreen;
the skill mirrors its codes as a superset. If the user has run it and provides
output, use that as the structural pass, then proceed to Step 3.
Before judging the expected value, classify each test:
| Class | Meaning | Oracle |
|---|---|---|
| spec/TDD | the test is the spec; code must match it | the test itself |
| characterization | intentionally freezes current behavior | current output is the oracle |
| regression | records a known bug fix | the bug report is the oracle |
| behavior | verifies a production rule or contract | spec / docstring / types |
A failing TDD test is not a false positive. A labeled characterization snapshot is not a frozen bug. Misclassifying here causes false alarms.
Judge each test across J1-J6. Flag only the first judgment that fails; do not double-report the same root cause.
J1: Does the assertion run? Does at least one assertion execute when the test is run normally? An assertion inside a branch that never fires, or after an unconditional return, passes vacuously.
J2: Is the expected value from an independent oracle?
Is the expected value derived from the spec, the API contract, or independent
human judgment, NOT from the current code output? If the test asserts
result == current_implementation(), both sides agree on the same wrong
number. An assertion that re-implements the production formula has the same
problem.
J3: Is the real unit under test? Is the test verifying the actual production unit, or a mock of it? Mocking the function/class under test and then asserting the mock value is not a test of the production code.
J4: Does the assertion verify enough, and the right thing? Does the assertion check a meaningful property of the result? Checking only that the result is truthy, or that an exception was raised without verifying its type, does not protect the behavior being tested.
J5: Is the test coupled to implementation internals? Does the test fail when internals change - private methods, internal state, call order - even though the public contract still holds?
J6: Does the test pass in isolation? Does the test depend on execution order, shared mutable state, or fixtures set up by a sibling test? A test that passes only in a specific order is not reliably testing anything.
Then screen every S-code, on every file, whatever the language. S1-S18 and
S21 are part of this step, not a preamble to it: walk them one by one against
## Patterns only the semantic pass can catch (AI-only) in reference.md, check
each candidate against the look-alike block that closes that section, then move
on. Step 4 is not complete until every S-code has been considered. The S-series
does not belong to Step 2b or 2c, so a Python run that skipped both still runs it
in full.
Case 18 (expected value contradicts what the code should do) is the highest- stakes finding: it means the test is freezing a bug as "correct". Before reporting it:
Never report case 18 based on gut feeling or pattern-matching alone.
For each finding, output:
{code} ({J}) - {confidence: HIGH | LOW} - {language} - {level: unit|integration|e2e|fixture} - {intent: spec|char|regression|behavior|scaffold}
Test: {function name, line range}
Finding: {one sentence describing what is wrong}
Evidence: {the specific line(s) that triggered this}
Oracle: {for case 18 only: cite the independent oracle}
Fix hint: {where and how to improve the code or test, one sentence}
{code} is any catalog id: a semantic case (CASE-10/11/12/15/18), a structural C-code (C*),
a JS/TS code (JS*), a Robot code (R*), or a semantic S-code (S*). {J} is the judgment that
failed (J1-J6). Intent is the classification from Step 3. Include it in every finding - it is
required for dataset analysis.
The level and intent axes carry two extra options for non-behavioral findings:
level: fixture (or a role: note such as role: testdata/role: example/role: perf)
for a finding in a file that is data, a shared resource, an example, or a perf fixture rather
than a behavioral suite - see the Robot file-role look-alikes in reference.md.intent: scaffold for an unimplemented placeholder (an empty stub, a TODO-only body, a
generated skeleton that was never filled in) where the finding is about the missing
implementation, not a wrong oracle.Then a summary block:
SUMMARY
Tests reviewed: N
Findings: M (H high, L low)
Clean: N-M
Use HIGH only when there is no plausible legitimate interpretation. Precision over recall: a wrong HIGH finding is worse than a missed LOW one.
Run this step only when the report contains 3 or more findings of the same code or pattern.
Add a note at the end of the SUMMARY block:
Pattern note: {code or pattern} appears {N} times. If intentional in this project,
add it to the conventions: block (Step 0) to suppress future findings.
Do not run Step 7 for reports with fewer than 3 findings. Do not call a separate model - append the note to the existing SUMMARY using what you already know from the analysis.
| Case | Judgment | Name | Caught by |
|---|---|---|---|
| 10 | J3 | Mocks the unit under test | Semantic |
| 11 | J2/J3 | Asserts the value fed to the mock | Semantic |
| 12 | J2 | Re-implements the production formula | Semantic |
| 15 | J6 | Passes only if another test ran first | Semantic |
| 18 | J2 | Expected value contradicts what the code should do | Semantic + adversarial verify |
Structural codes are handled by the static scanners - falsegreen for Python (56 C-codes) and falsegreen-js for TypeScript/JavaScript (shared C-codes plus the JS-series; see reference.md for the full emitted set). This skill adjudicates scanner findings when review is needed, and handles the same patterns directly for any language. The five semantic cases above need the LLM regardless of language.
Full case catalog with language examples: see reference.md.
Everything above is analysis mode (Mode A): given a test, judge it. When the user asks to write or create tests instead, switch to authoring mode (Mode B). The catalog becomes a generation guard: a test you write must pass the same J1-J6 it would be judged by, so you do not ship a false-green shape. The guard is on the shape, not the oracle's truth: it stops a test built from the code's current output, but it cannot tell a hand-written wrong oracle from a right one. That stays the user's responsibility.
Mode B runs two ways. In an editor host (Claude Code, Cursor, Gemini, Codex) it
elicits the missing answers interactively (Steps A0-A1), can render every
requested language in one pass, and repeats the self-check until clean (A4). On
the CLI, falsegreen-skill generate <spec-file> --lang <language> renders a
written test-spec - the oracle already supplied in the file - into one language
and self-checks it once (revising once). It does not elicit, so a spec with no
oracle is refused rather than guessed; and when its bounded revision cannot reach
clean it emits the draft with a FAILED/UNVERIFIED verdict and a non-zero exit,
rather than withholding. Use the host path when the oracle still has to be
discovered; use the CLI when it is already written down.
Before eliciting anything, run one triage pass. It answers three questions at once, in a single step - not a separate agent:
Do not generate a test from the code's current output - that produces a characterization test, which is false-green by design. Ask the user for what only they can supply:
If the user already gave any of these, do not re-ask it.
Capture the answers in one canonical spec, independent of language, conforming to
schema/test-spec.json:
level: unit | integration | e2e
unit: <function / endpoint / page under test>
scenario: <one behavior, stated as a sentence>
arrange: [<preconditions>]
act: <the call or interaction>
oracle:
source: spec | contract | example
expected: <value or condition, derived from the source - NOT from the code>
doubles: [database, network, clock] # only for unit/integration
One spec, then rendered into every requested language. The spec is the single source so the tests stay equivalent across stacks.
Multiple levels for one feature. Do not write a single test that "covers all levels" - that is a category error. Each level tests a different thing with a different oracle and different doubles. When the user wants unit AND integration AND E2E, produce one spec per level (a small scenario x level matrix):
Rules: elicit the oracle per level (it differs); do not repeat the unit-level assertion at e2e (wrong layer, redundant); respect pyramid shape - if the user asks for an e2e test where a unit test would catch the defect, say so before generating (inverted-pyramid warning). A real API/DB call is valid at integration but is itself a smell at unit (J3/J6).
Render the spec into each framework, using the canonical renders in
examples/authoring/ (apply-discount.spec.yaml rendered to .py, .test.js,
.test.ts, and .robot) as the few-shot template for a green-for-real test in
each language. The oracle form depends on the level:
oracle.expected.Robot emits a .robot test; extract any reusable step into a .resource
keyword (never put *** Test Cases *** in a .resource - that is R3).
This is the unification: do not invent a separate check. Run Mode A (Steps 1-6) on the test you just generated, at its level, as if a developer had handed it to you for review. Concretely, confirm it trips no catalog code:
Apply the precision-first rules in fragments/precision-rules.md during this
self-review, and use the BAD cases in examples/<language>/family_* and
examples/<language>/semantic_cases.* as the negative catalog: the generated
test must not resemble any of them.
If the Mode A pass returns any finding, revise the test and run Mode A again. In a host, repeat until the analysis is clean and only emit a test that passes its own review - that is what makes generation and validation one skill, not two. The CLI bounds this to one revision (it is a command, not an agent loop): if the test still trips a HIGH false-green finding it emits the draft with a FAILED verdict and exit 1, and if the self-check itself cannot run it reports UNVERIFIED and exit 3. The CLI never presents an unchecked or still-flagged test as verified.
For each generated test, output the language, level, the cited oracle, the test code, and one line confirming it passes the self-check. End with the canonical test-spec so the user can regenerate it in another language later.
Mode A judges a test; Mode B writes one. Mode C takes a finding from a falsegreen report and proposes a stronger test that closes it. It reuses Mode B end to end: a proposed fix is an authored test, so it is generated against the oracle and self-validated by running Mode A (Steps 1-6) over it until clean.
The boundary is explicit: the skill proposes the fix and the validation contract; it does not run the gate. Proving that the strengthened test fails when the code breaks is mutation testing's job, and that needs an executable environment the skill does not have (the skill does not run tests). The host or developer runs the bidirectional gate; the skill hands them a contract to fill.
Take one finding (schema/finding.json): its case, judgment, language,
level, the evidence lines, and, for case 18, the cited oracle. The finding
says what is wrong; the fix has to make the test able to fail on exactly that.
Derive a schema/test-spec.json from the finding and the existing test, then run
Mode B Steps A2-A4 on it. The oracle stays independent of the code (never lift the
expected value from current output, that re-freezes the bug). Strengthen at the
judgment that failed:
Run Mode A over the proposal (Step A4). If it trips any catalog code, revise and re-run until clean. Only propose a test that passes its own review.
Alongside the proposed test, emit the gate contract the host will run, conforming
to schema/fix-validation.json. The skill fills the finding reference and the
intended tier; the host fills clean_replica, mutated_replica, and the
verdict after running the gate.
The gate is bidirectional and the rule is fixed: run the strengthened test on a
clean replica (must pass) and on a mutated replica where the bug class is
reintroduced (must fail). accept requires clean_replica=pass AND
mutated_replica=fail; any other combination is reject. A test that still
passes on the mutated replica has not closed the finding. Two cost tiers, host's
choice: suite-rerun (rerun the whole suite, cheap and coarse) or
targeted-mutation (a focused mutant on the unit, costlier and precise). Tooling
is the host's: mutmut or cosmic-ray for Python, Stryker for JS/TS. The adjudication
rule, the tiers, and the flaky case live in reference.md (F7).
This bidirectional gate is the SENTINEL / Pizzini contribution credited in
CREDITS.md: a proposed fix must pass the original suite and fail on a mutation
before being accepted.
The CLI can run this gate locally (V1, Python/pytest only). In an editor host
the skill proposes and the host runs the gate. The npm CLI adds an opt-in
falsegreen-skill fix <test-file> --case <code> --line <n> --sut <file> command
that runs the whole gate on a clean replica: it asks the LLM for a test-file-only
patch, then runs parse (py_compile), preserve (pytest against the real SUT),
and a line-scoped mutation gate (a built-in operator on the SUT line; full mutmut
integration is deferred to a later version). It never auto-applies the patch and never edits the
SUT. Without --sut, or with --cheap (parse + preserve only), it degrades to
propose-only / unvalidated and labels the output as not proven. V1 fixes the
mechanical findings (C2b/C20/C21/C5/C7); JS/TS/Robot fix paths and the deep
semantic cases (10/11/12/18) are v2. The honest limit holds: the gate proves the
fix catches the targeted mutant, not every possible bug.
Output the finding being fixed, the proposed test (language, level, cited oracle,
code), the line confirming it passes its own Mode A self-check, and the
schema/fix-validation.json contract for the host to run. State plainly that acceptance
waits on the host's gate result; the skill does not run it.
--json (conforming to schema/report.json); persist it by redirecting
(... --json > report.json) and wire --fail-on-high into CI (exit 2). A
persisted report is a run artifact: keep it in a gitignored path
(.falsegreen/, reports/), never commit it.tests/unit/, tests/integration/,
tests/e2e/, a *.test.ts beside the source, a .robot suite with shared
steps in a .resource. Propose the path and confirm with the user; never
invent a new dump location.@pytest.mark.skip, @pytest.mark.xfail(strict=True),
or @unittest.skip that has no assertion body is NOT C2/C5. The marker stops
it from running (skip) or fails it on XPASS (strict xfail). A plain
@pytest.mark.xfail (no strict=) is exempt ONLY when the project turns on
strict xfail globally - xfail_strict = true in pytest config (pytest.ini,
[tool.pytest.ini_options], setup.cfg) or -o xfail_strict=true - because the
marker then inherits strict and an XPASS fails the run. Otherwise a plain xfail
still executes and an XPASS keeps exit status 0, so a no-assertion test stays
false-green. Check the pytest config (or a conventions: override) first.@given, @hypothesis, or @fuzz that have no
explicit assert are NOT C2. These frameworks generate and check
assertions internally.expectTypeOf(v).toEqualTypeOf<T>() in Vitest is a compile-time type
assertion. Not C5. Do not flag it.Tie-break (C9 / C28 / S17, same J4 family). When more than one of these would fire on the
same pytest.raises / toThrow, report only the most specific code; do not stack them. A broad
pytest.raises(Exception) (or toThrow() with no type) that is followed by an assertion on the
bound message - assert str(exc.value) == ... / exc.value.<attr> - fires NOTHING: the message
assertion narrows the type to the SUT's contract, so C9, C28, and S17 are all satisfied.
Severity is a ceiling, not a floor. The severity listed for each code (HIGH / LOW) is the maximum. Intent classification (Step 3) can only LOWER it - a HIGH code on a deliberate characterization/spec/scaffold test drops to LOW or is withdrawn. It can never RAISE a code above its catalog severity.
For a case 18 finding that requires high confidence (blocking a deploy, cited in a report), run a two-pass adversarial check:
Pass 1 (finder): Identify the expected value and the oracle. Report the finding with the cited oracle.
Pass 2 (refuter): Given the case 18 finding, argue that the expected value is actually correct. Consider: is this a characterization test? Is the oracle you cited authoritative for this specific test? Does the domain have a convention that makes the expected value correct?
If the refuter provides a plausible argument, downgrade to LOW or withdraw. Report case 18 HIGH only when the refuter cannot mount a credible defense.
schema/fix-validation.json contract; the host or
developer runs the test on the clean and mutated replicas (mutmut / cosmic-ray /
Stryker). The npm CLI is the exception: falsegreen-skill fix runs the gate
locally for Python/pytest (V1), still propose-only and never touching the SUT.ruff's PT rules
(Python) or eslint-plugin-jest (JS/TS) also cover this ground.LLM-based semantic analysis for false-positive test detection. Companion to falsegreen, the Python static scanner.
For Python, this skill applies the complete falsegreen catalog directly - all structural and semantic patterns - via LLM analysis, without requiring the static scanner to run first. For TypeScript, JavaScript, and Robot Framework it is the primary detection tool. It is a superset of the three static scanners (falsegreen, falsegreen-js, robotframework-falsegreen) plus semantic patterns only an LLM can detect.
The falsegreen family (install the one for your stack):
| Tool | Stack | Install | Package |
|---|---|---|---|
| falsegreen | Python / pytest | pip install falsegreen |
PyPI |
| falsegreen-js | JS / TS | npm i -D falsegreen-js (npx falsegreen-js) |
npm |
| robotframework-falsegreen | Robot Framework | pip install robotframework-falsegreen |
PyPI |
| falsegreen-skill | semantic LLM pass | npx falsegreen-skill analyze <path> |
npm |
This is an LLM skill that reads your tests and flags the false-green ones: tests that stay green even when the code they cover is wrong. It catches the semantic cases the static scanners cannot, because it reads the test as text and works out what the test was meant to prove.
A test like this passes forever, no matter what the code does:
# before - false-green: asserts the mock back to itself
def test_discount(mock_rate):
mock_rate.return_value = 0.1
result = apply_discount(100, mock_rate)
assert result == mock_rate.return_value # passes for ANY result, even a wrong one
The fix is an independent expected value, one the code did not produce:
# after - the test can now fail when apply_discount is wrong
def test_discount():
result = apply_discount(100, rate=0.1)
assert result == 90 # 10% off 100, computed by hand from the spec
The skill reads the first version and reports it as a J2 finding (the expected value is borrowed from the code, not from an independent source) with the line, the reason, and a fix hint. Three steps to try it:
npx falsegreen-skill analyze tests/test_discount.py,
or, in an editor host, just ask it to "analyze this test for false-positive smells".For a step-by-step walkthrough of all three modes with runnable examples and flow diagrams, read the user guide. The full catalog, the judgments, and the per-language reference live in the docs site and in reference.md. For a visual architecture overview, host and language routing plus the Mode A/B/C flow diagrams, see docs/architecture.md.
A test suite with 100% green tests is not a proof of correctness. It is a proof that no test failed - which is a different thing. Tests can pass permanently not because the code is right, but because the test never checks anything meaningful.
Static analysis tools catch some of these cases. Linters like ruff or
flake8-pytest-style catch syntax-level patterns: a bare assert True, a
missing assert call, an unreachable block. Mutation testing tools like
mutmut probe whether tests actually fail when the code changes. Both
approaches have limits: linters cannot reason about test intent, and
mutation testing requires the code to run.
This skill fills the gap between linters and mutation testing. It reads the test as text, reconstructs the intent, and asks six structural questions about whether the test can actually fail. The questions are derived from the taxonomy of false-positive test patterns collected in CREDITS.md.
The core insight: a test is useful if and only if there exists some incorrect implementation that would cause it to fail. If no such implementation exists, because the assertion is unreachable, tautological, or verifies the mock instead of the code, the test is structurally green regardless of whether the production code is correct.
One rule underlies every judgment: a test is useful only if it can fail when the code breaks.
The six-judgment framework (J1-J6) makes this rule concrete:
| # | Question | Catches |
|---|---|---|
| J1 | Does the assertion run? | Dead assertions, vacuous loops, swallowed failures |
| J2 | Is the expected value from an independent oracle? | Echo mocks, formula re-implementation, spec contradictions |
| J3 | Is the real unit under test, not a mock of it? | Mock-the-SUT, self-confirming literals |
| J4 | Does the assertion verify enough? | Truthiness-only, len > 0, repr coupling, broad raises |
| J5 | Is the test coupled to implementation internals? | Positional mock args, private method testing |
| J6 | Does the test pass in isolation, without ordering? | Shared mutable state, test-order dependency |
A test is flagged HIGH only when the first failed judgment has no plausible legitimate interpretation. A test is flagged LOW when the smell is likely but has plausible intent. Everything else is PASS.
Precision over recall. One wrong flag on a legitimate test costs more goodwill than a missed smell. Exemptions are explicit:
isinstance, .exists(), .is_dir()) are not weak assertions.Full protocol: SKILL.md.
Family A - The test never checks anything
| Code | Pattern | Example |
|---|---|---|
| C1 | Assert inside if/for that may not run |
if items: assert items[0].valid when items can be [] |
| C2 | No assertion at all | test body contains only setup calls |
| C2b | Calls SUT but discards result | result = process(x), result never asserted |
| C3 | Assert inside try whose except swallows it |
except Exception: pass catches AssertionError |
| C4 | Test function nested inside another function | pytest does not collect inner defs |
| C4b | Test class with __init__ |
pytest skips classes that have __init__ |
| C20 | Assertion after unconditional return/raise |
dead code, never runs |
| C21 | Every assert is conditional, none runs unconditionally | all asserts inside if/else branches |
| CC | Commented-out assertion | # assert result == 42 |
Family B - The check is weak or always true
| Code | Pattern | Example |
|---|---|---|
| C5 | Always-true check | assert True, assert (a, b) (non-empty tuple) |
| C6 | Truthiness / len > 0 / substring in str() |
assert result, assert len(x) > 0 |
| C6b | Positional mock arg via computed index | call_args.args[expected_args.index("target")] |
| C7 | Self-comparison | assert name == name |
| C8 | Exact float equality | assert ratio == 3.14159 |
| C9 | pytest.raises too broad or no match= |
with pytest.raises(Exception) |
| C11a | Self-confirming literal | product.price = 100; assert product.price == 100 |
| C13 | Mock assertion uncalled or misspelled | mock.assert_called_once (no parens) |
| C13b | @patch without autospec=True |
typos in kwargs pass silently |
| C14 | Golden file written from actual output | first run records any output as truth |
| C16 | Depends on wall clock, random, or sleep |
datetime.now() unfrozen, time.sleep() |
| C18 | str()/repr() comparison |
assert str(user) == "User(Alice, 30)" |
| C25 | @pytest.mark.xfail without strict=True |
XPASS silently accepted |
| C34 | Suboptimal assertion form | == True, == None, not x in y, len == 0 |
Family C - The test checks its own setup
| Code | Pattern | Example |
|---|---|---|
| C19 | pytest.raises wraps multiple calls |
setup call inside raises block may be the one that raises |
| C28 | pytest.raises binding variable never read |
as exc: but exc never asserted |
| C29 | os.environ mutated directly |
os.environ["KEY"] = "x" without monkeypatch |
Family D - Green depends on outside factors
| Code | Pattern | Example |
|---|---|---|
| C17 | pytest.skip() inside br |