# M13a W5 Verification

How to prove the W5 milestone (JWT gate on routerd / archiverd /
deliverd-fcm / deliverd-telegram) is done.

## What "W5 done" means

W3 shipped the JWT gate on admind and the shared
`authd.NewFromEnv()` helper, but left the other four
HTTP services out-of-scope (they were NATS-only consumers
with no admin HTTP). W5 picks up the work: add a real
admin route to each service, gate it with the same JWT
middleware, ship tests and smoke coverage.

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

1. Each of `cmd/routerd`, `cmd/archiverd`,
   `cmd/deliverd-fcm`, `cmd/deliverd-telegram` has a
   `wireAdminRoutes(mux, …, logger)` helper that:
   - No-ops when `BA_AUTHD_JWT_SECRET` is unset (the LAN
     deploy path keeps working unchanged).
   - Wires the gate with `ad.RequireAuth` /
     `ad.RequireRole` when the secret is set.
2. Each service exposes at least one real admin route:
   - `routerd`: `GET /v1/admin/dedupe/state` (read) +
     `POST /v1/admin/dedupe/flush` (destructive, admin role).
   - `archiverd`: `POST /v1/admin/archiver/run` (any
     authenticated user; the archiver is idempotent).
   - `deliverd-fcm`: `GET /v1/admin/dlq` +
     `GET /v1/admin/dlq/{id}` (channel=fcm, cross-channel
     safety: non-fcm row returns 404).
   - `deliverd-telegram`: same shape (channel=telegram).
3. Unit tests cover: no-secret → 404 (route unregistered),
   with-secret + no-token → 401, role policy per service.
   routerd also has an end-to-end flush test against a real
   `dedupe.Collapser`. archiverd has a coalescing test
   (pre-filled channel → 202).
4. `go build ./...`, `go vet ./...`, `go test -count=1 ./...`
   all clean.
5. `scripts/m13a_smoke.sh` covers the W5 routes end-to-end
   against a running stack (checks 9–12).
6. `docker-compose.yml` passes `BA_AUTHD_JWT_SECRET` to all
   four new services (sharing the same secret authd uses).

## Step-by-step

### 1. Build + test

```bash
cd /root/.openclaw/workspace/broad-announce
go build ./...
go vet ./...
go test -count=1 ./cmd/routerd ./cmd/archiverd ./cmd/deliverd-fcm \
                  ./cmd/deliverd-telegram ./internal/dlq
```

Expected:

```
ok  	git3.techno-world.net/lrosales/broad-announce/cmd/routerd
ok  	git3.techno-world.net/lrosales/broad-announce/cmd/archiverd
ok  	git3.techno-world.net/lrosales/broad-announce/cmd/deliverd-fcm
ok  	git3.techno-world.net/lrosales/broad-announce/cmd/deliverd-telegram
?   	git3.techno-world.net/lrosales/broad-announce/internal/dlq  [no test files]
```

Test counts:

| Package                  | Tests  | Subtests | Notes                            |
|--------------------------|--------|----------|----------------------------------|
| cmd/routerd              | 5      | 4        | incl. end-to-end Collapser flush |
| cmd/archiverd            | 5      | 3        | incl. coalescing path            |
| cmd/deliverd-fcm         | 3      | 3        | any-authenticated-user can read  |
| cmd/deliverd-telegram    | 3      | 3        | any-authenticated-user can read  |
| **Total new (W5)**       | **16** | **13**   |                                  |

### 2. Run the gate unit tests in isolation

```bash
cd /root/.openclaw/workspace/broad-announce
go test -count=1 -v -run 'TestWireAdminRoutes|TestAdminDedupe|TestAdminRunNow|TestAdminRolePolicy|TestAdminDLQ' \
  ./cmd/routerd ./cmd/archiverd ./cmd/deliverd-fcm ./cmd/deliverd-telegram
```

Each of the four services should show:

