Ship Backlot living storyboard release hardening

This commit is contained in:
calesthio
2026-07-02 12:19:06 -07:00
parent 1d60f0da14
commit 280400d479
11 changed files with 674 additions and 67 deletions

View File

@@ -0,0 +1,110 @@
"""Browser regressions from the Backlot UI bug bash."""
from __future__ import annotations
import os
import subprocess
import sys
import time
import urllib.request
import pytest
from scripts import backlot_screenshot_stage
pytest.importorskip("playwright.sync_api")
from playwright.sync_api import sync_playwright # noqa: E402
@pytest.fixture(scope="module")
def staged_backlot_server():
backlot_screenshot_stage.build_stage()
port = 4897
env = dict(os.environ)
env["OPENMONTAGE_PROJECTS_DIR"] = str(backlot_screenshot_stage.STAGE_DIR)
server = subprocess.Popen(
[sys.executable, "-m", "backlot", "serve", "--port", str(port)],
cwd=backlot_screenshot_stage.REPO_ROOT,
env=env,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
deadline = time.time() + 20
while time.time() < deadline:
try:
with urllib.request.urlopen(f"http://127.0.0.1:{port}/api/health", timeout=1):
break
except Exception:
time.sleep(0.2)
else:
server.terminate()
raise RuntimeError("Backlot server did not become healthy")
try:
yield f"http://127.0.0.1:{port}"
finally:
server.terminate()
try:
server.wait(timeout=5)
except subprocess.TimeoutExpired:
server.kill()
def test_project_pages_fit_mobile_and_tablet_widths(staged_backlot_server):
project_paths = [
"/p/signal-in-the-static?static=1",
"/p/the-slow-orchard?static=1",
"/p/the-last-lighthouse?static=1",
"/p/paper-boats?static=1",
]
viewports = [
{"width": 390, "height": 844},
{"width": 768, "height": 1024},
]
with sync_playwright() as pw:
browser = pw.chromium.launch(headless=True)
page = browser.new_page()
try:
for viewport in viewports:
page.set_viewport_size(viewport)
for path in project_paths:
page.goto(staged_backlot_server + path, wait_until="networkidle")
page.wait_for_timeout(300)
sizes = page.evaluate(
"""() => ({
scrollWidth: document.documentElement.scrollWidth,
clientWidth: document.documentElement.clientWidth
})"""
)
assert sizes["scrollWidth"] <= sizes["clientWidth"], (
path,
viewport,
sizes,
)
finally:
browser.close()
def test_static_navigation_invalid_route_and_active_takes(staged_backlot_server):
with sync_playwright() as pw:
browser = pw.chromium.launch(headless=True)
page = browser.new_page(viewport={"width": 1560, "height": 1000})
try:
page.goto(staged_backlot_server + "/?static=1", wait_until="networkidle")
href = page.locator("a.lib-card").first.get_attribute("href")
assert href and "static=1" in href
response = page.goto(
staged_backlot_server + "/p/..%2FAGENT_GUIDE.md?static=1",
wait_until="networkidle",
)
assert response and response.status == 200
assert "PROJECT NOT FOUND" in page.locator("body").inner_text()
page.goto(staged_backlot_server + "/p/the-last-lighthouse?static=1", wait_until="networkidle")
page.wait_for_timeout(300)
assert page.locator(".takes .tk.active").count() >= 1
finally:
browser.close()

View File

@@ -5,6 +5,8 @@ stage director skills, meta skills, and the animated-explainer pipeline.
"""
import sys
import builtins
import shutil
from pathlib import Path
import pytest
@@ -23,7 +25,7 @@ from lib.pipeline_loader import (
from lib.checkpoint import STAGES
from schemas.artifacts import list_schemas
from styles.playbook_loader import load_playbook, list_playbooks, validate_playbook
from tools.base_tool import ToolTier
from tools.base_tool import ToolTier, ToolStatus
from tools.audio.music_gen import MusicGen
from tools.tool_registry import ToolRegistry
from tools.audio.elevenlabs_tts import ElevenLabsTTS
@@ -73,6 +75,22 @@ class TestPiperTTS:
assert "text_to_speech" in tool.capabilities
assert "offline_generation" in tool.capabilities
def test_status_requires_piper_executable_even_if_python_package_imports(self, monkeypatch):
"""F-12 regression: Piper generation shells out to `piper`, so importing
the Python package is not enough to mark the provider available."""
original_import = builtins.__import__
original_which = shutil.which
def fake_import(name, *args, **kwargs):
if name == "piper":
return object()
return original_import(name, *args, **kwargs)
monkeypatch.setattr(shutil, "which", lambda cmd: None if cmd == "piper" else original_which(cmd))
monkeypatch.setattr(builtins, "__import__", fake_import)
assert PiperTTS().get_status() == ToolStatus.UNAVAILABLE
class TestMusicGen:
def test_identity(self):

View File

@@ -4,7 +4,9 @@ from pathlib import Path
from tools.base_tool import ToolStatus
from tools.tool_registry import ToolRegistry
from tools.video.stock_sources import Candidate
from tools.video.corpus_builder import CorpusBuilder
from tools.video.direct_clip_search import DirectClipSearch
from tools.video.video_compose import VideoCompose
@@ -196,3 +198,196 @@ def test_provider_menu_preserves_tool_discovery_metadata(monkeypatch):
assert entry["name"] == "corpus_builder"
assert entry["source_provider_summary"]["configured"] == 1
assert entry["source_provider_menu"][0]["name"] == "archive_org"
def test_direct_clip_search_honors_overall_timeout(monkeypatch, tmp_path):
"""F-13 regression: direct clip search must stop on its own deadline and
return partial progress instead of relying on an external PTY interrupt."""
import tools.video.direct_clip_search as direct_clip_search
import tools.video.stock_sources as stock_sources
class SlowSource(_DummySource):
def search(self, query: str, filters):
return [
Candidate(
source=self.name,
source_id="slow-1",
source_url="https://example.test/slow-1",
download_url="https://example.test/slow-1.mp4",
kind="video",
)
]
def download(self, candidate, out_path: Path):
out_path.parent.mkdir(parents=True, exist_ok=True)
out_path.write_bytes(b"0" * 2048)
return out_path
source = SlowSource("slow_source", True)
monkeypatch.setattr(stock_sources, "all_sources", lambda: [source])
monkeypatch.setattr(stock_sources, "available_sources", lambda: [source])
monkeypatch.setattr(
stock_sources,
"source_summary",
lambda: {
"configured": 1,
"total": 1,
"available_source_names": ["slow_source"],
"unavailable_source_names": [],
},
)
ticks = iter([0.0, 2.0, 2.0, 2.0])
monkeypatch.setattr(direct_clip_search.time, "time", lambda: next(ticks, 2.0))
result = DirectClipSearch().execute(
{
"output_dir": str(tmp_path / "clips"),
"queries": [{"query": "foggy harbor", "slot_id": "sc5"}],
"timeout_seconds": 1,
"extract_thumbnails": False,
}
)
assert not result.success
assert "timed out" in (result.error or "").lower()
assert result.data["timed_out"] is True
assert result.data["phase"] in {"query", "search", "download"}
assert result.data["clips"] == []
def test_direct_clip_search_times_out_streaming_download(monkeypatch, tmp_path):
"""F-13 regression: a streaming adapter download must not run past the
tool-level deadline just because bytes keep arriving."""
import tools.video.direct_clip_search as direct_clip_search
import tools.video.stock_sources as stock_sources
import requests
clock = {"now": 0.0}
class StreamingResponse:
def __enter__(self):
return self
def __exit__(self, exc_type, exc, tb):
return False
def raise_for_status(self):
return None
def iter_content(self, chunk_size=1024):
clock["now"] = 2.0
yield b"0" * 2048
class StreamingSource(_DummySource):
def search(self, query: str, filters):
return [
Candidate(
source=self.name,
source_id="stream-1",
source_url="https://example.test/stream-1",
download_url="https://example.test/stream-1.mp4",
kind="video",
)
]
def download(self, candidate, out_path: Path):
out_path.parent.mkdir(parents=True, exist_ok=True)
with requests.get(candidate.download_url, stream=True, timeout=300) as response:
response.raise_for_status()
with out_path.open("wb") as f:
for chunk in response.iter_content(chunk_size=1024):
if chunk:
f.write(chunk)
return out_path
source = StreamingSource("streaming_source", True)
monkeypatch.setattr(stock_sources, "all_sources", lambda: [source])
monkeypatch.setattr(stock_sources, "available_sources", lambda: [source])
monkeypatch.setattr(
stock_sources,
"source_summary",
lambda: {
"configured": 1,
"total": 1,
"available_source_names": ["streaming_source"],
"unavailable_source_names": [],
},
)
monkeypatch.setattr(direct_clip_search.time, "time", lambda: clock["now"])
monkeypatch.setattr(requests, "get", lambda *args, **kwargs: StreamingResponse())
result = DirectClipSearch().execute(
{
"output_dir": str(tmp_path / "clips"),
"queries": [{"query": "foggy harbor", "slot_id": "sc5"}],
"timeout_seconds": 1,
"extract_thumbnails": False,
}
)
assert not result.success
assert result.data["timed_out"] is True
assert result.data["phase"] == "download"
assert result.data["clips"] == []
def test_direct_clip_search_reports_downloaded_clip_when_thumbnail_times_out(
monkeypatch, tmp_path
):
"""F-13 regression: timeout data should include a clip that was already
downloaded and validated before thumbnail extraction hit the deadline."""
import tools.video.direct_clip_search as direct_clip_search
import tools.video.stock_sources as stock_sources
clock = {"now": 0.0}
class SlowThumbnailSource(_DummySource):
def search(self, query: str, filters):
return [
Candidate(
source=self.name,
source_id="thumb-1",
source_url="https://example.test/thumb-1",
download_url="https://example.test/thumb-1.mp4",
kind="video",
)
]
def download(self, candidate, out_path: Path):
out_path.parent.mkdir(parents=True, exist_ok=True)
out_path.write_bytes(b"0" * 2048)
clock["now"] = 2.0
return out_path
source = SlowThumbnailSource("thumb_source", True)
monkeypatch.setattr(stock_sources, "all_sources", lambda: [source])
monkeypatch.setattr(stock_sources, "available_sources", lambda: [source])
monkeypatch.setattr(
stock_sources,
"source_summary",
lambda: {
"configured": 1,
"total": 1,
"available_source_names": ["thumb_source"],
"unavailable_source_names": [],
},
)
monkeypatch.setattr(direct_clip_search.time, "time", lambda: clock["now"])
result = DirectClipSearch().execute(
{
"output_dir": str(tmp_path / "clips"),
"queries": [{"query": "foggy harbor", "slot_id": "sc5"}],
"timeout_seconds": 1,
"extract_thumbnails": True,
}
)
assert not result.success
assert result.data["timed_out"] is True
assert result.data["phase"] == "thumbnail"
assert result.data["clips_downloaded"] == 1
assert result.data["total_clips"] == 1
assert result.data["clips"][0]["clip_id"] == "thumb_source_thumb-1"
assert result.data["clips"][0]["thumbnail"] == ""

View File

@@ -870,6 +870,45 @@ def test_video_compose_blocks_hyperframes_when_runtime_unavailable(
assert "blocker" in err or "not available" in err
def test_video_compose_honors_hyperframes_runtime_before_atelier_mode(
tmp_path, monkeypatch
):
"""Regression for F-14: composition_mode='atelier' must not force the
Remotion atelier branch when render_runtime='hyperframes' is locked."""
monkeypatch.setattr(
VideoCompose, "_hyperframes_available", lambda self: False, raising=True
)
result = VideoCompose().execute(
{
"operation": "render",
"edit_decisions": {
"version": "1.0",
"cuts": [
{
"id": "c1",
"source": "a1",
"in_seconds": 0,
"out_seconds": 3,
}
],
"render_runtime": "hyperframes",
"composition_mode": "atelier",
"renderer_family": "animation-first",
},
"asset_manifest": {"assets": [{"id": "a1", "path": "does-not-matter.png"}]},
"output_path": str(tmp_path / "out.mp4"),
}
)
assert not result.success
err = (result.error or "").lower()
assert "hyperframes" in err
assert "not available" in err or "blocker" in err
assert "remotion entry" not in err
# ------------------------------------------------------------------
# Scaffold / workspace generation (no CLI invocation)
# ------------------------------------------------------------------