Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 29 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ jobs:
runs-on: ubuntu-latest
permissions:
contents: read
# 2026-07-08: fail-fast on the first matrix failure instead of
# wasting runner minutes on the remaining Python versions when
# the suite is already red. Speed gain is per-run, not per-test.
strategy:
fail-fast: true
matrix:
python: ["3.10", "3.11", "3.12"]

Expand All @@ -22,14 +26,29 @@ jobs:
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python }}
# Cache pip's download cache keyed on the lock-relevant
# surfaces of pyproject.toml. Skips the ~60-90s cold
# install on warm caches; the action also reuses the
# cache across matrix legs when the key matches.
cache: "pip"
cache-dependency-path: pyproject.toml

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -e ".[dev]"
# xdist ships in the dev tree already; pin it explicitly so
# a future deps churn can't drop it without breaking CI.
pip install -e ".[dev]" "pytest-xdist>=3.6"

- name: Run tests
run: pytest
# `-n auto` lets xdist pick a worker count from the runner's
# CPU count. With the transport cancellable-sleep fix the
# 5s-per-shutdown multiplier is gone, and xdist plus the
# existing respx-based mocking keeps the per-test wall clock
# near single-thread baseline (no shared state between
# workers — ``reset_runtime`` autouse fixture in conftest
# is per-process by construction under xdist).
run: pytest -n auto --durations=20

- name: Run ruff
run: ruff check src/
Expand All @@ -46,11 +65,16 @@ jobs:
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install -e ".[dev]"
- run: coverage run -m pytest
cache: "pip"
cache-dependency-path: pyproject.toml
- run: pip install -e ".[dev]" "pytest-xdist>=3.6"
# Single Python leg for coverage — multi-version coverage
# reports don't add signal and double the runner time. 3.12
# is the modern floor for typing-only changes.
- run: coverage run -m pytest -n auto
- uses: codecov/codecov-action@v4
if: always()
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./coverage.xml
fail_ci_if_error: false
fail_ci_if_error: false
8 changes: 5 additions & 3 deletions .github/workflows/publish-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,14 @@ jobs:
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: "pip"
cache-dependency-path: pyproject.toml

- name: Install dependencies
run: pip install -e ".[dev]"
run: pip install -e ".[dev]" "pytest-xdist>=3.6"

- name: Run tests
run: pytest tests/ -v
run: pytest tests/ -v -n auto

publish:
name: Build and publish to TestPyPI
Expand Down Expand Up @@ -62,4 +64,4 @@ jobs:
# re-runs of the same SHA a no-op (matching twine's
# --skip-existing behaviour). Production PyPI cannot
# overwrite anyway, so this flag is harmless there too.
skip-existing: true
skip-existing: true
9 changes: 7 additions & 2 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ jobs:
runs-on: ubuntu-latest
permissions:
contents: read
# 2026-07-08: parallel matrix kept (PyPI publish is a one-shot
# event and the runner is already paid for) but pip cache
# brought in for parity with ci.yml.
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12"]
Expand All @@ -22,12 +25,14 @@ jobs:
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: "pip"
cache-dependency-path: pyproject.toml

- name: Install dependencies
run: pip install -e ".[dev]"
run: pip install -e ".[dev]" "pytest-xdist>=3.6"

- name: Run tests
run: pytest tests/ -v
run: pytest tests/ -v -n auto

publish:
name: Build and publish
Expand Down
74 changes: 0 additions & 74 deletions docs/drift.md

This file was deleted.

36 changes: 0 additions & 36 deletions docs/sdk-v3-migration-gaps.md

This file was deleted.

