diff --git a/scripts/css_snapshot.py b/scripts/css_snapshot.py index 360fa11da..12baf4f40 100644 --- a/scripts/css_snapshot.py +++ b/scripts/css_snapshot.py @@ -97,7 +97,8 @@ def playwright_available(node="node", cwd=ROOT): def capture(origin, inventory=None, *, swap_rule=None, variants=None, - node="node", cwd=ROOT, timeout=CAPTURE_TIMEOUT_SECONDS): + measurement_delay_ms=0, node="node", cwd=ROOT, + timeout=CAPTURE_TIMEOUT_SECONDS): """Drive the browser capture and return ``{"snapshot": ..., "missing": ...}``. ``swap_rule`` swaps the first two top-level declarations of one selector @@ -107,6 +108,9 @@ def capture(origin, inventory=None, *, swap_rule=None, variants=None, ``variants`` restricts the run to the named variants, for a faster focused capture. + + ``measurement_delay_ms`` perturbs the capture timing for the determinism + self-test; elapsed wall time must not change an idle-state snapshot. """ inventory = inventory or load_inventory() selected = inventory["variants"] @@ -122,6 +126,7 @@ def capture(origin, inventory=None, *, swap_rule=None, variants=None, "variants": selected, "pages": inventory["pages"], "swapRule": swap_rule, + "measurementDelayMs": measurement_delay_ms, } result = subprocess.run( [node, str(CAPTURE_SCRIPT)], diff --git a/tests/css_snapshot/README.md b/tests/css_snapshot/README.md index 8d58e50df..3d8e86e82 100644 --- a/tests/css_snapshot/README.md +++ b/tests/css_snapshot/README.md @@ -92,6 +92,20 @@ same bytes, so the capture: - injects the theme and density classes into `` *before* first paint rather than toggling them afterwards, so no CSS transition is ever mid-interpolation while `getComputedStyle` runs; +- removes `autofocus` before parsing: focus states are outside this inventory, + and the browser's asynchronous autofocus step otherwise races the capture; +- pauses CSS animations at time zero and finishes CSS transitions before each + measurement, including newly revealed modals and newly mounted bench nodes. + Animation and transition declarations are still captured; the harness does + not inject `animation: none` or `transition: none`; +- pins Chromium's standard font preference to `Times New Roman` via CDP, + without overriding any author declaration; +- canonicalizes only the `BlinkMacSystemFont` family token to `"system-ui"`, + the spelling Chromium uses for that alias on macOS. Other family names and + their order remain significant; +- measures the `custom-system-prompt` element's `max-height` in `lh`, as opted + into by its inventory entry. Its authored `30lh` resolves to different pixel + heights with different fallback fonts; the line count remains significant; - aborts images, fonts and media, which cost time and change nothing in the pinned property set; - hides scrollbars, so a platform's scrollbar width cannot change the width @@ -111,11 +125,11 @@ same bytes, so the capture: - **JS-applied classes.** State the app adds at runtime (collapsed sidebar, open panels, active tabs) is not represented beyond what the served markup and the bench selectors already carry. -- **Cross-platform equality has not been measured.** The baseline was recorded - on macOS. The self-hosted Fira Code face means text metrics should not differ - from CI's Linux Chromium, and the layout-derived properties are excluded, but - until a Linux run confirms it, treat a CI-only drift as a possible harness - artifact and diff the dumps before assuming the CSS moved. +- **Browser upgrades and additional platforms.** The original macOS baseline + and Linux captures were compared property by property through exact hash + recovery; the proven platform differences are now controlled above. A new + capture on macOS has not been performed. New browser serialization changes + still need investigation rather than automatic baseline regeneration. - **The stylesheet is only one of the inputs.** `static/login.html` styles itself from an inline ` +
+
+ """ + page.write_text(source, encoding="utf-8") + inventory = { + "properties": ["font-family", "opacity", "max-height", "outline-offset", + "animation-name", "animation-duration", "animation-play-state", + "transition-duration"], + "variants": [snapshot.load_inventory()["variants"][0]], + "pages": [{"name": "fixture", "url": "/fixture.html", "elements": [ + {"key": key, "selector": f"#{key}", + "lineRelativeProperties": ["max-height"] if key in {"sample", "other"} else []} + for key in ("focus", "sample", "other", "alias", "serialized") + ]}], + } + origin, shutdown = snapshot.serve_repository(tmp_path) + try: + early = snapshot.capture(origin, inventory)["snapshot"] + late = snapshot.capture(origin, inventory, measurement_delay_ms=150)["snapshot"] + assert early == late + values = early["fixture"][inventory["variants"][0]["name"]] + assert values["focus"]["outline-offset"] == "0px" + assert values["sample"]["opacity"] == "0.25" + assert values["sample"]["animation-name"] == "fade" + assert values["sample"]["animation-duration"] == "0.4s" + assert values["sample"]["animation-play-state"] == "running" + assert values["sample"]["transition-duration"] == "0.1s" + assert values["sample"]["max-height"] == "30lh" + assert values["other"]["max-height"] == "31lh" + assert values["alias"]["font-family"] == values["serialized"]["font-family"] + page.write_text(source.replace("opacity: .25", "opacity: .5"), encoding="utf-8") + changed = snapshot.capture(origin, inventory)["snapshot"] + assert changed["fixture"][inventory["variants"][0]["name"]]["sample"]["opacity"] == "0.5" + assert snapshot.summarize(changed)["digest"] != snapshot.summarize(early)["digest"] + finally: + shutdown() + + @_requires_browser def test_reordering_two_conflicting_declarations_moves_the_digest(): """The harness has to fail when the cascade changes, or it proves nothing. diff --git a/tests/test_markdown_rendering_js.py b/tests/test_markdown_rendering_js.py index fc832eb4c..06dea0485 100644 --- a/tests/test_markdown_rendering_js.py +++ b/tests/test_markdown_rendering_js.py @@ -18,6 +18,14 @@ def node_available(): pytest.skip("node binary not on PATH") +def test_blockquoted_html_codefence_does_not_leak_placeholders(node_available): + result = subprocess.run( + ["node", "tests/markdown_codefence_placeholder_regression.mjs"], + cwd=_REPO, capture_output=True, text=True, timeout=15, + ) + assert result.returncode == 0, result.stderr + result.stdout + + def _run_markdown_case(markdown: str, render_expr: str = "mod.mdToHtml(input)", with_katex: bool = False): script = textwrap.dedent( r""" diff --git a/website/configuration-reference.md b/website/configuration-reference.md index 133036acc..519b979f6 100644 --- a/website/configuration-reference.md +++ b/website/configuration-reference.md @@ -226,7 +226,7 @@ Listed for completeness. Setting one of these on a real install is either a no-o | `ODYSSEUS_SFT_TRACE_CAPTURE` | `'1'` | `routes/chat_helpers.py:161` (+1 more) | On by default, but only for owners whose name starts with `sft_`. Set 0, false, no or off to stop writing training traces. | | `ODYSSEUS_SFT_TRACE_DIR` | *unset* | `routes/chat_helpers.py:195` (+2 more) | Directory the SFT trace JSONL files are written to. Defaults to `sft_traces` under the data directory. | | `ODYSSEUS_SKIP_RUN_HINT` | *unset* | `setup.py:284` | Any non-empty value suppresses the `start the server with` hint at the end of setup. `start-macos.sh` sets it because it starts the server itself. | -| `ODYSSEUS_TEST_STATIC_ORIGIN` | *unset* | `scripts/css_snapshot.py:249` (+6 more) | Origin an already-running static server is serving the repository from, so snapshot tooling reuses it instead of starting its own. | +| `ODYSSEUS_TEST_STATIC_ORIGIN` | *unset* | `scripts/css_snapshot.py:254` (+6 more) | Origin an already-running static server is serving the repository from, so snapshot tooling reuses it instead of starting its own. | | `ODYSSEUS_TEST_STATIC_PORT` | *unset* | `tests/conftest.py:137` | Fixed port for the test suite's static server. Unset takes an ephemeral port, which is what keeps parallel runs from colliding. | ### Build and release metadata