Rebrand sexp → sx across web platform (173 files)
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 11m37s

Rename all sexp directories, files, identifiers, and references to sx.
artdag/ excluded (separate media processing DSL).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-01 11:06:57 +00:00
parent 17cebe07e7
commit e8bc228c7f
174 changed files with 3126 additions and 2952 deletions

0
test/sx/__init__.py Normal file
View File

148
test/sx/dashboard.sx Normal file
View File

@@ -0,0 +1,148 @@
;; Test dashboard components
(defcomp ~test-status-badge (&key status)
(span :class (str "inline-flex items-center rounded-full border px-3 py-1 text-sm font-medium "
(if (= status "running") "border-amber-300 bg-amber-50 text-amber-700 animate-pulse"
(if (= status "passed") "border-emerald-300 bg-emerald-50 text-emerald-700"
(if (= status "failed") "border-rose-300 bg-rose-50 text-rose-700"
"border-stone-300 bg-stone-50 text-stone-700"))))
status))
(defcomp ~test-run-button (&key running csrf)
(form :method "POST" :action "/run" :class "inline"
(input :type "hidden" :name "csrf_token" :value csrf)
(button :type "submit"
:class (str "rounded bg-stone-800 px-4 py-2 text-sm font-medium text-white hover:bg-stone-700 "
"disabled:opacity-50 disabled:cursor-not-allowed transition-colors")
:disabled (if running "true" nil)
(if running "Running..." "Run Tests"))))
(defcomp ~test-filter-card (&key href label count colour-border colour-bg colour-text active)
(a :href href
:sx-get href
:sx-target "#main-panel"
:sx-select "#main-panel"
:sx-swap "outerHTML"
:sx-push-url "true"
:class (str "block rounded border p-3 text-center transition-colors no-underline hover:opacity-80 "
colour-border " " colour-bg " "
(if active "ring-2 ring-offset-1 ring-stone-500 " ""))
(div :class (str "text-3xl font-bold " colour-text) count)
(div :class (str "text-sm " colour-text) label)))
(defcomp ~test-summary (&key status passed failed errors skipped total duration last-run running csrf active-filter)
(div :class "space-y-4"
(div :class "flex items-center justify-between flex-wrap gap-3"
(div :class "flex items-center gap-3"
(h2 :class "text-2xl font-semibold text-stone-800" "Test Results")
(when status (~test-status-badge :status status)))
(~test-run-button :running running :csrf csrf))
(when status
(div :class "grid grid-cols-2 sm:grid-cols-3 md:grid-cols-6 gap-3"
(~test-filter-card :href "/" :label "Total" :count total
:colour-border "border-stone-200" :colour-bg "bg-white"
:colour-text "text-stone-800"
:active (if (= active-filter nil) "true" nil))
(~test-filter-card :href "/?filter=passed" :label "Passed" :count passed
:colour-border "border-emerald-200" :colour-bg "bg-emerald-50"
:colour-text "text-emerald-700"
:active (if (= active-filter "passed") "true" nil))
(~test-filter-card :href "/?filter=failed" :label "Failed" :count failed
:colour-border "border-rose-200" :colour-bg "bg-rose-50"
:colour-text "text-rose-700"
:active (if (= active-filter "failed") "true" nil))
(~test-filter-card :href "/?filter=errors" :label "Errors" :count errors
:colour-border "border-orange-200" :colour-bg "bg-orange-50"
:colour-text "text-orange-700"
:active (if (= active-filter "errors") "true" nil))
(~test-filter-card :href "/?filter=skipped" :label "Skipped" :count skipped
:colour-border "border-sky-200" :colour-bg "bg-sky-50"
:colour-text "text-sky-700"
:active (if (= active-filter "skipped") "true" nil))
(~test-filter-card :href "/" :label "Duration" :count (str duration "s")
:colour-border "border-stone-200" :colour-bg "bg-white"
:colour-text "text-stone-800" :active nil))
(div :class "text-sm text-stone-400" (str "Last run: " last-run)))))
(defcomp ~test-service-header (&key service total passed failed)
(tr :class "border-b-2 border-stone-300 bg-stone-100"
(td :class "px-3 py-2 text-sm font-bold text-stone-700" :colspan "4"
(span service)
(span :class "ml-2 text-xs font-normal text-stone-500"
(str total " tests, " passed " passed, " failed " failed")))))
(defcomp ~test-row (&key nodeid outcome duration longrepr)
(tr :class (str "border-b border-stone-100 "
(if (= outcome "passed") "bg-white"
(if (= outcome "failed") "bg-rose-50"
(if (= outcome "skipped") "bg-sky-50"
"bg-orange-50"))))
(td :class "px-3 py-2 text-sm font-mono text-stone-700 max-w-0 truncate" :title nodeid
(a :href (str "/test/" nodeid)
:sx-get (str "/test/" nodeid)
:sx-target "#main-panel"
:sx-select "#main-panel"
:sx-swap "outerHTML"
:sx-push-url "true"
:class "hover:underline hover:text-sky-600"
nodeid))
(td :class "px-3 py-2 text-center"
(span :class (str "inline-flex items-center rounded-full border px-2 py-0.5 text-xs font-medium "
(if (= outcome "passed") "border-emerald-300 bg-emerald-50 text-emerald-700"
(if (= outcome "failed") "border-rose-300 bg-rose-50 text-rose-700"
(if (= outcome "skipped") "border-sky-300 bg-sky-50 text-sky-700"
"border-orange-300 bg-orange-50 text-orange-700"))))
outcome))
(td :class "px-3 py-2 text-right text-sm text-stone-500 tabular-nums" (str duration "s"))
(td :class "px-3 py-2 text-sm text-rose-600 font-mono max-w-xs truncate" :title longrepr
(when longrepr longrepr))))
(defcomp ~test-results-table (&key rows has-failures)
(div :class "overflow-x-auto rounded border border-stone-200 bg-white"
(table :class "w-full text-left"
(thead
(tr :class "border-b border-stone-200 bg-stone-50"
(th :class "px-3 py-2 text-sm font-medium text-stone-600" "Test")
(th :class "px-3 py-2 text-xs font-medium text-stone-600 text-center w-24" "Status")
(th :class "px-3 py-2 text-xs font-medium text-stone-600 text-right w-20" "Time")
(th :class "px-3 py-2 text-xs font-medium text-stone-600 w-48" "Error")))
(tbody (when rows rows)))))
(defcomp ~test-running-indicator ()
(div :class "flex items-center justify-center py-12 text-stone-500"
(div :class "flex items-center gap-3"
(div :class "animate-spin h-6 w-6 border-2 border-stone-300 border-t-stone-600 rounded-full")
(span :class "text-sm" "Running tests..."))))
(defcomp ~test-no-results ()
(div :class "flex items-center justify-center py-12 text-stone-400"
(div :class "text-center"
(div :class "text-4xl mb-2" "?")
(div :class "text-sm" "No test results yet. Click Run Tests to start."))))
(defcomp ~test-detail (&key nodeid outcome duration longrepr)
(div :class "space-y-6 p-4"
(div :class "flex items-center gap-3"
(a :href "/"
:sx-get "/"
:sx-target "#main-panel"
:sx-select "#main-panel"
:sx-swap "outerHTML"
:sx-push-url "true"
:class "text-sky-600 hover:text-sky-800 text-sm"
"← Back to results")
(span :class (str "inline-flex items-center rounded-full border px-2 py-0.5 text-xs font-medium "
(if (= outcome "passed") "border-emerald-300 bg-emerald-50 text-emerald-700"
(if (= outcome "failed") "border-rose-300 bg-rose-50 text-rose-700"
(if (= outcome "skipped") "border-sky-300 bg-sky-50 text-sky-700"
"border-orange-300 bg-orange-50 text-orange-700"))))
outcome))
(div :class "rounded border border-stone-200 bg-white p-4 space-y-3"
(h2 :class "text-lg font-mono font-semibold text-stone-800 break-all" nodeid)
(div :class "flex gap-4 text-sm text-stone-500"
(span (str "Duration: " duration "s")))
(when longrepr
(div :class "mt-4"
(h3 :class "text-sm font-semibold text-rose-700 mb-2" "Error Output")
(pre :class "bg-stone-50 border border-stone-200 rounded p-3 text-xs text-stone-700 overflow-x-auto whitespace-pre-wrap"
longrepr))))))

