mirror of
https://github.com/calesthio/OpenMontage.git
synced 2026-08-25 17:42:50 +08:00
Ship Backlot living storyboard release hardening
This commit is contained in:
110
tests/backlot/test_ui_bug_bash.py
Normal file
110
tests/backlot/test_ui_bug_bash.py
Normal 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()
|
||||
@@ -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):
|
||||
|
||||
@@ -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"] == ""
|
||||
|
||||
@@ -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)
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user