fix(platform/onboarding): keep tests off the live Jev reply call, and document reply_kind "other"

The package conftest blanked plan's Jev key but not the new otto_reply
one, so with TYPESAFE_API_KEY set in .env the quality-rejected route
tests sent the rejected take's text to the live TypeSafe service.
no_jev_key now blanks both.

reply_kind was documented as null for the default line, but a take Jev
classes as "other" also shows the default line and is sent as "other".
The tracking plan, the event's comment and the FinalizeResponse
docstring now say both get the default line, and a test pins "other".

Co-Authored-By: Claude Opus 5.5 (Claude Code) <[email protected]>
Claude-Session: https://claude.ai/code/session_01G58r43gKL1t2whqRCQXASw
This commit is contained in:
Claude committed 2026-11-09 19:34:25 +00:00
1 parent b7471f468e
commit 44c4525b4f
5 files changed
+23 -11

No files matched your search

@@ -5,7 +5,7 @@ from unittest.mock import AsyncMock
import pytest
from pytest_mock import MockerFixture
from backend.api.features.onboarding_dump import plan
from backend.api.features.onboarding_dump import otto_reply, plan
@pytest.fixture(autouse=True)
@@ -30,10 +30,12 @@ def has_session(mocker: MockerFixture) -> AsyncMock:
def no_jev_key(mocker: MockerFixture) -> None:
"""No Jev call unless a test asks for one.
``plan._api_key`` comes from Settings, which reads ``.env``; a developer
who set ``TYPESAFE_API_KEY`` for the copilot gate would otherwise send
every finalize-based test's plan job to the live service, and its order
and owners would then depend on the model. Tests that want Jev set the
key themselves.
``plan._api_key`` and ``otto_reply._api_key`` come from Settings, which
reads ``.env``; a developer who set ``TYPESAFE_API_KEY`` for the copilot
gate would otherwise send every finalize-based test's plan job, and each
rejected take's text, to the live service, and the plan's order and
owners would then depend on the model. Tests that want Jev set the key
themselves.
"""
mocker.patch.object(plan, "_api_key", "")
mocker.patch.object(otto_reply, "_api_key", "")
@@ -108,8 +108,7 @@ class FinalizeResponse(BaseModel):
reject; never on ``no_usable_speech``."""
otto_reply_kind: OttoReplyKind | None = None
"""Which of Otto's fixed first lines fits what was said, on an
``insufficient_content`` reject (``otto_reply``). ``None`` means the
screen's default line."""
``insufficient_content`` reject. ``None`` and ``"other"`` get the default."""
error_code: str | None = None
error_reason: str | None = None
"""What decided a quality-gate rejection (``quality.QualityRejection``)."""
@@ -923,6 +923,17 @@ describe("useBrainDumpStep — Otto replies", () => {
});
});
it("says when no kind fit, apart from when there was no pick", async () => {
const { result } = await rejectVoiceTake(
"insufficient_content",
"I like pizza.",
"other",
);
expect(result.current.replyKind).toBe("other");
expect(shown()[0]?.[1]).toMatchObject({ heard: true, reply_kind: "other" });
});
it("drops a pick when no words came through", async () => {
const { result } = await rejectVoiceTake(
"insufficient_content",
@@ -77,8 +77,8 @@ export const BrainDumpEvent = {
BRAIN_DUMP_TYPED_FALLBACK: "brain_dump_typed_fallback",
// The "not enough" screen showed, after a voice or typed take, with the
// `mode`, the gate's `error_code`, whether "You" showed any text (`heard`),
// which of Otto's fixed first lines was picked for it (`reply_kind`, null
// for the default) and the role's `prompt_set`. Talk, Write and a skip on that screen
// the kind of take Jev picked for it (`reply_kind`; `other` and null both
// show the default line) and the role's `prompt_set`. Talk, Write and a skip on that screen
// (Continue without it or Skip for now) send `from: "insufficient"` on
// `brain_dump_restarted`, `brain_dump_typed_fallback` and
// `brain_dump_skipped`, and a take or text completed after it sends
+1 -1
View File
@@ -346,7 +346,7 @@ merges the website pageviews into the new user:
| `brain_dump_permission_denied` | browser | live | — | Microphone permission is refused, on the first ask and again on each refused retry. |
| `brain_dump_mic_failed` | browser | live | `error_name` | The microphone fails to open, for any reason, a refused permission included. Sent again on each failed retry, so count people, not events. |
| `brain_dump_typed_fallback` | browser | live | `reason` (`chose_to_type`, `permission_denied`), `prompt_set`, `from` (only `insufficient`) | The user types instead of speaking. `permission_denied` is sent once per visit, however many retries are refused. `from: "insufficient"` for Write on the not-enough screen. |
| `brain_dump_insufficient_shown` | browser | live | `mode` (`voice`, `typed`), `error_code` (`insufficient_content`, `no_usable_speech`), `heard` (whether "You" showed any text), `reply_kind` (which of Otto's fixed first lines was picked for what was said, such as `mic_check` or `wants_to_skip`; `null` for the default line), `prompt_set` | The not-enough screen ("Otto replies") shows after the quality gate rejects a take, voice or typed. Voice rejects also send `transcription_failed` as before. |
| `brain_dump_insufficient_shown` | browser | live | `mode` (`voice`, `typed`), `error_code` (`insufficient_content`, `no_usable_speech`), `heard` (whether "You" showed any text), `reply_kind` (the kind of take Jev picked for what was said, such as `mic_check` or `wants_to_skip`, which shows that kind's line; `other` when none fit and `null` when there was no pick (no words, no key, a timeout, an error or an unsure pick), both of which show the default line), `prompt_set` | The not-enough screen ("Otto replies") shows after the quality gate rejects a take, voice or typed. Voice rejects also send `transcription_failed` as before. |
| `brain_dump_prompt_advanced` | browser | live | `via` (`auto`, `click`), `screen` (`rest`, `typing`, `recording`), `prompt_index`, `prompt_set` | The "Otto might ask" question moves on, by its timer (12 s at rest on narrow screens, 20 s typing, 15 s recording; not while the ideas are paused) or "Another question". Not sent for the first question. |
| `brain_dump_permission_retried` | browser | live | `result` (`granted`, `denied`, `unavailable`), `via` (`retry_link`, `record_instead`, `permission_change`), `permission_state` (`prompt`, `granted`, `denied`, `unknown`) | After a refused microphone, the user asks for it again, or allows the site in its settings (`permission_change`). Sent before `brain_dump_started`. `result` is the browser's answer, not the take: `granted` with no `brain_dump_started` after it means the user had already sent their text, or left the step, while the prompt was open. |
| `transcription_failed` | browser | live | `error_code`, `finalize_latency_ms` | Finalize returns a failure. |