# Recipe — Testing apps that use `mgf-common`

> **Audience:** any consumer writing pytest tests.
> **Source:** `mgf.common.pytest_plugin`.

`mgf-common` ships a pytest plugin auto-registered via the `pytest11`
entry point. After `pip install mgf-common`, every pytest run gets
five fixtures + one helper for free. No conftest boilerplate.

## At a glance

```python
# tests/test_my_app.py
def test_with_mgf_common(mgf_context):
    # mgf_context is a fully-bootstrapped AppContext.
    # Logging is at WARNING level. OTel is disabled. Crash reporter
    # is installed but routes to a /tmp file.
    assert mgf_context.app_name == "test-app"
```

That's it. `mgf_context` shuts down cleanly when the test ends.

## Available fixtures

### `mgf_settings` — fresh testing settings

```python
def test_with_settings(mgf_settings):
    # MgfSettings.testing() — WARNING-level logs, OTel off, deterministic preset.
    assert mgf_settings.logging.level == "WARNING"
```

Each test gets a fresh instance; mutations don't leak between tests.

### `mgf_context` — bootstrapped AppContext (most common)

```python
def test_logging_works(mgf_context, caplog):
    import logging
    logging.warning("hello")
    assert "hello" in caplog.text
```

The context is fully wired (identity, logging, crash reporter, OTel
disabled). LIFO rollback on test end.

### `mgf_context_with_capture` — log + span capture

```python
def test_observability(mgf_context_with_capture):
    # Do work that emits logs and spans.
    import logging
    logging.warning("captured")

    # captured_logs is a list of LogRecord objects.
    assert any("captured" in r.getMessage() for r in mgf_context_with_capture.captured_logs)

    # captured_spans (only populated if [observability] extra installed).
    # Use mgf_otel_provider as a session-scoped dep for span capture.
```

Span capture requires the `[observability]` extra (`pip install
'mgf-common[observability]'`). Without it, only log capture works
(span list is empty).

### `redaction_recorder` — assert what got redacted

```python
def test_secrets_are_redacted(mgf_context, redaction_recorder):
    from mgf.common.observability import Redactor

    payload = {"Authorization": "Bearer secret", "user": "alice"}
    Redactor().redact(payload)

    # The recorder captured the redaction call.
    assert "Authorization" in redaction_recorder[0]["input_keys"]
```

Each test gets a fresh recorder; entries don't bleed between tests.

### `mgf_otel_provider` — session-scoped TracerProvider

```python
@pytest.fixture(scope="session")
def my_session_setup(mgf_otel_provider):
    # Now the TracerProvider is active for every test in the session.
    yield
```

Session-scoped because OTel's `set_tracer_provider` is one-shot per
process. Skipped automatically if `[observability]` extra is missing.
Most tests won't need this directly — depend on
`mgf_context_with_capture` instead, which already pulls it in.

## The helper — `decode_json_logs`

When tests use the JSON log format (`MgfSettings.testing(log_format="json")`),
caplog records carry JSON strings. Decode them in one call:

```python
from mgf.common.pytest_plugin import decode_json_logs

def test_log_shape(mgf_context, caplog):
    import logging
    logging.warning("hello world")

    decoded = decode_json_logs(caplog)
    assert decoded[0]["level"] == "WARNING"
    assert decoded[0]["message"] == "hello world"
```

Records that aren't JSON are silently skipped (no exceptions).

## Disabling the plugin

Per-run:

```bash
pytest -p no:mgf_common
```

Per-project (in `pyproject.toml`):

```toml
[tool.pytest.ini_options]
addopts = "-p no:mgf_common"
```

## Common patterns

### Async tests

```python
import pytest

@pytest.mark.asyncio
async def test_async(mgf_context):
    # AppContext is per-process; the asyncio task inherits it via
    # the contextvar set by bootstrap_for_test.
    ...
```

### Two-tenant isolation

For multi-tenant apps that need per-test tenant isolation:

```python
import pytest
from mgf.common._lifecycle import AppContext

@pytest.fixture
def two_tenants(mgf_context):
    # Scoped contexts — one per tenant. Pushed onto the contextvar
    # at __enter__, popped at __exit__.
    with AppContext("tenant-a", "0.0.1") as ctx_a, \
         AppContext("tenant-b", "0.0.1") as ctx_b:
        yield ctx_a, ctx_b
```

