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
996 lines
44 KiB
Python
996 lines
44 KiB
Python
"""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
|