- `TestWireAdminRoutes_NoSecret_Disabled` — when
  `BA_AUTHD_JWT_SECRET` is unset, the admin routes return
  404 (they're not registered). This is the LAN-deploy
  backward-compat path.
- `TestWireAdminRoutes_WithSecret_Gated` — when the secret
  is set, the admin routes return 401 without a token.

routerd adds:

- `TestAdminDedupeState_Auth` — an authenticated user can
  read the dedupe state JSON.
- `TestAdminRolePolicy_ViewerReadOnly_AdminCanFlush` —
  viewer can read state (200), cannot flush (403);
  tenant_admin and super_admin can flush (200).
- `TestAdminDedupeFlush_DrainsPending` — end-to-end: a
  pending collapse is observed, the handler is called, the
  pending count drops to 0.

archiverd adds:

- `TestAdminRunNow_Auth_Fires` — an authenticated user can
  trigger a run; the trigger channel receives the signal.
- `TestAdminRunNow_Coalesces` — a second concurrent
  request returns 202 with `triggered=false,
  reason=already_pending`.
- `TestAdminRolePolicy_ViewerCanTrigger` — viewer,
  tenant_admin, super_admin can all fire (idempotent op).

deliverd-fcm and deliverd-telegram add:

- `TestAdminDLQ_AnyAuthenticatedUser_CanList` — viewer,
  tenant_admin, super_admin can all read the per-channel
  DLQ (the gate passes; the handler then runs the SQL
  query).

### 3. Run the full M13a smoke (covers W5 end-to-end)

```bash
cd /root/.openclaw/workspace/broad-announce
docker compose up -d --build
bash scripts/bootstrap-super-admin.sh
bash scripts/m13a_smoke.sh
```

Expected: all 12 sections green, summary shows
`$PASS passed, $FAIL failed` with `0 failed`.

The W5-specific checks (9–12) verify:

- **9.** routerd `/v1/admin/dedupe/state` and
  `/v1/admin/dedupe/flush`:
  - No auth → 401.
  - With super_admin Bearer → 200.
- **10.** archiverd `/v1/admin/archiver/run`:
  - No auth → 401.
  - With Bearer → 200 (or 202 if a run is already in
    flight; both are accepted by the smoke check).
- **11.** deliverd-fcm `/v1/admin/dlq`:
  - No auth → 401.
  - With Bearer → 200, body contains `"channel":"fcm"`.
- **12.** deliverd-telegram `/v1/admin/dlq`:
  - No auth → 401.
  - With Bearer → 200, body contains `"channel":"telegram"`.

### 4. Backward-compat verification (LAN deploy)

To confirm the W5 gate doesn't break the LAN-only deploy
(no `BA_AUTHD_JWT_SECRET`):

```bash
cd /root/.openclaw/workspace/broad-announce
# Comment out the BA_AUTHD_JWT_SECRET env on the four
# new services in docker-compose.yml (or just leave it
# empty), then:
docker compose up -d --build
curl -s -o /dev/null -w "%{http_code}\n" \
  http://127.0.0.1:8801/v1/admin/dedupe/state
# expect: 404 (route unregistered)
```

## Notes

- The `internal/dlq/query.go` package is the shared SQL
  helper used by both per-channel admin endpoints. The
  `List` query has a 30-day hard cap on `created_at` (same
  as admind's global DLQ endpoint). The `Get` query
  returns the full row including payload, with a
  `pgx.ErrNoRows` → `(nil, nil)` idiom for idempotent
  callers.

- The archiverd `runLoop` got a new `triggerCh` parameter
  (buffered to size 1). The loop now selects on
  `{ctx.Done, tick.C, triggerCh}`. The default cadence
  (`BA_ARCHIVERD_RUN_EVERY_SECONDS`, 3600s) is unchanged;
  the admin route is a side door that fires a run on
  demand.

- The cross-channel safety net in the per-channel DLQ
  endpoints (`row.Channel != "fcm"` → 404) prevents a
  deliverd-fcm from accidentally exposing telegram-channel
  DLQ rows. The global `/v1/dlq` in admind doesn't need
  this; it serves the full cross-channel view.

## See also

- `M13a_PLAN.md` § W5 — the design rationale and exit
  criteria.
- `scripts/m13a_smoke.sh` — the E2E smoke, including the
  W5 checks 9–12.
- `internal/authd/middleware.go` — `RequireAuth` and
  `RequireRole` (W2). The W5 admin routes are thin
  wrappers around these.
