Files
6krrt/tests/test_admin_js_units.py
adlee-was-taken 5024a27805 fix(admin-ui): fit controls at 800px and seed Advanced collapsed per category
Overflow: the persisted-cell note badge is white-space:nowrap, so the
classifier.degraded_warn_min note set the column's min-content width and the
Knobs table measured 821px in a 797px frame (Layer column cut off). The
badge now wraps inside its own pill: 733px at 800, the pre-taxonomy figure.
The previous rule targeted .setting-control .ms-2, but d-inline-flex is
!important and the cell is a plain td, so it never matched.

Seeding: Advanced accordions were seeded collapsed once, on the first render.
The runtime payload lands before the config payload and watchdog.detector.* are
persisted-only, so Watchdog had no advanced rows on that render, was marked
seeded anyway, and loaded EXPANDED. seedAdvancedCollapsed now seeds a category
the first time it renders an advanced row. It is a pure function between
SEED_ADV_COLLAPSED markers, covered by three node tests (a config render after
a runtime-only render stays collapsed; a toggled-open section survives
re-render; a plain-only category is not marked seeded).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KkCGRantZsSwmcFpet6FTa
2026-10-05 01:43:40 -04:00

690 lines
29 KiB
Python

"""Shared harness for running admin JS units through node.
Why execute the file instead of re-deriving its behaviour in Python: a
Python re-implementation of the JS is a second copy waiting to drift from
the code it claims to test -- the exact failure mode navbar.js was
extracted to end. These tests slice the marker-delimited blocks out of
``admin/frontend/navbar.js`` and feed the real source to ``node -e``, so
a regression is caught where it lives.
Every test that shells out to node is skipped when node is absent, so
the suite stays green on hosts without it; where node IS installed the
contract is exercised for real. Items later in the cockpit plan append
further marker-based node tests to the helpers below.
"""
from __future__ import annotations
import re
import shutil
import subprocess
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parent.parent
NAVBAR_JS = ROOT / "admin" / "frontend" / "navbar.js"
NAV_LINKS_BEGIN = "/* NAV_LINKS:BEGIN */"
NAV_LINKS_END = "/* NAV_LINKS:END */"
ACTIVE_NAV_HREF_BEGIN = "/* ACTIVE_NAV_HREF:BEGIN */"
ACTIVE_NAV_HREF_END = "/* ACTIVE_NAV_HREF:END */"
skip_without_node = pytest.mark.skipif(
shutil.which("node") is None, reason="node not installed"
)
def _extract_between(path: Path, begin_marker: str, end_marker: str) -> str:
"""The text of ``path`` between two marker comments, markers excluded.
Both markers must be present and in order; anything else is a broken
extraction (the markers are load-bearing for this harness), not a
test failure to report softly.
"""
text = path.read_text(encoding="utf-8")
begin = text.find(begin_marker)
end = text.find(end_marker)
assert begin != -1, f"{path.name}: marker {begin_marker!r} not found"
assert end > begin, f"{path.name}: marker {end_marker!r} absent or before begin"
return text[begin + len(begin_marker) : end]
def _run_node(js_source: str) -> subprocess.CompletedProcess[str]:
"""Run ``js_source`` through ``node -e``; fail loudly on a nonzero exit."""
result = subprocess.run(
["node", "-e", js_source],
capture_output=True,
text=True,
check=False,
timeout=30,
)
assert result.returncode == 0, (
f"node exited {result.returncode}\nstdout: {result.stdout}\nstderr: {result.stderr}"
)
return result
def _navbar_units_source() -> str:
"""NAV_LINKS + activeNavHref from navbar.js, in file order, runnable as-is."""
return (
_extract_between(NAVBAR_JS, NAV_LINKS_BEGIN, NAV_LINKS_END)
+ "\n"
+ _extract_between(NAVBAR_JS, ACTIVE_NAV_HREF_BEGIN, ACTIVE_NAV_HREF_END)
)
def _assert_against_navbar_units(assertion_js: str) -> subprocess.CompletedProcess[str]:
"""Assert ``assertion_js`` against the real NAV_LINKS/activeNavHref source."""
return _run_node(
"const assert = require('assert');\n" + _navbar_units_source() + "\n" + assertion_js
)
@skip_without_node
def test_active_nav_href_lights_up_exactly_the_page_it_is_on():
"""Every served page but the landing page is in NAV_LINKS, so each one's
own pathname resolves to its own href."""
assertions = [
"assert.strictEqual(activeNavHref('/admin/models', NAV_LINKS), '/admin/models');",
"assert.strictEqual(activeNavHref('/admin/profiles', NAV_LINKS), '/admin/profiles');",
"assert.strictEqual(activeNavHref('/admin/proficiency', NAV_LINKS), '/admin/proficiency');",
"assert.strictEqual(activeNavHref('/admin/decisions', NAV_LINKS), '/admin/decisions');",
"assert.strictEqual(activeNavHref('/admin/quota', NAV_LINKS), '/admin/quota');",
"assert.strictEqual(activeNavHref('/admin/controls', NAV_LINKS), '/admin/controls');",
"assert.strictEqual(activeNavHref('/admin/providers', NAV_LINKS), '/admin/providers');",
]
_assert_against_navbar_units("\n".join(assertions))
@skip_without_node
@pytest.mark.parametrize(
"pathname",
[
"/admin", # prefix of everything, equal to nothing
"/admin/", # the landing page has no nav entry -- the logo is its door
"/admin/model", # near-miss singular
"/models", # missing the /admin prefix entirely
"/definitely-not-a-page",
"",
],
)
def test_active_nav_href_is_an_exact_match_or_null(pathname):
"""No prefix, no substring: a pathname that is not literally one of the
rendered hrefs highlights nothing, even though every href starts with
/admin/."""
_assert_against_navbar_units(
f"assert.strictEqual(activeNavHref({pathname!r}, NAV_LINKS), null);"
)
@skip_without_node
def test_active_nav_href_takes_its_links_from_the_argument():
"""activeNavHref is pure: it reads the links it is handed, nothing global."""
_assert_against_navbar_units(
"assert.strictEqual(activeNavHref('/x', [{ href: '/x', label: 'X' }]), '/x');\n"
"assert.strictEqual(activeNavHref('/x', []), null);\n"
"assert.strictEqual(activeNavHref('/y', [{ href: '/x', label: 'X' }]), null);"
)
DECISIONS_HTML = ROOT / "admin" / "frontend" / "decisions.html"
RESOLVE_URL_FILTER_VALUES_BEGIN = "/* RESOLVE_URL_FILTER_VALUES:BEGIN */"
RESOLVE_URL_FILTER_VALUES_END = "/* RESOLVE_URL_FILTER_VALUES:END */"
def _assert_against_decisions_units(assertion_js: str) -> subprocess.CompletedProcess[str]:
"""Assert ``assertion_js`` against the real resolveUrlFilterValues source."""
return _run_node(
"const assert = require('assert');\n"
+ _extract_between(
DECISIONS_HTML, RESOLVE_URL_FILTER_VALUES_BEGIN, RESOLVE_URL_FILTER_VALUES_END
)
+ "\n"
+ assertion_js
)
@skip_without_node
def test_resolve_url_filter_values_known_size_passes_through():
"""A size within the page-size options is applied as-is."""
_assert_against_decisions_units(
"const r = resolveUrlFilterValues(\n"
" { size: '250' },\n"
" { kind: [], category: [], profile: [], tier: [], size: ['50', '100', '250'] },\n"
");\n"
"assert.strictEqual(r.values.size, '250');\n"
"assert.strictEqual(r.sizeFallback, false);\n"
"assert.deepStrictEqual(r.addOptions, []);\n"
"assert.strictEqual(r.values.kind, null);"
)
@skip_without_node
def test_resolve_url_filter_values_unknown_size_falls_back_and_is_not_added():
"""size=999 falls back to the default 100 and is never added as an option."""
_assert_against_decisions_units(
"const r = resolveUrlFilterValues(\n"
" { size: '999' },\n"
" { kind: [], category: [], profile: [], tier: [], size: ['50', '100', '250'] },\n"
");\n"
"assert.strictEqual(r.values.size, '100');\n"
"assert.strictEqual(r.sizeFallback, true);\n"
"assert.deepStrictEqual(r.addOptions, []);"
)
@skip_without_node
def test_resolve_url_filter_values_unknown_category_is_added_as_an_option():
"""An unknown category is flagged add-as-option; tier follows the same rule."""
_assert_against_decisions_units(
"const r = resolveUrlFilterValues(\n"
" { category: 'diff_checking' },\n"
" { category: ['routing', 'triage'], tier: ['1', '2', '3'] },\n"
");\n"
"assert.strictEqual(r.values.category, 'diff_checking');\n"
"assert.deepStrictEqual(\n"
" r.addOptions, [{ id: 'f-category', value: 'diff_checking' }]\n"
");\n"
"const t = resolveUrlFilterValues(\n"
" { tier: '4' },\n"
" { category: ['routing'], tier: ['1', '2', '3'] },\n"
");\n"
"assert.deepStrictEqual(t.addOptions, [{ id: 'f-tier', value: '4' }]);\n"
"assert.strictEqual(t.values.tier, '4');\n"
"assert.strictEqual(t.values.category, null);"
)
@skip_without_node
def test_resolve_url_filter_values_empty_or_absent_params_leave_controls_untouched():
"""Empty and absent params produce null values everywhere - no control moves."""
_assert_against_decisions_units(
"const untouched = { kind: null, category: null, profile: null, tier: null, search: null, size: null };\n"
"const empty = resolveUrlFilterValues(\n"
" { kind: '', category: '', profile: '', tier: '', q: '', size: '' },\n"
" { kind: ['a'], size: ['50', '100', '250'] },\n"
");\n"
"assert.deepStrictEqual(empty.values, untouched);\n"
"assert.deepStrictEqual(empty.addOptions, []);\n"
"assert.strictEqual(empty.sizeFallback, false);\n"
"const absent = resolveUrlFilterValues({}, {});\n"
"assert.deepStrictEqual(absent.values, untouched);\n"
"assert.deepStrictEqual(absent.addOptions, []);\n"
"assert.strictEqual(absent.sizeFallback, false);"
)
INDEX_HTML = ROOT / "admin" / "frontend" / "index.html"
WARN_SEVERITY_BEGIN = "/* WARN_SEVERITY:BEGIN */"
WARN_SEVERITY_END = "/* WARN_SEVERITY:END */"
DISMISS_UNITS_BEGIN = "/* DISMISS_UNITS:BEGIN */"
DISMISS_UNITS_END = "/* DISMISS_UNITS:END */"
def _assert_against_dismiss_units(assertion_js: str) -> subprocess.CompletedProcess[str]:
"""Assert ``assertion_js`` against the real dismissal key units.
The units (normalizeDigits, dismissKey, migrateDismissedKeys,
pruneDismissed, openWarnings) live in index.html between the
DISMISS_UNITS markers and call two neighbours: warningSeverity (its own
marker pair) and escapeHtml (a one-liner in the UTILITIES section,
pulled by regex). Feeding all three together reproduces the page's
runtime environment without a DOM.
"""
text = INDEX_HTML.read_text(encoding="utf-8")
escape = re.search(r"function escapeHtml\(s\) \{.*?\n\}", text, re.DOTALL)
assert escape, "index.html: escapeHtml source not found"
return _run_node(
"const assert = require('assert');\n"
+ escape.group(0)
+ "\n"
+ _extract_between(INDEX_HTML, WARN_SEVERITY_BEGIN, WARN_SEVERITY_END)
+ "\n"
+ _extract_between(INDEX_HTML, DISMISS_UNITS_BEGIN, DISMISS_UNITS_END)
+ "\n"
+ assertion_js
)
@skip_without_node
def test_dismiss_key_includes_severity():
"""The severity is the key's first component, ahead of the normalized,
escaped text."""
_assert_against_dismiss_units(
"assert.strictEqual(\n"
" dismissKey('plan pace 1.2x sustainable', 'high'),\n"
" 'high:plan pace Nx sustainable'\n"
");\n"
"assert.strictEqual(dismissKey('classifier degraded', 'medium'), 'medium:classifier degraded');"
)
@skip_without_node
def test_dismiss_key_differs_by_severity():
"""Severity-keying is what keeps digit normalization from fusing
different warnings: '0 eligible' is high and '3 eligible' is medium even
though both normalize to 'N eligible'."""
_assert_against_dismiss_units(
"assert.strictEqual(dismissKey('0 eligible models', 'high'), 'high:N eligible models');\n"
"assert.notStrictEqual(\n"
" dismissKey('0 eligible models', 'high'),\n"
" dismissKey('0 eligible models', 'medium')\n"
");"
)
@skip_without_node
def test_migrate_digit_normalizes_to_one_key():
"""A count that moved between polls must not move the stored key: the
3-count and 50-count forms of one warning migrate to the same
digit-normalized key, and an entry already in the new format passes
through untouched so re-migration cannot mangle the store."""
_assert_against_dismiss_units(
"const three = migrateDismissedKeys(['3/52 routable models have no proficiency data']);\n"
"const fifty = migrateDismissedKeys(['50/52 routable models have no proficiency data']);\n"
"assert.deepStrictEqual([...three], [...fifty]);\n"
"assert.strictEqual([...three].length, 1);\n"
"assert.strictEqual(\n"
" [...three][0],\n"
" 'medium:N/N routable models have no proficiency data'\n"
");\n"
"assert.deepStrictEqual(\n"
" [...migrateDismissedKeys(['medium:N/N routable models have no proficiency data'])],\n"
" ['medium:N/N routable models have no proficiency data']\n"
");"
)
@skip_without_node
def test_same_warning_different_count_both_suppressed():
"""Dismissing one count of a warning suppresses the others: once the
3-count version is dismissed, the 50-count re-scan is already hidden,
and the key stays live so pruning does not drop it."""
_assert_against_dismiss_units(
"const dismissed = migrateDismissedKeys(['3/52 routable models have no proficiency data']);\n"
"assert.strictEqual(openWarnings(['3/52 routable models have no proficiency data'], dismissed).length, 0);\n"
"assert.strictEqual(openWarnings(['50/52 routable models have no proficiency data'], dismissed).length, 0);\n"
"assert.strictEqual(\n"
" pruneDismissed(['50/52 routable models have no proficiency data'], dismissed),\n"
" false\n"
");"
)
@skip_without_node
def test_stale_dismissal_pruned_and_new_warnings_open():
"""A dismissed warning whose text is gone has been resolved: the stale
key is pruned on the next render, nothing stays dismissed, and all 50
fresh warnings are open."""
_assert_against_dismiss_units(
"const dismissed = migrateDismissedKeys(['3 eligible models']);\n"
"const fresh = Array.from({ length: 50 }, (_, i) =>\n"
" 'issue ' + String.fromCharCode(65 + Math.floor(i / 26))\n"
" + String.fromCharCode(97 + (i % 26)));\n"
"assert.strictEqual(pruneDismissed(fresh, dismissed), true);\n"
"assert.strictEqual(dismissed.size, 0);\n"
"assert.strictEqual(openWarnings(fresh, dismissed).length, 50);"
)
CONTROLS_HTML = ROOT / "admin" / "frontend" / "controls.html"
MERGE_KNOB_ROWS_BEGIN = "/* MERGE_KNOB_ROWS:BEGIN */"
MERGE_KNOB_ROWS_END = "/* MERGE_KNOB_ROWS:END */"
def _assert_against_merge_units(assertion_js: str) -> subprocess.CompletedProcess[str]:
"""Assert ``assertion_js`` against the real mergeKnobRows source.
The units (normalizeKnobValue, knobValuesDrift, mergeKnobRows) live in
controls.html between the MERGE_KNOB_ROWS markers and are self-contained:
they take the two API payloads as arguments and touch no DOM, so the
extraction runs as-is under node.
"""
return _run_node(
"const assert = require('assert');\n"
+ _extract_between(CONTROLS_HTML, MERGE_KNOB_ROWS_BEGIN, MERGE_KNOB_ROWS_END)
+ "\n"
+ assertion_js
)
RUNTIME_BOTH_AND_ONLY = (
"{\n"
" log_route_decisions: { persisted: true, runtime: false, config_key: 'logging.log_route_decisions' },\n"
" circuit_breaker_enabled: { persisted: true, runtime: true, config_key: 'circuit_breaker.enabled' },\n"
"}"
)
PERSISTED_BOTH_AND_ONLY = (
"{\n"
" 'logging.level': { value: 'info', source: 'base' },\n"
" 'circuit_breaker.enabled': { value: true, source: 'base' },\n"
" 'objective.quality_tolerance': { value: 0.7, source: 'base' },\n"
"}"
)
@skip_without_node
def test_merge_knob_rows_row_set_is_the_union_of_both_key_sets():
"""Runtime rows pair by the config_key the API carries; persisted rows by
their own key. The row set is the union: a one-sided knob still gets a
row, with the other column empty."""
_assert_against_merge_units(
f"const rows = mergeKnobRows({RUNTIME_BOTH_AND_ONLY}, {PERSISTED_BOTH_AND_ONLY});\n"
"assert.deepStrictEqual(Object.keys(rows).sort(), [\n"
" 'circuit_breaker.enabled',\n"
" 'logging.level',\n"
" 'logging.log_route_decisions',\n"
" 'objective.quality_tolerance',\n"
"]);\n"
"assert.strictEqual(rows['logging.log_route_decisions'].persistedItem, null);\n"
"assert.strictEqual(rows['logging.log_route_decisions'].runtimeItem.knob, 'log_route_decisions');\n"
"assert.strictEqual(rows['objective.quality_tolerance'].runtimeItem, null);\n"
"assert.strictEqual(rows['objective.quality_tolerance'].persistedItem.value, 0.7);"
)
@skip_without_node
def test_merge_knob_rows_one_sided_inputs_yield_one_sided_rows():
"""Only one payload present (a failed fetch, or the runtime fetch landing
first): rows render from what is available, never from nothing."""
_assert_against_merge_units(
"const runtimeOnly = mergeKnobRows(\n"
" { k: { persisted: true, runtime: false, config_key: 'a.enabled' } },\n"
" null\n"
");\n"
"assert.deepStrictEqual(Object.keys(runtimeOnly), ['a.enabled']);\n"
"assert.strictEqual(runtimeOnly['a.enabled'].persistedItem, null);\n"
"const persistedOnly = mergeKnobRows(null, { 'a.enabled': { value: true, source: 'base' } });\n"
"assert.deepStrictEqual(Object.keys(persistedOnly), ['a.enabled']);\n"
"assert.strictEqual(persistedOnly['a.enabled'].runtimeItem, null);\n"
"assert.deepStrictEqual(mergeKnobRows(null, null), {});"
)
@skip_without_node
def test_merge_knob_rows_drift_rule_follows_the_runtime_payload_only():
"""A row WITH a runtime item drifts when its runtime value differs from
the payload's persisted value -- both-sides rows and runtime-only rows
alike, because a restart reverts them both. A persisted-only row never
drifts: no live value exists."""
_assert_against_merge_units(
f"const rows = mergeKnobRows({RUNTIME_BOTH_AND_ONLY}, {PERSISTED_BOTH_AND_ONLY});\n"
"assert.strictEqual(rows['logging.log_route_decisions'].drift, true);\n"
"assert.strictEqual(rows['circuit_breaker.enabled'].drift, false);\n"
"assert.strictEqual(rows['objective.quality_tolerance'].drift, false);"
)
@skip_without_node
def test_merge_knob_rows_ignores_the_persisted_columns_input_value():
"""Drift is computed from the runtime payload alone. A persisted column
holding an unsaved dirty edit ('debug') must not read as drift when the
router is running exactly what a restart would load ('info')."""
_assert_against_merge_units(
"const rows = mergeKnobRows(\n"
" { k: { persisted: 'info', runtime: 'info', config_key: 'logging.level' } },\n"
" { 'logging.level': { value: 'info', source: 'base' } }\n"
");\n"
"assert.strictEqual(rows['logging.level'].drift, false);"
)
@skip_without_node
@pytest.mark.parametrize(
"persisted,runtime,expect_drift",
[
("0.10", 0.1, False), # numbers compare numerically
(0.1, "0.10", False),
(2, 2.0, False),
(True, False, True),
(False, True, True),
("info", "debug", True),
(None, 5, False), # null/absent on either side is not drift
(5, None, False),
(None, None, False),
],
)
def test_merge_knob_rows_value_normalization(persisted, runtime, expect_drift):
"""Booleans as booleans, numbers numerically, null/absent never drifting."""
def js(v: object) -> str:
# Python repr is not JavaScript: True/False/None have other names.
return {True: "true", False: "false", None: "null"}.get(v, repr(v))
_assert_against_merge_units(
"const rows = mergeKnobRows(\n"
f" {{ k: {{ persisted: {js(persisted)}, runtime: {js(runtime)}, config_key: 'a.knob' }} }},\n"
" {}\n"
");\n"
f"assert.strictEqual(rows['a.knob'].drift, {str(expect_drift).lower()});"
)
SEED_ADV_COLLAPSED_BEGIN = "/* SEED_ADV_COLLAPSED:BEGIN */"
SEED_ADV_COLLAPSED_END = "/* SEED_ADV_COLLAPSED:END */"
def _assert_against_seed_units(assertion_js: str) -> subprocess.CompletedProcess[str]:
"""Assert ``assertion_js`` against the real seedAdvancedCollapsed source.
The function lives in controls.html between the SEED_ADV_COLLAPSED markers
and is self-contained: it takes the grouped rows and the two Sets as
arguments and touches no DOM. ``group`` below mirrors the grouping step in
renderKnobsTable so each test can drive it payload by payload.
"""
return _run_node(
"const assert = require('assert');\n"
+ _extract_between(CONTROLS_HTML, SEED_ADV_COLLAPSED_BEGIN, SEED_ADV_COLLAPSED_END)
+ "\n"
"const IDS = ['caching_context', 'watchdog'];\n"
"function group(rows) {\n"
" const byCategory = {};\n"
" for (const [key, row] of Object.entries(rows)) {\n"
" const item = row.runtimeItem || row.persistedItem;\n"
" (byCategory[item.category] = byCategory[item.category] || []).push(key);\n"
" }\n"
" return byCategory;\n"
"}\n"
"function render(rows, seeded, collapsed) {\n"
" seedAdvancedCollapsed(group(rows), rows, IDS, seeded, collapsed);\n"
"}\n"
# First render: runtime payload only. Watchdog has a plain runtime knob
# and NO advanced row (its advanced knobs are persisted-only);
# Caching has an advanced runtime knob.
"const runtimeOnly = {\n"
" 'session_cache.staleness_seconds': { runtimeItem: { category: 'caching_context', advanced: true }, persistedItem: null },\n"
" 'watchdog.enabled': { runtimeItem: { category: 'watchdog', advanced: false }, persistedItem: null },\n"
"};\n"
# Second render: the config payload lands and adds the persisted-only
# advanced watchdog knobs.
"const withConfig = {\n"
" ...runtimeOnly,\n"
" 'watchdog.detector.dup_min': { runtimeItem: null, persistedItem: { category: 'watchdog', advanced: true } },\n"
"};\n"
+ assertion_js
)
@skip_without_node
def test_seed_advanced_collapsed_seeds_each_category_when_its_advanced_rows_first_appear():
"""A config-only render after a runtime-only render must leave the Watchdog
Advanced section collapsed. Seeding once on the first render marked
Watchdog seeded while it had no advanced rows, so it loaded EXPANDED."""
_assert_against_seed_units(
"const seeded = new Set(), collapsed = new Set();\n"
"render(runtimeOnly, seeded, collapsed);\n"
"assert.deepStrictEqual([...collapsed], ['caching_context.adv']);\n"
"assert.ok(!seeded.has('watchdog'), 'no advanced rows yet: watchdog must stay unseeded');\n"
"render(withConfig, seeded, collapsed);\n"
"assert.deepStrictEqual([...collapsed].sort(), ['caching_context.adv', 'watchdog.adv']);"
)
@skip_without_node
def test_seed_advanced_collapsed_leaves_a_user_toggled_section_alone():
"""Once a category is seeded, a toggled-open section stays open across
every later re-render (the page re-renders on a 30s poll)."""
_assert_against_seed_units(
"const seeded = new Set(), collapsed = new Set();\n"
"render(runtimeOnly, seeded, collapsed);\n"
"render(withConfig, seeded, collapsed);\n"
"collapsed.delete('watchdog.adv');\n"
"collapsed.delete('caching_context.adv');\n"
"render(withConfig, seeded, collapsed);\n"
"render(withConfig, seeded, collapsed);\n"
"assert.strictEqual(collapsed.size, 0);"
)
@skip_without_node
def test_seed_advanced_collapsed_ignores_a_category_with_no_advanced_rows():
"""A category with only plain knobs gets no Advanced entry and is never
marked seeded, so an advanced row arriving later is still seeded."""
_assert_against_seed_units(
"const seeded = new Set(), collapsed = new Set();\n"
"const plain = { 'watchdog.enabled': runtimeOnly['watchdog.enabled'] };\n"
"render(plain, seeded, collapsed);\n"
"assert.strictEqual(collapsed.size, 0);\n"
"assert.strictEqual(seeded.size, 0);"
)
INDEX_HTML = ROOT / "admin" / "frontend" / "index.html"
LIFT_A2_HELPERS_BEGIN = "/* LIFT_A2_HELPERS:BEGIN */"
LIFT_A2_HELPERS_END = "/* LIFT_A2_HELPERS:END */"
def _assert_against_lift_a2_helpers(assertion_js: str) -> subprocess.CompletedProcess[str]:
"""Assert ``assertion_js`` against the real lift-a2 formatting helpers source."""
return _run_node(
"const assert = require('assert');\n"
+ _extract_between(INDEX_HTML, LIFT_A2_HELPERS_BEGIN, LIFT_A2_HELPERS_END)
+ "\n"
+ assertion_js
)
@skip_without_node
def test_format_money():
_assert_against_lift_a2_helpers(
"assert.strictEqual(formatMoney(1.5), '$1.50');\n"
"assert.strictEqual(formatMoney(0), '$0.00');\n"
"assert.strictEqual(formatMoney(null), 'n/a');\n"
"assert.strictEqual(formatMoney(undefined), 'n/a');"
)
@skip_without_node
def test_format_relative_time():
_assert_against_lift_a2_helpers(
"// just now (within 10s)\n"
"assert.strictEqual(formatRelativeTime(new Date().toISOString()), 'just now');\n"
"// a few minutes ago\n"
"const fiveMinAgo = new Date(Date.now() - 5 * 60 * 1000).toISOString();\n"
"assert.strictEqual(formatRelativeTime(fiveMinAgo), '5m ago');\n"
"// null/empty\n"
"assert.strictEqual(formatRelativeTime(null), '');\n"
"assert.strictEqual(formatRelativeTime(''), '');"
)
@skip_without_node
def test_truncate():
_assert_against_lift_a2_helpers(
"assert.strictEqual(truncate('hello world', 40), 'hello world');\n"
"assert.strictEqual(truncate('hello world', 5), 'hello...');\n"
"assert.strictEqual(truncate(null, 5), '');\n"
"assert.strictEqual(truncate(undefined, 5), '');"
)
MODELS_HTML = ROOT / "admin" / "frontend" / "models.html"
BLOCK_MODEL_BEGIN = "/* BLOCK_CONFIRM:BEGIN */"
BLOCK_MODEL_END = "/* BLOCK_CONFIRM:END */"
def _assert_against_block_model(assertion_js: str) -> subprocess.CompletedProcess[str]:
return _run_node(
"const assert = require('assert');\n"
+ _extract_between(MODELS_HTML, BLOCK_MODEL_BEGIN, BLOCK_MODEL_END)
+ "\n"
+ assertion_js
)
@skip_without_node
def test_block_model_exists():
_assert_against_block_model(
"assert.strictEqual(typeof blockModelConfirm, 'function');"
)
@skip_without_node
def test_block_button_uses_data_attrs_not_onclick_interpolation():
"""The Block button in both pages must carry data-model/data-provider/
data-reason attributes and wire via onclick=\"blockModelConfirm(event)\"
rather than interpolating runtime values into JS — interpolation broke
on titles containing a double quote."""
for path in (INDEX_HTML, MODELS_HTML):
text = path.read_text(encoding="utf-8")
assert 'onclick="blockModelConfirm(event)"' in text, (
f"{path.name}: Block button missing onclick handler"
)
assert "data-model=" in text, f"{path.name}: missing data-model attr"
assert "data-provider=" in text, f"{path.name}: missing data-provider attr"
assert "data-reason=" in text, f"{path.name}: missing data-reason attr"
@skip_without_node
def test_block_data_reason_escapes_double_quotes():
"""A looping-session title containing both a double and a single quote is
safe inside data-reason=\"...\": escapeHtml turns the double quote into
&quot; so it cannot terminate the attribute, and a bare single quote is
harmless inside a double-quoted HTML attribute."""
escape = re.search(r"function escapeHtml\(s\) \{.*?\n\}", INDEX_HTML.read_text(encoding="utf-8"), re.DOTALL)
assert escape, "index.html: escapeHtml source not found"
js = (
"const assert = require('assert');\n"
+ escape.group(0)
+ "\n"
"const title = 'he said \"hello\\'s\"';\n"
"const dataReason = 'looping session ' + title;\n"
"const html = 'data-reason=\"' + escapeHtml(dataReason) + '\"';\n"
"assert.ok(html.includes('&quot;'));\n"
"assert.ok(!html.includes('\"hello'));\n"
"assert.strictEqual(escapeHtml(dataReason), "
" 'looping session he said &quot;hello\\'s&quot;');\n"
)
_run_node(js)
@skip_without_node
def test_block_button_onclick_wires_to_blockModelConfirm():
"""The Block button carries onclick=\"blockModelConfirm(event)\" so the
click actually invokes the handler. Without this the data-* attributes
are inert and clicking Block does nothing."""
for path in (INDEX_HTML, MODELS_HTML):
text = path.read_text(encoding="utf-8")
assert 'onclick="blockModelConfirm(event)"' in text, (
f"{path.name}: Block button missing onclick handler"
)
@skip_without_node
def test_blockModelConfirm_reads_from_event_currentTarget_dataset():
"""blockModelConfirm(event) must read model/provider/reason from
event.currentTarget.dataset — the old hybrid signature that expected
positional args is broken because onclick passes a single Event."""
js = (
"const assert = require('assert');\n"
+ _extract_between(MODELS_HTML, BLOCK_MODEL_BEGIN, BLOCK_MODEL_END)
+ "\n"
"assert.strictEqual(typeof blockModelConfirm, 'function');\n"
"const src = blockModelConfirm.toString();\n"
"assert.ok(src.includes('event.currentTarget'), 'must use event.currentTarget');\n"
"assert.ok(src.includes('dataset.model'), 'must read dataset.model');\n"
"assert.ok(src.includes('dataset.provider'), 'must read dataset.provider');\n"
"assert.ok(src.includes('dataset.reason'), 'must read dataset.reason');\n"
)
_run_node(js)