238
test/sx/sx_components.py Normal file
View File

@@ -0,0 +1,238 @@
"""Test service s-expression page components."""
from __future__ import annotations
import os
from datetime import datetime
from shared.sx.jinja_bridge import load_service_components
from shared.sx.helpers import (
sx_call, SxExpr,
root_header_sx, full_page_sx, header_child_sx,
)
# Load test-specific .sx components at import time
load_service_components(os.path.dirname(os.path.dirname(__file__)))
def _format_time(ts: float | None) -> str:
"""Format a unix timestamp for display."""
if not ts:
return "never"
return datetime.fromtimestamp(ts).strftime("%-d %b %Y, %H:%M:%S")
# ---------------------------------------------------------------------------
# Menu / header
# ---------------------------------------------------------------------------
_FILTER_MAP = {
"passed": "passed",
"failed": "failed",
"errors": "error",
"skipped": "skipped",
}
def _filter_tests(tests: list[dict], active_filter: str | None,
active_service: str | None) -> list[dict]:
"""Filter tests by outcome and/or service."""
from runner import _service_from_nodeid
filtered = tests
if active_filter and active_filter in _FILTER_MAP:
outcome = _FILTER_MAP[active_filter]
filtered = [t for t in filtered if t["outcome"] == outcome]
if active_service:
filtered = [t for t in filtered if _service_from_nodeid(t["nodeid"]) == active_service]
return filtered
# ---------------------------------------------------------------------------
# Results partial
# ---------------------------------------------------------------------------
def test_detail_sx(test: dict) -> str:
"""Return s-expression wire format for a test detail view."""
inner = sx_call(
"test-detail",
nodeid=test["nodeid"],
outcome=test["outcome"],
duration=str(test["duration"]),
longrepr=test.get("longrepr", ""),
)
return (
f'(section :id "main-panel"'
f' :class "flex-1 md:h-full md:min-h-0 overflow-y-auto overscroll-contain js-grid-viewport"'
f' {inner})'
)
# ---------------------------------------------------------------------------
# Sx-native versions — return sx source (not HTML)
# ---------------------------------------------------------------------------
def _test_header_sx(ctx: dict, active_service: str | None = None) -> str:
"""Build the Tests menu-row as sx call."""
nav = _service_nav_sx(ctx, active_service)
return sx_call("menu-row-sx",
id="test-row", level=1, colour="sky",
link_href="/", link_label="Tests", icon="fa fa-flask",
nav=SxExpr(nav),
child_id="test-header-child",
)
def _service_nav_sx(ctx: dict, active_service: str | None = None) -> str:
"""Service filter nav as sx."""
from runner import _SERVICE_ORDER
parts = []
parts.append(sx_call("nav-link",
href="/", label="all",
is_selected="true" if not active_service else None,
select_colours="aria-selected:bg-sky-200 aria-selected:text-sky-900",
))
for svc in _SERVICE_ORDER:
parts.append(sx_call("nav-link",
href=f"/?service={svc}", label=svc,
is_selected="true" if active_service == svc else None,
select_colours="aria-selected:bg-sky-200 aria-selected:text-sky-900",
))
return "(<> " + " ".join(parts) + ")"
def _header_stack_sx(ctx: dict, active_service: str | None = None) -> str:
"""Full header stack as sx."""
hdr = root_header_sx(ctx)
inner = _test_header_sx(ctx, active_service)
child = header_child_sx(inner)
return "(<> " + hdr + " " + child + ")"
def _test_rows_sx(tests: list[dict]) -> str:
"""Render all test result rows as sx."""
parts = []
for t in tests:
parts.append(sx_call("test-row",
nodeid=t["nodeid"],
outcome=t["outcome"],
duration=str(t["duration"]),
longrepr=t.get("longrepr", ""),
))
return "(<> " + " ".join(parts) + ")"
def _grouped_rows_sx(tests: list[dict]) -> str:
"""Test rows grouped by service as sx."""
from runner import group_tests_by_service
sections = group_tests_by_service(tests)
parts = []
for sec in sections:
parts.append(sx_call("test-service-header",
service=sec["service"],
total=str(sec["total"]),
passed=str(sec["passed"]),
failed=str(sec["failed"]),
))
parts.append(_test_rows_sx(sec["tests"]))
return "(<> " + " ".join(parts) + ")"
def _results_partial_sx(result: dict | None, running: bool, csrf: str,
active_filter: str | None = None,
active_service: str | None = None) -> str:
"""Results section as sx."""
if running and not result:
summary = sx_call("test-summary",
status="running", passed="0", failed="0", errors="0",
skipped="0", total="0", duration="...",
last_run="in progress", running=True, csrf=csrf,
active_filter=active_filter,
)
return "(<> " + summary + " " + sx_call("test-running-indicator") + ")"
if not result:
summary = sx_call("test-summary",
status=None, passed="0", failed="0", errors="0",
skipped="0", total="0", duration="0",
last_run="never", running=running, csrf=csrf,
active_filter=active_filter,
)
return "(<> " + summary + " " + sx_call("test-no-results") + ")"
status = "running" if running else result["status"]
summary = sx_call("test-summary",
status=status,
passed=str(result["passed"]),
failed=str(result["failed"]),
errors=str(result["errors"]),
skipped=str(result.get("skipped", 0)),
total=str(result["total"]),
duration=str(result["duration"]),
last_run=_format_time(result["finished_at"]) if not running else "in progress",
running=running,
csrf=csrf,
active_filter=active_filter,
)
if running:
return "(<> " + summary + " " + sx_call("test-running-indicator") + ")"
tests = result.get("tests", [])
tests = _filter_tests(tests, active_filter, active_service)
if not tests:
return "(<> " + summary + " " + sx_call("test-no-results") + ")"
has_failures = result["failed"] > 0 or result["errors"] > 0
rows = _grouped_rows_sx(tests)
table = sx_call("test-results-table",
rows=SxExpr(rows),
has_failures=str(has_failures).lower(),
)
return "(<> " + summary + " " + table + ")"
def _wrap_results_div_sx(inner: str, running: bool) -> str:
"""Wrap results in a div with HTMX polling (sx)."""
attrs = ':id "test-results" :class "space-y-6 p-4"'
if running:
attrs += ' :sx-get "/results" :sx-trigger "every 2s" :sx-swap "outerHTML"'
return f'(div {attrs} {inner})'
async def render_dashboard_page_sx(ctx: dict, result: dict | None,
running: bool, csrf: str,
active_filter: str | None = None,
active_service: str | None = None) -> str:
"""Full page: test dashboard (sx wire format)."""
hdr = _header_stack_sx(ctx, active_service)
inner = _results_partial_sx(result, running, csrf, active_filter, active_service)
content = _wrap_results_div_sx(inner, running)
return full_page_sx(ctx, header_rows=hdr, content=content)
async def render_results_partial_sx(result: dict | None, running: bool,
csrf: str,
active_filter: str | None = None,
active_service: str | None = None) -> str:
"""HTMX partial: results section (sx wire format)."""
inner = _results_partial_sx(result, running, csrf, active_filter, active_service)
return _wrap_results_div_sx(inner, running)
async def render_test_detail_page_sx(ctx: dict, test: dict) -> str:
"""Full page: test detail (sx wire format)."""
root_hdr = root_header_sx(ctx)
test_row = _test_header_sx(ctx)
detail_row = sx_call("menu-row-sx",
id="test-detail-row", level=2, colour="sky",
link_href=f"/test/{test['nodeid']}",
link_label=test["nodeid"].rsplit("::", 1)[-1],
)
inner = "(<> " + test_row + " " + header_child_sx(detail_row, id="test-header-child") + ")"
hdr = "(<> " + root_hdr + " " + header_child_sx(inner) + ")"
content = sx_call("test-detail",
nodeid=test["nodeid"],
outcome=test["outcome"],
duration=str(test["duration"]),
longrepr=test.get("longrepr", ""),
)
return full_page_sx(ctx, header_rows=hdr, content=content)