31 changes: 29 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,18 @@ name = "nullrun"
# nullrun._singleton (the metaclass-backing descriptor) and
# nullrun._registry (the runtime registry) so runtime.py stays the
# orchestrator only. See __version__.py for the full changelog.
version = "0.13.3"
# 0.13.3 (2026-07-07): developer-ergonomics — `langgraph` import
# semantics + cleanup of dead `protos/` target. See __version__.py.
# 0.13.4 (2026-07-08): bug-fix — flatten the LangChain usage-
# extraction elif-chain so every attribute source is read (not just
# the first one with ``hasattr`` truthy). Pairs with PR #59.
# 0.13.5 (2026-07-08): perf — make ``Transport._flush_loop`` sleep
# cancellable (``threading.Event.wait`` instead of ``time.sleep``)
# so ``runtime.shutdown()`` returns in ms instead of waiting out
# the full ``flush_interval`` (5s default). Plus CI hygiene:
# pip cache, ``fail-fast`` matrix, ``pytest-xdist -n auto``. No
# on-wire change; backends on 1.0.0 keep working unchanged.
version = "0.13.5"
# Long form used by PyPI page meta-description and search snippets.
# Kept under the 200-char preview threshold so the full line is visible
# without an "expand" click. Keywords are matched against likely search
Expand Down Expand Up @@ -140,6 +151,14 @@ dev = [
"ruff>=0.5",
"coverage[toml]>=7.0",
"httpx>=0.27.0,<1.0",
# xdist pins the parallel runner as a first-class dev dep so
# `pip install -e ".[dev]"` brings it in for local runs and
# CI both. The CI workflow also installs it explicitly to
# survive a future pyproject prune. ``-n auto`` is set in
# the workflow rather than ``addopts`` so single-CPU local
# runs (e.g. ``pytest tests/test_one.py``) don't accidentally
# spawn a worker pool.
"pytest-xdist>=3.6",
# The SDK eagerly imports `nullrun.instrumentation.langgraph`
# (from `nullrun.decorators`, imported by `nullrun.__init__` at
# collection time), which itself does `from langchain_core.callbacks
Expand Down Expand Up @@ -477,7 +496,15 @@ ignore = [
[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]
addopts = "--tb=short -q"
# 2026-07-08: dropped the global ``-q`` so CI logs surface the
# full PASSED line for each test (handy when scanning a red run).
# Per-test verbosity stays low because ``--tb=short`` keeps the
# tracebacks compact. ``-n auto`` lives in the workflow file, not
# here, so a developer running ``pytest tests/test_x.py`` locally
# gets a single process — the worker pool is only worth it on
# the full suite, and some single-file debug sessions actively
# want serial execution.
addopts = "--tb=short"
# Make the tests/ directory importable as a top-level package so
# tests can use `from tests.conftest import BASE_URL`. Without this,
# `from tests.conftest` raises ModuleNotFoundError on Python 3.10/3.11
Expand Down
11 changes: 9 additions & 2 deletions src/nullrun/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def my_agent(query):
from nullrun.runtime import track_event, track_llm, track_tool


def shutdown(timeout: float = 2.0) -> None:
def shutdown(timeout: float = 2.0, flush: bool = True) -> None:
"""Gracefully shut down the NullRun runtime.

Sends a clean WebSocket close frame, drains in-flight events, and
Expand All @@ -62,6 +62,13 @@ def shutdown(timeout: float = 2.0) -> None:
``NullRunRuntime.shutdown `` already caps WS join at
0.5s and the WS close at 2.0s — this parameter is
reserved for future expansion and is currently unused.
flush: when True (default) the transport drains any
buffered events to the backend on the way out. Pass
False to cancel the flush thread without a final
network call — used by the test conftest to teardown
between tests without racing the respx context exit
(see ``NullRunRuntime.shutdown(flush=False)`` for the
full rationale).

Example::

Expand All @@ -75,7 +82,7 @@ def shutdown(timeout: float = 2.0) -> None:
runtime = NullRunRuntime._instance # type: ignore[attr-defined]
if runtime is None:
return
runtime.shutdown()
runtime.shutdown(flush=flush)


def status():
Expand Down
Loading
Loading