# M1 Verification

How to prove the M1 milestone is done.

## What "M1 done" means

SPEC §23: "HTTP POST ingest end-to-end — send a signed webhook →
FCM test push to a fake device; layers 1, 3, 4, 5 in."

Concretely, the **M1 exit criteria** are:

1. `docker compose up -d` brings up the full M0+M1 stack (the new
   `seed` sidecar runs once, applies migrations + seed, exits).
2. `seed` finishes `ok` and the `companies`/`individuals`/
   `fcm_tokens` tables are populated.
3. `fakefcmd` is healthy on `:8820` (its `/health` returns
   `{"status":"ok",...}`).
4. A signed POST to ingestd on `:8800` returns `202` (M0 still works).
5. Within ~2s, a row appears in the `deliveries` table with
   `status='sent'` and `channel='fcm'`.
6. fakefcmd's stderr log shows the corresponding `fakefcmd send`
   line with the right `alert_id`.
7. M0 protections still fire: bad signature → 401, payload too large
   → 413, invalid JSON → 400, rate-limit on burst → 429.

## Step-by-step

### 1. Bring up the stack

```bash
cd /root/.openclaw/workspace/broad-announce
docker compose build         # build all images
docker compose up -d seed    # run migrations + seed
docker compose up -d         # bring up the rest
docker compose ps            # all "Up (healthy)" except loadgen
```

Expected: the `seed` container exits 0 within ~5s after Postgres is
healthy. The other services come up.

### 2. Verify the seed

```bash
docker compose logs seed
```

Expected last line: `seed: ok`.

```bash
docker compose exec postgres psql -U ba -d ba -c \
  "SELECT id, name FROM companies; SELECT id, full_name FROM individuals; SELECT count(*) FROM fcm_tokens;"
```

Expected:
```
   id    |   name
---------+----------
 acme-001 | Acme Corp
   id       |   full_name
------------+---------------
 ind-acme-001 | Alice Operator
 count
-------
     1
```

### 3. fakefcmd health

```bash
curl -fsS http://localhost:8820/health
```

Expected: `{"failed":0,"received":0,"service":"fakefcmd","status":"ok"}`.

### 4. Signed POST to ingestd

```bash
SECRET='s3cret-acme'
BODY='{"company_id":"acme-001","source_id":"prom-prod","severity":"critical","category":"storage","title":"Disk full on db-prod-03","body":"92% used","data":{"host":"db-prod-03"},"dedupe_key":"m1-test-001"}'
TS=$(date +%s)
SIG=$(printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $2}')

curl -fsS -X POST http://localhost:8800/v1/ingest \
  -H "Content-Type: application/json" \
  -H "X-BA-Signature: t=$TS,v1=$SIG" \
  --data "$BODY"
```

Expected: `{"alert_id":"...","dedupe_count":1,"received_at":"..."}`.

### 5. delivery row appears

Within ~2s:

```bash
docker compose exec postgres psql -U ba -d ba -c \
  "SELECT alert_id, individual_id, channel, target, status, attempts
   FROM deliveries ORDER BY id DESC LIMIT 5;"
```

Expected: one row with `channel='fcm'`, `target='fake-fcm-token-acme-alice-001'`,
`status='sent'`, `attempts=1`, `alert_id` matching step 4.

### 6. fakefcmd logged the send

```bash
docker compose logs fakefcmd | tail -3
```

Expected last line:
```
fakefcmd send path=/v1/projects/fakefcmd/messages:send token=fake-fcm-token-acme-alice-001…
  title=Disk full on db-prod-03 data_keys=8 alert_id=018f…
```

### 7. M0 protections still fire (regression check)

Bad signature:
```bash
curl -i -X POST http://localhost:8800/v1/ingest \
  -H "Content-Type: application/json" \
  -H "X-BA-Signature: t=1,v1=deadbeef" \
  --data "$BODY"
```
Expected: `401 {"error":"bad_signature"}`.

Payload too large:
```bash
python3 -c 'import sys; sys.stdout.write("{\"x\":\"" + "a"*300000 + "\"}")' \
  | curl -i -X POST http://localhost:8800/v1/ingest \
      -H "Content-Type: application/json" --data-binary @-
```
Expected: `413 {"error":"payload_too_large"}`.

### 8. Burst through loadgen

```bash
docker compose --profile loadgen up loadgen-http
```

Expected: 30s of 50/s; 1500 messages accepted; ~1500 deliveries
written (1:1 with the single seeded token in M1); no failures.

```bash
docker compose exec postgres psql -U ba -d ba -c \
  "SELECT status, count(*) FROM deliveries GROUP BY status;"
```

Expected: ~1500 `sent`, 0 `failed`, 0 `dlq`.

### 9. Dedupe still works end-to-end

Send the same `dedupe_key` twice in quick succession. Both
deliveries succeed (one per the dedupe window), but
`dedupe_count` in the 202 response is 2 on the second call.

## What's NOT in M1 (the honest list)

- **Recipient resolution rules** (subscriptions, opt-in per
  source/severity, quiet hours, routing rules). M1 is broadcast.
  **M2**.
- **Real FCM** (Google service account, real HTTP v1 endpoint).
  M1 talks to fakefcmd. **M3**.
- **Other channels** (Telegram, SMS, email, Slack, Teams,
  webhook). **M3+**.
- **Retry with exponential backoff** (SPEC §9). M1 marks
  terminal status on the first attempt. **M3** with the per-channel
  retry worker.
- **DLQ table reads, replay UI**. **M8**.
- **Per-source `allowed_targets`** in the schema. **M2**.
- **ClickHouse archive, Timescale hypertables** for `alerts` and
  `deliveries`. **M7**.
- **HMAC secret in DB** (M1 reads from env). **M2**.
- **Multi-tenant query filters** at every query site. M1 has
  `WHERE company_id = $1` in routing. M2 audits every other query.
- **Per-IP concurrency cap, circuit breaker, quarantine** (M0
  honest scope). **M5 / M9**.

## If something fails

| symptom | first thing to check |
|---|---|
| seed: `connect: dial tcp ...:5432: connect: connection refused` | is postgres healthy? `docker compose ps postgres` |
| seed: `extension pgcrypto does not exist` | wrong image; we use `timescale/timescaledb:latest-pg16` which has it |
| ingestd returns 401 on a "valid" call | openssl pipe vs Go HMAC mismatch; recheck `printf '%s.%s'` |
| no rows in `deliveries` | is routerd healthy? `docker compose logs routerd` — it might not have created the consumer yet |
| `deliveries` row but `status=failed` | is fakefcmd up? `curl localhost:8820/health` |
| `last_error` says `status 503: UNAVAILABLE` | fakefcmd fault injection; check its log |
| consumer never picks up messages | NATS JetStream state retained from a previous run with different consumer config; `docker compose down -v` to wipe |
