|
3 | 3 | from __future__ import annotations |
4 | 4 |
|
5 | 5 | import json |
| 6 | +import re |
6 | 7 | from pathlib import Path |
7 | 8 |
|
8 | 9 | import pytest |
|
15 | 16 | bundle_scene_asset_violations, |
16 | 17 | compiled_scene_sync_violations, |
17 | 18 | dwell_overshoot_violations, |
| 19 | + generated_labels_from_source, |
18 | 20 | helper_api_violations, |
19 | 21 | hold_idle_violations, |
20 | 22 | motion_plan_from_source, |
| 23 | + play_run_times_from_source, |
21 | 24 | scene_asset_violations_for_segment, |
22 | 25 | ) |
23 | 26 | from docgen.scene_spec import ( |
@@ -285,6 +288,67 @@ def test_compiled_sync_fails_when_class_missing() -> None: |
285 | 288 | assert any("not in scenes.py" in i for i in issues) |
286 | 289 |
|
287 | 290 |
|
| 291 | +def test_compiled_sync_fails_when_generated_label_changed() -> None: |
| 292 | + """Leftover #11: motion_plan ignores label text; compile_sync must not.""" |
| 293 | + spec = _spec([_box("Alpha", wait_word=0), _box("Beta", wait_word=1)]) |
| 294 | + words = _wide_words() |
| 295 | + class_src = compile_scene_class(spec, words=words) |
| 296 | + tampered = class_src.replace("'Alpha'", "'Hacked'", 1) |
| 297 | + assert "'Hacked'" in tampered |
| 298 | + assert motion_plan_from_source(class_src) == motion_plan_from_source(tampered) |
| 299 | + assert generated_labels_from_source(class_src) != generated_labels_from_source(tampered) |
| 300 | + issues = compiled_scene_sync_violations(spec, words, BOOTSTRAP_HEADER + "\n" + tampered) |
| 301 | + assert issues |
| 302 | + assert any("compile_sync" in i and "stale" in i for i in issues) |
| 303 | + |
| 304 | + |
| 305 | +def test_compiled_sync_fails_when_run_time_changed() -> None: |
| 306 | + """Leftover #11: motion_plan ignores timed_play run_time; compile_sync must not.""" |
| 307 | + spec = _spec([_box("Alpha", wait_word=0), _box("Beta", wait_word=1)]) |
| 308 | + words = _wide_words() |
| 309 | + class_src = compile_scene_class(spec, words=words) |
| 310 | + tampered, n = re.subn(r"run_time=\d+(?:\.\d+)?", "run_time=9.99", class_src, count=1) |
| 311 | + assert n == 1 |
| 312 | + assert motion_plan_from_source(class_src) == motion_plan_from_source(tampered) |
| 313 | + assert play_run_times_from_source(class_src) != play_run_times_from_source(tampered) |
| 314 | + issues = compiled_scene_sync_violations(spec, words, BOOTSTRAP_HEADER + "\n" + tampered) |
| 315 | + assert issues |
| 316 | + assert any("compile_sync" in i and "stale" in i for i in issues) |
| 317 | + |
| 318 | + |
| 319 | +def test_scene_assets_fails_when_generated_label_changed(tmp_path: Path) -> None: |
| 320 | + """Change a generated-region label in scenes.py; scene_assets must fail.""" |
| 321 | + cfg = _bundle(tmp_path) |
| 322 | + specs = cfg.animations_dir / "specs" |
| 323 | + specs.mkdir(parents=True) |
| 324 | + spec = _spec([_box("Alpha", wait_word=0), _box("Beta", wait_word=1)]) |
| 325 | + (specs / "01-x.scene.yaml").write_text(yaml.dump(spec), encoding="utf-8") |
| 326 | + words = _wide_words() |
| 327 | + (cfg.animations_dir / "timing.json").write_text( |
| 328 | + json.dumps( |
| 329 | + { |
| 330 | + "01-x": { |
| 331 | + "text": "Alpha Beta tail", |
| 332 | + "words": words, |
| 333 | + "segments": [{"start": 1.2, "end": 16.4, "text": "Alpha Beta tail"}], |
| 334 | + } |
| 335 | + } |
| 336 | + ) |
| 337 | + + "\n", |
| 338 | + encoding="utf-8", |
| 339 | + ) |
| 340 | + class_src = compile_scene_class({**spec, "timing_key": "01-x"}, words=words) |
| 341 | + tampered = class_src.replace("'Alpha'", "'Hacked'", 1) |
| 342 | + (cfg.animations_dir / "scenes.py").write_text( |
| 343 | + BOOTSTRAP_HEADER + "\n" + tampered, encoding="utf-8" |
| 344 | + ) |
| 345 | + issues = scene_asset_violations_for_segment(cfg, "01") |
| 346 | + assert any("compile_sync" in i and "stale" in i for i in issues) |
| 347 | + check = Validator(cfg)._check_scene_assets("01") |
| 348 | + assert not check.passed |
| 349 | + assert any("compile_sync" in d for d in check.details) |
| 350 | + |
| 351 | + |
288 | 352 | def test_layout_budget_is_reported_as_overlap(tmp_path: Path) -> None: |
289 | 353 | cfg = _bundle(tmp_path) |
290 | 354 | specs = cfg.animations_dir / "specs" |
|
0 commit comments