Files
brain-of-reese/tests/unit/test_thinking_timeline.py
T
ducoterra 31bd903a93
Build and Push Containers / build-and-push-app (push) Successful in 8m38s
Build and Push Containers / build-and-push-db (push) Successful in 32s
phase: 158_thinking_timeline
All verification complete. Every gate is green and the docs check out. Final report:

## Phase 158 final verification pass — COMPLETE

**Verified (all tasks pre-completed; this pass re-ran the full gate):**
- Unit + integration: `uv run pytest --cov=app --cov-report=term-missing` → **3618 passed, 99% coverage on `app/`** (>90% gate)
- Phase E2E in isolation: `uv run pytest tests/e2e/test_thinking_timeline.py -v --no-cov` → **6/6 passed** (order pin, fold-at-rest, live-open, restore+`segments` array, shared page, no-thinking single disclosure)
- Regression E2E, each in isolation: `test_thinking_display` 5✓, `test_tool_call_compact` 4✓, `test_turn_progress_loader` 3✓ (per-run pin updated), `test_big_read_progress` 4✓, `test_agent_document_tools` 4✓, `test_share_chat` 4✓, `test_chat_persistence` 4✓, `test_smoke` 3✓
- Lint/types: `uv run ruff check . && uv run pyright` → **clean (0 errors)**

**Completion criteria:** all 9 met — timeline E2E pins ✓, restore/shared `segments` ✓, no-thinking single-disclosure + the three UNCHANGED regression suites green ✓, pre-phase byte-identical round-trip (integration pin: key absent, never null; >50 / >32 000 / empty text / unknown kind all 422) ✓, full suite + coverage + E2E + lint ✓, `git diff --stat` footprint limited to `app/schemas.py`, `frontend/assets/{app,shared}.js`, `tests/`, README, phase files (a pre-existing owner `TODO.md` edit is out of scope, not staged by the phase commit) ✓, D15→per-run supersession recorded in the updated pin ✓. (Commit + phase move are the harness's job; task dirs already in `complete/`.)

**Documentation:** README.md — Thinking section correctly describes the phase-158 timeline; config reference matches `.env.example` exactly; page table matches registered routes; no stale single-block wording. AGENTS.md — rules and quick-reference commands all accurate. **Verified consistent — no changes needed.**

**Decisions/deviations:** none. `test_tool_call_compact.py` carries a one-line docstring-only update (referenced the removed `closeToolCalls`); its test logic is byte-identical to the phase-117 contract and green.

**Next pending phase:** none — `todo/` contains only 158.
2026-10-05 02:52:55 -04:00

826 lines
39 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Unit: the phase-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_open_block_before_the_bubble() -> None:
"""D1/D2/D3: ``ensureThinkingRun`` returns the active thinking
run's block or — a NEW ``details.thinking`` (the phase-17 static
structure via ``createThinkingBlock``) created OPEN (D5),
inserted BEFORE the ``.bubble`` (D2 — the anchor is the bubble,
in arrival order). The plain ``Thinking`` literal (D3 — the
phase-17 literal, not a numbered variant) rides the static
template; 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 CLOSES the previous run first (D1)"
)
assert "createThinkingBlock()" in body, "the shared builder is reused"
assert "block.open = true" in body, "D5: created OPEN while its frames stream"
assert 'insertBefore(block, body.querySelector(".bubble"))' in body, (
"D2: the anchor is the BUBBLE — above it, in arrival order"
)
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_open_disclosure_before_the_bubble() -> None:
"""D1/D2/D5: ``ensureToolRun`` returns the active tool run's
disclosure or — a NEW ``details.tool-calls-disclosure`` (the
phase-117 structure via ``createToolDisclosure`` —
summary.tool-calls-summary + the .tool-calls list, role=list +
aria-label "Tool calls") created OPEN (D5), inserted BEFORE the
bubble (D2, in arrival order). 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 "createToolDisclosure()" in body
assert "disc.open = true" in body, "D5: created OPEN while its frames stream"
assert 'insertBefore(disc, body.querySelector(".bubble"))' in body
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:
"""D5's fold side: ``closeCurrentRun`` folds the ACTIVE run
(idempotent; a no-op without a run) and ``closeAllRuns`` closes
EVERY open ``details.thinking`` + ``details.tool-calls-disclosure``
in the wrap (defensive — a late double-close is a no-op; a no-op
without a wrap)."""
js = _js()
cur = _fn(js, "closeCurrentRun")
assert "currentRun.el.open = false" in cur
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``
COLLAPSED — a ``kind:"thinking"`` entry gets its OWN
``details.thinking`` (createElement structure, the plain
``Thinking`` literal via textContent — D3; NO ``open = true`` —
created closed), and CONSECUTIVE ``kind:"tool"`` entries group
into ONE ``details.tool-calls-disclosure`` (the shared
phase-117 builder — ``createToolDisclosure``, innerHTML ABSENT —
created closed), each inserted BEFORE the bubble (D2, arrival
order). 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 "document.createElement(\"details\")" in body, "the structure is createElement"
assert 'block.className = "thinking"' in body
assert "block.open = true" not in body, "D5: created COLLAPSED (no open state)"
assert 'summary.textContent = "Thinking"' in body, (
"the plain Thinking literal (D3 — the phase-17 literal; "
"position distinguishes the runs) via textContent"
)
assert 'textEl.className = "thinking-text"' in body
# the consecutive-tool grouping (one disclosure per run — the
# grouping loop): create on the first entry, seal on a kind
# change, insert before the bubble.
assert "createToolDisclosure()" in body, "the shared phase-117 builder (innerHTML ABSENT)"
assert "toolDisc.open = true" not in body, "D5: created COLLAPSED (no open state)"
assert "toolDisc.open = false" in body
assert "if (toolDisc) toolDisc = null" in body, (
"a thinking entry seals the tool run — ONE disclosure per run (D1)"
)
assert "body.insertBefore(block, bubble)" in body, "D2: above the bubble"
assert "body.insertBefore(toolDisc, bubble)" in body, "D2: above the bubble"
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"\b(\w+)\.innerHTML\s*=\s*([^;\n]+)", body)
assert len(writes) == 1, (
f"exactly ONE innerHTML write (the structure is createElement): {writes}"
)
assert writes[0][0] == "textEl", "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 thinking block's summary is
textContent; the disclosure rides the innerHTML-absent
``createToolDisclosure`` — 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 "document.createElement(\"summary\")" in body, (
"the summary is createElement + textContent"
)
assert "document.createElement(\"div\")" in body, "the .thinking-text is createElement"
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 details.thinking with
the plain ``Thinking`` literal via textContent + the phase-117
disclosure structure — summary.tool-calls-summary + the
.tool-calls list, role=list + aria-label "Tool calls"), 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 insertion BEFORE the bubble
(D2, arrival order) — all created CLOSED (no ``open = true``
anywhere; the addThinkingBlock / addToolLines collapsed
convention)."""
body = _fn(_js_shared(), "addSegments")
assert "document.createElement(\"details\")" in body, "the structure is createElement"
assert 'block.className = "thinking"' in body
assert "block.open = true" not in body, "D5: created COLLAPSED"
assert "block.open = false" in body
assert 'summary.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.
assert 'toolDisc.className = "tool-calls-disclosure"' in body
assert "toolDisc.open = true" not in body, "D5: created COLLAPSED"
assert "toolDisc.open = false" in body
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
# D2: above the bubble, in arrival order.
assert "body.insertBefore(block, bubble)" in body
assert "body.insertBefore(toolDisc, bubble)" in body
# 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