## Context Every genesis package is required to ship `@opentelemetry/api` instrumentation (counters, histograms, spans) on its public entrypoints, with a no-op fallback when no SDK is registered (`openspec/specs/testing/spec.md` "Observability" requirement; `CLAUDE.md` Quality gates). The audit at `/mnt/s/web/genesis/docs/superpowers/specs/2026-04-25-test-suite-audit.md` §1 and §4 records two related defects: there is no fixture that exercises the export path of that instrumentation, and the test runner itself emits no telemetry — so flaky-test triage has nothing to correlate against. This change addresses both at once because the runner-side requirement is meaningless without a collector to receive its export, and the collector fixture is hard to justify without a real producer driving it. ## Goals / Non-Goals **Goals:** - A `ServiceFixture` plugin for `otel/opentelemetry-collector-contrib` whose lifecycle, env-bridge, and health contract match the conventions established by the existing `testing-integration-*` plugins (CLAUDE.md package list). - A runner reporter at `@suaveplan/testing/reporters/otel` that wraps each `it`/`test` in a span, records assertion failures as span events, and serialises the full span tree of failed tests to disk + Garage S3. - Deterministic span ids and timestamps under test (the reporter accepts `Clock` and `IdGenerator` from `@suaveplan/types` so OTel-of-OTel-tests is itself reproducible). - Zero behaviour change when `OTEL_EXPORTER_OTLP_ENDPOINT` is unset. **Non-Goals:** - Replacing the per-package `@opentelemetry/api` instrumentation. The reporter wraps tests, not source code. - Shipping a Jaeger / Tempo / Honeycomb backend — the collector fixture supports `logging`, `memory`, `jaeger`, and `file` exporters; downstream backend choice is a consumer concern. - Sampling. Every test span is exported; tail-sampling is the consumer's job in their own collector config. ## Decisions - **Memory exporter is the assertion path.** The `getSpans({ filter })` helper queries the collector's in-memory exporter rather than parsing `logging` exporter stdout — assertion-grade reliability is required. - **Trace sink is opt-in.** The reporter serialises failure traces unconditionally to `playwright-report/traces/.json` (zero infra), but only uploads when `TESTING_TRACE_SINK_URL` is set. This avoids surprise S3 traffic from local runs. - **Reporter ships in `@suaveplan/testing`** (not in the new plugin) so that runner-side OTel works without taking the testcontainers dep — testing-infrastructure rule (`CLAUDE.md` rule 8) forbids `@suaveplan/*` runtime deps in `@suaveplan/testing` but not external `@opentelemetry/*` packages, which are already permitted. - **Config synthesis vs `configFile`.** Most consumers want "OTLP HTTP receiver + memory exporter"; the plugin synthesises that case from the typed config object. Power users pass `configFile` and the plugin mounts it verbatim — no template language. - **Image pin.** Tag `0.` initially (matches existing plugin practice); `tasks.md` tracks the digest-pin migration as a follow-up so this change does not block on a registry lookup. ## Risks / Trade-offs - **Collector startup latency** can dominate fast unit-test suites. Mitigated by `skipIfUnavailable()` falling through when the fixture cannot start (consistent with other `testing-integration-*` plugins) and by recommending nightly-only export in CI. - **Memory-exporter retention** is bounded; long suites can drop early spans. The plugin documents the bound and exposes a `flushSpans()` method that pages through the memory exporter at suite teardown. - **Failure-trace upload** introduces a network dependency on red runs. Mitigated by best-effort upload (failures logged, not thrown) and by the env-var gate.