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
924 lines
43 KiB
Python
924 lines
43 KiB
Python
"""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
|