Files

12 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 (~173 test files under backend/tests/, ~21 flat fixtures in backend/tests/fixtures/, one autouse conftest.py that resets the DB per-test) and frontend vitest (85 *.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: ~173 backend test files, ~21 flat backend fixtures (the original 13 plus additions for reissue-claims, validate-837, submit-batch, and SP41 rebill fixtures), 85 frontend *.test.ts(x) siblings, and a slimmed-down docs/prodfiles/references/ archive (the bulk of the prodfiles were moved out of the repo; remaining sample is good-claim-com-2026-04-16.837). Tests that previously reached into docs/prodfiles/claims/ and docs/prodfiles/FromHPE/ now skip when those dirs are absent (tests/test_prodfiles_smoke.py). The most recently shipped increment is SP41 (in-window rebill pipeline); the next free increment is SP43 after this SP42 doc-pass merges.

Auth flag (SP24)

The autouse conftest.py fixture at backend/tests/conftest.py flips cyclone.auth.deps.AUTH_DISABLED = True for the entire test session, so every test runs without a login round-trip. Any new test that reads cyclone.auth.deps.AUTH_DISABLED directly will see True — that's the test-suite reality, not a production reality. If you need a test that exercises the real gate, import from cyclone.auth.deps import matrix_gate and call it directly with a Request whose state carries a real session, or flip the flag back inside the test and reset it on teardown. The startup WARNING (backend/src/cyclone/__main__.py) is silent in tests by default because the conftest sets the flag before bootstrap.run() is called via import.

When to use

  • Adding a backend pytest case. You're about to add a new test_*.py under backend/tests/ and need the autouse DB-fixture rules, the fixture-path convention, and the right naming flavor (test_api_*.py vs test_<module>_*.py).
  • Adding a frontend vitest test. You're about to add a *.test.ts(x) sibling and need the // @vitest-environment happy-dom setup, the act-environment flag, and the right rendering helper (@testing-library/react vs the createRoot+Probe shim).
  • 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

  1. Frontend sibling rule. Every new file in src/ that contains testable logic gets a *.test.ts(x) next to it. useFoo.tsuseFoo.test.ts. ClaimDrawer.tsxClaimDrawer.test.tsx. The 59 existing siblings follow this; CI implicitly enforces it via the default src/**/*.test.ts(x) glob in vitest.config.ts.
  2. 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.
  3. 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 to backend/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-level Path constant. See ## Patterns for the exact line.
  4. Determinism. Time-sensitive tests must not depend on wall-clock time.
    • Frontend uses vi.useFakeTimers() + vi.setSystemTime(new Date("YYYY-MM-DDTHH:MM:SSZ")) (see src/components/TailStatusPill.test.tsx:54-58) and vi.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 pass datetime(2026, 6, 20, tzinfo=timezone.utc). The project does not currently use freezegun or mock.patch(datetime); if you need deterministic date mocking, propose adding freezegun to backend/pyproject.toml rather than rolling your own.
  5. No network. Tests must not hit the network.
    • Backend: fastapi.testclient.TestClient(app) runs in-process; no uvicorn. The autouse conftest.py fixture (backend/tests/conftest.py:20) points CYCLONE_DB_URL at tmp_path/test.db, calls db._reset_for_tests() + db.init_db(), and wires a fresh EventBus onto app.state.
    • Frontend: vi.stubGlobal("fetch", vi.fn().mockResolvedValue(...)) for hooks; vitest.config.ts sets VITE_API_BASE_URL=http://test.local so the api module doesn't throw notConfiguredError before the mock fires.
  6. pytest collection. Run cd backend && python -m pytest tests/<file>::<name> -v for the fastest single-test feedback loop. Run cd backend && python -m pytest tests/<file> -v for one file. Run cd backend && python -m pytest for the full suite — this is the merge gate. Frontend: npm test (alias for vitest run) for the full suite; npx vitest run src/hooks/useFoo.test.ts for 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.tsx for a component 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.tsuseFoo.test.ts).
  • Don't reach into docs/prodfiles/ directly from a test. Always copy to backend/tests/fixtures/<name>.txt first. 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)) (see src/components/SearchBar.test.tsx:281,323,373 and src/hooks/useSearch.test.ts:174) — these are known flaky in CI. Use vi.useFakeTimers() + vi.advanceTimersByTime(ms) instead, or waitFor(...) from @testing-library/react.
  • cyclone-spec — every SP-N spec lists test impact; load this when drafting or reviewing the spec to confirm fixture / .test.tsx implications 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 touches store.py, adds a new entity, or wires a new <entity>_written event.
  • cyclone-api-router — endpoint tests live in backend/tests/test_api_*.py; load when the increment adds or changes an HTTP endpoint.
  • cyclone-frontend-page — page-component tests live next to pages in src/pages/*.test.tsx; load when the increment adds or refactors a page.
  • cyclone-cli — CLI smoke tests live in backend/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.