"""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 ```` 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_an_open_details_disclosure() -> None: """Phase 158 (D1/D2/D5): ``ensureToolRun`` creates a native ``details.tool-calls-disclosure`` via ``createToolDisclosure`` — a ``summary.tool-calls-summary`` (first child) + the existing ``.tool-calls`` list (role=list + aria-label "Tool calls", second child) — inserted before the bubble in arrival order (D2). It starts OPEN while the run is live; 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, "createToolDisclosure") assert "document.createElement(\"details\")" in disc assert "className = \"tool-calls-disclosure\"" in disc assert "document.createElement(\"summary\")" in disc 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 "createToolDisclosure()" in run assert "disc.open = true", ( "D5: created OPEN on the first live tool frame of the run — the " "calls and the phase-87 .tool-elapsed suffix stay visible during " "the turn" ) assert 'insertBefore(disc, body.querySelector(".bubble"))' in run, ( "D2: above the answer, in arrival order" ) # 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 ```` 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_bubble_in_arrival_order() -> None: """Phase 158 (D2): the anchor is the BUBBLE for both run kinds — 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 still open while active (D5).""" js = _js() run = _fn(js, "ensureThinkingRun") assert 'querySelector(".bubble")' in run assert "block.open = true" in run assert "??" not in run, "the old anchor chain is GONE" d = _fn(js, "ensureToolRun") assert 'querySelector(".bubble")' in d assert "disc.open = true" in d 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 NEW ``.tool-calls-disclosure`` and ``.tool-calls-summary`` rules exist — the summary is a native focusable toggle carrying 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).""" 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)" ) assert "cursor: pointer" in summary, "a toggle affordance"