Files
brain-of-reese/tests/unit/test_reasoning_bubble.py
T
ducoterra 690fd98036
Build and Push Containers / build-and-push-app (push) Successful in 1m43s
Build and Push Containers / build-and-push-db (push) Successful in 15s
fix(rag): stop the per-run blocks from folding — the container is the one disclosure
The owner refinement (2026-10-06): the per-run thinking and tool-call
blocks inside the Reasoning bubble were individually collapsible
details elements — a fold inside the fold. A tool frame closed the
previous thinking block, so the visible thinking vanished mid-turn;
and the pinned-tail follow died on the append of every run after the
first (the pin was captured AFTER the new block's height growth
contaminated the 32px reading).

- app.js: the in-container runs are now plain divs —
  createThinkingRunEl (div.thinking + static .thinking-summary label +
  .thinking-text) and createToolRunEl (div.tool-calls-disclosure +
  static .tool-calls-summary label + the .tool-calls list); they never
  open or fold (the container is the ONE open/fold control, open
  through the turn, folded at settle — unchanged); the pin capture
  moves BEFORE ensureThinkingRun/ensureToolRun append (plus the
  first-frame edge: no container yet -> always pinned);
  closeCurrentRun just ends the run; closeAllRuns stays the defensive
  legacy guard; restoreTimeline/addSegments (shared.js) use the div
  builders; the legacy no-segments restore path keeps the old
  details-based blocks byte-identical
- styles.css: the in-container runs are distinct by label + rail
  (brand-ink Thinking label on a brand-soft rail; accent-ink
  Tool call(s) (N) label on a neutral rail); the interactive
  treatment (cursor + chevron) is scoped to the legacy <summary>; the
  inner windows stay disabled (the container's .reasoning-body is the
  ONE live scroll surface)
- unit + E2E: the D5-refinement contract is pinned — one
  details.reasoning disclosure, per-run blocks as plain divs, zero
  nested <details> inside the container, the container as the only
  open/fold control, the .reasoning-body window auto-follows the
  tail while streaming

unit+integration 3738 passed (coverage 99%), ruff + pyright clean, all
five affected E2E suites pass in isolation (reasoning_bubble 6,
thinking_timeline 6, thinking_display 5, thinking_scroll 8,
tool_call_compact 4), verified live: the follow holds through a long
thinking stream, the tool-frame transition, and the second thinking
run; restore renders the div structure without client-side errors

LLM-Generated: true
2026-10-06 14:44:41 -04:00

996 lines
44 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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 <name>…" / "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 ``<selector> { … }`` 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 <name>…" (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 <name>…' — 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