.rulesync/skills/test/SKILL.md
required reading for all test/spec files or test related queries.
npx skillsauth add washingtonguilhardes/example.hr-module testInstall 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.
Quality guardrails for AI-generated tests at any layer (unit, integration, HTTP, E2E), plus a coverage workflow for adding tests to existing code.
[!IMPORTANT] This skill defines what makes a correct test. For Playwright-specific mechanics (selectors, fixtures, Docker verification), also load
/e2e. For RC smoke test coverage rules, also load/e2e-rc.
[!IMPORTANT] Test names must describe expected user behavior only. Test names must never include implementation details. Test names must never use vague wording like "as expected", "works correctly", or "should work".
Required: Describe what the user does and what the user observes. Focus on externally visible behavior, not internals.
Forbidden in test names:
Good examples:
"when I login with valid credentials, I am redirected to the dashboard""when I remove an item from cart, the total updates immediately""sending a message in a space makes it visible in the thread"Bad examples:
"useLoginMutation returns success as expected""cart reducer updates state correctly""POST /api/files returns 200"If the current behavior differs from what the title describes, either fix the code or use it.fails().
tests/, e2e/helpers/, e2e/fixtures/, and colocated test files for established patterns, seed builders, and assertion utilities.Use this workflow when adding test coverage to existing code. For new features or bugfixes, use /tdd (test-first).
rules/e2e-testing.md..test.ts alongside source. HTTP integration tests go in tests/harpoon/. E2E tests go in e2e/specs/.Finding a bug during test writing is a deliverable, not an obstacle.
When a test reveals unexpected behavior in the system under test:
it.fails() (Vitest) or test.fail() (Playwright), link a filed issue, and document expected vs. actual behavior. Get human approval before adding the xfail. See xfail Guidance.Your objective is verify correctness and surface bugs, not make tests green. A failing test that exposes a real bug is more valuable than a passing test that hides one.
Every test must verify at minimum:
| Layer | Status/Return | Response Shape | Behavioral Side Effect | |-------|:---:|:---:|:---:| | HTTP integration | Required | Required (at least 1 field) | Required for mutations | | Unit | N/A | Return value verified | State change verified if stateful | | E2E (browser) | N/A | UI element verified | Action outcome visible |
A test with only a status code assertion is INCOMPLETE. It proves the server didn't crash, not that it did the right thing.
HTTP / Integration tests:
200 { status: "failed" } patterns as valid.Unit tests:
E2E (browser) tests:
/e2e skill for selector strategy)Verify each item in the batch result, not just the first. A common shortcut is to assert result.length > 0 without checking individual items.
Five categories of AI test-writing failures, ordered by severity.
The test asserts broken behavior as correct. It is green, looks intentional, and actively hides the bug.
Detection signals:
expect(status).toBe(500) in a happy-path test (500 is never correct behavior)Fix: Fix the application code, or use it.fails() with a linked issue. Never assert broken behavior.
The assertion has a syntax error or omits the matcher, so nothing is actually asserted.
Detection signals:
expect(result) with no .toBe()/.toEqual() (always passes)expect(x).not.toBeNull (compares against function reference)expect(arr.length > 0) (always passes)in expression: expect(['.tag'] in body) (unintended coercion)Fix: Correct the syntax. The test was asserting nothing.
The test avoids triggering a bug by changing test setup instead of asserting the bug.
Detection signals:
Fix: Diagnose whether the omitted behavior is correct or broken. Fix the right side, restore the parameter, test both paths.
The test claims to be an integration test but hits a mock, stub, or wrong endpoint.
Detection signals:
vi.mock() or jest.mock() in files labeled "integration"/api/v2/endpoint but real code is at /api/v1/endpointFix: Remove the mock or re-label the test. Integration tests hit real code.
The test checks that the endpoint returns 200 but never verifies the response or side effects.
Detection signals:
expect(status).toBe(200)body or response variable read after the callFix: Add response shape and behavioral assertions per the Assertion Depth Checklist.
The highest-risk layer for quality failures. Concrete requirements:
Mutations (POST, PUT, PATCH, DELETE):
Reads (GET, LIST):
Error cases:
expectDropboxError)[!IMPORTANT]
it.fails()/test.fail()is ONLY for known product bugs that are out of scope. It is not a general-purpose tool.
For intended negative-path behavior (expected errors, validation failures): use normal assertion-level error checks like expect(...).rejects.toThrow(), expect(status).toBe(400), or equivalent. These are normal tests, not xfails.
For known product bugs out of scope: use it.fails() (Vitest) or test.fail() (Playwright) with:
it.fails('append_v2 returns 200 for empty body', async () => {
// BUG: adapter crashes on void result (HOT-3043)
// Expected: 200 with null body
// Actual: 500 due to NextResponse.json(undefined)
const result = await httpV1.post('/files/upload_session/append_v2', { cursor, close: false });
expect(result.status).toBe(200);
});
Key inversion: The assertion inside it.fails() describes correct behavior. When the bug is fixed, the test passes and it.fails() flags it for removal. Changing the assertion to match the bug IS the anti-pattern.
Never use xfail to:
Where expected behavior is allowed to come from, in priority order:
If your test asserts current behavior rather than specified behavior, add a comment: // Characterization test: asserts current behavior, not spec. Characterization tests are legitimate for legacy safety nets but must never be passed off as verification.
Every test should be able to answer two questions:
Before writing a test, ask: "Would this test break under a legitimate refactor?" If the answer is yes, the test is coupled to implementation, not behavior. Don't write it. Tests should break when behavior changes, not when code is reorganized.
Tests must use the transport they claim to test.
If the transport under test is broken, that is the bug to fix, not a reason to switch transports.
Before marking any test work complete, verify each test against:
expect(500) or expect(4xx) in happy-path testsexpect() call has a matcher (.toBe, .toEqual, .toMatchObject, etc.)vi.mock()/jest.mock() in integration test filesBehavioral follow-up read (HTTP integration):
// Create, then verify via independent GET
const createResult = await api.post('/files/create_folder_v2', { path: '/new-folder' });
expect(createResult.status).toBe(200);
expect(createResult.body).toMatchObject({ metadata: { name: 'new-folder' } });
// Behavioral verification: folder actually exists
const listResult = await api.post('/files/list_folder', { path: '' });
const names = listResult.body.entries.map(e => e.name);
expect(names).toContain('new-folder');
Structured error assertion:
// Use existing helpers when available
await expectDropboxError(result, 409, 'path/not_found');
// Or assert error shape directly
expect(result.status).toBe(409);
expect(result.body).toMatchObject({
error_summary: expect.stringContaining('path/not_found'),
error: { '.tag': 'path', path: { '.tag': 'not_found' } }
});
Detailed failure message for debugging:
// Include response body in failure message for easier debugging
const result = await api.post('/files/list_locked_files', body);
expect(
result.status,
`Expected 200 but got ${result.status}. Body: ${JSON.stringify(result.body)}`
).toBe(200);
xfail with linked issue (known bug, out of scope):
it.fails('list_locked_files accepts empty body (HOT-3044)', async () => {
// BUG: z.void().optional() rejects {} over HTTP
// tRPC caller works because it omits the body entirely
const result = await api.post('/files/list_locked_files', {});
expect(result.status).toBe(200);
});
Round-trip symmetry (invariant-based):
it('upload then download returns identical content', async () => {
const content = Buffer.from('test file content');
await api.upload('/files/upload', { path: '/test.txt' }, content);
const downloaded = await api.download('/files/download', { path: '/test.txt' });
expect(downloaded.body).toEqual(content);
});
testing
Analyze distributed traces to find blocking queries, N+1 patterns, and missing tRPC encapsulation, then produce prioritized fixes
testing
Transform Product Brief into Technical Specification
development
Detect drift between code implementations and specification documents
development
Enforce relational schema design over JSON columns, and prevent TypeScript type-safety evasion (any, oxlint-disable, type casting)