mirror of
https://github.com/turnstonelabs/turnstone.git
synced 2026-08-12 23:12:23 -06:00
3bfb40f60f
Dead-code removal, all provable (no JS creator / no reachable caller): - interactive.js drops the !this._embedded branches: focus tracking, the right-click context menu, and the header with split/close buttons all referenced shell globals (setFocusedPane, splitPane, splitRoot, showPaneContextMenu, countLeaves, closePane) that exist nowhere since the step-6 fork collapse — reaching them was a guaranteed ReferenceError. The embedded flag goes with them (every pane is L-shell-hosted; pane--embedded is now unconditional), as do the call-less updateWsName() and the host adapter's getWsName seam. - ui/static/style.css drops the orphaned split-pane/tab-bar vocabulary: .ws-tab*, #new-tab-btn, #split-btn, .split-handle, the pane-header/ action-button block, the unused dropdown-in keyframe, and the dead entries in the reduced-motion list. - ui/static/app.js drops the retired settings-gear menu remnants (state vars for a builder that no longer exists + an always-false Escape guard) and fixes a real crash: hideNewWsModal() focused the removed #new-tab-btn unguarded, throwing a TypeError on every create/fork modal close; focus now returns to the shell's [+] new-session button. - pane.js exports openPopupMenu — items, positioning (flip + clamp), dismissal, aria-expanded mirroring, and arrow-key roving in one place. The tab-action dropdown delegates to it, and shell.js's footer user menu replaces its hand-rolled duplicate (which lacked Tab-close and arrow roving — it inherits both). test_interactive_pane_js.py: the embedded-gate pins flip to retired- symbol pins (the gate is gone, not gated).
212 lines
10 KiB
Python
212 lines
10 KiB
Python
"""Static smoke guards for the shared interactive pane module.
|
|
|
|
``turnstone/shared_static/interactive.js`` is the per-workstream conversational
|
|
``Pane`` lifted out of ``ui/static/app.js`` (L-shell step 5a) so BOTH the
|
|
standalone ``turnstone-server`` UI and the console L-shell can mount it. The
|
|
load-bearing invariants of that extraction are pinned here — like the rest of
|
|
the WebUI, the module has no JS test framework, so these are Python-side
|
|
string-presence assertions that catch the silent one-line regression.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from pathlib import Path
|
|
|
|
_ROOT = Path(__file__).resolve().parent.parent
|
|
_INTERACTIVE = _ROOT / "turnstone/shared_static/interactive.js"
|
|
_APP = _ROOT / "turnstone/ui/static/app.js"
|
|
_UI_INDEX = _ROOT / "turnstone/ui/static/index.html"
|
|
|
|
|
|
def _strip_comments(js: str) -> str:
|
|
js = re.sub(r"/\*.*?\*/", "", js, flags=re.S)
|
|
js = re.sub(r"//[^\n]*", "", js)
|
|
return js
|
|
|
|
|
|
def test_interactive_is_esm_imported_by_the_shell() -> None:
|
|
"""Real ES module: it ``export``s the factory the shell imports in BOTH
|
|
deployments. Step 6 retired the window bridge (no window.InteractivePane)
|
|
and the standalone HTML no longer script-tags interactive.js — shell.js
|
|
pulls it via ``import``."""
|
|
body = _INTERACTIVE.read_text(encoding="utf-8")
|
|
assert "export { Pane as InteractivePane, createInteractivePane };" in body
|
|
assert "window.InteractivePane = Pane" not in body, (
|
|
"the window bridge is retired — the shell imports the factory (ESM)."
|
|
)
|
|
html = _UI_INDEX.read_text(encoding="utf-8")
|
|
assert "/shared/interactive.js" not in html, (
|
|
"the standalone HTML must NOT script-tag interactive.js — shell.js imports it."
|
|
)
|
|
|
|
|
|
def test_pane_constructor_takes_transport_and_host_seam() -> None:
|
|
"""The constructor takes the ``(wsId, opts)`` seam: a transport ``base``
|
|
(the node-proxy prefix) and a ``host`` adapter for the few things only the
|
|
surrounding shell knows. The old ``embedded`` flag is gone — every pane is
|
|
L-shell-hosted since the step-6 fork collapse."""
|
|
body = _INTERACTIVE.read_text(encoding="utf-8")
|
|
assert "constructor(wsId, opts) {" in body
|
|
for field in (
|
|
"this._base = opts.base",
|
|
"this._host = opts.host || INTERACTIVE_DEFAULT_HOST",
|
|
):
|
|
assert field in body, f"missing constructor seam: {field!r}"
|
|
assert "opts.embedded" not in body, (
|
|
"the embedded flag is retired — every pane is L-shell-hosted."
|
|
)
|
|
|
|
|
|
def test_transport_urls_are_base_prefixed() -> None:
|
|
"""Every per-ws request is prefixed with ``this._base`` so a console pane
|
|
proxies through ``/node/{id}`` (the LOCALITY invariant: an interactive
|
|
session lives on a cluster node). A bare ``/v1/api/workstreams/`` URL would
|
|
hit the console instead of the node and silently 404 / cross-talk."""
|
|
body = _strip_comments(_INTERACTIVE.read_text(encoding="utf-8"))
|
|
# Collapse whitespace so a prettier line-wrap (``this._base +`` on the line
|
|
# ABOVE the URL string) doesn't read as a bare URL: every workstream URL
|
|
# must be preceded by ``this._base +``.
|
|
collapsed = re.sub(r"\s+", " ", body)
|
|
bad = re.findall(r'(?<!this\._base \+ )"/v1/api/workstreams/"', collapsed)
|
|
assert not bad, (
|
|
"found a bare '/v1/api/workstreams/' URL not prefixed by this._base — "
|
|
"a console pane would route it to the console, not the owning node."
|
|
)
|
|
# The EventSource + history + send all go through the base.
|
|
assert 'this._base + "/v1/api/workstreams/"' in collapsed
|
|
assert "new EventSource(evtUrl" in body
|
|
|
|
|
|
def test_split_pane_chrome_is_retired() -> None:
|
|
"""The standalone split-pane chrome is GONE, not gated: no pane header
|
|
(name / persona / state live in the tab + rail; the conversation owns the
|
|
full pane height), no focus tracking, no split/close buttons. The dead
|
|
``!this._embedded`` branches referenced shell globals (setFocusedPane,
|
|
splitPane, splitRoot…) that no longer exist anywhere — reaching them was a
|
|
guaranteed ReferenceError, so their removal is a bugfix too."""
|
|
body = _INTERACTIVE.read_text(encoding="utf-8")
|
|
assert "_embedded" not in body, "the embedded gate is retired (always-on)"
|
|
assert 'className = "pane pane--embedded"' in body, (
|
|
"the pane root must carry pane--embedded unconditionally — "
|
|
"interactive.css scopes the slim-chrome layout to it"
|
|
)
|
|
for gone in (
|
|
"setFocusedPane",
|
|
"showPaneContextMenu",
|
|
"splitPane(",
|
|
"splitRoot",
|
|
"this.headerEl",
|
|
'"pane-header"',
|
|
'"pane-action-btn"',
|
|
"updateWsName",
|
|
):
|
|
assert gone not in body, f"retired split-pane symbol {gone!r} resurfaced"
|
|
# The persona tag stays gone (the rail's INT/COORD vocabulary shows it).
|
|
assert '"pane-persona-tag"' not in body
|
|
assert '"INTERACTIVE"' not in body
|
|
|
|
|
|
def test_factory_returns_lifecycle_over_node_proxy() -> None:
|
|
"""``createInteractivePane`` is the console factory (mirrors
|
|
``createCoordinatorPane``): it derives the node-proxy base from ``nodeId``
|
|
and returns the lifecycle controller the shell drives."""
|
|
body = _INTERACTIVE.read_text(encoding="utf-8")
|
|
assert "function createInteractivePane(root, wsId, opts) {" in body
|
|
assert '"/node/" + encodeURIComponent(opts.nodeId)' in body
|
|
for hook in ("connect()", "deactivate()", "onLogin()", "destroy()"):
|
|
assert hook in body, f"factory controller missing lifecycle hook {hook!r}"
|
|
# Teardown must close the stream so a backgrounded pane can't leak an
|
|
# upstream node connection.
|
|
assert "pane.disconnectSSE();" in body
|
|
|
|
|
|
def test_host_seam_routes_shell_couplings() -> None:
|
|
"""Every coupling to the surrounding shell goes through ``this._host`` — so
|
|
the same Pane works standalone (real adapter) and console-embedded (no-op /
|
|
Tier-1 adapter). No direct ``focusedPaneId`` / ``workstreams`` / consent
|
|
badge reference survives in the module."""
|
|
body = _INTERACTIVE.read_text(encoding="utf-8")
|
|
for call in (
|
|
"this._host.isFocused(this)",
|
|
"this._host.onStreamError(this)",
|
|
"this._host.warningTarget(this)",
|
|
"this._host.onConsentDetected(",
|
|
):
|
|
assert call in body, f"missing host seam call {call!r}"
|
|
assert "getWsName" not in body, (
|
|
"getWsName left the host seam with the pane header — the tab + rail "
|
|
"own the workstream name now."
|
|
)
|
|
code = _strip_comments(body)
|
|
# The classic split-pane shell globals must not leak into the module as
|
|
# bare code references (URL path strings excepted, handled above).
|
|
assert not re.search(r"(?<![\w$./\"])focusedPaneId(?![\w$])", code), (
|
|
"focusedPaneId leaked into the shared module — route it through "
|
|
"host.isFocused so the console (which has no such global) still works."
|
|
)
|
|
assert "_pendingConsentServers" not in code, (
|
|
"the consent-badge state must stay in the standalone shell; the pane "
|
|
"only notifies via host.onConsentDetected."
|
|
)
|
|
|
|
|
|
def test_standalone_opens_sessions_via_the_shell_pane_manager() -> None:
|
|
"""Step 6 retired the standalone's local split-pane construction: app.js no
|
|
longer builds panes via window.InteractivePane / STANDALONE_HOST. Sessions
|
|
open through the shared shell's PaneManager — openSessionPane delegates to
|
|
openPane('interactive', wsId)."""
|
|
app = _APP.read_text(encoding="utf-8")
|
|
assert "STANDALONE_HOST" not in app, "the standalone host adapter is retired."
|
|
assert "new window.InteractivePane(" not in app, (
|
|
"the standalone no longer constructs panes locally."
|
|
)
|
|
start = app.index("function openSessionPane(wsId)")
|
|
fn = app[start : start + 300]
|
|
assert 'openPane("interactive", wsId)' in fn, (
|
|
"openSessionPane must open the session as a pane via the shell PaneManager."
|
|
)
|
|
|
|
|
|
def test_approval_keyboard_shortcuts_wired() -> None:
|
|
"""The converged card advertises y/n/a (+Enter/Esc) kbd hints, so the pane
|
|
must route those keys to resolveApproval when a pending approval is up —
|
|
pane-owned on this.el (the fork collapse retired the old app.js global
|
|
handler + getFocusedPane). Guards against the chips over-promising."""
|
|
body = _INTERACTIVE.read_text(encoding="utf-8")
|
|
assert "if (!this.pendingApproval || !this.approvalBlockEl) return;" in body, (
|
|
"approval keydown must early-return unless a pending approval is up"
|
|
)
|
|
assert "e.key.toLowerCase()" in body, "the y/n/a shortcut branch"
|
|
assert ".conv-feedback" in body, (
|
|
"the feedback field uses the converged .conv-feedback, not the retired "
|
|
".ts-approval-feedback"
|
|
)
|
|
|
|
|
|
def test_controller_terminal_dead_state() -> None:
|
|
"""Lifecycle round 2: the console controller must STOP reconnect-polling a
|
|
session that is gone (closed / evicted / node restarted) — three consecutive
|
|
CLOSED recovery beats → give up: stream closed, status bar terminal,
|
|
``opts.onDead()`` fired once. A successful stream open resets the counter
|
|
(the new host.onStreamOpen seam). ``isDead()`` / ``markDead()`` / ``base``
|
|
are the shell's revive surface; a dead controller also ignores the login
|
|
re-arm (recovery may need a DIFFERENT node — the shell's revive owns it)."""
|
|
body = _INTERACTIVE.read_text(encoding="utf-8")
|
|
# The give-up ladder.
|
|
assert "let dead = false;" in body and "let failCount = 0;" in body
|
|
assert "const giveUp = function () {" in body
|
|
assert "failCount += 1;" in body and "if (failCount >= 3) giveUp();" in body
|
|
assert 'pane._sbTokens.textContent = "Disconnected"' in body, (
|
|
"the terminal state must be worded distinctly from the transient Reconnecting…"
|
|
)
|
|
assert "opts.onDead" in body, "the shell must hear about the give-up"
|
|
# The reset seam: Pane.connectSSE onopen → host.onStreamOpen → failCount = 0.
|
|
assert "this._host.onStreamOpen(this)" in body
|
|
assert "onStreamOpen() {}" in body, "the default host must carry the no-op"
|
|
# The shell-facing surface.
|
|
assert "isDead()" in body and "markDead: giveUp," in body
|
|
assert "base: base," in body, "the controller must expose its transport base"
|
|
# Dead controllers don't reconnect on re-auth.
|
|
assert "if (connected && !dead) pane._loadHistoryThenConnect(wsId);" in body
|