writing-tests
Skill1.4k starsChanged 38 days agoMITView on GitHub
Gates whether a new test should exist and forces it to be efficient, protecting CI from low-value test bloat. Use before any change to what a pytest, Jest, or Playwright test asserts or sets up, down to one fixture or one assertion added to an existing block. Front-loads the value bar (every test must catch a realistic regression no existing test already catches; extend the nearest existing test before writing a new standalone one; test behavior through the public interface, not implementation details; collapse near-duplicates into parameterized cases) and the efficiency bar (deterministic, isolated, fast; pick the cheapest test level; Django TestCase over TransactionTestCase; no sleeps, no real network; no time bombs from absolute dates left to age against the real clock; no database a test never uses). Includes a "don't write it" decision tree. For fixing an existing flaky test use `/fixing-flaky-tests`; after this gate says a Playwright test is warranted, use `/playwright-test` for mechanics.
39 other projects use a version of this, 2 of them edited.
Adopted by
Steady: 0 new projects took it in the last 4 weeks, none the 4 weeks before.thin data
Most recently taken up by
- scheron/Dailytheir own version, first seen Nov 8, 2026
- mcp-use/mcp-usetheir own version, first seen Nov 8, 2026
- yuri-xyz/skillstheir own version, first seen Nov 8, 2026
- lox/agent-skillstheir own version, first seen Nov 7, 2026
- JetBrains/intellij-communitytheir own version, first seen Nov 7, 2026
- iliaal/ai-skillstheir own version, first seen Nov 7, 2026
Who's talking about amplifthq/openmeld
- sudosubin/agents.nix“agent-skills.github.amplifthq.openmeld: 0.3.294 → 0.3.295”GitHub · Nov 13, 2026
- sudosubin/agents.nix“agent-skills.github.amplifthq.openmeld: 0.3.293 → 0.3.294”GitHub · Nov 12, 2026
- sudosubin/agents.nix“agent-skills.github.amplifthq.openmeld: 0.3.291 → 0.3.293”GitHub · Nov 11, 2026
- sudosubin/agents.nix“agent-skills.github.amplifthq.openmeld: 0.3.290 → 0.3.291”GitHub · Nov 6, 2026
- sudosubin/agents.nix“agent-skills.github.amplifthq.openmeld: 0.3.288 → 0.3.290”GitHub · Nov 3, 2026
The biggest 5 of 31 mentions linking to the repository that our crawl found. Each line is their own headline.
See all 31
Talked about on GitHub (31 times).
On GitHub · 31
- Nov 13, 2026agent-skills.github.amplifthq.openmeld: 0.3.294 → 0.3.295 — sudosubin/agents.nix0 reactions
- Nov 12, 2026agent-skills.github.amplifthq.openmeld: 0.3.293 → 0.3.294 — sudosubin/agents.nix0 reactions
- Nov 11, 2026agent-skills.github.amplifthq.openmeld: 0.3.291 → 0.3.293 — sudosubin/agents.nix0 reactions
- Nov 6, 2026agent-skills.github.amplifthq.openmeld: 0.3.290 → 0.3.291 — sudosubin/agents.nix0 reactions
- Nov 3, 2026agent-skills.github.amplifthq.openmeld: 0.3.288 → 0.3.290 — sudosubin/agents.nix0 reactions
26 more on GitHub
- Nov 2, 2026agent-skills.github.amplifthq.openmeld: 0.3.284 → 0.3.288 — sudosubin/agents.nix0 reactions
- Nov 1, 2026agent-skills.github.amplifthq.openmeld: 0.3.281 → 0.3.284 — sudosubin/agents.nix0 reactions
- Oct 29, 2026agent-skills.github.amplifthq.openmeld: 0.3.279 → 0.3.281 — sudosubin/agents.nix0 reactions
- Oct 29, 2026agent-skills.github.amplifthq.openmeld: 0.3.278 → 0.3.279 — sudosubin/agents.nix0 reactions
- Oct 28, 2026agent-skills.github.amplifthq.openmeld: 0.3.276 → 0.3.278 — sudosubin/agents.nix0 reactions
- Oct 28, 2026agent-skills.github.amplifthq.openmeld: 0.3.274 → 0.3.276 — sudosubin/agents.nix0 reactions
- Oct 28, 2026agent-skills.github.amplifthq.openmeld: 0.3.273 → 0.3.274 — sudosubin/agents.nix0 reactions
- Oct 26, 2026agent-skills.github.amplifthq.openmeld: 0.3.272 → 0.3.273 — sudosubin/agents.nix0 reactions
- Oct 23, 2026agent-skills.github.amplifthq.openmeld: 0.3.271 → 0.3.272 — sudosubin/agents.nix0 reactions
- Oct 22, 2026agent-skills.github.amplifthq.openmeld: 0.3.270 → 0.3.271 — sudosubin/agents.nix0 reactions
- Oct 22, 2026agent-skills.github.amplifthq.openmeld: 0.3.267 → 0.3.270 — sudosubin/agents.nix0 reactions
- Oct 21, 2026agent-skills.github.amplifthq.openmeld: 0.3.264 → 0.3.267 — sudosubin/agents.nix0 reactions
- Oct 17, 2026[Candidate] amplifthq/openmeld/release/openmeld-cli-skill - openmeld-cli — ProSkillsMD/proskills0 reactions · 1 comment
- Oct 16, 2026agent-skills.github.amplifthq.openmeld: 0.3.263 → 0.3.264 — sudosubin/agents.nix0 reactions
- Oct 16, 2026[Candidate] amplifthq/openmeld/.claude/skills/release-runbook - release-runbook — ProSkillsMD/proskills0 reactions · 1 comment
- Oct 16, 2026[Candidate] amplifthq/openmeld/.claude/skills/openmeld-changelog - openmeld-changelog — ProSkillsMD/proskills0 reactions · 1 comment
- Oct 16, 2026[Candidate] amplifthq/openmeld/.claude/skills/docs-lifecycle-scan - docs-lifecycle-scan — ProSkillsMD/proskills0 reactions · 1 comment
- Oct 16, 2026agent-skills.github.amplifthq.openmeld: init at 0.3.263 — sudosubin/agents.nix0 reactions
- Oct 15, 2026[Candidate] amplifthq/openmeld/.agents/skills/writing-tests - writing-tests — ProSkillsMD/proskills0 reactions · 1 comment
- Oct 15, 2026[Candidate] amplifthq/openmeld/.agents/skills/react-doctor - react-doctor — ProSkillsMD/proskills0 reactions · 1 comment
- Oct 15, 2026[Candidate] amplifthq/openmeld/.agents/skills/postgresql-planning - postgresql-planning — ProSkillsMD/proskills0 reactions · 1 comment
- Oct 15, 2026[Candidate] amplifthq/openmeld/.agents/skills/openmeld-changelog - openmeld-changelog — ProSkillsMD/proskills0 reactions · 1 comment
- Oct 15, 2026[Candidate] amplifthq/openmeld/.agents/skills/fixing-flaky-tests - fixing-flaky-tests — ProSkillsMD/proskills0 reactions · 1 comment
- Oct 15, 2026[Candidate] amplifthq/openmeld/.agents/skills/debugging-ci-failures - debugging-ci-failures — ProSkillsMD/proskills0 reactions · 1 comment
- Oct 7, 2026feat: share selected connection health and bounded native API reads — amplifthq/open-connector0 reactions · 2 comments
- Oct 6, 2026feat: support selected GitHub connections for Git streams — amplifthq/open-connector0 reactions · 1 comment
Used with
Projects with it also use reviewing-code, commits and 5 more.
Other files people use alongside this one.
Files found in the same public projects as a version of this one, more often than they turn up anywhere else. Each project counts once, large collections count for less, and this file's own repository is left out.
bostonaholicSkillreviewing-code
Code review methodology for evaluating implementation changes. Use when reviewing code changes for quality, design, correctness, and maintainability. Focuses on changes made during implementation using Conventional Comments for clear, actionable feedback.
Also in 4 projects that use this
JetBrainsSkillcommits
Use this skill whenever the user asks to commit changes, write or fix a commit message, amend or rename a commit, or do a workflow that includes committing in the IntelliJ repository. This is a thin repo-specific overlay: use IntelliJ commit format, write full commit messages by default, and keep requested suffixes such as IJ-MR trailers in a final separate paragraph.
Also in 3 projects that use this
OpenAISkillrunning-tests
Guide for running tests in the openai-dotnet repository. Use this when asked to run, debug, or validate tests, or when writing new tests. Explains test modes (Playback, Record, Live), how to identify recorded vs non-recorded tests, environment variable configuration, and what to do when recordings are missing or stale.
Also in 3 projects that use this
BlockSkilldebugging
Debugging guidelines and best practices
Also in 5 projects that use this
VercelSkilltechnical-writing
Write, edit, review, or audit user-facing documentation for the eve repository. Use for changes under docs/, documentation tied to eve APIs or CLI behavior, docs work based on Slack or support feedback, and requests to make eve docs clearer, more natural, or less AI-patterned while verifying claims against current source, tests, CLI help, public releases, and repository conventions.
Also in 3 projects that use this
SentrySkillsecurity-review
Finds exploitable application security vulnerabilities in code changes. Use for Warden security scans, appsec review, OWASP-style checks, authentication or authorization bugs, injection, XSS, SSRF, path traversal, secrets, unsafe crypto, webhook verification, open redirects, or sensitive data exposure.
Also in 3 projects that use this
Other versions39 projects use a version of this.
Word for word · 1
- amplifthq/opentag★ 1.4k100% same
Edited · 2
- PostHog/posthog★ 40k91% same
- PostHog/posthog-foss★ 72891% same
Wrote their own version · 36
- remotion-dev/remotion★ 72ksame name
- JetBrains/intellij-community★ 21ksame name
- quarkusio/quarkus★ 16ksame name
- mcp-use/mcp-use★ 11ksame name
- DetachHead/rebased★ 6.5ksame name
- chattocorp/chatto★ 2.9ksame name
- starkware-libs/cairo★ 1.9ksame name
- bholmesdev/hubble.md★ 1.5ksame name
- cashew-labs/libretto★ 898same name
- saffron-health/libretto★ 883same name
26 more not listed
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository12 other files it gives its agents.
- AGENTS.mdAGENTS.md
- .claude/CLAUDE.mdCLAUDE.md
- CLAUDE.mdCLAUDE.md
- debugging-ci-failuresSkill
- fixing-flaky-testsSkill
- openmeld-changelogSkill
- postgresql-planningSkill
- react-doctorSkill
- docs-lifecycle-scanSkill
- openmeld-changelogSkill
- release-runbookSkill
- wake-traceSkill
The file
Show its rules and text
We split this skill into the rules it gives an agent
25 rules. 0 are common, 15 are distinctive, and 10 are very specific to this project.
0
Common
Most of the 38 edited versions we read keep these.
15
Distinctive
This author's own choices. Worth a look.
10
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 18
Before writing any test, answer both in one sentence each.
- Distinctive: line 24
If you can't answer it concretely — name the bug, the code path, the input that would break — do not write the test.
- Distinctive: line 32
If your test doesn't map to one of them, be skeptical it's worth keeping.
- Very specific: line 41
When your case is a variation of that behavior, extend it: a @parameterized case in Python, a test.each row in Jest.
- Distinctive: line 49
Search before you write. If you haven't looked for the nearest existing test, you can't answer this question.
- Distinctive: line 50
If you haven't looked for the nearest existing test, you can't answer this question.
- Distinctive: line 57
Don't test getters, setters, constants, dataclass field assignment, that Django saved a row, that DRF serialized a field, or that a library …
- Distinctive: line 62
Test _observable behavior through the public interface_ (return value, persisted state, emitted event, HTTP response), not the choreography …
- Very specific: line 67
If a new test is a variation of an existing one, it's a @parameterized case (Python) or a test.each row (Jest) — not a new test function.
- Distinctive: line 71
Don't add tests to hit a number.
- Distinctive: line 72
If the only reason to test a branch is the coverage report, the branch probably doesn't need a test — or the code is dead and should be …
- Very specific: line 75
Never read or regex-parse one language's source from another language's test — a Python test that Path(...).read_text()s a .ts file and …
- Distinctive: line 79
When the answer is "don't write it," the right move is often to extend an existing test (add a parameterized case) or delete code rather …
- Distinctive: line 97
If the only way to exercise your logic is to stand up a database, a request, and a render, the logic is tangled with its dependencies.
- Distinctive: line 101
If logic is in (or can be moved to) a pure function, test the function — don't stand up a DB, a request, or a render.
- Very specific: line 114
Use TransactionTestCase only when the regression genuinely requires committed transaction boundaries that TestCase hides.
- Very specific: line 120
Test the serializer directly and assert on .errors:
- Distinctive: line 136
Don't mock your own internal helpers (that's how change-detector tests are born).
- Very specific: line 137
Person/group/cohort data: use the helpers in posthog/test/persons.py (create_person, create_group, create_group_type_mapping, …
- Distinctive: line 144
Prefer logic tests over component renders (see the ladder above).
- Very specific: line 148
Prefer getByText, getByTestId/data-attr selectors, or getByLabelText.
- Very specific: line 163
Do not assume a debounce, setTimeout, kea breakpoint, or fast mock will still be pending on the next line.
- Very specific: line 170
If anything under test measures that date against now — an age, a window, a "recent" flag, an expiry — the assertion holds today and fails …
- Very specific: line 174
Ask this of every absolute date in a test, not only of an explicit freeze_time: _what does this assert when today is a year past it?_ If …
- Distinctive: line 197
If you can't write that line, you've found a test that shouldn't be in the PR.
“Common” is measured: kept by at least 6 in 10 of the 38 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Writing tests worth keeping
The rationale and the same rules in human-facing form live in the handbook: Backend coding conventions › Testing (docs/published/handbook/engineering/conventions/backend-coding.md).
This skill is the operational gate — run it before writing tests. It carries the decision procedure plus a catalog of the bug shapes we actually ship.
The gate covers added coverage, not new test functions. One fixture and one assertion added to an existing block goes through the same two questions, because that is the shape this skill asks you to prefer.
The gate: two questions
Before writing any test, answer both in one sentence each.
1. Does it earn its place?
What realistic regression does this test catch that no existing test already catches?
If you can't answer it concretely — name the bug, the code path, the input that would break — do not write the test. "Increases coverage", "good practice", and "the function exists" are not answers.
A good answer sounds like: "if someone makes parse_filters drop the team_id clause, this fails" or "empty-cohort input used to 500; this locks in the 400."
That is a test worth keeping.
Aim each test at a failure mode we actually hit, not a hypothetical. The bugs PostHog ships and reverts cluster into a handful of shapes — cataloged with the test that catches each, and the failure modes no unit test should, in references/mistakes-we-make.md. If your test doesn't map to one of them, be skeptical it's worth keeping.
2. Where does it go?
Passing the first question buys the coverage, not a new test function. Find the test that already covers the nearest behavior and answer:
Why can't this be a case in that test?
When your case is a variation of that behavior, extend it: a @parameterized case in Python, a test.each row in Jest.
A new standalone test is what you write when extending doesn't work, and you should be able to say why in a sentence: the setup differs, the behavior belongs to a different unit, or no relevant test exists.
"It's cleaner as its own test" isn't a reason on its own; name which of those three applies.
Extend to remove duplication, not to save setup time.
A parameterized case is still its own test invocation, so setUp and beforeEach run for it just as they would for a standalone function.
That sets the limit too: fold in variations of the same behavior, and don't bolt assertions about unrelated behavior onto a test that already passes.
Search before you write. If you haven't looked for the nearest existing test, you can't answer this question.
Don't write it — the five no's
Most low-value tests fall into one of these. Recognize and skip them.
- Trivial / framework behavior. Don't test getters, setters, constants, dataclass field assignment, that Django saved a row, that DRF serialized a field, or that a library does what its docs say. You're testing someone else's code, not yours.
- Change-detector tests. A test that just mirrors the implementation — asserting which private methods were called, in what order, with mocks wired to match the current code — fails on every refactor and catches no real bug. Test observable behavior through the public interface (return value, persisted state, emitted event, HTTP response), not the choreography that produces it. See Change-Detector Tests Considered Harmful and Prefer Testing Public APIs.
- Redundant coverage.
Ten tests that exercise the same path with different data are one parameterized test.
If a new test is a variation of an existing one, it's a
@parameterizedcase (Python) or atest.eachrow (Jest) — not a new test function. AI-generated suites bloat here first: hundreds of near-identical cases turning a 30s suite into 3 minutes. - Coverage-chasing. Don't add tests to hit a number; an uncovered line is information, not a defect. If the only reason to test a branch is the coverage report, the branch probably doesn't need a test — or the code is dead and should be deleted instead.
- Cross-language source-scraping.
Never read or regex-parse one language's source from another language's test — a Python test that
Path(...).read_text()s a.tsfile and matchescategory: '...', a TS test that greps a.pyfile, and so on. It couples two trees through a brittle string match that breaks on edits that change nothing about behavior (a rename, a reformat, a moved file, a comment), and it proves nothing about runtime — the two sides never actually run together in the test. If two sides genuinely must agree on a set of values, give them one source of truth — a generated artifact or a checked-in data file both import — and assert against that. Otherwise let the drift surface where the two sides really meet (an API contract test, a rendered output, a round-trip), not by scraping the other language's source for strings.
When the answer is "don't write it," the right move is often to extend an existing test (add a parameterized case) or delete code rather than test it. Both shrink the suite's surface.
If you write it — weight it down the pyramid
A test that earns its place still has to be cheap. Cost is a ladder; each rung is roughly an order of magnitude slower and flakier than the one below:
pure function / unit → kea logic test → Django TestCase → ClickHouse-backed test → Playwright e2e
cheapest most expensiveThe goal is a ratio, not a cap: many tests at the bottom, very few at the top. A thousand pure unit tests are cheaper and more reliable than ten that boot Django, which are cheaper than one Playwright run. Want more coverage? Add it at the bottom.
When something is hard to test cheaply, that's a design signal — extract, don't escalate. If the only way to exercise your logic is to stand up a database, a request, and a render, the logic is tangled with its dependencies. Pull it into a pure function or a kea logic and test that directly: you get a faster test and better-factored code at once. Escalating to the next rung is the last resort, not the default.
- If logic is in (or can be moved to) a pure function, test the function — don't stand up a DB, a request, or a render.
- Frontend: test the kea logic (
logic.actions/logic.values), not a full component render, whenever the behavior lives in the logic. Arender()+ DOM-query test is for things only the DOM can show. - Reach for ClickHouse or a browser only when the regression genuinely lives there — not because it was the first way the test came to mind.
Python (pytest / Django)
TestCase, notTransactionTestCase, unless you truly need it.TransactionTestCaseflushes the DB between tests instead of rolling back a transaction — dramatically slower, and a common source of cross-test interference. It's a Postgres-isolation choice, orthogonal to which datastore you touch: a ClickHouse-backed test is still a plainTestCase(ClickhouseTestMixinsets ClickHouse up), so reaching ClickHouse is not a reason to switch. Common cases that people reach forTransactionTestCaseto solve usually have cheaper alternatives:- testing
transaction.on_commitside effects → useTestCase+self.captureOnCommitCallbacks(execute=True). - needing a connection visible across a real separate thread (
thread_sensitive) →async_to_sync(...), notasyncio.run(...). UseTransactionTestCaseonly when the regression genuinely requires committed transaction boundaries thatTestCasehides.
- testing
- A test class only takes a database it uses.
BaseTestandAPIBaseTestinherit DjangoTestCase, so a class on either one needs a database to run:setUpTestDatawrites an organization, a project, a team and a user for it, and every test method runs inside a transaction. A class of pure assertions pays all of that and reads no row. Put those cases ondjango.test.SimpleTestCase, which refuses database access and therefore proves they never needed one. Theposthog/test/repo_invariants/test_database_free_test_classes.pyratchet enforces it: it holds the frozen list of the classes already here, and fails the pull request that adds another. When it trips, references/database-free-test-classes.md covers what the failure means, the three ways to answer it, and how to regenerate the baseline. - Dedicated data migration tests are temporary. Remove a dedicated test after every supported environment has applied the migration, the rollback window has closed, and no supported upgrade relies on the old data state. Delete the expired test instead of marking it skipped. Keep the migration file and tests for migration tooling, safety checks, reusable backfill systems, and backfills people can still run. Use
/django-migrationsfor the migration safety workflow. DRF input-validation belongs in a
SimpleTestCase, not anAPIBaseTestround-trip. A test that posts a malformed body to an endpoint and asserts a 400 pays forAPIBaseTestto build an Organization + Team + User in Postgres and wrap the test in a transaction — just to exercise validation that runs entirely in memory. DRF field validators (required, type coercion,choices,min/max, regex) andvalidate_<field>methods run insideSerializer(data=...).is_valid()with no database and no request: field-level validation happens into_internal_value, before the object-levelvalidate()that typically needsself.context. So an invalid-field case never reaches the DB-touching code. Test the serializer directly and assert on.errors:class TestTeamValidation(SimpleTestCase): # no DB — not APIBaseTest def test_sample_rate_too_many_digits(self) -> None: s = TeamSerializer(data={"session_recording_sample_rate": "30001"}, partial=True) assert not s.is_valid() assert s.errors["session_recording_sample_rate"][0].code == "max_digits"When you push the case matrix down to the serializer, keep (or add) one DB-backed endpoint test as a wiring guard — that the viewset actually invokes this serializer, so a bad request is rejected with a 400. The no-DB serializer test proves the validation logic; it does not prove the viewset is wired to that serializer (a refactor that drops the
serializer_class, skipsis_valid(), or stops callingis_valid(raise_exception=True)would pass everySimpleTestCaseand still ship a broken endpoint). One endpoint case closes that gap; the matrix stays in theSimpleTestCase. For a query serializer instantiated inline (e.g.Serializer(data=request.query_params).is_valid(raise_exception=True)), the wiring guard is a bad-query-param → 400 assertion. Two more caveats. First,.errorscarries DRF's raw code (invalid,max_digits); the{"attr", "code", "detail", "type"}HTTP envelope is rendered later byexceptions-hog(which mapsinvalid→invalid_input) — that rendering is framework behavior, so don't re-assert it per case (the wiring-guard test covers the envelope once). Second, validation that genuinely needs the DB stays at the endpoint — uniqueness checks,PrimaryKeyRelatedFieldqueryset lookups, related-object existence, permission/team scoping, password-hash checks. Don't force those into aSimpleTestCase.- Parameterize repeated assertions with the
parameterizedlibrary — don't copy-paste test bodies. - No doc comments in Python tests (house rule).
- Mock only true boundaries — network, external APIs, the clock, queues. Don't mock your own internal helpers (that's how change-detector tests are born).
- Person/group/cohort data: use the helpers in
posthog/test/persons.py(create_person,create_group,create_group_type_mapping,add_cohort_members, etc.) — neverPerson.objects.create()or similar ORM calls directly. Seeposthog/test/AGENTS.mdfor the full API reference and rationale. - Whole-repo guards go in
posthog/test/repo_invariants/. If a test's input is the entire repo — it walksapps.get_models(), inspectssys.modulesafter a colddjango.setup(), enumerates every route or signal receiver, orrglobs the tree against a baseline — any file anywhere can break it and diff-based test selection can't select it. That directory runs unconditionally in therepo-checksCI job on every backend PR (products-only and drafts included) with no Postgres/ClickHouse, and the Core shards skip it. Tests with a bounded input stay next to the code they cover.
Frontend (Jest)
- One top-level
describeblock per file (house rule). - Prefer logic tests over component renders (see the ladder above).
test.eachfor variations rather than copied test bodies.- Avoid
*ByRole(..., { name: ... })on components that render a large DOM (calendars, the taxonomic filter, whole scenes). Role queries walk every element runninggetComputedStyle-based checks and accessible-name computation — seconds per query in jsdom, multiplied by everywaitFor/findBy*retry; this pattern has produced single tests with 40s p95 in CI. PrefergetByText,getByTestId/data-attrselectors, orgetByLabelText; scope withwithin(<small container>)if a role query is genuinely needed. Thejest-no-byrole-name-queriessemgrep rule flags this. - No huge snapshots. A snapshot over a large rendered tree or serialized blob is a change-detector test that bloats the repo and breaks on every unrelated change. Snapshot a small, intentional value, or assert specific fields instead.
Always — determinism and isolation
- Control the state transition instead of racing it. Choose the control that matches
the behavior under test:
- For elapsed-time behavior, enable fake timers before starting the work and advance exactly the duration the behavior requires.
- For a final async result, use
wait_for/waitForon the observable result. - For an in-flight state, make the mocked boundary await a promise controlled by the
test. Assert the pending state, release the promise, then assert the final state.
Do not assume a debounce,
setTimeout, kea breakpoint, or fast mock will still be pending on the next line. Test setup may shorten internal delays without changing production behavior.
- No
time.sleep/ arbitrary waits. A sleep is a flake waiting to happen, and it slows every run. Replace it with the matching control above orfreeze_time. - An absolute date in a test is a time bomb until you pin the clock.
A fixture date keeps its meaning only while the real clock stays where you left it.
If anything under test measures that date against
now— an age, a window, a "recent" flag, an expiry — the assertion holds today and fails some weeks later, on every open branch at once. Pinning a date into application state is not pinning the clock: a test that sets an "evaluated at" value to a fixed instant, and leaves the wall clock real, still fails when real time drifts past the window, because the code re-readsnowand the two stop agreeing. Pin the process clock to the instant the fixtures speak in —freeze_timein Python,jest.useFakeTimers()withjest.setSystemTime()in Jest, released byjest.useRealTimers()in afinally— or write the fixture relative tonow(now - 2 days), so the distance is what the test states. Pinning the clock covers only what reads it inside your process; the next rule covers the rest. Ask this of every absolute date in a test, not only of an explicitfreeze_time: what does this assert when today is a year past it? If the answer is not "the same thing", fix it before you commit. - A frozen clock doesn't freeze the infrastructure.
freeze_timepatches the clock inside your process; everything outside it still runs on the real one — ClickHouse TTL, Postgresnow()defaults, S3 lifecycle rules, another service's token-expiry check. So freezing to an absolute date and then writing rows that something judges by age builds a time bomb: green for weeks, then red forever once wall-clock time drifts past the retention window. It fails on every branch at once, so it reads as though whichever PR is in front of you caused it — and that misattribution, not the fix, is where the time goes. Most ClickHouse tables are already safe: they build their TTL throughttl_period()(posthog/clickhouse/kafka_engine.py), which returns""undersettings.TEST, so tests get no TTL at all. A table that hardcodes itsTTLclause opts out of that guard —ai_eventsis one. Check rather than assume:grep -rlE '^\s*TTL ' posthog/models/ posthog/clickhouse/ --include=*.py. Make the row's lifetime independent of the ambient clock instead — pin the retention column on insert, the waybulk_create_ai_eventswritesretention_days=10000. Moving the frozen date forward only resets the timer. - No real network / live external services. Mock the boundary.
- No cross-test ordering. Tests must pass in any order and in isolation; don't rely on state a previous test left behind.
- No
@skip/xfail/.skipwithout a one-line reason and a linked issue. A permanently-skipped test is dead weight — delete it or fix it. - Never commit
.only(it.only/describe.only). It doesn't skip one test, it skips every other test in the file — turning the suite green on a sliver.
Before you open the PR
The PR template prompts for this under "How did you test this code?" — state the justification where a reviewer will see it. One line per group of tests is enough:
"Added 3 cases to
test_cohort_querycovering empty / single / oversized cohorts — guards the 500 we just fixed; couldn't extend an existing test because none exercised the empty path."
If you can't write that line, you've found a test that shouldn't be in the PR.
Related skills
- Fixing an existing flaky test → use
/fixing-flaky-tests(reproduce, root-cause, validate). Use this skill too only if the fix adds or substantially changes coverage. That skill applies the gate above retroactively: a flaky test has already proven its cost, so if you can't name the regression it catches, deleting it is a valid outcome there — not just a stabilization job. - Authoring a non-flaky Playwright test → first use this skill to decide whether a browser test earns its cost; if it does, use
/playwright-testfor the mechanics.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

