# Spec tests — zasady

Testy w tym katalogu to **żywa specyfikacja systemu**. Padający spec
oznacza albo bug w produkcji, albo drift od dokumentowanego kontraktu
(`run_sql.md` / `prompt.txt`). Naprawia się ten, który ma rację — kod
albo dokument — nie test.

## Źródło prawdy

- **Storage / SQL:** `admin/mcp/run_sql.md` — to jest description toola
  `run_sql`, który MCP wysyła agentowi. Cokolwiek tam jest udokumentowane
  (schemat, semantyka kolumn, jednostki czasu, model `event_tags`,
  replaceable rules) jest kontraktem.
- **Tracking / discovery / frontier:** `mcp_nostr_db/prompt.txt` — sekcje
  „Tracked pubkeys — model trój-rolowy", „MentionSweep", „Auto-disable",
  „Lock convention", „Role ownership".

**Nie pisz testów patrząc w ingest_sql.py.** Implementacja ma odpowiadać
kontraktowi, a nie odwrotnie. Jeśli kontrakt jest niejasny, popraw
najpierw kontrakt.

## Fixturki (`conftest.py`)

- `core` — świeży `AdminCore` na temp DuckDB, **bez wątków startowanych**.
  Dla testów które weryfikują stan tabel po synchronicznym tool-callu.
- `running_core` — `core` z `start()` zawołanym (bus + auto-tracker +
  mention-sweep żywe). Auto-tracker tickuje co 0.1s (override dla
  testów). Dla testów obejmujących relay/discovery/live-sub.
- `mock_relay` — `MockRelay` na losowym porcie 127.0.0.1, zarejestrowany
  w `relays` (przez `core.add_relay(url)`, nie surowy SQL). Default
  handler: replayuje `relay.staged_events` na każdy REQ + EOSE.

Teardown każdej fixturki idempotentny.

## Helpery (`_helpers.py`)

- `new_keypair() -> (prv, pub)` — coincurve `PrivateKey` + 64-char hex.
- `new_pub() -> str` — sam hex pub gdy nie potrzeba podpisywać.
- `signed_event(prv, kind, *, content, created_at, tags)` — pełny NIP-01
  event z realnym schnorr-sig.
- `RelayHelper.expect_reqs(relay, filters, *, timeout=5.0)` — czeka aż
  wszystkie podane filtry pojawią się jako REQ na relayu.
- `DbHelper.expect_row(core, sql, params, *, timeout=3.0)` — czeka na
  DOKŁADNIE jeden wiersz. **Twardy błąd przy 2+** (nie ucisza duplikatów).
- `DbHelper.expect_rows(core, sql, params, *, count=N, timeout=3.0)` —
  czeka na co najmniej N wierszy; zwraca wszystkie.
- `wait_until(predicate, *, timeout, message)` — niski-poziom polling
  gdy potrzebujesz czegoś bardziej niestandardowego.

## Konwencje pisania testu

1. **Jedno zachowanie per test.** Test mieszający „A działa, B nie działa"
   wskaż w dwóch testach. Padający test ma od razu wskazać *co* się
   zepsuło.
2. **Jednolinijkowy docstring po polsku.** Opisuje *co* test weryfikuje
   i *dlaczego to ma znaczenie*. Spec ma się czytać jak prosty opis
   systemu, nie jak komentarz do kodu.
3. **`prv` / `pub`** dla kluczy. Nie `sk` ani `pubkey_hex` —
   konsekwentnie.
4. **Asercje na konkretnych wartościach.** `row == {...}` zamiast
   `assert row is not None`. Spec mówi *jak wygląda* dany wpis, nie
   *że istnieje*.
5. **Czytaj DB przez `core.run_sql(...)`.** To samo API co MCP-agent
   widzi. `core.daemon._conn` jest dla wewnętrznego setupu, nie dla
   asercji.

## Wzorce synchronizacji

- **Sentinel** — w testach z wieloma eventami, ostatni event w sekwencji
  jest sync pointem: `DbHelper.expect_row(core, "SELECT 1 FROM events
  WHERE id = ?", [last_evt["id"]])`. Po nim wszystkie wcześniejsze są
  już zaingestowane (ingest serialized pod `_sql_lock`).
- **Filtrowanie SQL po docelowym stanie** — gdy weryfikujesz *zmianę*
  (np. promotion ignored→secondary), SQL musi mieć `WHERE
  role='secondary'`, inaczej `expect_row` zwróci pierwszy wiersz (stary
  stan) i asercja padnie zanim system zdąży się zaktualizować.
- **DISTINCT dla append-only tabel** — `fake_events` nie ma PK; każda
  redelivery dodaje nowy wiersz. Asercje końcowe na takich tabelach
  używają `SELECT DISTINCT`.

## Pliki

- `test_<obszar>.py` — testy pojedynczych zachowań.
- `test_<obszar>_extended.py` — scenariusze z bałaganem (wiele aktorów,
  pętle, role flips). Asercja TYLKO na końcu, sentinel jako sync point.
  Każdy basics-spec już dowiódł rule w izolacji; extended ma łapać
  interakcje między rules.

## Co testujemy gdzie

- `test_primary_secondary.py` — role lifecycle, forward/reverse frontier,
  set_tracking_status transitions, ignored revive, discovery shape per
  role.
- `test_primary_secondary_extended.py` — role oscillation pod naporem
  eventów, multi-pubkey chaos, frozen-at-ingest-time forward frontier.
- `test_event_storage.py` — mapowanie eventu NIP-01 na tabele DuckDB
  (events, event_tags, event_seen_on, profiles, addressables [widok],
  pubkey_relays, fake_events).
- `test_event_storage_extended.py` — bulk inserts, replaceable shuffle,
  mixed valid/invalid sequences.
