Guidance for working with the tests. Point up to the root CLAUDE.md and
src/aignostics/CLAUDE.md for architecture and the module index.
Tests mirror the source package under tests/aignostics/<module>/, with one
<name>_test.py per source unit and a per-module conftest.py where needed.
Regenerate the current tree with find tests -name '*_test.py'; the main areas:
tests/aignostics/—application/,bucket/,dataset/,notebook/,platform/(+platform/resources/),qupath/,system/,utils/,wsi/.- Top level:
conftest.py(global fixtures),constants_test.py,main.py. tests/fixtures/— package dir, currently only__init__.py.tests/resources/— the actual test data (see below).
Real fixtures live here, not in fixtures/:
cells.json,cells_broken.json— valid / malformed cell payloads.single-channel-ome.tiff,sm-thumbnail.dcm,unsupported.any— WSI / DICOM inputs.run/small-pyramidal.dcm— small pyramidal DICOM for run tests.mcp_dummy_plugin/— installable dummy MCP plugin (has its ownpyproject.toml).
Every test needs at least one of unit, integration, e2e or it will not run
in CI. The authoritative marker list and descriptions live in pyproject.toml
[tool.pytest.ini_options] (also long_running, very_long_running,
scheduled, scheduled_only, sequential, stress, stress_only, docker,
no_extras, skip_with_act). See .github/CLAUDE.md for how CI selects them
and the Makefile for the test_* targets.
pytest -m sequential
pytest -m "unit and not long_running"Global fixtures are in tests/conftest.py; module-specific ones in the nearest
conftest.py. A few non-obvious ones:
- NiceGUI plugin is auto-registered only when installed:
if find_spec("nicegui"): pytest_plugins = ("nicegui.testing.plugin",). clean_env(platform/sdk_metadata_test.py) usesmonkeypatchto stripGITHUB_*/PYTEST_*/NICEGUI_*/AIGNOSTICS_*before building SDK metadata — use it whenever env detection matters.- QuPath / subprocess tests terminate leftover processes in teardown; don't disable that or CI runners leak processes.
Cover metadata building, strict Pydantic validation (extra fields forbidden),
GitHub/pytest CI context capture, interface/source detection, and JSON-schema
generation. Do not hardcode the schema version — it comes from
SDK_METADATA_SCHEMA_VERSION / ITEM_SDK_METADATA_SCHEMA_VERSION in
src/aignostics/platform/_sdk_metadata.py. Assert against those constants:
from aignostics.platform._sdk_metadata import (
SDK_METADATA_SCHEMA_VERSION,
build_run_sdk_metadata,
)
metadata = build_run_sdk_metadata()
assert metadata["schema_version"] == SDK_METADATA_SCHEMA_VERSIONcached_operation (in src/aignostics/platform/_operation_cache.py) takes a
nocache=True kwarg that skips the cache read but still writes the fresh result.
The decorator intercepts nocache and does not pass it to the wrapped function.
Client.me(), Runs.list(), etc. expose the same kwarg. Minimal check:
@cached_operation(ttl=60)
def f() -> int: ...
f() # populates cache
f() # cache hit, f not re-run
f(nocache=True) # re-runs f, refreshes cacheSee nocache_test.py for the full decorator / client / edge-case coverage.
Enforced by Codecov, not by any in-repo dict: project target 70%, patch target
75% (codecov.yml). Local report:
uv run pytest --cov=aignostics --cov-report=htmlSymptom: inside an @ui.refreshable, UI element state (button
enabled/disabled, input values) resets after an interaction.
Cause: NiceGUI 3.0 added observable props/classes/styles. Setting .value on
an element inside @ui.refreshable can trigger element recreation, resetting
local variables. ui.state() makes it worse for parameterised refreshables:
its setter calls refresh() with no arguments, so the refreshable's
parameters fall back to their defaults.
Fix: for an @ui.refreshable that takes parameters, hold state in a mutable
dict rather than ui.state(), so updating state does not trigger a refresh:
@ui.refreshable
def dialog_content(qupath_project: bool = False) -> None:
folder_state: dict[str, str] = {"value": ""}
selected_folder = ui.input("Folder", value=folder_state["value"])
download_button = ui.button("Download")
if not folder_state["value"]:
download_button.disable()
def on_select() -> None:
folder_state["value"] = "/path/to/folder" # no refresh triggered
selected_folder.value = folder_state["value"]
download_button.enable()| Pattern | Use when |
|---|---|
ui.state() |
refreshable takes no parameters, or reset-on-refresh is fine |
| Mutable dict | refreshable takes parameters that must survive state updates |
Ref: https://github.com/zauberzeug/nicegui/releases/tag/v3.0.0