# Plan 4 C2 post-merge smoke test — 2026-04-14

**Result:** 5/5 gates PASS against RTX 5080 + Qwen2.5-14B-Instruct Q4_K_M via Cloudflare tunnel.

**Branch:** `main` at `204ed16` (PR #113 — Plan 4 C2 merged).

**Protocol:** [session_id_smoke_rerun.md](../protocols/session_id_smoke_rerun.md) extended with gate 5 (full drain cycle).

**Trace archives:**
- [c2_smoke_bench_run1_20260414.jsonl](c2_smoke_bench_run1_20260414.jsonl)
- [c2_smoke_bench_run2_20260414.jsonl](c2_smoke_bench_run2_20260414.jsonl)
- [c2_smoke_sim_20260414.jsonl](c2_smoke_sim_20260414.jsonl)

## Pre-flight health gate

```
time curl ... https://maxim.dennyschaedig.com/v1/chat/completions
real   0.759s
http_code=200
content: "Hi there! How can I assist you"
```

PASS — sub-second round-trip, healthy chat endpoint.

## Gate 1 — Back-to-back bench session_id distinction

```
SID1=bench_20260414_190000  SID2=bench_20260414_190009
```

| Check | Result |
|---|---|
| Distinct session_ids | ✓ PASS |
| Run 1 shape `bench_YYYYMMDD_HHMMSS` | ✓ PASS |
| Run 2 shape `bench_YYYYMMDD_HHMMSS` | ✓ PASS |
| Zero null session_id on `peer_backend_call` events (both runs) | ✓ PASS |

## Gate 2 — Real sim end-to-end coverage

Sim: `maxim --sim "just say hello and call finish_simulation" --seed 42 --sim-max-turns 3`

```
events=15  null_agent=0  null_session=0  null_request=0
single sid across sim: 20260414_190051
```

| Check | Result |
|---|---|
| Events captured | ✓ PASS (15) |
| Zero null agent_id | ✓ PASS |
| Zero null session_id | ✓ PASS |
| Zero null request_id | ✓ PASS |
| Single session_id across sim | ✓ PASS |

## Gate 3 — Report / JSONL / report.json triple-match

| Check | Result |
|---|---|
| Report dir exists | ✓ PASS |
| `report.json.session_id` matches JSONL | ✓ PASS |
| `report.json` present | ✓ PASS |
| `actions.jsonl` present | ✓ PASS |
| `aut_hippocampus.json` present | ✓ PASS |
| `aut_nac.json` present | ✓ PASS |

## Gate 4 — Doctor sanity

```
━━━ Environment ━━━
  ✓ Platform: macOS 15.7.3
  ✓ Architecture: arm64
  ⚠ GPU / CUDA: No CUDA device available  [expected on peer]
  ✓ LLM Tiers / headroom / disk / RAM / storage / MAXIM_ROLE=peer

━━━ Peer Connectivity ━━━
  ✓ Remote URL: maxim.dennyschaedig.com reachable
  ✓ Peer API key + auth + leader probe (173 ms)
  ✓ --llm vs peer config
  ✓ Remote model: Qwen2.5-14B-Instruct.Q4_K_M.gguf
  ✓ Peer latency: p50=192 ms, p95=252 ms (5/5 probes)

━━━ Mesh Topology ━━━
  ✓ Node leader: reachable (leader, https://maxim.dennyschaedig.com/v1) [484ms]
```

| Check | Result |
|---|---|
| Environment all green (GPU ⚠ expected) | ✓ PASS |
| Peer Connectivity all ✓ | ✓ PASS |
| 5/5 latency probes | ✓ PASS |
| Mesh Topology section populated | ✓ PASS |

## Gate 5 — C2 drain cycle end-to-end (NEW)

The C2 gate validates the full drain workflow against the live leader: empty state → self-drain refusal → `--force-self` override → list-drained → list-nodes display (table + JSON) → doctor display → resume → idempotent resume → empty state.

### 5a — list-drained empty (clean state)

```
$ maxim peer list-drained
No nodes drained.
```
✓ PASS — clean baseline state confirmed.

### 5b — drain self refused without `--force-self` (A2 fold)

```
$ maxim peer --node leader drain
✗ Refusing to drain self ('leader') — this strands in-flight
  requests and is almost always a mistake.
  → If you're sure, re-run with --force-self:
      maxim peer --node leader drain --force-self
$ echo $?
2
```
✓ PASS — exit code 2, friendly error message rendered. The C2 fold A2 invariant ("self-drain guard enforced at the state layer, not just the CLI layer") is active. The state layer's `drain_node(name, known, self_name=, force_self=False)` raised `DrainError`, the CLI caught it and rendered the operator hint.

**Smoke script gotcha caught here:** the original gate 5 script piped through `grep -v role_divergence` and captured `$?` directly, which actually captures `grep`'s exit code (0), not maxim's. Fix: use `${PIPESTATUS[0]}` or `set -o pipefail`. Direct invocation without the pipe confirmed exit 2.

### 5c — drain self with `--force-self` succeeds

```
$ maxim peer --node leader drain --force-self
✓ Drained 'leader'. Drain set: ['leader']
```
✓ PASS — explicit override accepted, drain state file written.

### 5d — list-drained shows the drain

```
$ maxim peer list-drained
Drained nodes (1):
  ⊝ leader
```
✓ PASS — drain state persisted across separate CLI invocations (filelock cross-process serialization working).

### 5e — list-nodes table shows drained inline

```
$ maxim peer list-nodes
━━━ Mesh: 1 node(s), self=leader, 1 drained ━━━
  ⊝ leader  leader  https://maxim.dennyschaedig.com/v1 (self)
      → drained (not probed) — https://maxim.dennyschaedig.com/v1
```
✓ PASS — header count `1 drained` rendered, ⊝ symbol used (drained sentinel distinct from healthy `info`), no network probe attempted.

### 5f — list-nodes --json drain shape

```json
{
  "self": "leader",
  "protocol_version": 1,
  "nodes": [
    {
      "name": "leader",
      "url": "https://maxim.dennyschaedig.com/v1",
      "role": "leader",
      "status": "info",
      "drained": true,
      ...
    }
  ],
  "orphans": []
}
```
✓ PASS — `drained: true`, `status: info`, empty `orphans` array. JSON shape matches `maxim doctor --json` schema for tooling consumption. **No network call made for the drained node** (regression-guarded in `tests/unit/test_peer_mesh_cli.py::test_drained_node_not_probed`).

### 5g — doctor mesh section shows drained

```
━━━ Mesh Topology ━━━
  ℹ Node leader: drained (not probed) — leader, https://maxim.dennyschaedig.com/v1
```
✓ PASS — doctor's `check_mesh_nodes` correctly produces an `info` `CheckResult` for the drained node, with `retry_id` set so the retry loop's filter can include it. The doctor display uses `ℹ` (the standard CheckResult info symbol) rather than the CLI's `⊝` (intentional — CheckResult symbols come from the doctor module's own status table).