### Custom settings overrides

```python
import pytest
from mgf.common.settings import MgfSettings

@pytest.fixture
def my_settings():
    return MgfSettings.testing(
        logging=MgfSettings.testing().logging.model_copy(update={"level": "DEBUG"}),
    )

@pytest.fixture
def my_context(my_settings, mgf_context):
    # mgf_context already used MgfSettings.testing(); for custom
    # settings, use bootstrap_for_test directly with overrides.
    ...
```

## Perf attribution — `perf_span` + `mtest --perf` (TS-28)

When a test (or the code it drives) is mysteriously slow, you want to
know *where* the time goes — at the function level and at the
section level. That's TS-28.

**Section/log-level** — wrap a unit of work in `perf_span`:

```python
from mgf.common.testing import perf_span

def test_checkout(client):
    with perf_span("seed fixtures"):
        seed(client)
    with perf_span("POST /checkout"):      # nestable
        resp = client.post("/checkout")
    assert resp.status_code == 200
```

`perf_span` is nestable, fail-open, and costs ~nothing when capture
is off (a single context-var read), so you can leave it wrapped
around hot paths in production code — not just tests.

**Function-level** — no code needed: capture runs a per-test
`cProfile` and reports the top frames by cumulative time, covering
the test body *and* the code under test.

**Turn it on** via the test supervisor:

```sh
mtest --perf                 # whole selection, with perf capture
mtest --perf --tier 0-1      # composes with tiering / budget
pytest --mgf-perf --mgf-perf-dir .gates/last-run/perf   # plugin alone, no supervisor
```

Where it shows up: the `perf` field of each `run.jsonl` record, a
`perf attribution` section in every failure bundle, and a run-level
`.gates/last-run/perf-summary.md` (slowest tests + hottest functions
by self-time). Capture is **opt-in** and **fail-open** — it never
changes a test's outcome — and a `--perf` run is treated as
diagnostic, so its (cProfile-inflated) timings don't pollute the
budget/auto-tier baseline. See `mgf-fed standards TESTING` §19.7.

## The OTel one-shot caveat

OpenTelemetry's `set_tracer_provider` is a process-wide one-shot.
Once a `TracerProvider` is set, every subsequent call is silently a
no-op. This means:

- Test classes that re-bootstrap inside a fixture lose span capture
  silently.
- Tests that pollute the provider via direct OTel calls break later
  tests.

The `mgf_otel_provider` fixture is session-scoped specifically to
solve this: it claims the slot ONCE for the whole test run.

If your test suite needs to install its own `TracerProvider` (e.g.,
for span exporter testing), do it BEFORE any test that depends on
`mgf_otel_provider`. The plugin's fixture defers to an already-
installed SDK provider, so coexistence is safe.

## Verifying the plugin is loaded

```bash
pytest --co -q 2>&1 | head -5
# Look for: "plugins: mgf_common-X.Y.Z, ..."
```

Or programmatically:

```python
from importlib.metadata import entry_points
assert "mgf_common" in {ep.name for ep in entry_points(group="pytest11")}
```

## Editable-install caveat (PAPER-23)

Editable installs of `mgf-common` via `[tool.uv.sources]` (or any tool
that wires the source tree into a sibling project without writing the
dist-info `entry_points.txt`) silently skip the `pytest11`
registration. The fixtures still exist; they just aren't auto-loaded.

If `pytest --fixtures | grep mgf_` returns nothing in your sibling
checkout, opt in explicitly via `tests/conftest.py`:

```python
# tests/conftest.py
pytest_plugins = ["mgf.common.pytest_plugin"]
```

Pytest discovers this at collection start and loads the plugin before
fixtures resolve. Survives `uv sync` re-runs and is more discoverable
than the entry-point form (a reader of conftest sees the dependency
at a glance).

PyPI-installed consumers do NOT need this — the entry point fires
during `pip install` / `uv pip install` and pytest finds the plugin
automatically.

## See also

- `docs/reference/observability.md` — log + OTel + redaction primitives.
- `mgf.common._test_helpers.bootstrap_for_test` — the underlying helper
  the fixtures wrap.
- `docs/reference/full_app.md` — full worked example (when bootstrap +
  fixtures combine for a real app's test suite).
