fix(l1): answer buttons must match the question — yes_label/no_label end-to-end

Live walk defect: the builder generated alternatives questions ("Is Jane's
account a Microsoft account or a local account?") while the UI could only
offer Yes/No. Root cause: SYSTEM_PROMPT mandated a label-less
'<yes/no question>' shape with no way to express the two answers.

- SYSTEM_PROMPT: question nodes must carry yes_label/no_label — the literal
  button texts; alternatives questions must use the alternatives as labels.
- validate_node: labels hard-floor-scanned, must be distinct non-empty strings.
- _ensure_labels: server defaults missing labels to Yes/No.
- advance_ai_build: records answer_label (and both labels) in walked_path,
  derived from the server-held pending_node — never client-supplied.
- _build_context: LLM context shows the chosen label, not a bare yes/no
  (a raw "-> yes" on an alternatives question degrades the next generation).
- normalize_walked_path: captured flywheel trees keep question labels.
- Frontend: buttons render yes_label/no_label; walk transcript and
  L1EscalationsSection render answer_label.

Phase 2A backend set: 137 passed / 0 failed / 8 deselected. tsc, eslint,
vite build clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-11 15:03:15 -04:00
parent db446e1fd6
commit 9c34d1e82d
7 changed files with 182 additions and 9 deletions

View File

@@ -1157,6 +1157,41 @@ async def test_advance_ai_build_replays_pending_node_without_regenerating(
assert replay["id"] == first["id"]
@pytest.mark.asyncio
async def test_advance_ai_build_records_answer_label_from_pending_node(
test_db: AsyncSession, monkeypatch,
):
"""When the served question carried yes_label/no_label, answering it must
record the chosen label (answer_label) in walked_path — derived server-side
from pending_node, never trusted from the client. 'Microsoft account or
local account? -> yes' is meaningless in the transcript and the LLM context."""
from app.services import l1_session_service as svc
from app.services import ai_tree_builder
account = await _make_account(test_db)
l1_user = await _make_user(test_db, account_id=account.id)
s = await svc.start_ai_build_session(
test_db, account_id=account.id, user=l1_user,
ticket_id="t-label", ticket_kind="internal",
category="account_login", problem_text="login issue")
async def fake_next(problem, category, walked):
return {"node_type": "question", "id": "q-acct",
"text": "Is the account a Microsoft account or a local account?",
"yes_label": "Microsoft account", "no_label": "Local account"}
monkeypatch.setattr(ai_tree_builder, "generate_next_node", fake_next)
first = await svc.advance_ai_build(
test_db, session_id=s.id, problem_text="login issue",
category="account_login", node_id=None)
await svc.advance_ai_build(
test_db, session_id=s.id, problem_text="login issue",
category="account_login",
node_id=first["id"], node_text=first["text"], answer="yes")
refreshed = await test_db.get(L1WalkSession, s.id)
assert refreshed.walked_path[0]["answer"] == "yes"
assert refreshed.walked_path[0]["answer_label"] == "Microsoft account"
# ---------------------------------------------------------------------------
# Finding 10: escalation recipient resolution
# ---------------------------------------------------------------------------