Write tests that prove the right invariant at the smallest honest layer. Use when adding, reviewing, deleting, or refactoring tests; choosing test boundaries; turning bug reproductions into invariant tests; or deciding whether coverage, fuzzing, property tests, unit tests, integration tests, or end-to-end tests are appropriate.
A good test states the property the system must preserve. It should read like a small piece of design documentation with executable consequences.
When something breaks, the natural sequence is: reproduce the bug, write the failing test, patch the code, ship. That produces tests that describe what broke once, not what must always be true.
Before writing a test, answer:
What system property was violated?Write the test for that property, not for the incident that revealed the bug.
Good names:
invariant_failed_operation_does_not_change_visible_state
invariant_retry_does_not_duplicate_side_effects
invariant_transfer_is_atomic_to_observers
design_unrecognized_state_returns_error_not_panicWeak names:
test_issue_123_repro
test_timeout_case
test_special_caseThe test name is a contract. It should be readable as a sentence that could appear in an architecture doc.
invariant_*: A property that must always hold. This is the default prefix.design_*: An intentional policy decision that could reasonably go another way.Bad:
test_setenv_rejection_continues_session
run_once_continues_after_per_workspace_status_patch_failure
test_list_excludes_image_version_from_responseGood:
invariant_session_survives_guest_env_rejection
invariant_single_workspace_failure_does_not_block_node_reconciliation
invariant_internal_fields_are_not_exposed_in_public_apiSmallest honest means the test can fail for the bug class you care about. A helper test is not enough when the bug is in boundary ordering. An end-to-end test is too expensive when the property is a pure merge rule.
The test body should exercise the general case, not only the specific trigger that found the bug.
Bad:
Send a 409 Conflict response, assert the loop continues.Good:
Send any per-workspace error for workspace A, assert workspace B still reconciles.If a specific error matters, keep the invariant name and put the concrete scenario in a short comment inside the test body. The name stays stable; the implementation detail stays local.
When a bug exposes an unrealistic fake, fix the default fake behavior instead of adding a one-off fake. The fake should be a small model of reality, not a mock that returns success for everything.
Failure output should be terse and useful:
FAIL failed operation visibility (got changed, want unchanged)For a bug fix, run the new test before the fix when feasible. Report how many tests were red before the fix and which command makes them green after.
When changing tests, report:
Coverage does not prove correctness. Use it to find unvisited code, then write tests for the missing behavior.
Fuzzing is useful when the input space is large and hand-written examples miss cases. Property tests are useful when a simple model can say what every result must look like.
Before committing a test, ask:
If the answer to 4 is yes, elevate the name to the invariant.
These former standalone skills are bundled here as references to keep the runtime list compact. Load only the reference that matches the user's exact product, framework, or failure mode.
| Former skill | Reference | Description |
|---|---|---|
dedalus-tests | references/skills/dedalus-tests/SKILL.md | Write tests that prove the right invariant at the smallest honest layer. Use when adding, reviewing, deleting, or refactoring tests; choosing test boundaries; turning bug reproductions into invariant tests; or deciding whether coverage, fuzzing, property tests, unit tests, integration tests, or end-to-end tests are appropriate. |