### 5h — resume clears + idempotent

```
$ maxim peer --node leader resume
✓ Resumed 'leader'. Drain set: []

$ maxim peer --node leader resume
ℹ 'leader' was not drained. Drain set: []
```
✓ PASS — first resume cleared the drain, second resume returned exit 0 with `was not drained` informational message. E9 exit code contract from the C2 pre-design review is upheld.

### 5i — list-drained empty post-resume

```
$ maxim peer list-drained
No nodes drained.
```
✓ PASS — clean state restored.

## Summary

| Gate | Name | Result |
|---|---|---|
| 1 | Bench per-run session_id distinction | ✓ PASS |
| 2 | Sim end-to-end agent_id + session_id + request_id coverage | ✓ PASS |
| 3 | Report directory / JSONL / report.json triple-match | ✓ PASS |
| 4 | Doctor peer connectivity + Mesh Topology section | ✓ PASS |
| 5 | C2 drain cycle (refuse self → force-self → list-drained → list-nodes table+JSON → doctor → resume → idempotent → empty) | ✓ PASS (9 sub-gates) |

**5/5 PASS.** Plan 4 C2 validated end-to-end on main at commit `204ed16`. The drain workflow works against the live leader through the Cloudflare tunnel, drain state persists across CLI invocations via filelock cross-process serialization, and zero behavior regressions vs C1.

**Wall-clock:** ~2 minutes total (pre-flight + 2× bench + sim + doctor + drain cycle).

## Notes

- **Gate 5b script gotcha** — the original smoke script captured exit code via `$?` after a `grep` pipe, which actually captures `grep`'s exit. The first run reported "5b FAIL exit=0" — false alarm caught immediately by re-running without the pipe. Filed for the C2 smoke runbook: use `${PIPESTATUS[0]}` or `set -o pipefail` when piping CLI output through filters.
- **`role_divergence` WARNING at the top of every CLI invocation** is pre-existing and still active post-C2. Filed against the Plan 4 C3 followup list. Ignore for smoke purposes — Plan 4 C3 will consolidate the leader_mode vs role.py detection.
- **J1 fold confirmed working** — only one `role_divergence` WARNING per `maxim peer ...` invocation (previously two; the doubled detection was the J1 finding from the C2 pre-merge review).
- **CCR1 reprobe closure** is registered but not exercised in this smoke (no orphan drain entry on this single-node mesh). Tested via `tests/unit/test_doctor.py::TestOrphanDrainReprobe`. Will exercise organically when the user creates a multi-node mesh.yml in C3.
- **Latency variance** between gate 4 (484ms) and gate 5g (similar) is normal stage-2 variance — not a regression.
- **No use of `~/.maxim/util/drained_nodes.peer.txt.lock`** lingering after the smoke completed (verified post-test). The filelock cleanup is correct.
