mirror of
https://github.com/turnstonelabs/turnstone.git
synced 2026-08-12 23:12:23 -06:00
1e2ab91ec2
Four follow-ups to the preview pane: - Probe-mode preflight: the pane preflights src-loaded kinds with GET ?probe=1 (204, real hardening headers, no body) instead of HEAD — the console reverse proxy forwards HEAD as a full GET, so the old preflight dragged the whole blob across the node→console hop twice. Ownership gate + renderable-type check still run on probes. - Legacy-charset text: table/text/markdown now transcode to UTF-8 at store time (declared charset → UTF-8 → cp1252-replace ladder), same model the web kind already used. The ladder applies only when the text kind was DECLARED (MIME/extension/override); the bare no-hint fallback stays strict UTF-8 and NUL bytes still hard-reject, so binary rejection is unchanged. - Remote assets default OFF: previewed pages are now served under "sandbox; default-src 'none'; style-src 'unsafe-inline'; img-src data:; font-src data:" — they render with inline styling but cannot contact their origin site (no viewer IP/traffic disclosure). A per-pane "Load remote images & styles" checkbox (web previews only, sticky, not persisted) reloads with ?assets=1 for the permissive bare-sandbox mode. - Markdown vendor parity: preview markdown now runs renderer.js's postRenderMarkdown (hljs token coloring + lazy mermaid diagrams) like the conversation pane, with preview-scoped code-block/KaTeX chrome (the conversation theme is .msg.assistant-scoped). Tests: probe/assets HTTP + policy coverage, charset ladder units + stored-bytes round-trip, JS static guards for the probe form, the default-off toggle, and the post-pass; headless-chrome harness grew to 41 assertions (probe-not-HEAD, toggle visibility/default, fenced-code render). Full suite green.
158 lines
6.9 KiB
Python
158 lines
6.9 KiB
Python
"""Static guards for the preview pane frontend (shared_static/preview.js and
|
|
its wiring through conversation.js / interactive.js / shell.js).
|
|
|
|
Same posture as ``test_shell_js.py``: Python-side string-presence assertions
|
|
that catch the silent one-line regression (a renamed export, a dropped
|
|
sandbox attribute, a de-registered pane type). Parse + sink + var guards for
|
|
``preview.js`` itself live in ``test_shell_js.py``'s bundle sweeps.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
_ROOT = Path(__file__).resolve().parent.parent
|
|
_SHARED = _ROOT / "turnstone/shared_static"
|
|
_PREVIEW_JS = _SHARED / "preview.js"
|
|
_CONVERSATION_JS = _SHARED / "conversation.js"
|
|
_INTERACTIVE_JS = _SHARED / "interactive.js"
|
|
_SHELL_JS = _SHARED / "shell.js"
|
|
_PREVIEW_CSS = _SHARED / "preview.css"
|
|
_UI_INDEX = _ROOT / "turnstone/ui/static/index.html"
|
|
_CONSOLE_INDEX = _ROOT / "turnstone/console/static/index.html"
|
|
|
|
|
|
def _read(p: Path) -> str:
|
|
return p.read_text(encoding="utf-8")
|
|
|
|
|
|
class TestPreviewPaneModule:
|
|
def test_factory_exported(self) -> None:
|
|
assert "export function createPreviewPane" in _read(_PREVIEW_JS)
|
|
|
|
def test_web_iframe_is_fully_sandboxed(self) -> None:
|
|
"""The web renderer must keep the empty-sandbox attribute — every
|
|
capability (scripts, same-origin, forms, popups) stays off. Dropping
|
|
or loosening it turns fetched pages into live documents."""
|
|
body = _read(_PREVIEW_JS)
|
|
assert 'frame.setAttribute("sandbox", "")' in body
|
|
assert 'frame.setAttribute("referrerpolicy", "no-referrer")' in body
|
|
|
|
def test_pdf_iframe_is_not_sandboxed(self) -> None:
|
|
"""Deliberate asymmetry: Chromium's PDF viewer refuses to paint in a
|
|
sandboxed context. The renderer comment carries the rationale; this
|
|
pins that renderPdf never gained a sandbox attribute by copy-paste."""
|
|
body = _read(_PREVIEW_JS)
|
|
pdf_fn = body.split("const renderPdf")[1].split("const renderImage")[0]
|
|
assert "sandbox" not in pdf_fn or "No sandbox attribute" in pdf_fn
|
|
|
|
def test_content_loads_through_authfetch_probe(self) -> None:
|
|
"""src-loaded kinds preflight with a probe request (authFetch of
|
|
?probe=1), NOT a HEAD. The console reverse proxy forwards a HEAD as a
|
|
full GET, so a real HEAD would drag the whole blob across the hop just
|
|
to discard it; the probe still surfaces the persist race + auth
|
|
failures as a typed error card and rides the 401-refresh retry a bare
|
|
iframe/img src can't."""
|
|
body = _read(_PREVIEW_JS)
|
|
assert "authFetch(probeUrl)" in body
|
|
assert "probe=1" in body
|
|
# The old full-GET HEAD preflight is gone.
|
|
assert 'method: "HEAD"' not in body
|
|
|
|
def test_markdown_uses_the_sanctioned_html_lane(self) -> None:
|
|
body = _read(_PREVIEW_JS)
|
|
assert "setSafeHtml(doc, renderMarkdown(text))" in body
|
|
|
|
def test_markdown_runs_vendor_post_pass(self) -> None:
|
|
"""The pane runs renderer.js's post-render pass (hljs token coloring +
|
|
mermaid) like the conversation pane — dropping it silently regresses
|
|
code highlighting and diagram rendering in previews."""
|
|
body = _read(_PREVIEW_JS)
|
|
assert "postRenderMarkdown(" in body
|
|
|
|
def test_remote_assets_toggle_is_default_off(self) -> None:
|
|
"""The remote-assets opt-in defaults OFF: a previewed page must not
|
|
contact its origin site until the user asks. Pins the label / tooltip
|
|
copy and the sticky-boolean initializer."""
|
|
body = _read(_PREVIEW_JS)
|
|
assert "Load remote images & styles" in body
|
|
assert "Off keeps this preview from contacting the site" in body
|
|
assert "pane._assetsOn = false" in body
|
|
|
|
def test_assets_flag_only_rides_behind_toggle(self) -> None:
|
|
"""assets=1 reaches the URL only when the per-pane toggle is on."""
|
|
body = _read(_PREVIEW_JS)
|
|
assert "assets=1" in body
|
|
assert "pane._assetsOn" in body
|
|
|
|
def test_history_is_bounded(self) -> None:
|
|
assert "HISTORY_CAP" in _read(_PREVIEW_JS)
|
|
|
|
def test_table_renderer_caps_rows(self) -> None:
|
|
assert "TABLE_ROW_CAP" in _read(_PREVIEW_JS)
|
|
|
|
def test_url_builder_encodes_path_parts(self) -> None:
|
|
body = _read(_PREVIEW_JS)
|
|
assert "encodeURIComponent(ws)" in body
|
|
assert 'encodeURIComponent(descriptor.attachment_id || "")' in body
|
|
|
|
|
|
class TestTranscriptChip:
|
|
def test_chip_builder_exported(self) -> None:
|
|
assert "export function buildPreviewChip" in _read(_CONVERSATION_JS)
|
|
|
|
def test_live_path_gates_auto_open_on_focus(self) -> None:
|
|
"""A backgrounded session must not commandeer the split — the live
|
|
path auto-opens only while the originating pane is focused; the chip
|
|
is the deliberate reopen everywhere else."""
|
|
body = _read(_INTERACTIVE_JS)
|
|
assert "if (this._host.isFocused(this)) this._host.onPreview(preview);" in body
|
|
|
|
def test_replay_path_renders_chip_without_auto_open(self) -> None:
|
|
body = _read(_INTERACTIVE_JS)
|
|
# The replay branch builds the chip…
|
|
assert "buildPreviewChip(msg.preview" in body
|
|
# …and the auto-open call appears exactly once (the live path).
|
|
assert body.count("this._host.onPreview(preview)") == 1
|
|
|
|
def test_tool_result_event_passes_preview(self) -> None:
|
|
assert "evt.preview," in _read(_INTERACTIVE_JS)
|
|
|
|
def test_host_bridge_carries_transport_ctx(self) -> None:
|
|
"""The preview pane fetches blobs from the ORIGINATING workstream
|
|
through the same node proxy — the bridge must pass both base and
|
|
wsId, not just the descriptor."""
|
|
body = _read(_INTERACTIVE_JS)
|
|
assert "window.TS_SHELL.openPreview(descriptor, { base: base, wsId: wsId })" in body
|
|
|
|
|
|
class TestShellWiring:
|
|
def test_pane_type_registered(self) -> None:
|
|
body = _read(_SHELL_JS)
|
|
assert 'pm.registerType("preview"' in body
|
|
assert "createPreviewPane" in body
|
|
|
|
def test_opens_beside_the_conversation(self) -> None:
|
|
"""openPaneBeside is the load-bearing gesture — the preview coexists
|
|
with the conversation that spawned it instead of replacing it."""
|
|
body = _read(_SHELL_JS)
|
|
assert 'pm.openPaneBeside("preview")' in body
|
|
|
|
def test_seam_exported_on_ts_shell(self) -> None:
|
|
assert "openPreview," in _read(_SHELL_JS)
|
|
|
|
|
|
class TestStylesheets:
|
|
def test_both_surfaces_link_preview_css(self) -> None:
|
|
for page in (_UI_INDEX, _CONSOLE_INDEX):
|
|
assert "/shared/preview.css" in _read(page), page.name
|
|
|
|
def test_stylesheet_uses_ds_tokens_not_legacy_vars(self) -> None:
|
|
"""conv-* card rule: DS tokens only — chat.css legacy vars
|
|
(--green/--red/--fg) must not creep into the new sheet."""
|
|
body = _read(_PREVIEW_CSS)
|
|
assert "var(--ink-" in body
|
|
assert "var(--hair)" in body
|
|
for legacy in ("var(--green)", "var(--red)", "var(--fg)"):
|
|
assert legacy not in body
|