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
321 lines
14 KiB
Python
321 lines
14 KiB
Python
"""Unit: the phase-117 tool-calls DISCLOSURE contract (task 01).
|
||
|
||
The owner's visual-glitch report (2026-09-15): one per-call tool "card"
|
||
per ``tool`` frame — 6+ stacked full-width bordered cards on a phone —
|
||
swamped a completed answer, and the flex row broke the label
|
||
mid-word. The fix (phase 117, owner decisions D1–D6) wraps the
|
||
existing ``.tool-calls`` list in a native ``details`` disclosure with a
|
||
plain-text "Tool call(s) (N)" summary — phase 158 (D1): one
|
||
disclosure per CONTIGUOUS tool-call run, created OPEN on the first
|
||
live ``tool`` frame of the run, folded at rest (delta folds the
|
||
active run via ``closeCurrentRun``; done/stop/finalizeFailedTurn fold
|
||
every run via ``closeAllRuns``), and rendered CLOSED on the restore
|
||
path and the shared page (D3). The count is per run (the per-run
|
||
"Tool call(s) (N)", D3). The line bytes, the four label literals, and
|
||
the ``<code>`` textContent arguments stay byte-identical (D6), so the
|
||
phase-37/70/87/95 pins stay green.
|
||
|
||
No ``app/`` logic (frontend-only, D1) — like
|
||
``test_frontend_tool_states.py``, this module pins the JS/CSS markers
|
||
at source level: the disclosure/summary build (task 01) and the
|
||
debox/dechip + disclosure CSS contract (task 02). The E2E story suite
|
||
(task 03) completes the phase.
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import re
|
||
from pathlib import Path
|
||
|
||
FRONTEND = Path(__file__).resolve().parents[2] / "frontend"
|
||
APP_JS = FRONTEND / "assets" / "app.js"
|
||
SHARED_JS = FRONTEND / "assets" / "shared.js"
|
||
STYLES_CSS = FRONTEND / "assets" / "styles.css"
|
||
|
||
#: The four pinned tool-line label literals (D6) — the emoji guard and
|
||
#: the phase-37/70 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 _shared_js() -> str:
|
||
return SHARED_JS.read_text(encoding="utf-8")
|
||
|
||
|
||
def _css() -> str:
|
||
return STYLES_CSS.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 test_tool_run_builds_a_div_disclosure_in_the_container() -> None:
|
||
"""Phase 158 (D1) + 161 (D1) + the D5 refinement (owner
|
||
2026-10-06): ``ensureToolRun`` creates a plain
|
||
``div.tool-calls-disclosure`` via ``createToolRunEl`` — a
|
||
``span.tool-calls-summary`` (first child — the static label, NOT
|
||
a control: the container is the ONE disclosure) + the existing
|
||
``.tool-calls`` list (role=list + aria-label "Tool calls",
|
||
second child) — appended into the phase-161 reasoning
|
||
container's ``.reasoning-body`` in arrival order (D1 — the
|
||
container sits above the bubble; the per-run bubble anchor is
|
||
superseded). The run is ALWAYS VISIBLE while the container is
|
||
open (never open/foldable — no ``.open`` anywhere); every
|
||
append re-renders the plain-text per-run "Tool call(s) (N)" count
|
||
on the summary through textContent (no innerHTML anywhere)."""
|
||
js = _js()
|
||
disc = _fn(js, "createToolRunEl")
|
||
assert "document.createElement(\"div\")" in disc, (
|
||
"the D5 refinement: a plain div — never collapsible"
|
||
)
|
||
assert "className = \"tool-calls-disclosure\"" in disc
|
||
assert "document.createElement(\"span\")" in disc, (
|
||
"the label is a static span (not a <summary> control)"
|
||
)
|
||
assert "className = \"tool-calls-summary\"" in disc
|
||
# The list keeps its exact phase-37 shape, nested in the disclosure.
|
||
assert "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)"
|
||
)
|
||
run = _fn(js, "ensureToolRun")
|
||
assert "createToolRunEl()" in run
|
||
assert ".open =" not in run, (
|
||
"D5 refinement: the in-container runs are non-collapsible divs"
|
||
)
|
||
assert 'insertBefore(disc, body.querySelector(".bubble"))' not in run, (
|
||
"the per-run bubble anchor is GONE (phase 161 D1)"
|
||
)
|
||
assert (
|
||
'ensureReasoningWrap(wrap).querySelector(".reasoning-body")'
|
||
".appendChild(disc)" in run
|
||
), (
|
||
"phase 161 (D1): appended into the container's body — the "
|
||
"container sits above the answer, arrival order preserved"
|
||
)
|
||
# D5: the count rides the summary — the template fragment itself
|
||
# (singular for one call, plural otherwise), textContent-built.
|
||
append = _fn(js, "appendToolLine")
|
||
assert "Tool call${n === 1 ? \"\" : \"s\"} (${n})" in append, (
|
||
"the per-run plain-text count template (D3) on every append"
|
||
)
|
||
assert "innerHTML" not in append, (
|
||
"no HTML injection surface on tool lines — textContent only"
|
||
)
|
||
|
||
|
||
def test_append_tool_line_keeps_the_four_pinned_labels() -> None:
|
||
"""D6: the four ``line.textContent`` label literals and the
|
||
``<code>`` textContent arguments stay byte-identical — the emoji
|
||
guard, the phase-37/70 unit pins, and the E2E text assertions all
|
||
key off these exact bytes."""
|
||
body = _fn(_js(), "appendToolLine")
|
||
for label in PINNED_LABELS:
|
||
assert label in body, f"pinned label literal changed: {label!r}"
|
||
assert "code.textContent = argument" in body, (
|
||
"the path/pattern/scope is data — textContent, never innerHTML"
|
||
)
|
||
|
||
|
||
def test_close_all_runs_defined_and_called_at_every_rest_site() -> None:
|
||
"""Phase 158 (D5): ``closeAllRuns`` — the terminal settle (the
|
||
phase-117 ``closeToolCalls`` + the D15 block close, merged):
|
||
closes EVERY open ``details.thinking`` +
|
||
``details.tool-calls-disclosure`` in the wrap (defensive; a late
|
||
double-close is a no-op), called at ALL rest sites — the done
|
||
handler (the turn ended), the stop/abort settle (the turn was
|
||
stopped), and the failed-turn funnel — so the definition + ≥3
|
||
calls. The `delta` handler folds only the active run
|
||
(``closeCurrentRun``)."""
|
||
js = _js()
|
||
body = _fn(js, "closeAllRuns")
|
||
assert "details.thinking, details.tool-calls-disclosure" in body, (
|
||
"targets both run kinds in the given wrap"
|
||
)
|
||
assert "el.open = false" in body, "fold: open=false (idempotent)"
|
||
assert js.count("closeAllRuns(wrap)") >= 3, (
|
||
"the definition + the done handler + the stop/abort settle + "
|
||
"the failed-turn funnel — the runs settle exactly where the "
|
||
"Thinking blocks settle"
|
||
)
|
||
delta_idx = js.find('ev.type === "delta"')
|
||
done_idx = js.find('ev.type === "done"')
|
||
delta_branch = js[delta_idx:done_idx]
|
||
assert "closeCurrentRun()" in delta_branch, (
|
||
"the delta folds the ACTIVE run (not every run — a later "
|
||
"thinking frame opens a new one)"
|
||
)
|
||
|
||
|
||
def test_restore_path_folds_the_disclosure() -> None:
|
||
"""renderStoredMessage (D3): a restored brain record with `tools`
|
||
re-renders through the SAME appendToolLine (byte-identical — the
|
||
caller passes the run's disclosure, phase 158), and the
|
||
disclosure is FOLDED right after the loop (``restoreDisc.open =
|
||
false``) — a completed turn from storage is one compact
|
||
"Tool calls (N)" line, not N stacked cards (the space fix;
|
||
mirrors the thinking restore rendering collapsed)."""
|
||
js = _js()
|
||
fn = js.find("function renderStoredMessage")
|
||
assert fn != -1
|
||
body = js[fn : js.find("function restoreConversation", fn)]
|
||
assert "appendToolLine(restoreDisc, t.name, arg, tOffset, tLimit, tPath)" in body
|
||
assert "restoreDisc.open = false" in body
|
||
assert (
|
||
body.find("appendToolLine(restoreDisc, t.name, arg, tOffset, tLimit, tPath)")
|
||
< body.find("restoreDisc.open = false")
|
||
), "the fold sits AFTER the tool-line restore loop"
|
||
|
||
|
||
def test_run_anchors_are_the_container_in_arrival_order() -> None:
|
||
"""Phase 158 (D2) + 161 (D1): the anchor moved to the REASONING
|
||
CONTAINER for both run kinds (the phase-161 documented
|
||
supersession — the container sits before the bubble; the runs
|
||
append into its ``.reasoning-body``) — the old
|
||
``.tool-calls-disclosure ?? .tool-calls ?? .bubble`` anchor chain
|
||
is GONE (with per-run disclosures, "above the tool lines" was the
|
||
wrong order — arrival order is the contract), and the runs are
|
||
ALWAYS VISIBLE inside the open container (the D5 refinement —
|
||
never open/foldable)."""
|
||
js = _js()
|
||
run = _fn(js, "ensureThinkingRun")
|
||
assert (
|
||
'ensureReasoningWrap(wrap).querySelector(".reasoning-body")'
|
||
".appendChild(block)" in run
|
||
), "phase 161 (D1): the thinking run anchors on the container"
|
||
assert "createThinkingRunEl()" in run, (
|
||
"the D5 refinement: a plain div — never collapsible"
|
||
)
|
||
assert ".open =" not in run, (
|
||
"the in-container runs are non-collapsible divs"
|
||
)
|
||
assert "??" not in run, "the old anchor chain is GONE"
|
||
d = _fn(js, "ensureToolRun")
|
||
assert (
|
||
'ensureReasoningWrap(wrap).querySelector(".reasoning-body")'
|
||
".appendChild(disc)" in d
|
||
), "phase 161 (D1): the tool run anchors on the container"
|
||
assert "createToolRunEl()" in d, (
|
||
"the D5 refinement: a plain div — never collapsible"
|
||
)
|
||
assert ".open =" not in d, (
|
||
"the in-container runs are non-collapsible divs"
|
||
)
|
||
|
||
|
||
def test_shared_page_add_tool_lines_builds_a_closed_disclosure() -> None:
|
||
"""shared.js addToolLines (D3 parity): the SAME
|
||
``details.tool-calls-disclosure`` + summary + ``.tool-calls`` list —
|
||
but created CLOSED (a pure render has no live turn to open it),
|
||
with the count set once at the end. The four label literals, the
|
||
three textContent-only argument lines, and the phase-95 marker stay
|
||
byte-identical (no innerHTML anywhere)."""
|
||
body = _fn(_shared_js(), "addToolLines")
|
||
assert "document.createElement(\"details\")" in body
|
||
assert "className = \"tool-calls-disclosure\"" in body
|
||
assert "document.createElement(\"summary\")" in body
|
||
assert "open = false" in body, (
|
||
"D3: the shared page renders the disclosure CLOSED — never an "
|
||
"auto-expanded record"
|
||
)
|
||
assert "className = \"tool-calls\"" in body
|
||
for label in PINNED_LABELS:
|
||
assert label in body, f"pinned label literal changed: {label!r}"
|
||
assert "Tool call${n === 1 ? \"\" : \"s\"} (${n})" in body, (
|
||
"the same plain-text count template (D5), set once at the end"
|
||
)
|
||
assert body.count("code.textContent = argument") == 3, (
|
||
"all three argument-bearing lines (read / grep / ls) stay "
|
||
"textContent-only"
|
||
)
|
||
assert "innerHTML" not in body, (
|
||
"no HTML injection surface on shared tool lines — textContent only"
|
||
)
|
||
|
||
|
||
def test_tool_call_rule_is_deboxed_inline_flow() -> None:
|
||
"""D4: the ``.tool-call`` rule is DEBOXED — no ``display: flex``,
|
||
no card (no background / border / radius / padding), and no
|
||
``var(--accent-line)`` left border. The accent now rides the TEXT
|
||
(``var(--accent-ink)``, ≈10.4:1 on the surface), and the line keeps
|
||
the compact status metrics + ``overflow-wrap: anywhere`` so the
|
||
label + inline ``code`` flow as ONE continuous run — a long path
|
||
wraps to the left edge and the label never breaks mid-word (the
|
||
"Rea/ding" glitch is gone)."""
|
||
m = re.search(r"\.tool-call \{([^}]*)\}", _css())
|
||
assert m, "the .tool-call rule must exist"
|
||
row = m.group(1)
|
||
assert "display: flex" not in row, (
|
||
"deboxed: no flex — the label + code are one inline run"
|
||
)
|
||
assert "var(--accent-line)" not in row, "deboxed: no accent left border"
|
||
assert "border" not in row, "deboxed: no card borders at all"
|
||
assert "background" not in row, "deboxed: no card background"
|
||
assert "padding" not in row, "deboxed: no card padding"
|
||
assert "var(--accent-ink)" in row, (
|
||
"the accent now rides the text color (≈10.4:1 on the surface)"
|
||
)
|
||
assert "font-size: 0.8rem" in row, "the compact status metrics stay"
|
||
assert "overflow-wrap: anywhere" in row, "the path wraps inside the run"
|
||
|
||
|
||
def test_tool_call_code_rule_is_dechipped() -> None:
|
||
"""D4: the ``.tool-call code`` rule KEEPS ``var(--mono)`` +
|
||
``var(--ink)`` (≈11.5:1 on the surface — AA) but LOSES the chip
|
||
(no background, no padding, no radius) — the path is inline text
|
||
in the line's run now."""
|
||
m = re.search(r"\.tool-call code \{([^}]*)\}", _css())
|
||
assert m, "the .tool-call code rule must exist"
|
||
code = m.group(1)
|
||
assert "var(--mono)" in code
|
||
assert "var(--ink)" in code
|
||
assert "background" not in code, "dechipped: no chip background"
|
||
assert "padding" not in code, "dechipped: no chip padding"
|
||
assert "border-radius" not in code, "dechipped: no chip radius"
|
||
assert "overflow-wrap: anywhere" in code
|
||
|
||
|
||
def test_tool_calls_disclosure_and_summary_rules_exist() -> None:
|
||
"""Phase 117 (D2/D4) + the D5 refinement (owner 2026-10-06): the
|
||
NEW ``.tool-calls-disclosure`` and ``.tool-calls-summary`` rules
|
||
exist — the label carries the accent TEXT (``var(--accent-ink)``,
|
||
≈10.4:1 on the surface; text + color, never color alone, B5),
|
||
modeled on the details.thinking family, with no new color literal
|
||
(the phase-92 zero-literal invariant). The D5 refinement: the
|
||
INTERACTIVE treatment (cursor + the chevron) is scoped to the
|
||
legacy ``<summary>`` element — the in-container run's label is a
|
||
static span, not a control (the container is the ONE
|
||
disclosure)."""
|
||
css = _css()
|
||
assert re.search(r"\.tool-calls-disclosure \{", css), (
|
||
"the .tool-calls-disclosure rule must exist"
|
||
)
|
||
m = re.search(r"\.tool-calls-summary \{([^}]*)\}", css)
|
||
assert m, "the .tool-calls-summary rule must exist"
|
||
summary = m.group(1)
|
||
assert "var(--accent-ink)" in summary, (
|
||
"the accent rides the summary text (≈10.4:1 on the surface)"
|
||
)
|
||
toggle = re.search(r"summary\.tool-calls-summary \{([^}]*)\}", css)
|
||
assert toggle, (
|
||
"the D5 refinement: the toggle affordance is scoped to the "
|
||
"legacy <summary> (the in-container label is a static span)"
|
||
)
|
||
assert "cursor: pointer" in toggle.group(1), "a toggle affordance"
|