"""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 "`Thinking
`" 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