← Choice Health calendar-remote-control-runbook.md raw .md

Runbook — Testing the Calendar Shift-Left + Companion Remote-Control Work

Date: 2026-07-02 · Branch: voice-capability-hierarchy

Everything here runs locally, no phone call and no cloud creds — except the last step (the live voice booking demo), which is called out. Each step lists the command, what success looks like, and what it proves.


0. Prerequisites (once)

cd ~/src/riff
set -a; . ./.env; set +a          # load env
which .venv/bin/python            # confirm the venv exists

1. Fast deterministic suite — the whole session's work (~1 min)

The highest-confidence check. Proves the calendar guardrail, the companion-WS wire + hardening, the scheduler time_range fix, and the booking invariants.

RIFF_SKIP_EVAL=1 .venv/bin/python -m pytest -q -p no:cacheprovider \
  tests/test_calendar_auth_load_checks.py \
  tests/test_companion_control.py tests/phone/test_companion_control_ws.py \
  tests/phone/test_observer.py \
  tests/providers/test_slot_mapper.py \
  tests/primitives/test_book_appointment_invariants.py

Expected: 89 passed, 2 xfailed. Proves: the load-time auth guardrail (9), the tap→ladder wire + concurrency hardening (28), the time_range normalization incl. the tool boundary (47), and book-appointment invariants I-1..I-5 (rest).


2. The calendar shift-left guardrail — catch a bad flow at BUILD time ⭐

The headline. A calendar mutation flow that skips the verification gate fails to load — so one caller can never move/cancel another's appointment, and the bug is caught at build, not on a live call.

RIFF_SKIP_EVAL=1 .venv/bin/python - <<'PY'
from riff.loader import _check_calendar_mutation_verified as chk
from riff.types import Flow, StateDef
mk = lambda *s: Flow(flow_id='demo', display_name='', business_profile={},
                     system_prompt='', initial_state=s[0].id,
                     states={x.id: x for x in s})
# an UNGATED cancel state (declares calendar.cancel, no caller_verified gate)
bad = StateDef(id='cancel_it', speech_act='act',
               allowed_tools=('schedule_event',), effects=('calendar.cancel',))
try:
    chk(mk(bad)); print('NO RAISE — bug!')
except ValueError as e:
    print('GUARDRAIL FIRED ✔\n ', e)
PY

Expected: GUARDRAIL FIRED ✔ with a message naming cancel_it and telling you to add preconditions: [caller_verified]. Proves: the load-time enforcement (§2a) works. Sanity: the four real calendar flows still load (tests/test_calendar_auth_load_checks.py covers this).


The page a caller gets by SMS, following a call live. No phone needed — a sim drives the real Flask+Sock app.

.venv/bin/python scripts/companion_sim_server.py 8807
# then open in a browser:
#   http://127.0.0.1:8807/companion.html?call=sim-call-1

Expected: the page follows a simulated Space Channel call live — menu → launches → space weather → "text link" → send. Ctrl-C to stop. Proves: the companion channel (the supplementary-info transport the calendar live-offer-mirror design builds on).


4. The tap control wire — flag on (plumbing proven; honest v0 limit)

RIFF_COMPANION_CONTROL=1 .venv/bin/python scripts/companion_sim_server.py 8807

Open the page, open DevTools Console, paste:

const ws = new WebSocket("ws://127.0.0.1:8807/ws/companion?call=sim-call-1");
ws.onmessage = e => console.log("REPLY:", e.data);
ws.onopen = () => ws.send(JSON.stringify({
  v:"0.1", call_id:"sim-call-1", role:"caller", channel:"companion",
  session_id:"tab1", client_id:"tab1", client_seq:1, decision_epoch:1,
  type:"tap", payload:{node:"suggestions", token:"launches"}
}));

Expected: a REPLY: {"type":"control_result", ...} in the console. Send the same frame again → the reply reason becomes duplicate (dedupe works). Honest v0 limit: the reply status is dropped / no_decision — a tap routes and replies (the wire is proven) but doesn't yet move the FSM. Full tap-drives-the-call needs the surface renderer (item 3) + a live call (item 1).


5. The humanness fix — no leaked placeholders

grep -rn "(internal)" flows/modules/book_appointment_v1.yaml flows/modules/scheduling_v1.yaml \
  && echo "LEAK STILL PRESENT" || echo "CLEAN ✔ — internal placeholders replaced"

Expected: CLEAN ✔. Proves: the act-state entry.lines that were being spoken aloud ("(internal) booking now") are now natural fillers ("Great — booking that in for you now.").


6. Live voice booking demo (needs the voice server + cal-provider creds)

The end-to-end booking path with the time_range P1 fix — the demo that used to escalate now books.

# start the voice/phone server (see scripts/start-phone-quick.sh on a tunneled host),
# then open:  http://127.0.0.1:8765/voice.html?flow=tutorial_book_appointment
# Say: "Tuesday afternoon" → pick an offered slot → give name + phone →
#      say "yes" at the read-back → hear "you're booked".

Expected: a real booking (an event_id), no escalation on "12:00 to 15:00" / "2-4pm" style phrasing. Proves: the P1 fix end-to-end. (This is the one step needing a running voice server + cal-provider; steps 1–5 need neither.)


What each step covers, at a glance

Step Needs Proves
1 nothing all deterministic logic (89 tests)
2 nothing build-time auth guardrail
3 browser companion "follow along" page
4 browser tap→ladder wire + dedupe (v0: routes, doesn't act)
5 nothing humanness placeholder fix
6 voice server + creds the booking path end-to-end