Files
brain-of-reese/tests/unit/test_thinking_timeline.py
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

924 lines
43 KiB
Python
Raw Permalink 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-158 per-run timeline contract (tasks 02 + 03).
The owner's report (2026-10-04, live shared page): the turn's thinking
lumped into ONE block and every tool call into ONE disclosure, so the
DOM always read "thought everything → called all the tools →
answered" — and the phase-109 D15 re-open made it worse (thinking
resumed ABOVE the in-progress answer). The fix (phase 158, owner
decisions D1–D5): each CONTIGUOUS thinking run streams into its own
``details.thinking`` block and each CONTIGUOUS tool-call run into its
own ``details.tool-calls-disclosure``, in arrival order, all ABOVE
the answer bubble (D2, Option B); the runs open live and fold at rest
(D5); the phase-87 elapsed clock re-targets the TRACKED line
reference (the ``:last-child`` query breaks with multiple disclosures
and is gone); the save payloads persist the ``segments`` array
(D4, non-empty only).
Task 03 adds the RESTORE-path and SHARED-page pins: a record WITH
``segments`` renders the collapsed per-run timeline
(``restoreTimeline`` in app.js / ``addSegments`` in shared.js — the
parity copy); a record WITHOUT ``segments`` (every pre-phase record)
restores byte-identically through the untouched legacy path (the
dispatch guard is the only change); the builders are
createElement/textContent for the structure (the only HTML write is
the escape-first renderMarkdown into ``.thinking-text``); unknown
``kind`` entries are skipped (never render from storage).
No new ``app/`` logic in this phase — the behavior lives in
``frontend/assets/app.js`` / ``shared.js`` and is E2E-gated by the
story suite (``tests/e2e/test_thinking_timeline.py``, task 04).
Like the other frontend-adjacent unit files (the house
read-the-assets-as-text pattern, cf. ``test_frontend_tool_states.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"
#: The four pinned tool-line label literals (phase 37/70/117 D6) —
#: the emoji guard and the pins key off these exact bytes.
PINNED_LABELS = (
'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 _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 run builders (D1/D2/D3/D5) ----------
def test_ensure_thinking_run_builds_a_new_div_block_in_the_container() -> None:
"""D1/D3 + the D5 refinement (owner 2026-10-06):
``ensureThinkingRun`` returns the active thinking run's block or
— a NEW plain ``div.thinking`` (the run builder —
``createThinkingRunEl``: the static ``.thinking-summary``
"Thinking" literal + the ``.thinking-text``; NEVER open/foldable
— the container is the ONE disclosure, always visible while it
is open), appended into the phase-161 reasoning container's
``.reasoning-body`` (D1 — the container sits before the bubble,
a new run appends after the previous run's block, so arrival
order is preserved; the per-run bubble anchor is superseded).
The plain ``Thinking`` literal (D3 — the
phase-17 literal, not a numbered variant) rides the builder;
the fresh run joins segRuns + currentRun."""
js = _js()
body = _fn(js, "ensureThinkingRun")
assert 'currentRun?.kind === "thinking"' in body, (
"the active thinking run is extended, not recreated"
)
assert "closeCurrentRun()" in body, (
"a frame of a new kind ends the previous run first (D1)"
)
assert "createThinkingRunEl()" in body, (
"the D5 refinement: a plain div — never collapsible"
)
assert ".open =" not in body, (
"the in-container runs are non-collapsible divs"
)
assert (
'ensureReasoningWrap(wrap).querySelector(".reasoning-body")'
".appendChild(block)" in body
), (
"phase 161 (D1): appended into the container's body — the "
"container sits above the bubble, arrival order preserved"
)
assert "insertBefore" not in body, (
"the per-run bubble anchor is GONE (phase 161 D1)"
)
assert 'kind: "thinking"' in body
assert "segRuns.push(run)" in body
assert "currentRun =" in body
def test_create_thinking_block_is_the_phase_17_static_template_without_data() -> None:
"""D3: the thinking block's STATIC structure is the phase-17
template — the plain ``Thinking`` summary literal + the
``.thinking-text`` div — built with NO data in the template (the
house rule: a static template is fine, turn data must never flow
through innerHTML; the live run writes
``renderMarkdown(currentRun.run.text)`` into ``.thinking-text``,
the restore/shared builders are createElement/textContent, task
03)."""
body = _fn(_js(), "createThinkingBlock")
assert 'block.className = "thinking"' in body
assert "block.innerHTML" in body, (
"the phase-17 static-structure convention is kept (the live "
"builder reuses the template)"
)
assert "`<summary>Thinking</summary><div class=\"thinking-text\"></div>`" in body, (
"the plain Thinking literal (D3 — the phase-17 literal, zero "
"new literals — position distinguishes the runs)"
)
assert "${" not in body, (
"NO data in the static template — the run's text flows through "
"renderMarkdown / textContent"
)
def test_ensure_tool_run_builds_a_new_div_disclosure_in_the_container() -> None:
"""D1 + the D5 refinement (owner 2026-10-06): ``ensureToolRun``
returns the active tool run's disclosure or — a NEW plain
``div.tool-calls-disclosure`` (the run builder —
``createToolRunEl``: the static ``.tool-calls-summary`` + the
.tool-calls list, role=list + aria-label "Tool calls"; NEVER
open/foldable — the container is the ONE disclosure, always
visible while it is open), appended into the phase-161
reasoning container's ``.reasoning-body`` (D1 — the
container sits before the bubble, arrival order preserved; the
per-run bubble anchor is superseded). One disclosure per
CONTIGUOUS tool-call run; the per-run "Tool call(s) (N)" count
rides appendToolLine's summary update."""
js = _js()
body = _fn(js, "ensureToolRun")
assert 'currentRun?.kind === "tool"' in body
assert "closeCurrentRun()" in body
assert "createToolRunEl()" in body, (
"the D5 refinement: a plain div — never collapsible"
)
assert ".open =" not in body, (
"the in-container runs are non-collapsible divs"
)
assert (
'ensureReasoningWrap(wrap).querySelector(".reasoning-body")'
".appendChild(disc)" in body
), (
"phase 161 (D1): appended into the container's body — the "
"container sits above the bubble, arrival order preserved"
)
assert "insertBefore" not in body, (
"the per-run bubble anchor is GONE (phase 161 D1)"
)
assert 'kind: "tool"' in body
assert "segRuns.push(run)" in body
disc = _fn(js, "createToolDisclosure")
assert 'container.className = "tool-calls-disclosure"' in disc
assert 'summary.className = "tool-calls-summary"' in disc
assert 'listEl.className = "tool-calls"' in disc
assert 'setAttribute("role", "list")' in disc
assert 'setAttribute("aria-label", "Tool calls")' in disc
assert "innerHTML" not in disc, (
"the tool builder stays createElement/textContent (phase 117 "
"style — innerHTML ABSENT)"
)
def test_the_single_block_and_single_container_derivations_are_gone() -> None:
"""D1: the phase-109 single shared block and the phase-117
single-container derivation are GONE — no ``ensureThinkingBlock``
/ ``closeThinkingBlock`` / ``closeToolCalls`` anywhere, the old
anchor chain (``.tool-calls-disclosure ?? .tool-calls ??
.bubble``) is gone, and ``appendToolLine`` no longer derives a
container (the caller passes the run's disclosure)."""
js = _js()
for gone in (
"function ensureThinkingBlock",
"function closeThinkingBlock",
"function closeToolCalls",
"closeThinkingBlock(wrap)",
"closeToolCalls(wrap)",
'body.querySelector(".tool-calls-disclosure") ??',
'body.querySelector(".tool-calls") ?? body.querySelector(".bubble")',
):
assert gone not in js, f"the pre-phase-158 machinery {gone!r} must be gone"
append = _fn(js, "appendToolLine")
assert 'querySelector(".tool-calls-disclosure")' not in append, (
"the single-container derivation is GONE — the caller passes "
"the run's disclosure"
)
assert "document.createElement(\"details\")" not in append, (
"the disclosure's build moved to createToolDisclosure"
)
# ---------- appendToolLine: the container signature ----------
def test_append_tool_line_takes_the_container_and_returns_the_line() -> None:
"""The signature is ``appendToolLine(container, name, argument,
offset = null, limit = null, scope = null)`` — the CALLER passes
the run's disclosure (no single-container derivation); everything
else in the function stays byte-identical (the four pinned label
literals, the phase-139 range suffix, the phase-146 scope span,
the per-run ``Tool call(s) (N)`` count), and the function returns
the appended ``line`` element (the caller arms its clock)."""
js = _js()
assert (
"function appendToolLine(container, name, argument, offset = null, "
"limit = null, scope = null)" in js
), "the first param is the container"
body = _fn(js, "appendToolLine")
for label in PINNED_LABELS:
assert label in body, f"pinned label literal changed: {label!r}"
assert "const list = container.querySelector(\".tool-calls\")" in body
assert "return line" in body, (
"the function returns the appended line (armToolLineClock's "
"second param)"
)
assert (
"Tool call${n === 1 ? \"\" : \"s\"} (${n})" in body
), "the per-run count template on every append"
assert "innerHTML" not in body, "no HTML injection surface — textContent only"
# ---------- the phase-87 clock re-target ----------
def test_the_clock_targets_the_tracked_line_reference() -> None:
"""Phase 158 (D1): the ``:last-child`` query is GONE — it would
find the last line of the WRONG run's list (multiple
disclosures). ``armToolLineClock`` takes the line as its SECOND
param and stores it in the module-scope ``toolLineEl``; the tick
is null-guarded on the reference (a New-Chat reset mid-gap is a
no-op); the suffix element + the ``(Ns)`` text + the
``TOOL_LINE_ELAPSED_AFTER_MS`` gate stay byte-identical."""
js = _js()
assert '".tool-calls .tool-call:last-child"' not in js, (
"the :last-child query is GONE (multi-disclosure)"
)
assert "let toolLineEl = null" in js, "the tracked line is module scope"
body = _fn(js, "armToolLineClock")
assert "function armToolLineClock(wrap, lineEl)" in body, (
"the NEW second param"
)
assert "toolLineEl = lineEl" in body, "the re-arm resets the reference"
assert "if (!toolLineEl) return" in body, (
"null-guarded: a wrap reset mid-gap (New Chat) is a no-op"
)
assert 'toolLineEl.querySelector(".tool-elapsed")' in body
assert 'el.className = "tool-elapsed"' in body
assert 'document.createElement("span")' in body
assert "toolLineEl.appendChild(el)" in body
assert "`(${secs}s)`" in body, "the parenthesized '(Ns)' suffix stays"
assert "TOOL_LINE_ELAPSED_AFTER_MS" in body
assert "innerHTML" not in body, "no HTML write in the clock"
def test_settle_resets_the_tracked_line() -> None:
"""``settleToolLine`` keeps the ``.tool-elapsed`` removal (it
clears EVERY line in the wrap — correct with multiple
disclosures) and resets ``toolLineEl = null`` alongside (the
stop path — ``stopToolLineClock`` — resets via it)."""
js = _js()
body = _fn(js, "settleToolLine")
assert "clearInterval(toolLineTimer)" in body
assert 'querySelectorAll?.(".tool-elapsed")' in body and "el.remove()" in body
assert "toolLineEl = null" in body, "the settle forgets the line target"
stop = _fn(js, "stopToolLineClock")
assert "settleToolLine();" in stop
assert "toolLineWrap = null" in stop
def test_the_arm_follows_the_line_append_with_the_new_signature() -> None:
"""A6 (live-only): the tool branch arms the clock with the NEW
signature — ``armToolLineClock(wrap, line)`` right after
``appendToolLine(disc, …)`` (the baseline resets per line), and
the arm is still the definition + exactly one live call site (the
restore path never arms)."""
js = _js()
assert js.count("armToolLineClock(") == 2, (
"the arm must be the definition + exactly one live call site"
)
branch = _branch(js, "tool", "retry")
append = branch.find(
"appendToolLine(\n disc,\n name,\n argument,\n"
" frameOffset,\n frameLimit,\n framePath\n"
" )"
)
arm = branch.find("armToolLineClock(wrap, line)")
assert -1 < append < arm, (
"the arm follows the line's append — the baseline resets per line"
)
assert "ensureToolRun(wrap)" in branch, (
"the tool frame opens/extends the run's disclosure"
)
restore_fn = js.find("function renderStoredMessage")
restore_end = js.find("function restoreConversation")
assert -1 < restore_fn < restore_end
assert "armToolLineClock" not in js[restore_fn:restore_end], (
"the restore path must never arm the clock (A6)"
)
# ---------- the fold sites ----------
def test_close_current_run_and_close_all_runs_are_defined() -> None:
"""The D5 refinement's (owner 2026-10-06) fold side:
``closeCurrentRun`` ENDS the ACTIVE run (nothing folds — the
in-container runs are non-collapsible divs; idempotent; a
no-op without a run) and ``closeAllRuns`` closes EVERY open
``details.thinking`` + ``details.tool-calls-disclosure`` in the
wrap (the DEFENSIVE legacy guard — the pre-phase-161
details blocks the restore path creates, already closed; a
no-op in every live path)."""
js = _js()
cur = _fn(js, "closeCurrentRun")
assert ".open" not in cur, (
"D5 refinement: nothing folds — the run simply ends"
)
assert "currentRun = null" in cur, "the run is cleared (a later close is a no-op)"
all_runs = _fn(js, "closeAllRuns")
assert "details.thinking, details.tool-calls-disclosure" in all_runs
assert "el.open = false" in all_runs
def test_the_delta_folds_the_active_run_and_the_terminals_fold_every_run() -> None:
"""D5: the FIRST `delta` folds the open run (``closeCurrentRun``
in the delta branch — the D15 ``block.open = true`` re-open is
gone), and the terminal settles — `done`, the stop/abort path,
and ``finalizeFailedTurn`` — fold EVERY open run
(``closeAllRuns(wrap)``)."""
js = _js()
delta = _branch(js, "delta", "done")
assert "closeCurrentRun()" in delta, "the first delta folds the active run"
assert "block.open = true" not in delta, "the D15 re-open is gone from the delta"
done = _branch(js, "done", "tool_result")
assert "closeAllRuns(wrap)" in done, "done folds every open run"
stop_idx = js.find('stoppedByUser || err?.name === "AbortError"')
stop = js[stop_idx : js.find("} else {", stop_idx)]
assert "closeAllRuns(wrap)" in stop, "the stop path folds every open run"
failed = _fn(js, "finalizeFailedTurn")
assert "closeAllRuns(wrap)" in failed, "the failed-turn funnel folds every open run"
# ---------- the run tracker + the save payloads (D4) ----------
def test_seg_runs_is_module_scope_and_reset_in_run_turn() -> None:
"""The run tracker is MODULE scope (the pagehide partial reads it
while a turn is in flight — like ``thinkingAcc``) and resets with
the other turn locals at the top of ``runTurn``."""
js = _js()
assert "let segRuns = []" in js, "module-scope run tracker"
assert "let currentRun = null" in js, "the active run is module scope"
anchor = js.find("let thinkingAcc = \"\"")
assert anchor != -1
assert js.find("let segRuns = []") - anchor < 1500, (
"the run tracker sits next to thinkingAcc (the pagehide "
"readers)"
)
turn_start = js.find("async function runTurn")
turn_end = js.find('window.addEventListener("pagehide"')
assert -1 < turn_start < turn_end
turn = js[turn_start:turn_end]
assert "segRuns = []" in turn, "reset with the other turn locals"
assert "currentRun = null" in turn
def test_build_segments_maps_runs_in_order_with_the_empty_run_skip() -> None:
"""D4: ``buildSegments`` maps the runs IN ORDER — a thinking run
→ ``{ kind: "thinking", text }`` ONLY when its text is
non-empty (a zero-char run stores nothing); a tool run → one
``{ kind: "tool", …record }`` per record, in order (the record
object is spread — the additive offset/limit/path + the phase-95
truncation stamp ride through; the records are the SAME toolAcc
objects, so the stamp flows with no extra wiring). A turn with no
thinking and no tools returns []. No re-validation on the client
(the server's ChatMessage boundary validates)."""
body = _fn(_js(), "buildSegments")
assert "if (r.kind === \"thinking\")" in body
assert "if (r.text) out.push({ kind: \"thinking\", text: r.text })" in body, (
"the EMPTY thinking run stores NOTHING"
)
assert "for (const rec of r.records) out.push({ kind: \"tool\", ...rec })" in body, (
"one entry per record, in order — the record spread"
)
assert "return out" in body
def test_the_save_payload_sites_carry_segments_non_empty_only() -> None:
"""D4: the FIVE save sites persist ``segments`` non-empty only
(the undefined-drops-the-key house pattern) — the `done` save
point, ``finalizeFailedTurn`` (the function record + BOTH call
sites pass ``buildSegments(segRuns)``), the STOP path's partial,
and the pagehide partial — while the ``thinking`` / ``tools``
keys stay UNCHANGED at every site. The zero-frame fallback
save needs nothing (zero frames → no runs → no key)."""
js = _js()
# done save point
done = _branch(js, "done", "tool_result")
assert "const turnSegments = buildSegments(segRuns)" in done
assert "segments: turnSegments.length ? turnSegments : undefined" in done
assert "thinking: thinkingAcc || undefined" in done
assert "tools: toolAcc.length ? toolAcc : undefined" in done
# finalizeFailedTurn: the parameter + the record + both call sites
failed = _fn(js, "finalizeFailedTurn")
assert "segments" in failed.split(") {")[0], "the parameter object gains segments"
assert "segments: segments?.length ? segments : undefined" in failed
assert js.count("segments: buildSegments(segRuns)") >= 2, (
"both call sites (stream-drop + network error) pass the segments"
)
# the stop path
stop_idx = js.find('stoppedByUser || err?.name === "AbortError"')
stop = js[stop_idx : js.find("} else {", stop_idx)]
assert "const stopSegments = buildSegments(segRuns)" in stop
assert "segments: stopSegments.length ? stopSegments : undefined" in stop
assert "stopped: true" in stop
# the pagehide partial
pagehide_start = js.find('window.addEventListener("pagehide"')
assert pagehide_start != -1
pagehide = js[pagehide_start : js.find("});", pagehide_start)]
assert "const pagehideSegments = buildSegments(segRuns)" in pagehide
assert (
"segments: pagehideSegments.length ? pagehideSegments : undefined"
in pagehide
)
def test_the_live_thinking_branch_extends_the_run_and_renders_the_run_text() -> None:
"""D1: the `thinking` branch keeps the phase-17/87 pre-work
(settleToolLine, thinkingAcc, sawThinking, clearTurnTimeout, the
wrap get-or-create, removeTyping) and REPLACES the D15
ensureThinkingBlock + re-open with ``ensureThinkingRun`` — the
RUN's text accumulates ``ev.text`` and re-renders
``renderMarkdown(currentRun.run.text)`` into THAT block's
``.thinking-text`` (per-block — the pre-update pin capture +
bottom-follow logic moves verbatim, measured BEFORE the
re-render)."""
js = _js()
branch = _branch(js, "thinking", "tool")
assert "settleToolLine();" in branch
assert "thinkingAcc += ev.text" in branch
assert "sawThinking = true" in branch
assert "clearTurnTimeout()" in branch
assert 'addMessage("brain", "")' in branch
assert "removeTyping()" in branch
assert "const block = ensureThinkingRun(wrap)" in branch
assert "currentRun.run.text += ev.text || \"\"" in branch, (
"the RUN's text accumulates (the block renders the run, not "
"the whole-turn accumulator)"
)
capture = 'const pinned = block.open && isThinkingNearBottom(textEl);'
assert capture in branch, "the pre-update pin capture moves verbatim (per-block)"
assert "textEl.innerHTML = renderMarkdown(currentRun.run.text)" in branch
cap_idx = branch.find(capture)
render_idx = branch.find("textEl.innerHTML = renderMarkdown(currentRun.run.text)")
assert -1 < cap_idx < render_idx, (
"the pin state is measured BEFORE the re-render (phase-17 "
"regression — the 2-newline-gap fix)"
)
assert "textEl.scrollTop = textEl.scrollHeight" in branch, (
"pinned tail-following is intact"
)
assert "block.open = true" not in branch, (
"the D15 re-open assignment is GONE — ensureThinkingRun owns "
"the open state (a new run opens; the active run stays open)"
)
def test_the_tool_branch_records_the_run_and_arms_the_tracked_clock() -> None:
"""D1: the `tool` branch keeps the phase-139/146 guards,
``toolAcc.push(toolRecord)``, ``clearTurnTimeout()``, the wrap
get-or-create and the status-label logic UNCHANGED, and REPLACES
the single-container append + bare arm with:
``ensureToolRun(wrap)`` → the run's ``records.push(toolRecord)``
(the SAME object toolAcc holds — the phase-95 stamp flows into
both) → ``appendToolLine(disc, …)`` →
``armToolLineClock(wrap, line)``."""
js = _js()
branch = _branch(js, "tool", "retry")
assert (
"Number.isInteger(ev.offset) && ev.offset >= 1 ? ev.offset : null"
in branch
)
assert "Number.isInteger(ev.limit) && ev.limit >= 1 ? ev.limit : null" in branch
assert (
'typeof ev.path === "string" && ev.path ? ev.path : null' in branch
), "the phase-146 guard is unchanged"
assert "const toolRecord = { name, argument }" in branch
assert "toolAcc.push(toolRecord)" in branch
assert "clearTurnTimeout()" in branch
assert 'addMessage("brain", "")' in branch
assert "const disc = ensureToolRun(wrap)" in branch
assert "currentRun.run.records.push(toolRecord)" in branch, (
"the run's records hold the SAME toolAcc objects"
)
assert "armToolLineClock(wrap, line)" in branch
def test_the_legacy_restore_path_uses_the_shared_builders() -> None:
"""The renderStoredMessage LEGACY path (a pre-phase record — no
``segments``; the segments dispatch is task 03's) restores the
thinking block COLLAPSED above the bubble through the SHARED
``createThinkingBlock`` builder, and the tool lines through ONE
disclosure (``createToolDisclosure``, created with the live
lifecycle — folded after the loop, phase 117 D3) via the SAME
``appendToolLine`` container signature. The tOffset/tLimit/tPath
guards + the phase-95 truncation note stay verbatim."""
js = _js()
fn = js.find("function renderStoredMessage")
end = js.find("function restoreConversation")
assert -1 < fn < end
body = js[fn:end]
assert "if (m.thinking)" in body
assert "createThinkingBlock()" in body
assert "block.open = false" in body, "stored blocks restore collapsed"
assert "block.open = true" not in body, (
"the restore path must never open a stored block"
)
assert "Array.isArray(m.tools)" in body
assert "createToolDisclosure()" in body
assert "restoreDisc.open = false" in body, (
"the restored disclosure folds after the loop (phase 117, D3)"
)
assert "appendToolLine(restoreDisc, t.name, arg, tOffset, tLimit, tPath)" in body
assert "Number.isInteger(t.offset) && t.offset >= 1 ? t.offset : null" in body
assert "Number.isInteger(t.limit) && t.limit >= 1 ? t.limit : null" in body
assert "typeof t.path === \"string\" && t.path ? t.path : null" in body
assert "appendTruncatedNote(wrap, arg" in body, "the phase-95 note is untouched"
# The established phase-17 render is the ONLY innerHTML write in
# the function — the new restore builders (createThinkingBlock /
# createToolDisclosure) add no HTML injection surface.
assert body.count("innerHTML") == 1
assert (
'block.querySelector(".thinking-text").innerHTML = renderMarkdown(m.thinking)'
in body
)
def test_the_pre_phase_narrative_is_replaced() -> None:
"""The narrative flips: the phase-109 D15 re-open contract is
superseded — "never reopens" survives nowhere in app.js (the
phase-109 pin), and the section comment names the per-run
timeline (D1), the arrival-order placement (D2), and the
clock re-target (the multi-disclosure fix, phase 158 D1)."""
js = _js()
assert "never reopens" not in js, (
"the one-way-door narrative must not survive anywhere"
)
comment_start = js.rfind("/*", 0, js.find("function createThinkingBlock"))
assert comment_start != -1
section = js[comment_start : js.find("function createThinkingBlock")]
assert "per-run" in section.lower(), "the contract is named: per-run"
assert "arrival order" in section, "D2: arrival order is named"
assert "REPLACED" in section or "superseded" in section.lower(), (
"the D15 re-open contract is named as replaced"
)
clock_comment = js.rfind("/*", 0, js.find("function armToolLineClock"))
assert clock_comment != -1
clock_section = js[clock_comment : js.find("function armToolLineClock")]
assert "re-target" in clock_section, (
"the clock comment names the re-target (the multi-disclosure "
"fix, phase 158 D1)"
)
# ---------- task 03: the restore path (app.js) ----------
def test_restore_timeline_builds_the_collapsed_per_run_structure() -> None:
"""D5: ``restoreTimeline`` renders the record's ``segments``
inside the container COLLAPSED — a ``kind:"thinking"`` entry
gets its OWN plain ``div.thinking`` (the D5 refinement — the
run builder, never collapsible), and CONSECUTIVE
``kind:"tool"`` entries group into ONE plain
``div.tool-calls-disclosure`` (the run builder — the static
label + the .tool-calls list; innerHTML ABSENT). Phase 161
(task 02, D5/D6): the per-run blocks nest inside ONE
details.reasoning container — the shared task-01 builder,
created CLOSED and inserted ONCE before the bubble (the
per-run bubble anchor is superseded; arrival order
preserved). The per-run count rides ``appendToolLine``'s summary
update; a kind change seals the tool run (one disclosure per
run, D1)."""
body = _fn(_js(), "restoreTimeline")
assert "createThinkingRunEl()" in body, (
"the D5 refinement: the in-container run is a plain div "
"(never collapsible — the container is the ONE disclosure)"
)
assert "createToolRunEl()" in body, (
"the 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, (
"the in-container runs are non-collapsible divs"
)
# the consecutive-tool grouping (one disclosure per run — the
# grouping loop): create on the first entry, seal on a kind
# change, append into the container's body.
assert "if (toolDisc) toolDisc = null" in body, (
"a thinking entry seals the tool run — ONE disclosure per run (D1)"
)
# Phase 161 (task 02, D5/D6): the per-run timeline nests inside
# ONE details.reasoning container — the shared task-01 builder,
# created CLOSED and inserted ONCE before the bubble (the
# per-run bubble anchor is superseded; arrival order preserved).
assert "createReasoningWrap()" in body, "the shared task-01 builder is reused"
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 bubble — the ONE anchor"
)
assert "runsEl.appendChild(block)" in body, (
"D1: the per-run block nests into the container's body"
)
assert "runsEl.appendChild(toolDisc)" in body, (
"D1: the per-run disclosure nests into the container's body"
)
assert "insertBefore(block" not in body, (
"the per-run bubble anchor is GONE (phase 161 D5)"
)
assert "insertBefore(toolDisc" not in body, (
"the per-run bubble anchor is GONE (phase 161 D5)"
)
assert "for (const entry of segments" in body, "iterate the runs in order"
def test_restore_timeline_reuses_the_restore_guards_and_the_truncation_note() -> None:
"""The line path reuses the SAME ``appendToolLine`` container
signature with the tOffset/tLimit/tPath well-formedness guards
(``Number.isInteger(x) && x >= 1`` / the string guard —
byte-identical, the phase-139/146 restore precedent) and the
phase-95 ``appendTruncatedNote`` call moved verbatim inside the
grouping; a malformed entry (a non-string thinking ``text`` / a
non-string tool ``name``) and an entry with an UNKNOWN ``kind``
are SKIPPED (never render from storage — textContent only, the
house rule)."""
body = _fn(_js(), "restoreTimeline")
assert (
"Number.isInteger(entry.offset) && entry.offset >= 1 ? entry.offset : null"
in body
), "the phase-139 offset guard is verbatim"
assert (
"Number.isInteger(entry.limit) && entry.limit >= 1 ? entry.limit : null"
in body
), "the phase-139 limit guard is verbatim"
assert 'typeof entry.path === "string" && entry.path ? entry.path : null' in body, (
"the phase-146 path guard is verbatim"
)
assert "appendToolLine(toolDisc, entry.name, arg, tOffset, tLimit, tPath)" in body, (
"the SAME container signature as the live path"
)
assert "appendTruncatedNote(wrap, arg" in body, "the phase-95 note moved verbatim"
# the skips: malformed + unknown kind never render from storage.
assert 'if (!entry || typeof entry !== "object") continue' in body
assert 'if (typeof entry.text !== "string") continue' in body, "malformed thinking skipped"
assert 'if (typeof entry.name !== "string") continue' in body, "malformed tool skipped"
assert 'else if (entry.kind === "tool")' in body, (
"an entry whose kind is neither thinking nor tool falls through "
"— SKIPPED (the guard comment names the skip)"
)
assert "unknown" in body, "the skip is named in a guard comment"
def _single_escape_first_write(body: str) -> None:
"""The house rule for the restore/shared builders (the
structure is createElement/textContent): the function has
exactly ONE innerHTML write — the escape-first renderMarkdown
into ``.thinking-text`` (the existing restore convention;
renderMarkdown's escape-first contract keeps storage
XSS-safe)."""
writes = re.findall(r"([\w.$()\"'-]+\.?)\.innerHTML\s*=\s*([^;\n]+)", body)
assert len(writes) == 1, (
f"exactly ONE innerHTML write (the structure is 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"
)
def test_restore_timeline_structure_is_create_element_and_the_only_write_is_escape_first() -> None:
"""innerHTML ABSENT from the STRUCTURE — it is built via
createElement/textContent (the in-container runs ride the
innerHTML-absent D5-refinement run builders — the static
label via textContent; the .tool-calls list, role=list +
aria-label "Tool calls" — the task-02 pin). The ONLY HTML
write in the function is the escape-first renderMarkdown into
``.thinking-text`` (the existing restore convention — the
phase-17 pattern; renderMarkdown's escape-first contract keeps
storage XSS-safe)."""
body = _fn(_js(), "restoreTimeline")
_single_escape_first_write(body)
assert "createThinkingRunEl()" in body, (
"the in-container run rides the innerHTML-absent run builder"
)
assert "createToolRunEl()" in body, (
"the in-container run rides the innerHTML-absent run builder"
)
def test_the_restore_dispatch_gives_segments_precedence_over_the_legacy_path() -> None:
"""The ``renderStoredMessage`` dispatch: a record WITH
``segments`` (a non-empty array) renders ``restoreTimeline`` and
SKIPS the legacy ``m.thinking`` block + ``m.tools`` loop (the
runs carry the same data in order — rendering both would double
them); a record WITHOUT ``segments`` restores byte-identically
through the legacy path (the dispatch guard is the only change
— the legacy statements stay verbatim, the task-02 pins).
``restoreTimeline`` is defined BEFORE renderStoredMessage —
outside the legacy region the task-02 pin slices (that region's
single innerHTML write stays the established phase-17 render)."""
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"
assert "restoreTimeline(wrap, m.segments)" in body
# precedence: the legacy blocks sit in the ELSE branch.
guard_i = body.find("Array.isArray(m.segments)")
else_i = body.find("} else {", guard_i)
think_i = body.find("if (m.thinking)")
tools_i = body.find("if (Array.isArray(m.tools))")
assert -1 < guard_i < else_i < think_i < tools_i, (
"segments take precedence; the legacy path is the else branch"
)
# the legacy path is intact (the task-02 pin's markers).
assert "createThinkingBlock()" in body
assert "block.open = false" in body
assert "renderMarkdown(m.thinking)" in body
assert "createToolDisclosure()" in body
assert "appendToolLine(restoreDisc, t.name, arg, tOffset, tLimit, tPath)" in body
# restoreTimeline is OUTSIDE the sliced region (before the
# renderer) — the legacy region's HTML write stays the ONE the
# task-02 pin asserts.
assert js.find("function restoreTimeline") < fn, (
"restoreTimeline is defined before renderStoredMessage — the "
"legacy-region pin's innerHTML count is untouched"
)
assert body.count("innerHTML") == 1, "the legacy path's single write is unchanged"
# ---------- task 03: the shared page (shared.js) ----------
def test_add_segments_builds_the_collapsed_parity_timeline() -> None:
"""The shared-page parity copy (D5 — pure render, always
COLLAPSED): the same structure (a per-run plain div.thinking
with the static ``.thinking-summary`` "Thinking" label via
textContent + the phase-117 disclosure structure as a plain
div — ``.tool-calls-summary`` + the .tool-calls list,
role=list + aria-label "Tool calls"; the D5 refinement —
NEVER collapsible, always visible while the container is
open), the same LINE BYTES as ``addToolLines`` (the four
pinned label literals + the phase-70 legacy names + the
phase-139 readRangeSuffix + the phase-95 truncation note), the
per-run ``Tool call(s) (N)`` count, and nesting inside the
phase-161 details.reasoning container (the local parity
builder, created CLOSED and inserted ONCE before the bubble —
the per-run bubble anchor is superseded, arrival order
preserved) — all created COLLAPSED (the container is the ONE
disclosure; the addThinkingBlock / addToolLines collapsed
convention)."""
body = _fn(_js_shared(), "addSegments")
assert "document.createElement(\"div\")" in body, "the structure is createElement"
assert 'block.className = "thinking"' in body
assert "block.open" not in body, (
"D5 refinement: the in-container runs are non-collapsible divs"
)
assert 'label.textContent = "Thinking"' in body, "the plain Thinking literal (D3)"
assert 'textEl.className = "thinking-text"' in body
assert "renderMarkdown(entry.text)" in body, "the escape-first renderer (parity)"
# the phase-117 disclosure structure (the D5 refinement — a plain
# div, never collapsible).
assert 'toolDisc.className = "tool-calls-disclosure"' in body
assert "toolDisc.open" not in body, (
"D5 refinement: the in-container runs are non-collapsible divs"
)
assert 'summary.className = "tool-calls-summary"' in body
assert 'container.className = "tool-calls"' in body
assert 'setAttribute("role", "list")' in body
assert 'setAttribute("aria-label", "Tool calls")' in body
# the parity line bytes — the four pinned label literals + the
# phase-139 suffix + the phase-95 note (the addToolLines parity).
for label in PINNED_LABELS:
assert label in body, f"pinned label literal changed: {label!r}"
assert "readRangeSuffix(entry.offset, entry.limit)" in body, "the phase-139 suffix parity"
assert '"truncated-note"' in body, "the phase-95 note parity"
# the per-run count template.
assert "Tool call${n === 1 ? \"\" : \"s\"} (${n})" in body
# Phase 161 (task 02, D5): the per-run timeline nests inside ONE
# details.reasoning container — the local parity builder, created
# CLOSED and inserted ONCE before the bubble (the per-run bubble
# anchor is superseded; arrival order preserved).
assert "createReasoningWrap()" in body, "the local parity builder is reused"
assert "cont.open = false" in body, (
"D5: collapsed (the pure-render convention)"
)
assert "body.insertBefore(cont, bubble)" in body, (
"inserted ONCE before the bubble"
)
assert "runsEl.appendChild(block)" in body, (
"D1: the per-run block nests into the container's body"
)
assert "runsEl.appendChild(toolDisc)" in body, (
"D1: the per-run disclosure nests into the container's body"
)
assert "insertBefore(block" not in body, (
"the per-run bubble anchor is GONE (phase 161 D5)"
)
assert "insertBefore(toolDisc" not in body, (
"the per-run bubble anchor is GONE (phase 161 D5)"
)
# the consecutive-tool grouping (one disclosure per run, D1).
assert "if (toolDisc) toolDisc = null" in body, (
"a thinking entry seals the tool run — ONE disclosure per run"
)
def test_add_segments_skips_unknown_kinds_and_is_create_element() -> None:
"""An entry with an UNKNOWN ``kind`` (or a malformed shape) is
SKIPPED — never render from storage (textContent only, the
house rule; the guard comment names the skip). innerHTML ABSENT
from the structure — the ONLY HTML write is the escape-first
renderMarkdown into ``.thinking-text`` (the addThinkingBlock
convention)."""
body = _fn(_js_shared(), "addSegments")
assert 'if (!entry || typeof entry !== "object") continue' in body
assert 'if (typeof entry.text !== "string") continue' in body, "malformed thinking skipped"
assert 'if (typeof entry.name !== "string") continue' in body, "malformed tool skipped"
assert 'else if (entry.kind === "tool")' in body, (
"an unknown kind falls through — SKIPPED"
)
assert "unknown" in body, "the skip is named in a guard comment"
_single_escape_first_write(body)
def test_the_shared_dispatch_gives_segments_precedence_over_the_legacy_path() -> None:
"""The ``renderSharedMessage`` dispatch: a record WITH
``segments`` (a non-empty array) renders ``addSegments`` and
SKIPS the legacy ``addThinkingBlock`` + ``addToolLines`` (the
runs carry the same data in order); a record WITHOUT
``segments`` — every pre-phase record — renders byte-identically
through the legacy path (the guard is the only change; the
``addThinkingBlock`` / ``addToolLines`` functions themselves are
UNCHANGED — the test_shared_page.py pins)."""
js = _js_shared()
body = _fn(js, "renderSharedMessage")
assert "Array.isArray(m.segments) && m.segments.length" in body, "the dispatch guard"
assert "addSegments(wrap, m.segments)" in body
# precedence: the legacy calls sit in the ELSE branch.
guard_i = body.find("Array.isArray(m.segments)")
else_i = body.find("} else {", guard_i)
think_i = body.find("addThinkingBlock(wrap, m.thinking)")
tools_i = body.find("addToolLines(wrap, m.tools)")
assert -1 < guard_i < else_i < think_i < tools_i, (
"segments take precedence; the legacy path is the else branch"
)
# the legacy path is intact.
assert 'if (typeof m.thinking === "string" && m.thinking)' in body
def test_the_legacy_shared_builders_are_untouched() -> None:
"""The ``addThinkingBlock`` / ``addToolLines`` functions are
UNCHANGED (the legacy path is intact — the shared page's
pre-phase records render byte-identically; the
test_shared_page.py pins stay green): the collapsed thinking
block (the phase-17 convention) + the single-disclosure tool
lines (the phase-117 structure, the four label literals, the
phase-95 note, the per-render count)."""
js = _js_shared()
think = _fn(js, "addThinkingBlock")
assert 'block.className = "thinking"' in think
assert "block.open = false" in think
assert 'summary.textContent = "Thinking"' in think
assert "renderMarkdown(thinking)" in think
tools = _fn(js, "addToolLines")
for label in PINNED_LABELS:
assert label in tools, f"pinned label literal changed: {label!r}"
assert 'container.className = "tool-calls"' in tools
assert 'disclosure.className = "tool-calls-disclosure"' in tools
assert "disclosure.open = false" in tools
assert 'summary.className = "tool-calls-summary"' in tools
assert '"truncated-note"' in tools
assert "Tool call${n === 1 ? \"\" : \"s\"} (${n})" in tools
# no reference to the new builder from the legacy functions.
assert "addSegments" not in think and "addSegments" not in tools