Skip to content

Test/coverage improvements 0.5.2#25

Merged
maltsev-dev merged 5 commits into
masterfrom
test/coverage-improvements-0.5.2
Jun 19, 2026
Merged

Test/coverage improvements 0.5.2#25
maltsev-dev merged 5 commits into
masterfrom
test/coverage-improvements-0.5.2

Conversation

@maltsev-dev

Copy link
Copy Markdown
Member

What

Why

How

Test plan

  • Unit tests pass (per-repo, e.g. cd backend && cargo test, cd frontend && npm test)
  • Lint passes (per-repo, e.g. cd frontend && npm run lint)
  • Type-check passes (per-repo, e.g. cd frontend && npm run type-check)
  • Manually verified in dev / staging

Risk

Checklist

  • I have read the repo's CONTRIBUTING.md (if present)
  • My change does not introduce new lint warnings
  • I have updated the CHANGELOG (if user-visible)
  • I have considered backwards compatibility

maltsev-dev and others added 5 commits June 19, 2026 14:11
Closes the P0/P1/P2/P3 issues from the security review (plan §10/§11.4).

Security / PCI-DSS / GDPR

- P0-1: Mask positional PII in `_enforce_sensitive_tool` by introspecting
  the wrapped function's signature and applying `SENSITIVE_ARG_KEYS` to
  positional params. Pre-fix, `charge("4111-…-1111", 50)` forwarded the
  PAN into `/execute` and the audit log.
- P0-6 / P3-3: `_safe_repr` now redacts BEFORE truncating. The pre-fix
  order truncated first, so `details={…}` past position 50 leaked
  verbatim. `_safe_repr` is now the single source of truth for the
  redact-then-truncate flow.

Cost-audit / reliability

- P0-3: Bounded chunked reads on the sync + async httpx transports
  (`MAX_RESPONSE_BYTES`, default 16 MiB, `NULLRUN_MAX_RESPONSE_BYTES`
  env override). Above the cap, tracking is skipped and
  `_coverage_streaming_skipped` is incremented. Replaces the
  `response.read()` / `await response.aread()` unbounded buffer that
  held entire LLM streaming bodies in memory.
- P0-4: `_do_flush_locked` re-queue on CB OPEN now drops the NEWEST
  non-critical events instead of the oldest. The oldest events
  (incident start, billing-period start) are exactly what a billing
  investigator needs; losing them silently broke monthly rollups.
  Control-plane events (`state_change`, `kill_received`,
  `policy_invalidated`, `key_rotated`) are preserved unconditionally
  so the dashboard KILL switch lands even under sustained backend
  outage.

Identity

- S-8 / P2-4: `agent()` now emits `str(uuid.uuid4())` (with dashes).
  Pre-fix the format was `f"agent-{uuid.uuid4().hex}"` — 32 hex chars,
  no dashes — and backend UUID-typed columns dropped these to NULL
  on insert. User-supplied names are still preserved verbatim.
- §7.2 #16: `workflow()` context manager now resets `span_id` (not
  only `workflow_id` / `trace_id`) so nested `with span()` blocks
  don't leave the inner span_id visible inside the workflow scope.

Resource leaks

- S-9: `_active_runs` on `NullRunCallback` is now an `OrderedDict`
  capped at 4096 with FIFO eviction. Pre-fix the dict grew
  unbounded when `on_chain_end` did not fire (some LangChain
  versions short-circuit the end hook on chain-body errors).
- S-10: WebSocket reconnect loop is now capped at 10 consecutive
  failures, then falls back to HTTP-poll. Pre-fix the loop ran
  forever when the backend was permanently down, leaking the
  WS thread.

Transport

- §7.2 #6: Separate `hmac_verify_expired_total` counter so SRE can
  distinguish clock-skew (NTP drift) from forged packets. Mirrored
  in both the HTTP and WebSocket verify paths.
