10 KiB
name, description
| name | description |
|---|---|
| cyclone-tests | Cyclone pytest + vitest fixture patterns, prodfiles layout, backend/tests/fixtures/ conventions, .test.tsx sibling rule. Use when: adding a backend pytest case, adding a frontend vitest test, or wiring in a real-EDI prodfiles sample. |
cyclone-tests
The Cyclone test suite is split two ways: backend pytest (92 test files under backend/tests/, 13 flat fixtures in backend/tests/fixtures/, one autouse conftest.py that resets the DB per-test) and frontend vitest (59 *.test.ts(x) siblings across src/, two rendering styles — @testing-library/react and a custom createRoot+Probe shim). This skill codifies the conventions so additions stay consistent with what's already there.
As of this writing: 92 backend test files, 13 flat backend fixtures, 59 frontend *.test.ts(x) siblings, and 23 prodfiles samples across docs/prodfiles/{837p-from-axiscare,835fromco,FromHPE,claims}/. The next increment is SP22.
When to use
- Adding a backend pytest case. You're about to add a new
test_*.pyunderbackend/tests/and need the autouse DB-fixture rules, the fixture-path convention, and the right naming flavor (test_api_*.pyvstest_<module>_*.py). - Adding a frontend vitest test. You're about to add a
*.test.ts(x)sibling and need the// @vitest-environment happy-domsetup, the act-environment flag, and the right rendering helper (@testing-library/reactvs thecreateRoot+Probeshim). - Dropping in a prodfiles fixture. You have a real EDI sample under
docs/prodfiles/<source>/and need the copy step that makes it test-runnable without coupling the test to the prodfiles archive. - Debugging a flaky test. A test passes locally but flakes in CI — load this skill to check the determinism + no-network rules before chasing the symptom.
Conventions
- Frontend sibling rule. Every new file in
src/that contains testable logic gets a*.test.ts(x)next to it.useFoo.ts→useFoo.test.ts.ClaimDrawer.tsx→ClaimDrawer.test.tsx. The 59 existing siblings follow this; CI implicitly enforces it via the defaultsrc/**/*.test.ts(x)glob invitest.config.ts. - Backend test location. Tests live under
backend/tests/test_*.py. Two flavors, two naming patterns:- Integration tests (FastAPI surface) →
test_api_<topic>_<verb>.py— e.g.test_api_parse_persists.py,test_api_999.py. - Pure-unit tests (parsers, validators, store internals) →
test_<module>_<behavior>.py— e.g.test_cas_codes.py,test_pubsub.py.
- Integration tests (FastAPI surface) →
- Prodfiles drop-in. Real EDI samples live under
docs/prodfiles/<source>/<file>.txt(sources seen so far:837p-from-axiscare/,835fromco/,FromHPE/,claims/). To use one in a test: copy the file tobackend/tests/fixtures/<descriptive-name>.txt(flat — no per-test subdirectories; the existing 13 fixtures all sit at the top level) and reference it from the test as a module-levelPathconstant. See## Patternsfor the exact line. - Determinism. Time-sensitive tests must not depend on wall-clock time.
- Frontend uses
vi.useFakeTimers()+vi.setSystemTime(new Date("YYYY-MM-DDTHH:MM:SSZ"))(seesrc/components/TailStatusPill.test.tsx:54-58) andvi.advanceTimersByTime(ms)to drive interval / backoff code deterministically. - Backend dates are passed explicitly as fixture values — e.g.
datetime.now(timezone.utc)is fine for "now-ish" anchors, but for fixed dates passdatetime(2026, 6, 20, tzinfo=timezone.utc). The project does not currently usefreezegunormock.patch(datetime); if you need deterministic date mocking, propose addingfreezeguntobackend/pyproject.tomlrather than rolling your own.
- Frontend uses
- No network. Tests must not hit the network.
- Backend:
fastapi.testclient.TestClient(app)runs in-process; no uvicorn. The autouseconftest.pyfixture (backend/tests/conftest.py:20) pointsCYCLONE_DB_URLattmp_path/test.db, callsdb._reset_for_tests()+db.init_db(), and wires a freshEventBusontoapp.state. - Frontend:
vi.stubGlobal("fetch", vi.fn().mockResolvedValue(...))for hooks;vitest.config.tssetsVITE_API_BASE_URL=http://test.localso theapimodule doesn't thrownotConfiguredErrorbefore the mock fires.
- Backend:
- pytest collection. Run
cd backend && python -m pytest tests/<file>::<name> -vfor the fastest single-test feedback loop. Runcd backend && python -m pytest tests/<file> -vfor one file. Runcd backend && python -m pytestfor the full suite — this is the merge gate. Frontend:npm test(alias forvitest run) for the full suite;npx vitest run src/hooks/useFoo.test.tsfor one file.
Patterns
Backend pytest using a fixture + per-test SQLite DB
Canonical shape — see backend/tests/test_api_999.py:1-50 for the full file. The autouse conftest.py already provides the DB init + EventBus reset; most tests just add a client fixture.
"""Tests for the FastAPI surface in cyclone.api for the 999 endpoint."""
from __future__ import annotations
from pathlib import Path
import pytest
from fastapi.testclient import TestClient
from cyclone.api import app
# Fixture reference — flat, module-level Path constant. NEVER reach into
# docs/prodfiles/ from a test; the fixtures/ dir is the test-consumed surface.
ACCEPTED = Path(__file__).parent / "fixtures" / "minimal_999.txt"
REJECTED = Path(__file__).parent / "fixtures" / "minimal_999_rejected.txt"
@pytest.fixture
def client() -> TestClient:
return TestClient(app)
def test_parse_999_endpoint_happy_path(client: TestClient):
text = ACCEPTED.read_text()
resp = client.post(
"/api/parse-999",
files={"file": ("minimal_999.txt", text, "text/plain")},
headers={"Accept": "application/json"},
)
assert resp.status_code == 200, resp.text
body = resp.json()
assert body["ack"]["ack_code"] == "A"
Override the autouse DB fixture only when you need a custom env (e.g. a per-test backup directory) — see backend/tests/test_999_rejected_state.py:20-25:
@pytest.fixture(autouse=True)
def _setup(tmp_path, monkeypatch):
monkeypatch.setenv("CYCLONE_DB_URL", f"sqlite:///{tmp_path}/inbox.db")
from cyclone import db
db._reset_for_tests()
db.init_db()
yield
Frontend vitest *.test.ts for a hook using fake timers
Pattern taken from src/components/TailStatusPill.test.tsx:1-62 — uses vi.useFakeTimers() + vi.setSystemTime(...) to drive interval-based code deterministically.
// @vitest-environment happy-dom
// React's act warnings need an act-aware environment — mirror the other
// hook tests in this repo.
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { MyComponent } from "./MyComponent";
describe("MyComponent", () => {
beforeEach(() => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-06-20T12:00:30Z"));
});
afterEach(() => {
vi.useRealTimers();
});
it("test_renders_after_interval_tick", () => {
vi.advanceTimersByTime(30_000); // drive the setInterval
// …assert…
});
});
Frontend vitest *.test.tsx for a component using @testing-library/react + happy-dom
Pattern taken from src/hooks/useInboxLanes.test.ts:1-80. (A handful of older tests — useClaimDetail.test.ts, useDrawerUrlState.test.ts, TailStatusPill.test.tsx — roll a custom createRoot+Probe shim instead. Both styles are accepted; @testing-library/react is preferred when the hook has async dependencies because waitFor is built in.)
// @vitest-environment happy-dom
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
import { afterEach, describe, expect, it, vi } from "vitest";
import { act, cleanup, renderHook, waitFor } from "@testing-library/react";
import { useMyHook } from "./useMyHook";
vi.mock("@/lib/api", () => ({
api: { fetchFoo: vi.fn() },
}));
afterEach(() => {
cleanup();
vi.unstubAllGlobals();
vi.useRealTimers();
});
describe("useMyHook", () => {
it("loads data on mount", async () => {
vi.stubGlobal("fetch", vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ items: [] }),
}));
const { result } = renderHook(() => useMyHook());
await waitFor(() => expect(result.current.loading).toBe(false));
expect(result.current.items).toEqual([]);
});
});
Anti-patterns
- Don't put frontend tests in
src/__tests__/. No such directory exists in the codebase —__tests__only appears in the SP-catalog plan itself. Use the sibling rule (useFoo.ts→useFoo.test.ts). - Don't reach into
docs/prodfiles/directly from a test. Always copy tobackend/tests/fixtures/<name>.txtfirst. The prodfiles directory is the source-of-truth archive and may be reorganized; the fixtures directory is the test-consumed surface and is stable. - Don't use wall-clock sleeps for timing. A handful of legacy tests use
await new Promise((r) => setTimeout(r, 200))(seesrc/components/SearchBar.test.tsx:281,323,373andsrc/hooks/useSearch.test.ts:174) — these are known flaky in CI. Usevi.useFakeTimers()+vi.advanceTimersByTime(ms)instead, orwaitFor(...)from@testing-library/react.
Related skills
cyclone-spec— every SP-N spec lists test impact; load this when drafting or reviewing the spec to confirm fixture /.test.tsximplications for the increment.cyclone-edi— most backend tests cover parser + validator behavior; load when the increment touches an EDI parser or adds a validator rule (R200/R210/NPI Luhn/EIN/CAS).cyclone-tail— most frontend tests cover hook behavior (useTailStream,useMergedTail); load when the increment changes the wire format or adds a streaming hook.cyclone-store— write-path tests live here; load when the increment touchesstore.py, adds a new entity, or wires a new<entity>_writtenevent.cyclone-api-router— endpoint tests live inbackend/tests/test_api_*.py; load when the increment adds or changes an HTTP endpoint.cyclone-frontend-page— page-component tests live next to pages insrc/pages/*.test.tsx; load when the increment adds or refactors a page.cyclone-cli— CLI smoke tests live inbackend/tests/test_cli_*.py; load when the increment adds a CLI subcommand.superpowers:test-driven-development(global) — the upstream TDD workflow. Load first when starting any new feature increment; this skill only codifies the Cyclone-specific test layout on top of TDD.