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).
3. Companion "text follow along" — the texted-link page (visual)
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 |