"""Unit: the phase-161 live reasoning container contract (task 01). The owner's report (2026-10-05, live chat): the per-run thinking / tool-call blocks (phase 158) stacked above the answer take up a ton of space and obscure the answer. The fix (phase 161, owner decisions D1–D6): ONE ``details.reasoning`` container per brain message wraps the ENTIRE per-run timeline — created on the turn's FIRST thinking/tool frame, OPEN from creation until the turn settles (done / stop / the failed-turn funnel), condensed in its ONE bounded ``.reasoning-body`` live window (320px, task 03) with the pinned-tail follow (the phase-17 pre-measure convention); the per-run blocks keep their structure and open/fold behavior — only their PARENT changed (the per-run bubble anchor is superseded by the container's ``.reasoning-body`` append, arrival order preserved). The container's summary carries the D6 status literals: the static "Reasoning" + a live span ("Thinking…" / "Calling …" / "N steps"). A turn with no thinking and no tools creates NO container (byte-identical DOM — D1). Task 01 pins the LIVE path: the static builder (createElement only, innerHTML ABSENT), the get-or-create wrapper (open at creation, before the bubble, module-scope declaration + reset in runTurn — the pagehide partial reads the live container, like segRuns), the re-anchored per-run inserts, the ``settleReasoning`` fold at the THREE settle sites (and NOT at the mid-round delta — D2's core contract), the D6 status literals, and the D3 pre/post follow split. Task 02 pins the RESTORE + SHARED path: the collapsed container in ``restoreTimeline`` (the shared task-01 builder, created CLOSED, inserted ONCE before the bubble; the per-run blocks append into the container's ``.reasoning-body`` — the per-run bubble anchor is GONE), the D6 at-rest step count (the incremental contiguous-run semantics — each thinking entry is ONE run; a contiguous tool group of N entries is ONE run; the uniform ``${runs} steps`` literal), the legacy no-segments path (the ``renderStoredMessage`` dispatch UNCHANGED — a record without ``segments`` never reaches ``restoreTimeline`` — no container, D4), and the ``shared.js`` ``addSegments`` parity (the LOCAL parity builder — the shared page has no import of app.js — created CLOSED, the SAME step-count semantics, the SAME line bytes, innerHTML ABSENT from both wrappers). No ``app/`` logic in this phase (D4 — zero persistence change) — the behavior lives in ``frontend/assets/app.js`` and is E2E-gated by the story suite (``tests/e2e/test_reasoning_bubble.py``, task 04). Like the other frontend-adjacent unit files (the house read-the-assets-as-text pattern, cf. ``test_thinking_timeline.py``), this module pins the JS markers the phase depends on. """ from __future__ import annotations import re from pathlib import Path FRONTEND = Path(__file__).resolve().parents[2] / "frontend" APP_JS = FRONTEND / "assets" / "app.js" SHARED_JS = FRONTEND / "assets" / "shared.js" STYLES_CSS = FRONTEND / "assets" / "styles.css" # The shared-page parity line bytes (the same four label literals # addSegments mirrors from addToolLines — unchanged by task 02). PINNED_LABELS_SHARED = ( 'line.textContent = "📄 Reading "', 'line.textContent = "🔎 Searching for "', 'line.textContent = "🔎 Listing documents in "', 'line.textContent = "🔎 Listing documents"', ) def _js() -> str: return APP_JS.read_text(encoding="utf-8") def _js_shared() -> str: return SHARED_JS.read_text(encoding="utf-8") def _css() -> str: return STYLES_CSS.read_text(encoding="utf-8") def _rule(css: str, selector: str) -> str: """The first `` { … }`` rule body — the house pin pattern (the reasoning selectors are unique in the sheet).""" m = re.search(re.escape(selector) + r" \{([^}]*)\}", css) assert m, f"styles.css must style {selector}" return m.group(1) def _comment_before(css: str, selector: str) -> str: """The comment region immediately preceding the rule (the sheet documents each rule's phase + decision right above it).""" i = css.find(selector) assert i != -1, f"styles.css must style {selector}" return css[max(0, i - 1600) : i] def _fn(src: str, name: str) -> str: """The flat function body (house extraction: first ``\\n}\\n`` after the definition — the function must stay flat, no nested declarations).""" fn = src.find(f"function {name}") assert fn != -1, f"{name} must exist" return src[fn : src.find("\n}\n", fn)] def _branch(js: str, open_type: str, close_type: str) -> str: """One turn-handler branch, between the named event types.""" start = js.find(f'ev.type === "{open_type}"') end = js.find(f'ev.type === "{close_type}"') assert -1 < start < end, f"the {open_type} branch must exist" return js[start:end] # ---------- the static builder (D1/D6) ---------- def test_create_reasoning_wrap_builds_the_container_with_create_element() -> None: """D1/D6: ``createReasoningWrap`` builds ``details.reasoning`` wrapping ``summary.reasoning-summary`` (the "Reasoning" literal — D6's ONLY new static literal — + the EMPTY ``span.reasoning-status`` a frame fills) and the bounded ``.reasoning-body`` window (task 03), with createElement only (innerHTML ABSENT — the house rule; the literal is a static text node, no data flows through it). The caller owns open/fold + insertion.""" body = _fn(_js(), "createReasoningWrap") assert 'document.createElement("details")' in body assert 'cont.className = "reasoning"' in body assert 'document.createElement("summary")' in body assert 'summary.className = "reasoning-summary"' in body assert 'summary.textContent = "Reasoning"' in body, ( "D6: the ONLY new static literal" ) assert 'document.createElement("span")' in body assert 'status.className = "reasoning-status"' in body assert "summary.appendChild(status)" in body, ( "the status span rides the summary (after the literal)" ) assert 'document.createElement("div")' in body assert 'body.className = "reasoning-body"' in body assert "cont.appendChild(body)" in body assert "return cont" in body assert "innerHTML" not in body, ( "createElement only — innerHTML ABSENT (the house rule)" ) def test_the_reasoning_status_span_starts_empty() -> None: """D6: the ``span.reasoning-status`` is EMPTY until a frame sets it — the builder never writes a status into the span itself.""" body = _fn(_js(), "createReasoningWrap") assert "status.textContent" not in body, ( "the span starts empty — a frame (or the settle) fills it" ) # ---------- the get-or-create wrapper (D1) ---------- def test_ensure_reasoning_wrap_is_get_or_create_open_before_bubble() -> None: """D1: ``ensureReasoningWrap`` is get-or-create — an existing container is returned as-is; otherwise it is created (the shared builder), OPEN (``open = true`` — open from creation until the turn settles, D2), inserted BEFORE the bubble (the ONE new anchor), and stored in the module-scope ``reasoningWrapEl``. The container is created by the FIRST run's ensure call — no other call site.""" body = _fn(_js(), "ensureReasoningWrap") assert "if (reasoningWrapEl) return reasoningWrapEl;" in body, ( "get-or-create: the existing container is returned as-is" ) assert "createReasoningWrap()" in body, "the shared builder is reused" assert "cont.open = true" in body, ( "D1: OPEN at creation (it folds at the settle — D2)" ) assert 'wrap.querySelector(".msg-body")' in body assert 'body.insertBefore(cont, body.querySelector(".bubble"))' in body, ( "the container sits ABOVE the answer bubble" ) assert "reasoningWrapEl = cont" in body, ( "stored in the module scope (the pagehide partial reads it)" ) assert "return cont" in body def test_the_reasoning_wrap_is_module_scoped_and_reset_in_run_turn() -> None: """D1 (module-scope readability): ``reasoningWrapEl`` is declared at MODULE SCOPE (like segRuns / currentRun — the pagehide partial reads the live container while a turn is in flight) and is reset per turn in ``runTurn`` alongside the other turn locals.""" js = _js() assert "\nlet reasoningWrapEl = null;" in js, ( "module-scope declaration (column 0 — not a function local)" ) run = _fn(js, "runTurn") assert "reasoningWrapEl = null;" in run, ( "reset per turn in runTurn — like segRuns" ) # reset with the other turn locals (the reset block, one place) assert "segRuns = []" in run assert "currentRun = null" in run assert run.find("segRuns = []") < run.find("reasoningWrapEl = null;") # ---------- the re-anchored per-run inserts (D1) ---------- def test_per_run_inserts_append_into_the_reasoning_body() -> None: """D1: the per-run blocks append into the container's ``.reasoning-body`` — ``ensureThinkingRun`` / ``ensureToolRun`` insert via ``ensureReasoningWrap(wrap).querySelector( ".reasoning-body").appendChild(…)`` (arrival order preserved: a new run appends after the previous run's block). The D5 refinement (owner 2026-10-06): the per-run elements are plain ``div``s through the run builders (never open/foldable — the container is the ONE disclosure); everything else in both functions is unchanged (closeCurrentRun, the segRuns push, currentRun).""" js = _js() t = _fn(js, "ensureThinkingRun") assert ( 'ensureReasoningWrap(wrap).querySelector(".reasoning-body")' ".appendChild(block)" in t ), "the thinking run appends into the container's body" assert "closeCurrentRun()" in t assert "createThinkingRunEl()" in t, ( "D5 refinement: a plain div — never collapsible" ) assert "segRuns.push(run)" in t assert "currentRun =" in t d = _fn(js, "ensureToolRun") assert ( 'ensureReasoningWrap(wrap).querySelector(".reasoning-body")' ".appendChild(disc)" in d ), "the tool run appends into the container's body" assert "closeCurrentRun()" in d assert "createToolRunEl()" in d, ( "D5 refinement: a plain div — never collapsible" ) assert "segRuns.push(run)" in d assert ".open =" not in t, ( "D5 refinement: the in-container runs are non-collapsible divs" ) assert ".open =" not in d, ( "D5 refinement: the in-container runs are non-collapsible divs" ) def test_the_per_run_bubble_anchor_is_gone() -> None: """D1 (the documented anchor supersession): the per-run ``insertBefore(…, .bubble)`` anchor is GONE from ``ensureThinkingRun`` / ``ensureToolRun`` — the container is the ONE new anchor (it sits before the bubble; the runs nest inside it). The container itself still anchors on the bubble (one site). """ js = _js() t = _fn(js, "ensureThinkingRun") d = _fn(js, "ensureToolRun") assert "insertBefore" not in t, ( "the per-run bubble anchor is gone from ensureThinkingRun" ) assert "insertBefore" not in d, ( "the per-run bubble anchor is gone from ensureToolRun" ) er = _fn(js, "ensureReasoningWrap") assert 'body.insertBefore(cont, body.querySelector(".bubble"))' in er, ( "the ONE anchor: the container before the bubble" ) # ---------- the settle fold (D2) ---------- def test_settle_reasoning_is_defined_and_folds_the_container() -> None: """D2: ``settleReasoning`` removes the container's ``open`` attribute — idempotent; a no-op without a container (the plain-answer turn settles byte-identically). Wrap-scoped (the caller's wrap), like closeAllRuns.""" body = _fn(_js(), "settleReasoning") assert ( 'wrap?.querySelector("details.reasoning")?.removeAttribute("open")' in body ), "the container fold — the open attribute is removed" assert "reasoningWrapEl" not in body, ( "wrap-scoped (the caller's wrap), like closeAllRuns" ) def test_settle_reasoning_runs_at_the_three_settle_sites() -> None: """D2: the container folds at the THREE settle sites — the ``done`` branch, the stop/abort path, the failed-turn funnel — alongside ``closeAllRuns(wrap)`` (immediately after it), with the D6 "N steps" summary (the live run count — ``segRuns``, module scope).""" js = _js() done = _branch(js, "done", "tool_result") assert "closeAllRuns(wrap)" in done assert "settleReasoning(wrap)" in done assert done.find("closeAllRuns(wrap)") < done.find( "settleReasoning(wrap)" ), "done: settleReasoning sits alongside (after) closeAllRuns" assert "`${segRuns.length} steps`" in done # the stop path: the catch's stop branch up to the error else start = js.find('stoppedByUser || err?.name === "AbortError"') end = js.find("\n } else {\n", start) assert -1 < start < end, "the stop path must exist" stop = js[start:end] assert "closeAllRuns(wrap)" in stop assert "settleReasoning(wrap)" in stop assert stop.find("closeAllRuns(wrap)") < stop.find( "settleReasoning(wrap)" ), "stop: settleReasoning sits alongside (after) closeAllRuns" assert "`${segRuns.length} steps`" in stop ff = _fn(js, "finalizeFailedTurn") assert "closeAllRuns(wrap)" in ff assert "settleReasoning(wrap)" in ff assert ff.find("closeAllRuns(wrap)") < ff.find( "settleReasoning(wrap)" ), "failed: settleReasoning sits alongside (after) closeAllRuns" assert "`${segRuns.length} steps`" in ff def test_settle_reasoning_does_not_fold_at_the_mid_round_delta() -> None: """D2 (the TODO's core contract): a mid-round ``delta`` FOLDS the active run (``closeCurrentRun`` — phase 158 D5, unchanged) but the container STAYS OPEN — ``settleReasoning`` is NOT called in the delta branch (the container collapses only once the final response has been returned and the session has stopped).""" js = _js() delta = _branch(js, "delta", "done") assert "closeCurrentRun()" in delta, ( "the active run still folds at the first delta (phase 158 D5)" ) assert "settleReasoning" not in delta, ( "the container STAYS OPEN through a mid-round delta (D2)" ) # ---------- the D6 status literals ---------- def test_the_d6_status_literals_are_pinned() -> None: """D6: the live status literals — "Thinking…" (the thinking frame branch), "Calling …" (the tool frame branch — the display name the tool line already shows, the phase-70 legacy-name mapping already produced it) — and "N steps" (the settle sites, pinned in the settle test) all write to ``.reasoning-status`` through the ``setReasoningStatus`` setter (a no-op without a container — a plain-answer turn never writes one).""" js = _js() thinking = _branch(js, "thinking", "tool") assert '"Thinking…"' in thinking, "the live 'Thinking…' suffix" tool = _branch(js, "tool", "retry") assert "`Calling ${name}…`" in tool, ( "'Calling …' — the display name the tool line shows" ) assert "`${segRuns.length} steps`" in js, "the settle 'N steps' count" assert js.count('setReasoningStatus(`${segRuns.length} steps`)') == 3, ( "the 'N steps' template at the three settle sites" ) def test_the_status_setter_is_the_no_op_guarded_chain() -> None: """D6: the status writes go through ``setReasoningStatus`` — a direct ``reasoningWrapEl?.…()?.textContent = x`` left-hand side is a syntax error (an LHS allows at most one trailing ``?.`` member — not a call), which kills the module at parse time, so the setter reads the span into a LOCAL reference and writes it only when the container exists (a no-op without a container).""" js = _js() setter = _fn(js, "setReasoningStatus") assert 'reasoningWrapEl?.querySelector(".reasoning-status")' in setter assert "el.textContent = text" in setter, ( "the span is filled by a frame (or the settle)" ) assert 'reasoning-status")?.textContent' not in js, ( "the multi-optional LHS assignment is GONE (it is a syntax " "error — the parse-time module killer)" ) assert js.count("setReasoningStatus(") == 6, ( "one definition + exactly the five status sites " "(thinking / tool / done / stop / failed)" ) def test_the_status_lands_after_the_run_ensures_the_container() -> None: """D6: the status is set AFTER ``ensureThinkingRun`` / ``ensureToolRun`` (the container exists by then — ``ensureReasoningWrap`` runs inside the run ensure), so the first frame's status lands on a live container.""" js = _js() thinking = _branch(js, "thinking", "tool") assert thinking.find("ensureThinkingRun(wrap)") >= 0 assert thinking.find("ensureThinkingRun(wrap)") < thinking.find( '"Thinking…"' ), "the thinking status follows the run's ensure" tool = _branch(js, "tool", "retry") assert tool.find("ensureToolRun(wrap)") >= 0 assert tool.find("ensureToolRun(wrap)") < tool.find( "`Calling ${name}…`" ), "the tool status follows the run's ensure" # ---------- the D3 live follow ---------- def test_the_reasoning_follow_band_is_pinned_at_32px() -> None: """D3: ``REASONING_NEAR_BOTTOM_PX = 32`` — the same 32px band as the phase-17 per-block window (exported — the unit-pin convention, same pattern as TURN_TIMEOUT_MS). The pin check is the phase-17 pre-measure convention (the distance-to-bottom form, mirrored).""" js = _js() assert "export const REASONING_NEAR_BOTTOM_PX = 32;" in js, ( "the band is the phase-17 32px threshold, exported" ) pin = _fn(js, "isReasoningPinned") assert "el.scrollHeight - REASONING_NEAR_BOTTOM_PX" in pin assert "el.scrollTop + el.clientHeight" in pin def test_the_follow_is_the_phase_17_pre_update_post_append_split() -> None: """D3: the pre-update capture (BEFORE the chunk/line grows the content — the phase-17 pre-measure convention) and the post-append apply (``el.scrollTop = el.scrollHeight``) — called from BOTH the thinking frame branch (pre: before the block re-render; post: after it) and the tool frame branch (pre: before ``appendToolLine``; post: after it), on ``reasoningWrapEl?.querySelector(".reasoning-body")`` (a no-op when absent — the plain-answer turn never creates one).""" js = _js() scroll = _fn(js, "scrollReasoningToBottom") assert "el.scrollTop = el.scrollHeight" in scroll, "the post-append apply" thinking = _branch(js, "thinking", "tool") pre_t = thinking.find("isReasoningPinned(") render_t = thinking.find("textEl.innerHTML = renderMarkdown") post_t = thinking.find("scrollReasoningToBottom(") assert 0 < pre_t < render_t < post_t, ( "thinking: the pre capture is BEFORE the re-render, the " "post apply AFTER it" ) assert 'reasoningWrapEl?.querySelector(".reasoning-body")' in thinking tool = _branch(js, "tool", "retry") pre = tool.find("isReasoningPinned(") append = tool.find("appendToolLine(") post = tool.find("scrollReasoningToBottom(") assert 0 < pre < append < post, ( "tool: the pre capture is BEFORE the line append, the post " "apply AFTER it" ) assert 'reasoningWrapEl?.querySelector(".reasoning-body")' in tool def test_the_existing_per_block_follow_is_untouched() -> None: """D3 (harmless): the EXISTING per-block ``isThinkingNearBottom(textEl)`` follow is unchanged — inside the container the ``.thinking-text`` is no longer scrollable (task 03's ``max-height: none``), so it no-ops there; the legacy no-container path keeps its behavior, and it costs nothing.""" js = _js() thinking = _branch(js, "thinking", "tool") assert "isThinkingNearBottom(textEl)" in thinking assert "textEl.scrollTop = textEl.scrollHeight" in thinking # ---------- task 02: the collapsed restore wrapper (app.js) ---------- def test_restore_creates_the_container_closed_once_before_the_bubble() -> None: """D5: ``restoreTimeline`` creates the container via the SHARED task-01 builder, created CLOSED (``cont.open = false`` — the restore renders collapsed; explicit for the pin), and inserted ONCE before the bubble (a get-or-create shape — a single ``details.reasoning`` per message: exactly one builder call and one container insertion). The ``.reasoning-body`` window is referenced for the per-run appends.""" body = _fn(_js(), "restoreTimeline") assert "createReasoningWrap()" in body, "the shared task-01 builder is reused" assert body.count("createReasoningWrap()") == 1, ( "the container is created ONCE per message (a get-or-create shape)" ) assert "cont.open = false" in body, ( "D5: the restore renders collapsed (explicit for the pin)" ) assert "body.insertBefore(cont, bubble)" in body, ( "the container sits ABOVE the answer bubble (the ONE anchor)" ) assert body.count("body.insertBefore(cont, bubble)") == 1, "inserted ONCE" assert 'cont.querySelector(".reasoning-body")' in body, ( "the .reasoning-body window is the per-run append target" ) def test_restore_per_run_blocks_append_into_the_reasoning_body() -> None: """D1 (the documented anchor supersession): the per-run blocks append into the container's ``.reasoning-body`` — the per-run ``insertBefore(…, bubble)`` anchor is GONE from ``restoreTimeline`` (the container sits before the bubble; the runs nest inside it, arrival order preserved). The D5 refinement (owner 2026-10-06, parity with app.js): the in-container runs are plain ``div``s through the run builders (never collapsible — the container is the ONE disclosure, created COLLAPSED by the restore); everything else in the loop is unchanged (the tool-run seal, the well-formedness guards, the truncation note).""" body = _fn(_js(), "restoreTimeline") assert "runsEl.appendChild(block)" in body, ( "the thinking block appends into the container's body" ) assert "runsEl.appendChild(toolDisc)" in body, ( "the tool disclosure appends into the container's body" ) assert "insertBefore(block" not in body, ( "the per-run bubble anchor is GONE from restoreTimeline (thinking)" ) assert "insertBefore(toolDisc" not in body, ( "the per-run bubble anchor is GONE from restoreTimeline (tool)" ) # the per-run create is the div builder (the D5 refinement). assert "createThinkingRunEl()" in body, ( "D5 refinement: the in-container run is a plain div (never " "collapsible — the container is the ONE disclosure)" ) assert "createToolRunEl()" in body, ( "D5 refinement: the in-container run is a plain div (never " "collapsible)" ) assert "block.open = true" not in body, "the in-container runs never open" assert "toolDisc.open" not in body, ( "D5 refinement: the in-container runs are non-collapsible divs" ) assert "if (toolDisc) toolDisc = null" in body, "the tool-run seal is unchanged" def test_restore_computes_the_step_count_incrementally() -> None: """D6: the at-rest step count is computed INCREMENTALLY inside the loop — matching the LIVE ``segRuns.length`` semantics (contiguous runs, NOT entry count): a ``thinking`` entry after a tool group starts a NEW run (count+1); two ADJACENT thinking entries are TWO runs (each thinking entry counts, the increment is NOT gated on the previous kind); a tool group of N entries is ONE run (count+1 only at the group's first entry, i.e. when ``toolDisc`` was null before it created it). After the loop, ``.reasoning-status`` is set to ``"${runs} steps"`` (the D6 literal is UNIFORM — no singular/plural special-case).""" body = _fn(_js(), "restoreTimeline") assert "runs = 0" in body, "the step counter starts at zero" # the thinking branch increments per thinking entry. think_i = body.find('entry.kind === "thinking"') tool_i = body.find('entry.kind === "tool"') assert -1 < think_i < tool_i think_branch = body[think_i:tool_i] assert "runs += 1" in think_branch, ( "a thinking entry (after a tool group) starts a NEW run" ) # the increment is NOT gated on a previous-kind check — two # adjacent thinking entries are TWO runs (the increment is # unconditional within the thinking branch). inc_i = think_branch.find("runs += 1") inc_line = think_branch.splitlines()[think_branch.count("\n", 0, inc_i)] assert not inc_line.lstrip().startswith("if ("), ( "two adjacent thinking entries are TWO runs (no kind gate)" ) # the tool branch increments only at a NEW contiguous group # (count+1 when toolDisc was null before it created it) — a # group of N entries is ONE run. assert "if (!toolDisc)" in body, "the contiguous tool-group guard" tool_branch = body[tool_i:] assert "runs += 1" in tool_branch, "a tool group's first entry is ONE run" # exactly ONE tool increment — at the group's first entry # (inside the !toolDisc guard); the group's other entries do # not increment. assert tool_branch.count("runs += 1") == 1, ( "exactly ONE tool increment — at the group's first entry " "(a group of N entries is ONE run)" ) # the uniform D6 literal (no singular/plural special-case). assert 'textContent = `${runs} steps`' in body, ( "the D6 literal is UNIFORM — `${runs} steps`" ) assert "steps`" in body assert "runs} step`" not in body, "no singular special-case" def test_restore_sets_the_status_after_the_loop() -> None: """D6: the ``.reasoning-status`` is filled with the at-rest count AFTER the loop (the count is complete) — written to the container's ``.reasoning-status`` span via the ``cont`` reference (the container the loop appended into).""" body = _fn(_js(), "restoreTimeline") loop_end = body.rfind("for (const entry of segments") # the status write is after the loop's last append and before the # function's close. status_i = body.find('.reasoning-status")') assert status_i != -1 # the status write uses the runs counter and the uniform literal. status_line_region = body[status_i : status_i + 80] assert "runs} steps" in status_line_region # it is positioned after the loop (the count is complete). assert loop_end < status_i, "the status is set AFTER the loop" def test_the_restore_dispatch_keeps_the_legacy_path_untouched() -> None: """D4: the ``renderStoredMessage`` dispatch is UNCHANGED — a record WITHOUT ``segments`` never reaches ``restoreTimeline`` (the legacy path restores byte-identically — no container); a record WITH segments takes the ``restoreTimeline`` path (which now wraps in the collapsed container). The dispatch comment cites the container.""" js = _js() fn = js.find("function renderStoredMessage") end = js.find("function restoreConversation") assert -1 < fn < end body = js[fn:end] assert "Array.isArray(m.segments) && m.segments.length" in body, ( "the dispatch guard is unchanged" ) assert "restoreTimeline(wrap, m.segments)" in body, ( "the segments path renders restoreTimeline" ) # the legacy path (the else branch) creates NO container. else_i = body.find("} else {") legacy = body[else_i:] assert "createReasoningWrap" not in legacy, ( "the legacy path (no segments) creates NO container (D4)" ) # the dispatch comment cites the container (the segments path # wraps in the collapsed container). assert "Phase 161" in body, "the dispatch comment cites the container" # ---------- task 02: the shared-page parity wrapper (shared.js) ---------- def test_shared_create_reasoning_wrap_is_the_local_parity_builder() -> None: """D1/D6 (the shared page has NO import of app.js): ``createReasoningWrap`` in shared.js is a LOCAL parity builder mirroring the app.js one — the same ``reasoning`` / ``reasoning-summary`` / ``reasoning-status`` / ``reasoning-body`` classes, the ``Reasoning`` literal via textContent, the EMPTY status span, createElement only (innerHTML ABSENT — the house rule; the literal is a static text node, no data flows through it).""" body = _fn(_js_shared(), "createReasoningWrap") assert 'document.createElement("details")' in body assert 'cont.className = "reasoning"' in body assert 'document.createElement("summary")' in body assert 'summary.className = "reasoning-summary"' in body assert 'summary.textContent = "Reasoning"' in body, ( "the Reasoning literal (D6 — parity with app.js)" ) assert 'document.createElement("span")' in body assert 'status.className = "reasoning-status"' in body assert "summary.appendChild(status)" in body assert 'document.createElement("div")' in body assert 'body.className = "reasoning-body"' in body assert "cont.appendChild(body)" in body assert "return cont" in body assert "status.textContent" not in body, ( "the span starts empty — the at-rest count fills it" ) assert "innerHTML" not in body, ( "createElement only — innerHTML ABSENT (the house rule)" ) def test_shared_add_segments_creates_the_container_closed_once_before_the_bubble() -> None: """D5: ``addSegments`` creates the container via the LOCAL parity builder, created CLOSED (``cont.open = false`` — the pure-render convention; explicit for the pin), and inserted ONCE before the bubble (the shared page's EXISTING insertion convention — mirror the ``addSegments``/``addToolLines`` anchor). A get-or-create shape: exactly one builder call and one container insertion per message.""" body = _fn(_js_shared(), "addSegments") assert "createReasoningWrap()" in body, "the local parity builder is reused" assert body.count("createReasoningWrap()") == 1, ( "the container is created ONCE per message (a get-or-create shape)" ) assert "cont.open = false" in body, ( "D5: created CLOSED — the pure-render convention (explicit for the pin)" ) assert "body.insertBefore(cont, bubble)" in body, ( "inserted before the bubble (the shared page's existing convention)" ) assert body.count("body.insertBefore(cont, bubble)") == 1, "inserted ONCE" assert 'cont.querySelector(".reasoning-body")' in body, ( "the .reasoning-body window is the per-run append target" ) def test_shared_add_segments_per_run_blocks_append_into_the_reasoning_body() -> None: """D1 (the documented anchor supersession): the per-run blocks append into the container's ``.reasoning-body`` — the per-run ``insertBefore(…, bubble)`` anchor is GONE from ``addSegments`` (the container sits before the bubble; the runs nest inside it, arrival order preserved). The LINE BYTES mirror ``addToolLines`` — UNCHANGED (the four label literals, the phase-95 truncation note, the phase-139/146 suffixes).""" body = _fn(_js_shared(), "addSegments") assert "runsEl.appendChild(block)" in body, ( "the thinking block appends into the container's body" ) assert "runsEl.appendChild(toolDisc)" in body, ( "the tool disclosure appends into the container's body" ) assert "insertBefore(block" not in body, ( "the per-run bubble anchor is GONE from addSegments (thinking)" ) assert "insertBefore(toolDisc" not in body, ( "the per-run bubble anchor is GONE from addSegments (tool)" ) # the line bytes mirror addToolLines — the pinned label literals. for label in PINNED_LABELS_SHARED: assert label in body, f"pinned label literal changed: {label!r}" assert "truncated-note" in body, "the phase-95 note is unchanged" assert "readRangeSuffix(entry.offset, entry.limit)" in body, ( "the phase-139 suffix is unchanged" ) def test_shared_add_segments_computes_the_step_count_parity() -> None: """D6 (the SAME incremental step count as ``restoreTimeline`` — a parity copy, not an import): a ``thinking`` entry after a tool group starts a NEW run (count+1); two ADJACENT thinking entries are TWO runs (the increment is NOT kind-gated); a tool group of N entries is ONE run (count+1 only at the group's first entry). After the loop, ``.reasoning-status`` is set to the UNIFORM ``"${runs} steps"`` literal.""" body = _fn(_js_shared(), "addSegments") assert "runs = 0" in body, "the step counter starts at zero" think_i = body.find('entry.kind === "thinking"') tool_i = body.find('entry.kind === "tool"') assert -1 < think_i < tool_i think_branch = body[think_i:tool_i] assert "runs += 1" in think_branch, ( "a thinking entry (after a tool group) starts a NEW run" ) inc_i = think_branch.find("runs += 1") inc_line = think_branch.splitlines()[think_branch.count("\n", 0, inc_i)] assert not inc_line.lstrip().startswith("if ("), ( "two adjacent thinking entries are TWO runs (no kind gate)" ) tool_branch = body[tool_i:] assert tool_branch.count("runs += 1") == 1, ( "exactly ONE tool increment — at the group's first entry " "(a group of N entries is ONE run)" ) assert "if (!toolDisc)" in tool_branch, "the contiguous tool-group guard" assert 'textContent = `${runs} steps`' in body, ( "the D6 literal is UNIFORM — parity with app.js" ) def test_the_shared_dispatch_keeps_the_legacy_path_untouched() -> None: """D4: the ``renderSharedMessage`` dispatch is UNCHANGED — a record WITHOUT ``segments`` renders the legacy ``addThinkingBlock`` + ``addToolLines`` path (byte-identical — no container); a record WITH segments renders ``addSegments`` (which wraps in the collapsed container).""" body = _fn(_js_shared(), "renderSharedMessage") assert "Array.isArray(m.segments) && m.segments.length" in body, ( "the dispatch guard is unchanged" ) assert "addSegments(wrap, m.segments)" in body, ( "the segments path renders addSegments" ) else_i = body.find("} else {") legacy = body[else_i:] assert "createReasoningWrap" not in legacy, ( "the legacy path (no segments) creates NO container (D4)" ) assert "addThinkingBlock(wrap, m.thinking)" in body assert "addToolLines(wrap, m.tools)" in body def test_inner_html_is_absent_from_both_wrappers() -> None: """The house rule: ``innerHTML`` is ABSENT from BOTH the restore wrapper's and the shared wrapper's container builders (createElement/textContent only — the ``Reasoning`` literal is a static text node; the ONLY HTML write in each timeline function remains the escape-first renderMarkdown into ``.thinking-text``).""" app_builder = _fn(_js(), "createReasoningWrap") assert "innerHTML" not in app_builder, ( "innerHTML ABSENT from the app.js builder (the house rule)" ) shared_builder = _fn(_js_shared(), "createReasoningWrap") assert "innerHTML" not in shared_builder, ( "innerHTML ABSENT from the shared.js builder (the house rule)" ) # the ONLY HTML write in each timeline function is the # escape-first renderMarkdown into .thinking-text (the shared # builder's local textEl; app.js's restore targets the div # block's .thinking-text through querySelector). for fn_name, js in (("restoreTimeline", _js()), ("addSegments", _js_shared())): body = _fn(js, fn_name) writes = re.findall(r"([\w.$()\"'-]+\.?)\.innerHTML\s*=\s*([^;\n]+)", body) assert len(writes) == 1, ( f"{fn_name}: exactly ONE innerHTML write (the structure is " f"createElement): {writes}" ) assert ( writes[0][0].rstrip(".") == "textEl" or 'querySelector(".thinking-text")' in writes[0][0] ), "the single write is into .thinking-text" assert writes[0][1].strip() == "renderMarkdown(entry.text)", ( "the single write is the escape-first renderMarkdown" ) # ---------- task 03: the container's CSS (styles.css) ---------- def test_the_reasoning_container_rule_is_the_family_treatment() -> None: """Task 03 item 1: ``details.reasoning`` carries the ``details.thinking`` (phase-17) family treatment — the SAME surface / line / brand-soft / radius / margin tokens + ``overflow: hidden`` (the corner radius clips the scrollable body). The comment block cites this phase + D3, and the block sits ADJACENT to the ``details.thinking`` family — after the ``.thinking-text`` margin rule, before the phase-117 ``.tool-calls-disclosure`` comment.""" css = _css() body = _rule(css, "details.reasoning") assert "background: var(--surface)" in body assert "border: 1px solid var(--line)" in body assert "border-left: 3px solid var(--brand-soft)" in body assert "border-radius: var(--radius-sm)" in body assert "margin: 0 0 0.5rem" in body assert "overflow: hidden" in body comment = _comment_before(css, "details.reasoning {") assert "Phase 161" in comment, "the comment cites this phase" assert "D3" in comment, "the comment cites the D3 window decision" # adjacency: after the family's last rule, before phase 117. margins_i = css.find("details.thinking .thinking-text ul") reason_i = css.find("details.reasoning {") p117_i = css.find("/* Phase 117: the tool-call record") assert -1 < margins_i < reason_i < p117_i, ( "the family blocks stay adjacent" ) def test_the_reasoning_summary_rule_is_the_family_convention() -> None: """Task 03 item 2: ``summary.reasoning-summary`` — the ``details.thinking`` summary convention: the flex row, the WCAG ≥44px target (``min-height: 44px``), the family ``--brand-ink`` (8.7:1 on ``--surface`` — AA), the webkit marker suppressed, and the house 3px ``:focus-visible`` ring. """ css = _css() body = _rule(css, "summary.reasoning-summary") assert "display: flex" in body assert "align-items: center" in body assert "gap: 0.5rem" in body assert "padding: 0.5rem 0.75rem" in body assert "min-height: 44px" in body, "the WCAG ≥44px target" assert "color: var(--brand-ink)" in body, ("the family AA token") assert "font-size: 0.9rem" in body assert "cursor: pointer" in body assert "list-style: none" in body # the webkit marker suppression. assert ( "summary.reasoning-summary::-webkit-details-marker {" " display: none; }" in css ) # the house 3px :focus-visible ring. focus = _rule(css, "summary.reasoning-summary:focus-visible") assert "outline: 3px solid var(--brand)" in focus assert "outline-offset: 2px" in focus def test_the_reasoning_chevron_rotates_when_open() -> None: """Task 03 item 2: the CSS chevron ``▸`` (``::before``, ``display: inline-block``) with the 0.15s ease transform transition, rotating 90° when the container is ``[open]`` — the ``details.thinking`` convention.""" css = _css() chevron = _rule(css, "summary.reasoning-summary::before") assert 'content: "▸"' in chevron assert "display: inline-block" in chevron assert "transition: transform 0.15s ease" in chevron open_rule = _rule(css, "details.reasoning[open] summary.reasoning-summary::before") assert "transform: rotate(90deg)" in open_rule def test_the_reasoning_status_rule_uses_the_aa_token() -> None: """Task 03 item 3: ``.reasoning-status`` (the D6 live suffix span, inside the summary row) — ``--ink-soft`` (6.9:1 on ``--surface`` — AA), ``font-size: 0.8rem``, ``line-height: 1.4``, ``margin-left: 0.25rem`` (text + color, never color alone — B5).""" body = _rule(_css(), ".reasoning-status") assert "color: var(--ink-soft)" in body, ("the AA token") assert "font-size: 0.8rem" in body assert "line-height: 1.4" in body assert "margin-left: 0.25rem" in body def test_the_reasoning_body_is_the_one_bounded_live_window() -> None: """Task 03 item 4: ``.reasoning-body`` — the ONE bounded live window (D3): ``max-height: 320px`` (the phase-17 window convention) + ``overflow-y: auto`` (user-scrollable — the pinned-tail follow in app.js) + the family padding. The comment cites D3 + the pinned-tail follow.""" css = _css() body = _rule(css, ".reasoning-body") assert "max-height: 320px" in body, ("the phase-17 320px window") assert "overflow-y: auto" in body, ("user-scrollable (D3)") assert "padding: 0 0.75rem 0.75rem" in body comment = _comment_before(css, ".reasoning-body {") assert "D3" in comment, ("the comment cites the D3 window") assert "pinned-tail" in comment, ( "the comment cites the pinned-tail follow" ) def test_the_inner_runs_are_compacted_to_one_scroll_surface() -> None: """Task 03 item 5 + the D5 refinement (owner 2026-10-06): the inner runs nest as PLAIN DIVS inside ONE scroll surface — ``details.reasoning .thinking`` (the family's brand-soft rail, the 2px weight — distinct from the tool run's neutral rail) and ``details.reasoning .tool-calls-disclosure`` (the neutral ``--line`` rail), de-bordered (transparent background, no nested box); the inner ``.thinking-text`` 320px window is DISABLED (``max-height: none`` + ``overflow-y: visible``) so the container's window is the single scroll surface. The runs are distinct from each other by LABEL + RAIL: the brand-ink "Thinking" label vs the accent-ink "Tool call(s) (N)" label. """ css = _css() run = _rule(css, "details.reasoning .thinking") assert "border-left: 2px solid var(--brand-soft)" in run, ( "the thinking run carries the brand-soft rail" ) inner_text = _rule(css, "details.reasoning .thinking .thinking-text") assert "max-height: none" in inner_text, ( "the inner 320px window is disabled" ) assert "overflow-y: visible" in inner_text disc = _rule(css, "details.reasoning .tool-calls-disclosure") assert "border-left: 2px solid var(--line)" in disc, ( "the tool run carries the neutral rail (distinct from the " "thinking run's brand-soft rail)" ) label = _rule(css, "details.reasoning .thinking .thinking-summary") assert "var(--brand-ink)" in label, ( "the thinking label is brand-ink (the family pairing)" ) def test_the_reasoning_still_is_in_the_house_reduced_motion_block() -> None: """Task 03 item 6: the chevron rotation stills under ``prefers-reduced-motion`` — the still is in the SAME media block as the phase-17 / phase-117 stills (the house chat/stream block, the one that stills ``details.thinking``).""" css = _css() i = css.find("details.thinking summary::before { transition: none; }") assert i != -1, ("the phase-17 still must exist (the reference)") start = css.rfind("@media (prefers-reduced-motion: reduce) {", 0, i) end = css.find("\n}", start) assert -1 < start < end, ("the stills must be in a media block") block = css[start:end] assert ( "summary.reasoning-summary::before { transition: none; }" in block ), ("the reasoning chevron stills with the family stills") def test_the_legacy_no_container_rules_are_untouched() -> None: """The completion gate: the ``details.thinking`` / ``.tool-calls-disclosure`` rules OUTSIDE the container are unchanged — the legacy no-container path renders byte-identically (the family block, the 320px ``.thinking-text`` window, the phase-117 disclosure + its chevron).""" css = _css() fam = _rule(css, "details.thinking") assert "background: var(--surface)" in fam assert "border-left: 3px solid var(--brand-soft)" in fam assert "overflow: hidden" in fam text = _rule(css, "details.thinking .thinking-text") assert "max-height: 320px" in text, ("the legacy window is unchanged") assert "overflow-y: auto" in text # the phase-117 disclosure rules are unchanged (one base rule — # line-anchored: the nested details.reasoning selector is a # different rule). assert len(re.findall(r"(?m)^\.tool-calls-disclosure \{", css)) == 1 disc = re.search( r"(?m)^\.tool-calls-disclosure \{([^}]*)\}", css ) assert disc, "styles.css must style .tool-calls-disclosure (base)" assert "margin: 0.25rem 0" in disc.group(1) chevron = _rule(css, ".tool-calls-summary::before") assert 'content: "▸"' in chevron assert "margin-right: 0.5rem" in chevron