# Classroom End-to-End Smoke Test — 2026-04-25

Clean tmpdir HOME, fresh `axi 0.10.11` (post-FastAPI, post-AEOS, post-Vega
consolidation). Walked the full instructor + student command surface to find
what regressed after the recent landings.

## Path that works end-to-end

`prep init` → `prep corpus` → `prep prompt --set ... --test ... && prep prompt
--approve` → `prep rag` → `prep lms --fake` → `publish` → `invite
--coordinator-url http://host:port/classroom/join` → `serve` → `join` →
`ask` → `briefs generate` → `briefs review --approve` → `me` (student) →
`ask-instructor` → `threads` → `reply` → `quiz broadcast` → `quiz pending`
(student) → `quiz take --json` → `quiz results` → `status` → `modes` →
`evals --bank-preset ne101-core` → `brief` (CHALKE) → `archive`.

All 23 commands above run, return, and exchange the right state. The end-to-end
ceremony is intact.

## Findings

### F1 — `prep prompt --set X --test Y --approve` rejects in one call

```
$ axi classroom prep prompt $CID --set "..." --test "..." --approve
No test prompt to approve — call test_prompt() first
```

Splitting into `--set X --test Y` then a separate `--approve` works. The CLI
accepts the three-flag combo at argparse but the handler treats `--approve` as
mutually exclusive with `--set/--test` instead of "test then approve in
sequence."

### F2 — `/healthz` endpoint missing on FastAPI coordinator

The coordinator boots and serves the documented endpoints (`/classroom/join`,
`/classroom/policy`, `/classroom/materials/*`, `/classroom/briefs/*`,
`/classroom/threads/*`, `/classroom/quizzes/*`, `/index.html`, `/webui/{rel}`)
but no `/healthz`. Old stdlib server had it; the FastAPI factory dropped it.

Cheap fix: add `@app.get("/healthz")` returning `{"ok": true,
"classroom_id": ...}` in `classroom_api.py`.

### F3 — `invite --coordinator-url http://host:port` (no `/classroom/join`
suffix) silently produces a non-functional invite

Student-side `join` POSTs the literal `coordinator_url` from the invite —
which means the URL must already end with `/classroom/join`. If the
instructor passes the bare base URL, the student gets a confusing
"couldn't reach the classroom server" error even though the server is up.

Two reasonable fixes (pick one):
- `_cmd_invite` normalises the URL to ensure `/classroom/join` suffix before
  storing.
- `ClassroomJoinClient.join` appends `/classroom/join` if `coordinator_url`
  doesn't already end with it.

The first is preferable — the CLI already has `_strip_join_suffix` for
materials/briefs/threads, so symmetric normalisation at write-time keeps
the invariant clear.

### F4 — Archive freeze is publish-only

`archive` flips state, and `publish` correctly refuses to re-publish an
archived classroom. But every other write path runs unaffected on an
archived classroom:

- `briefs generate` ✗ writes
- `quiz broadcast` ✗ writes
- `prep corpus` ✗ writes
- `prep prompt`, `prep rag`, `prep lms`, `prep assessment` not retested but
  almost certainly the same.
- `ask-instructor` (student) and `reply` (instructor) — likely the same.

Lifecycle gate needs to be a single check at the top of every state-mutating
command, not scattered. Cleanest path: a `_require_active(classroom_id)`
helper used by every write command, with `publish` and `archive` themselves
allowed past it.

### F5 — `quiz broadcast --bank-preset list` is not the listing affordance

```
$ axi classroom quiz broadcast $CID --bank-preset list
✗ No bank preset 'list'. Available: ne101_core
```

Help text suggests "`list` to see available", but `list` is treated as a
real preset name. Either special-case `list` in the broadcast handler or
add a dedicated `axi classroom quiz banks` subcommand. The error is
self-documenting so this is low-priority — but the help string lies.

## Things that surprised me in a good way

- **Auto-identity**: first invocation of `invite` and `join` in clean HOMEs
  both auto-generated identity with no prompt. UX is exactly the
  proactive-prerequisites doctrine in `feedback_proactive_ux_minimize_cognitive_load`.
- **Materials sync on join**: student got the corpus + index automatically,
  so `ask` worked offline-after-join with no extra command. This is the
  Phase 5 work paying off.
- **Brief generation actually summarises real activity**: the brief reflected
  the question the student asked, not a stub. `briefs generate` is wired to
  the interaction log, not faked.
- **Quiz keyword scoring**: stub answers correctly graded as 0%, with the
  missing keywords surfaced. Simple but it works.

## Pre-Sunday gate items

Of the 5 findings, F3 + F4 are the only ones that would visibly hurt the two
nuclear engineering researchers in the Monday session. F2 is a 5-line patch worth doing for monitoring.
F1 + F5 are CLI papercuts that don't block ceremony.

Order of fixes for the rest of this session:
1. F3 — invite URL normalisation (hardest to discover, easiest to misdiagnose)
2. F4 — `_require_active` archive gate on writes
3. F2 — `/healthz` for the coordinator
4. F1, F5 — papercuts, fix opportunistically while in the same files

Then the remaining feature-completeness items: Light Canvas roster sync,
file locking, memory-transparency `axi classroom me --memory`.