- §7.2 #35: `CircuitBreaker.call` now dispatches the OPEN→HALF_OPEN
  jitter through `_maybe_apply_open_jitter_sync` /
  `_maybe_apply_open_jitter_async`. Pre-fix the jitter used
  `time.sleep` before dispatching to async, which blocked the
  caller's event loop on every transition.
- P2-1: `_coverage_seen` now bumps in the httpx path (sync + async).
  Pre-fix the counter was only bumped by the `requests` transport,
  so the dashboard's coverage view was empty for the dominant
  OpenAI / Anthropic / Gemini / Mistral / Cohere traffic.
- P2-3: `is_sensitive_tool` match is case-insensitive. Pre-fix
  `"stripe.charge"` did not match `"Stripe.Charge"`, bypassing the
  sensitive gate.

Concurrency

- §7.2 #39: New `_tools_lock` guards every mutation of
  `_strict_mode_tools` / `_sensitive_tools`. Same lock guards the
  coverage-counter bump+prune sequence (§7.2 #33) so two threads
  can't both observe the dict at length 4095 and both grow it to
  4097 before either prune lands.
- §7.2 #47: New `_langchain_lock` / `_langgraph_lock` guard the
  patch sequences end-to-end. Pre-fix two threads racing through
  `auto_instrument` could both pass the early `_x_patched` check
  and double-wrap `BaseCallbackManager` / `Pregel`.
- §7.2 #33: `_COVERAGE_CAP` (4096) bounds the per-host coverage
  dicts.

Webhook delivery

- P3-2: Exponential backoff (0.5s, 1s, 2s, 4s, 8s, 16s, 30s cap)
  replaces the previous linear schedule. Linear didn't back off
  fast enough under sustained outage — each KILL/PAUSE spawned
  its own delivery thread, producing 1000+ spinning threads
  hammering the dead endpoint.

WAL crash-recovery

- P1-5b: Atomic WAL writes (tmp + `fsync` + `os.replace`), 64 MiB
  rotation with `os.replace(wal, wal.1)`, replay drains both
  `wal.1` and `wal`. New `NULLRUN_WAL_PATH` / `NULLRUN_WAL_MAX_BYTES`
  env overrides for containers with `readOnlyRootFilesystem: true`.

Tests

8 new regression test files (57 tests total):
  test_agent_id_uuid.py, test_args_pii_masked.py,
  test_streaming_oom_cap.py, test_lru_active_runs.py,
  test_reconnect_cap.py, test_coverage_seen_httpx.py,
  test_webhook_backoff.py, test_redact.py

`test_buffer_invariants.py` extended with drop-newest +
critical-event preservation cases. `test_release_polish.py`
updated to pin the 5s cap on both the sync and async jitter
helpers (post §7.2 #35 split).

Full incident write-ups in CHANGELOG.md under the same P0/S/P tags.
Three CI lint failures on `ruff check src/` — fixes only, no
behavioural changes:

- **B905** (`src/nullrun/decorators.py:162`): `zip(bound_params,
  args)` now passes `strict=False` explicitly. Pre-fix the two
  iterables can be different lengths — `bound_params` is sliced to
  `[: len(args)]` but the function may have fewer positional
  parameters than args provided (e.g. *args-style callables), in
  which case the trailing loop below handles the excess. `strict=`
  was implicit and triggered B905. Now explicit so the intent is
  documented in code.

- **I001** (`src/nullrun/instrumentation/auto.py:1146`): the late
  `import os as _os` was moved to the top-of-file import block as
  `import os` (alphabetical order: hashlib, json, logging, os,
  threading). The `_os` alias was only there to avoid shadowing —
  there is no top-level `os` in scope, so the plain name is fine.
  Call site updated to use `os.environ.get(...)`.

- **S108** (`src/nullrun/transport.py:632`): replaced the
  hardcoded `/tmp/nullrun.wal` with
  `os.path.join(tempfile.gettempdir(), "nullrun.wal")`. The
  hardcoded `/tmp` flagged S108 (insecure / non-portable temp
  path) and would have broken the SDK on Windows out of the box.
  `gettempdir()` returns the OS-appropriate temp dir
  (`/tmp` on Linux, `/var/folders/...` on macOS, `%TEMP%` on
  Windows). `NULLRUN_WAL_PATH` env override still wins, so
  containers with `readOnlyRootFilesystem: true` are unaffected.
  Added `import tempfile` to the top-of-file imports.

Verified:
  - `ruff check src/` → All checks passed!
  - `mypy src/` → Success: no issues found in 23 source files
  - `pytest` → 493 passed, 13 skipped (CI default, no `-W error`)
- Promote [Unreleased] to [0.5.2] — 2026-06-19; merge the two
  [Unreleased] sections that had drifted during Sprint 2.5 +
  Phase 0 development so release tooling scanning for the
  [Unreleased] anchor picks up the complete change set exactly
  once.
- Add PEP 561 marker (py.typed) — the package ships inline type
  annotations; the marker tells mypy / pyright / pylance to honour
  them.
- runtime.py (S-4): case-insensitive state compare in
  check_control_plane. Defensive against any backend casing drift
  beyond the current PascalCase (handlers.rs:9258). Pinned by
  tests/test_state_compare_case_insensitive.py (10 cases covering
  PascalCase / UPPERCASE / lowercase / mixed-case).

Working-notes file docs/integration-baseline-2026-06-19.md is
deliberately left untracked, matching the analyze.md pattern from
d74712e.
Lifts the SDK's Codecov score from 70.92 % to 84.52 % (+13.6 pp) by
adding 347 new tests across 10 files that exercise previously-untested
branches in the auto-instrumentation patches, runtime gates, transport
fallback modes, circuit breaker Redis path, and the @Protect decorator
fail-CLOSED contract.

pyproject.toml
  - Enable branch coverage so error / fallback paths count.
  - Raise fail_under from 70 → 82 (enforced in CI via `coverage run -m
    pytest && coverage report`).
  - Add precision=2 and skip_empty=true to keep the report readable.

New tests (all 817 pass locally, all 4 CI jobs green):

  tests/test_autogen_patch.py          — 13 tests
  tests/test_crewai_patch.py           — 15 tests
  tests/test_llama_index_patch.py      — 13 tests
  tests/test_langgraph_callback.py     — 38 tests
  tests/test_auto_requests.py          — 24 tests
  tests/test_runtime_branches.py       — 43 tests
  tests/test_transport_branches.py     — 44 tests
  tests/test_circuit_breaker_branches.py — 31 tests
  tests/test_protect_branches.py       — 43 tests
  tests/test_actions_context_init.py   — 50 tests

Per-file coverage deltas:

  instrumentation/autogen.py        21.33 → 93.41 %
  instrumentation/crewai.py         22.97 → 90.82 %
  instrumentation/llama_index.py    28.30 → 100.00 %
  instrumentation/langgraph.py      23.75 → 93.69 %
  instrumentation/auto_requests.py  33.72 → 99.09 %
  breaker/circuit_breaker.py        59.76 → 90.21 %
  transport.py                     82.57 → 84.79 %
  transport_websocket.py           68.70 → 64.10 % (msg-type branches
                                                  still need live ws
                                                  round-trip tests)
  decorators.py                    83.33 → 95.49 %
  runtime.py                       80.14 → 83.24 %
  context.py                       82.76 → 100.00 %
  actions.py                       92.12 → 96.89 %
  breaker/exceptions.py             98.51 → 97.26 %

All 4 CI jobs pass locally (pytest, ruff check, mypy, coverage).

Working-notes file docs/integration-baseline-2026-06-19.md is
deliberately left untracked, matching the analyze.md pattern from
d74712e.
@codecov

codecov Bot commented Jun 19, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@maltsev-dev maltsev-dev merged commit e078452 into master Jun 19, 2026
8 checks passed
@maltsev-dev maltsev-dev deleted the test/coverage-improvements-0.5.2 branch June 19, 2026 17:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant