"""VideoEngine coverage: authoring-task origination + held-draft materialization. Mirrors the X-engine tests: flag-gated, dedup, rolling open-cap, and a deterministic ux-dev balance. The authoring task (source=video) is a normal ASSIGNED delivery task — never held; the post draft (source=video_post) is Secretary-owned and held for the CEO. Asserted against a real Postgres DB. """ from __future__ import annotations import asyncio from typing import TYPE_CHECKING, cast from unittest.mock import AsyncMock from uuid import UUID, uuid4 import pytest from roboco.config import settings as cfg from roboco.db.tables import AgentTable, ProjectTable from roboco.foundation import identity as _foundation from roboco.foundation.policy.content import markers from roboco.models.base import AgentRole, AgentStatus, Complexity, Team from roboco.models.base import TaskStatus as TS from roboco.services import video_engine as video_engine_module from roboco.services.company_goals import get_company_goals_service from roboco.services.heartbeat_mutex import HeartbeatLockUnavailable from roboco.services.task import VIDEO_POST_SOURCE, VIDEO_SOURCE, get_task_service from sqlalchemy import delete, select if TYPE_CHECKING: from sqlalchemy.ext.asyncio import AsyncSession SYSTEM_UUID = _foundation.AGENTS["system"].uuid SECRETARY_UUID = _foundation.AGENTS["secretary-1"].uuid UX_DEV_1_UUID = _foundation.AGENTS["ux-dev-1"].uuid UX_DEV_2_UUID = _foundation.AGENTS["ux-dev-2"].uuid SLUG = "roboco" ONE = 1 TWO = 2 THREE = 3 FOUR = 4 FIVE = 5 SEVEN = 7 _AC_ITEM_CHAR_CAP = 200 # mirrors task_completeness._AC_MAX_ITEM_CHARS async def _seed(session: AsyncSession) -> None: for uuid, slug, role, team in ( (SYSTEM_UUID, "system", AgentRole.SYSTEM, None), (SECRETARY_UUID, "secretary-1", AgentRole.SECRETARY, None), (UX_DEV_1_UUID, "ux-dev-1", AgentRole.DEVELOPER, Team.UX_UI), (UX_DEV_2_UUID, "ux-dev-2", AgentRole.DEVELOPER, Team.UX_UI), ): if await session.get(AgentTable, uuid) is None: session.add( AgentTable( id=uuid, name=slug, slug=slug, role=role, team=team, status=AgentStatus.ACTIVE, model_config={}, system_prompt="x", capabilities=[], permissions={}, metrics={}, ) ) await session.flush() existing = await session.execute( select(ProjectTable).where(ProjectTable.slug == SLUG) ) if existing.scalar_one_or_none() is None: session.add( ProjectTable( name="RoboCo", slug=SLUG, git_url="https://github.com/x/roboco.git", default_branch="master", protected_branches=["master"], assigned_cell=Team.BACKEND, created_by=SYSTEM_UUID, is_active=True, video_engine_enabled=True, ) ) await session.flush() def _enable(monkeypatch: pytest.MonkeyPatch, **overrides: object) -> None: monkeypatch.setattr(cfg, "video_engine_enabled", True) monkeypatch.setattr(cfg, "self_heal_project_slug", SLUG) monkeypatch.setattr(cfg, "video_max_open_posts", 5) for key, value in overrides.items(): monkeypatch.setattr(cfg, key, value) def _mock_local_model(monkeypatch: pytest.MonkeyPatch, reply: str | None) -> AsyncMock: mock = AsyncMock(return_value=reply) monkeypatch.setattr(video_engine_module, "_chat", mock) return mock class _AlwaysAcquiredMutex: """Stand-in for ``HeartbeatMutex``: always acquires immediately, no live Redis required (matches the project's ``_no_live_redis`` fixture). Used as the default so every scenario below that isn't specifically testing the occasion-lock behavior is unaffected by its introduction.""" def __init__(self, *_args: object, **_kwargs: object) -> None: pass async def acquire(self) -> str | None: return "tok" async def release(self, _token: str) -> None: return None @pytest.fixture(autouse=True) def _stub_occasion_lock(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(video_engine_module, "HeartbeatMutex", _AlwaysAcquiredMutex) # --------------------------------------------------------------------------- # # open_video_task # --------------------------------------------------------------------------- # @pytest.mark.asyncio async def test_disabled_opens_no_video_task( db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch ) -> None: await _seed(db_session) monkeypatch.setattr(cfg, "video_engine_enabled", False) engine = video_engine_module.VideoEngine(db_session) task = await engine.open_video_task( occasion="release v1.0.0", script="script", platforms=["x"], brief="brief" ) assert task is None assert await get_task_service(db_session).list_open_video_posts() == [] @pytest.mark.asyncio async def test_open_video_task_creates_assigned_authoring_task( db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch ) -> None: await _seed(db_session) _enable(monkeypatch) engine = video_engine_module.VideoEngine(db_session) task = await engine.open_video_task( occasion="release v1.0.0", script="Here's what shipped", platforms=["x", "tiktok"], brief="Announce the release", ) assert task is not None assert task.team == Team.UX_UI assert task.assigned_to == UX_DEV_1_UUID # deterministic first pick assert task.source == VIDEO_SOURCE assert task.status == TS.PENDING assert task.confirmed_by_human is True # normal delivery task, not CEO-held # LOW so it clears _check_dev_needs_subtasks; a medium/high root dev task # would auto-block for subtasks it never owns and deadlock. assert task.estimated_complexity == Complexity.LOW assert task.acceptance_criteria # non-empty # Third AC line: composition follows the design bar / demo-kit register. assert len(task.acceptance_criteria) == FOUR assert "motion/README.md" in task.acceptance_criteria[2] assert "panel-demo" in task.acceptance_criteria[2] # Fourth AC line: request_render preview frames must be verified. assert "request_render" in task.acceptance_criteria[3] project = await db_session.get(ProjectTable, task.project_id) assert project is not None assert project.slug == SLUG draft = markers.get_video_draft(task) assert draft is not None assert draft["occasion"] == "release v1.0.0" assert draft["script"] == "Here's what shipped" assert draft["platforms"] == ["x", "tiktok"] # brief is enriched: raw content + motion design-bar pointer appended. assert draft["brief"].startswith("Announce the release") assert "motion/README.md" in draft["brief"] assert "motion/kit/README.md" in draft["brief"] assert "compositions/panel-demo/" in draft["brief"] assert "request_render" in draft["brief"] # verify-the-render pointer assert "Brand voice" not in draft["brief"] # unset -> omitted assert draft["suggested_input_props"] == {} # none supplied assert task.description == draft["brief"] @pytest.mark.asyncio async def test_open_video_task_balances_across_ux_devs( db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch ) -> None: await _seed(db_session) _enable(monkeypatch) engine = video_engine_module.VideoEngine(db_session) first = await engine.open_video_task( occasion="release v1.0.0", script="s", platforms=["x"], brief="b" ) second = await engine.open_video_task( occasion="release v2.0.0", script="s", platforms=["x"], brief="b" ) assert first is not None assert second is not None assert first.assigned_to == UX_DEV_1_UUID assert second.assigned_to == UX_DEV_2_UUID # balanced onto the less-loaded dev @pytest.mark.asyncio async def test_open_video_task_dedupes_same_occasion( db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch ) -> None: await _seed(db_session) _enable(monkeypatch) engine = video_engine_module.VideoEngine(db_session) await engine.open_video_task( occasion="release v1.0.0", script="s", platforms=["x"], brief="b" ) second = await engine.open_video_task( occasion="release v1.0.0", script="s2", platforms=["x"], brief="b2" ) assert second is None open_tasks = await get_task_service(db_session).list_open_video_posts() assert len(open_tasks) == ONE @pytest.mark.asyncio async def test_open_video_task_returns_none_when_occasion_lock_held( db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch ) -> None: """A held per-occasion lock (another call already owns it) is a no-op, not an error — it opens nothing and leaves the shared session usable.""" await _seed(db_session) _enable(monkeypatch) class _HeldMutex: def __init__(self, *_a: object, **_kw: object) -> None: pass async def acquire(self) -> str | None: return None # another call already holds this occasion's lock async def release(self, _token: str) -> None: raise AssertionError("release must not be called when acquire failed") monkeypatch.setattr(video_engine_module, "HeartbeatMutex", _HeldMutex) engine = video_engine_module.VideoEngine(db_session) task = await engine.open_video_task( occasion="release v1.0.0", script="s", platforms=["x"], brief="b" ) assert task is None assert await get_task_service(db_session).list_open_video_posts() == [] @pytest.mark.asyncio async def test_open_video_task_returns_none_when_lock_unavailable( db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch ) -> None: """A Redis outage on the occasion lock fails closed (no-op), not a crash.""" await _seed(db_session) _enable(monkeypatch) class _BrokenMutex: def __init__(self, *_a: object, **_kw: object) -> None: pass async def acquire(self) -> str | None: raise HeartbeatLockUnavailable("redis down") async def release(self, _token: str) -> None: raise AssertionError("release must not be called when acquire failed") monkeypatch.setattr(video_engine_module, "HeartbeatMutex", _BrokenMutex) engine = video_engine_module.VideoEngine(db_session) task = await engine.open_video_task( occasion="release v1.0.0", script="s", platforms=["x"], brief="b" ) assert task is None assert await get_task_service(db_session).list_open_video_posts() == [] @pytest.mark.asyncio async def test_open_video_task_concurrent_same_occasion_creates_only_one( db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch ) -> None: """Regression (engine dedup race): ``open_video_task`` is reachable from several genuinely concurrent callers for the same occasion (a double-click on ``/video/request``, or an on-demand call racing the release hook) — unlike the other engines' single-loop ``run_cycle``, two overlapping calls are NOT serialized by the orchestrator's own scheduling. A real ``asyncio.Lock`` stands in for the Redis SET NX mutex's mutual exclusion (no live Redis in tests): the first caller to reach the DB's genuinely suspending await wins the lock and creates the task; the second finds it held and returns None immediately — never both passing the dedup check.""" await _seed(db_session) _enable(monkeypatch) engine = video_engine_module.VideoEngine(db_session) real_lock = asyncio.Lock() class _RaceMutex: def __init__(self, *_a: object, **_kw: object) -> None: pass async def acquire(self) -> str | None: if real_lock.locked(): return None await real_lock.acquire() return "tok" async def release(self, _token: str) -> None: real_lock.release() monkeypatch.setattr(video_engine_module, "HeartbeatMutex", _RaceMutex) results = await asyncio.gather( engine.open_video_task( occasion="race me", script="s1", platforms=["x"], brief="b1" ), engine.open_video_task( occasion="race me", script="s2", platforms=["x"], brief="b2" ), ) created = [r for r in results if r is not None] assert len(created) == ONE open_tasks = await get_task_service(db_session).list_open_video_posts() assert len(open_tasks) == ONE @pytest.mark.asyncio async def test_open_video_task_respects_open_cap( db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch ) -> None: await _seed(db_session) _enable(monkeypatch, video_max_open_posts=1) engine = video_engine_module.VideoEngine(db_session) await engine.open_video_task( occasion="release v1.0.0", script="s", platforms=["x"], brief="b" ) second = await engine.open_video_task( occasion="release v2.0.0", script="s", platforms=["x"], brief="b" ) assert second is None open_tasks = await get_task_service(db_session).list_open_video_posts() assert len(open_tasks) == ONE @pytest.mark.asyncio async def test_open_video_task_unresolvable_project_opens_nothing( db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch ) -> None: await _seed(db_session) _enable(monkeypatch) monkeypatch.setattr(cfg, "self_heal_project_slug", "no-such-project") engine = video_engine_module.VideoEngine(db_session) task = await engine.open_video_task( occasion="release v1.0.0", script="s", platforms=["x"], brief="b" ) assert task is None assert await get_task_service(db_session).list_open_video_posts() == [] @pytest.mark.asyncio async def test_open_video_task_no_op_when_project_not_opted_in( db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch ) -> None: await _seed(db_session) # Flip the per-project opt-in back off — the global flag stays on. project = ( await db_session.execute(select(ProjectTable).where(ProjectTable.slug == SLUG)) ).scalar_one() project.video_engine_enabled = False await db_session.flush() _enable(monkeypatch) engine = video_engine_module.VideoEngine(db_session) task = await engine.open_video_task( occasion="release v1.0.0", script="s", platforms=["x"], brief="b" ) assert task is None assert await get_task_service(db_session).list_open_video_posts() == [] @pytest.mark.asyncio async def test_open_video_task_insert_error_returns_none_session_usable( db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch ) -> None: """F042 guard: a real DBAPI error on the authoring insert (assignee FK absent) rolls back ONLY the savepoint and returns None; the shared session stays usable, so a caller's later commit is not poisoned.""" if await db_session.get(AgentTable, SYSTEM_UUID) is None: db_session.add( AgentTable( id=SYSTEM_UUID, name="system", slug="system", role=AgentRole.SYSTEM, team=None, status=AgentStatus.ACTIVE, model_config={}, system_prompt="x", capabilities=[], permissions={}, metrics={}, ) ) # ux-devs deliberately absent -> the assigned_to FK violates at flush. await db_session.execute( delete(AgentTable).where(AgentTable.id.in_([UX_DEV_1_UUID, UX_DEV_2_UUID])) ) has_project = ( await db_session.execute(select(ProjectTable).where(ProjectTable.slug == SLUG)) ).scalar_one_or_none() if has_project is None: db_session.add( ProjectTable( name="RoboCo", slug=SLUG, git_url="https://github.com/x/roboco.git", default_branch="master", protected_branches=["master"], assigned_cell=Team.BACKEND, created_by=SYSTEM_UUID, is_active=True, video_engine_enabled=True, ) ) await db_session.flush() _enable(monkeypatch) engine = video_engine_module.VideoEngine(db_session) task = await engine.open_video_task( occasion="fk-fail", script="s", platforms=["x"], brief="b" ) assert task is None # Not poisoned: a follow-up statement runs cleanly (this raised # PendingRollbackError before the savepoint fix). check = await db_session.execute( select(ProjectTable).where(ProjectTable.slug == SLUG) ) assert check.scalar_one_or_none() is not None # --------------------------------------------------------------------------- # # open_video_task(project_id=...) — the on-demand caller's own project scope # --------------------------------------------------------------------------- # @pytest.mark.asyncio async def test_open_video_task_with_explicit_project_id_ignores_self_heal_slug( db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch ) -> None: """A caller-supplied project_id resolves independent of self_heal_project_slug — the authoring task lands on THAT project, not the fixed RoboCo one.""" await _seed(db_session) _enable(monkeypatch) other = ProjectTable( name="Other Repo", slug="other-repo", git_url="https://github.com/x/other.git", default_branch="master", protected_branches=["master"], assigned_cell=Team.BACKEND, created_by=SYSTEM_UUID, is_active=True, video_engine_enabled=True, ) db_session.add(other) await db_session.flush() monkeypatch.setattr(cfg, "self_heal_project_slug", "no-such-slug") engine = video_engine_module.VideoEngine(db_session) task = await engine.open_video_task( occasion="on-demand other repo", script="s", platforms=["x"], brief="b", project_id=cast("UUID", other.id), ) assert task is not None assert task.project_id == other.id @pytest.mark.asyncio async def test_open_video_task_explicit_project_id_not_opted_in_opens_nothing( db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch ) -> None: await _seed(db_session) _enable(monkeypatch) other = ProjectTable( name="Not Opted In", slug="not-opted-in", git_url="https://github.com/x/notopted.git", default_branch="master", protected_branches=["master"], assigned_cell=Team.BACKEND, created_by=SYSTEM_UUID, is_active=True, video_engine_enabled=False, ) db_session.add(other) await db_session.flush() engine = video_engine_module.VideoEngine(db_session) task = await engine.open_video_task( occasion="on-demand not opted", script="s", platforms=["x"], brief="b", project_id=cast("UUID", other.id), ) assert task is None @pytest.mark.asyncio async def test_open_video_task_explicit_project_id_unresolvable_opens_nothing( db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch ) -> None: await _seed(db_session) _enable(monkeypatch) engine = video_engine_module.VideoEngine(db_session) task = await engine.open_video_task( occasion="on-demand missing project", script="s", platforms=["x"], brief="b", project_id=uuid4(), ) assert task is None # --------------------------------------------------------------------------- # # _originate_video_post # --------------------------------------------------------------------------- # @pytest.mark.asyncio async def test_originate_video_post_holds_draft_for_secretary( db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch ) -> None: await _seed(db_session) _enable(monkeypatch) engine = video_engine_module.VideoEngine(db_session) source_task = await engine.open_video_task( occasion="release v1.0.0", script="Here's what shipped", platforms=["x", "tiktok"], brief="Announce the release", ) assert source_task is not None draft_task = await engine._originate_video_post( source_task=source_task, mp4_paths={ "vertical": "/render/out/a-vertical.mp4", "square": "/render/out/a-square.mp4", }, captions={ "x": "We just shipped v1.0.0!", "tiktok": "New release, check it out", }, platforms=["x", "tiktok"], ) assert draft_task.team == Team.MAIN_PM assert draft_task.assigned_to == SECRETARY_UUID assert draft_task.source == VIDEO_POST_SOURCE assert draft_task.status == TS.PENDING assert draft_task.confirmed_by_human is False # HELD; never dispatched draft = markers.get_video_draft(draft_task) assert draft is not None assert draft["occasion"] == "release v1.0.0" # carried forward from the source assert draft["script"] == "Here's what shipped" assert draft["mp4_paths"] == { "vertical": "/render/out/a-vertical.mp4", "square": "/render/out/a-square.mp4", } assert draft["x_caption"] == "We just shipped v1.0.0!" assert draft["tiktok_caption"] == "New release, check it out" assert draft["platforms"] == ["x", "tiktok"] assert draft["render_status"] == "rendered" assert draft["source_task_id"] == str(source_task.id) # traceability back-ref @pytest.mark.asyncio async def test_originate_video_post_sends_telegram_push( db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch ) -> None: await _seed(db_session) _enable(monkeypatch) notify = AsyncMock() monkeypatch.setattr( "roboco.services.notification_delivery.NotificationDeliveryService." "notify_ceo_of_queue_item", notify, ) engine = video_engine_module.VideoEngine(db_session) source_task = await engine.open_video_task( occasion="release v1.0.0", script="Here's what shipped", platforms=["x", "tiktok"], brief="Announce the release", ) assert source_task is not None draft_task = await engine._originate_video_post( source_task=source_task, mp4_paths={"vertical": "/a.mp4", "square": "/b.mp4"}, captions={"x": "shipped!", "tiktok": "shipped!"}, platforms=["x", "tiktok"], ) notify.assert_awaited_once() assert notify.await_args is not None _args, kwargs = notify.await_args assert kwargs["kind"] == "video" assert kwargs["id8"] == str(draft_task.id)[:8] assert kwargs["title"] == "release v1.0.0" @pytest.mark.asyncio async def test_originate_video_post_survives_telegram_push_failure( db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch ) -> None: """A Telegram send failure must never block the draft itself.""" await _seed(db_session) _enable(monkeypatch) monkeypatch.setattr( "roboco.services.notification_delivery.NotificationDeliveryService." "notify_ceo_of_queue_item", AsyncMock(side_effect=RuntimeError("boom")), ) engine = video_engine_module.VideoEngine(db_session) source_task = await engine.open_video_task( occasion="release v1.0.0", script="s", platforms=["x"], brief="b" ) assert source_task is not None draft_task = await engine._originate_video_post( source_task=source_task, mp4_paths={"vertical": "/a.mp4", "square": "/b.mp4"}, captions={"x": "shipped!"}, platforms=["x"], ) assert draft_task is not None @pytest.mark.asyncio async def test_originate_video_post_not_counted_by_dedupe_against_new_occasion( db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch ) -> None: """A materialized post draft still occupies the shared open-cap/dedupe pool (list_open_video_posts spans both sources) — a fresh occasion is unaffected, but the cap still counts it.""" await _seed(db_session) _enable(monkeypatch, video_max_open_posts=2) engine = video_engine_module.VideoEngine(db_session) source_task = await engine.open_video_task( occasion="release v1.0.0", script="s", platforms=["x"], brief="b" ) assert source_task is not None source_task.status = TS.COMPLETED await db_session.flush() await engine._originate_video_post( source_task=source_task, mp4_paths={"vertical": "a.mp4", "square": "b.mp4"}, captions={"x": "caption"}, platforms=["x"], ) # One open (the held draft; the source authoring task is now COMPLETED and # therefore excluded) plus room for exactly one more before the cap bites. open_tasks = await get_task_service(db_session).list_open_video_posts() assert len(open_tasks) == ONE second = await engine.open_video_task( occasion="release v2.0.0", script="s", platforms=["x"], brief="b" ) assert second is not None open_tasks = await get_task_service(db_session).list_open_video_posts() assert len(open_tasks) == TWO # --------------------------------------------------------------------------- # # draft_release_video # --------------------------------------------------------------------------- # _CHANGELOG = "## [1.0.0]\n\n### Added\n- a huge new release\n" @pytest.mark.asyncio async def test_draft_release_video_disabled_opens_nothing( db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch ) -> None: await _seed(db_session) _enable(monkeypatch, video_on_release=True) monkeypatch.setattr(cfg, "video_engine_enabled", False) engine = video_engine_module.VideoEngine(db_session) task = await engine.draft_release_video(version="1.0.0", changelog=_CHANGELOG) assert task is None assert await get_task_service(db_session).list_open_video_posts() == [] @pytest.mark.asyncio async def test_draft_release_video_sub_switch_off_opens_nothing( db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch ) -> None: await _seed(db_session) _enable(monkeypatch, video_on_release=False) engine = video_engine_module.VideoEngine(db_session) task = await engine.draft_release_video(version="1.0.0", changelog=_CHANGELOG) assert task is None assert await get_task_service(db_session).list_open_video_posts() == [] @pytest.mark.asyncio async def test_draft_release_video_opens_authoring_task( db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch ) -> None: await _seed(db_session) _enable(monkeypatch, video_on_release=True) _mock_local_model(monkeypatch, "RoboCo v1.0.0 just shipped a huge release.") engine = video_engine_module.VideoEngine(db_session) task = await engine.draft_release_video(version="1.0.0", changelog=_CHANGELOG) assert task is not None assert task.source == VIDEO_SOURCE assert task.team == Team.UX_UI assert task.confirmed_by_human is True draft = markers.get_video_draft(task) assert draft is not None assert draft["occasion"] == "release 1.0.0" assert draft["platforms"] == ["x", "tiktok"] assert draft["script"] == "RoboCo v1.0.0 just shipped a huge release." # brief is now the structured changelog block, not the LLM one-liner — # the whole CHANGELOG section (not one bullet) plus a highlights list. assert draft["brief"] != draft["script"] assert _CHANGELOG.strip() in draft["brief"] assert "a huge new release" in draft["brief"] assert "motion/README.md" in draft["brief"] assert draft["suggested_input_props"] == { "version": "1.0.0", "highlights": ["a huge new release"], } @pytest.mark.asyncio async def test_draft_release_video_falls_back_to_template_on_local_model_failure( db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch ) -> None: await _seed(db_session) _enable(monkeypatch, video_on_release=True) mock = AsyncMock(side_effect=RuntimeError("ollama down")) monkeypatch.setattr(video_engine_module, "_chat", mock) engine = video_engine_module.VideoEngine(db_session) task = await engine.draft_release_video(version="1.0.0", changelog=_CHANGELOG) assert task is not None draft = markers.get_video_draft(task) assert draft is not None assert "1.0.0" in draft["script"] assert "a huge new release" in draft["script"] # The structured brief is built independent of the local model, so a # model outage still produces the full changelog-derived brief. assert _CHANGELOG.strip() in draft["brief"] assert draft["suggested_input_props"]["highlights"] == ["a huge new release"] @pytest.mark.asyncio async def test_draft_release_video_falls_back_to_template_on_empty_local_reply( db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch ) -> None: await _seed(db_session) _enable(monkeypatch, video_on_release=True) _mock_local_model(monkeypatch, None) # non-success / empty local-model reply engine = video_engine_module.VideoEngine(db_session) task = await engine.draft_release_video(version="2.0.0", changelog="- no bullets") assert task is not None draft = markers.get_video_draft(task) assert draft is not None assert draft["script"] == "RoboCo v2.0.0 just shipped: no bullets." assert "- no bullets" in draft["brief"] assert draft["suggested_input_props"] == { "version": "2.0.0", "highlights": ["no bullets"], } @pytest.mark.asyncio async def test_draft_release_video_brief_contains_brand_voice_when_set( db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch ) -> None: await _seed(db_session) _enable(monkeypatch, video_on_release=True) await get_company_goals_service(db_session).upsert( {"brand_voice": "Dry wit, never an exclamation point."} ) _mock_local_model(monkeypatch, "shipped!") engine = video_engine_module.VideoEngine(db_session) task = await engine.draft_release_video(version="1.0.0", changelog=_CHANGELOG) assert task is not None draft = markers.get_video_draft(task) assert draft is not None assert "Dry wit, never an exclamation point." in draft["brief"] # --------------------------------------------------------------------------- # # Product-name resolution (release-video prompts brand off the target # project, not a hardcoded "RoboCo" literal). The fallback-chain unit # coverage (project name -> company_name -> "RoboCo") lives on the shared # helper, CompanyGoalsService.resolve_product_name, in # test_company_goals_service.py — this only asserts the end-to-end wiring # through draft_release_video. # --------------------------------------------------------------------------- # @pytest.mark.asyncio async def test_draft_release_video_uses_project_name_when_set( db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch ) -> None: await _seed(db_session) _enable(monkeypatch, video_on_release=True) acme = ProjectTable( name="Acme Robotics", slug="acme-robotics", git_url="https://github.com/x/acme.git", default_branch="master", protected_branches=["master"], assigned_cell=Team.BACKEND, created_by=SYSTEM_UUID, is_active=True, video_engine_enabled=True, ) db_session.add(acme) await db_session.flush() _mock_local_model(monkeypatch, None) # force the deterministic fallback template engine = video_engine_module.VideoEngine(db_session) task = await engine.draft_release_video( version="1.0.0", changelog=_CHANGELOG, project_id=cast("UUID", acme.id) ) assert task is not None draft = markers.get_video_draft(task) assert draft is not None assert "Acme Robotics" in draft["script"] assert "RoboCo" not in draft["script"] assert "Acme Robotics" in draft["brief"] assert "RoboCo" not in draft["brief"] # --------------------------------------------------------------------------- # # brief enrichment shared by every occasion — brand voice + motion pointer # (spec Task 2: "Feed the video brief") # --------------------------------------------------------------------------- # @pytest.mark.asyncio async def test_open_video_task_brief_carries_motion_pointer_no_brand_voice_by_default( db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch ) -> None: """Simulates the on-demand (`POST /video/request`) caller, which passes its brief through verbatim — enrichment must land centrally here since that route can't append it itself.""" await _seed(db_session) _enable(monkeypatch) engine = video_engine_module.VideoEngine(db_session) task = await engine.open_video_task( occasion="on-demand demo", script="script", platforms=["x"], brief="CEO's on-demand brief, verbatim.", ) assert task is not None draft = markers.get_video_draft(task) assert draft is not None assert draft["brief"].startswith("CEO's on-demand brief, verbatim.") assert "motion/README.md" in draft["brief"] assert "motion/kit/README.md" in draft["brief"] assert "compositions/panel-demo/" in draft["brief"] assert "Brand voice" not in draft["brief"] # unset -> omitted assert task.description == draft["brief"] @pytest.mark.asyncio async def test_open_video_task_brief_includes_brand_voice_when_set( db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch ) -> None: await _seed(db_session) _enable(monkeypatch) await get_company_goals_service(db_session).upsert( {"brand_voice": "Dry wit, never an exclamation point."} ) engine = video_engine_module.VideoEngine(db_session) task = await engine.open_video_task( occasion="on-demand demo 2", script="script", platforms=["x"], brief="CEO's on-demand brief.", ) assert task is not None draft = markers.get_video_draft(task) assert draft is not None assert "Dry wit, never an exclamation point." in draft["brief"] @pytest.mark.asyncio async def test_open_video_task_does_not_compress_spotlight_brief( db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch ) -> None: """Mirrors content_actions._open_spotlight_video's `f"{title}: {body}"` shape — the spotlight body is already rich; enrichment only appends, never truncates or rewrites the original content.""" await _seed(db_session) _enable(monkeypatch) engine = video_engine_module.VideoEngine(db_session) feature_brief = ( "Organizational Memory Loop: Did you know RoboCo agents learn from " "every completed task? A local model distills one high-signal lesson " "per task, retrieved back into future briefings." ) task = await engine.open_video_task( occasion="spotlight org-memory", script=feature_brief, platforms=["x", "tiktok"], brief=feature_brief, ) assert task is not None draft = markers.get_video_draft(task) assert draft is not None assert draft["brief"].startswith(feature_brief) # not truncated/compressed assert "motion/README.md" in draft["brief"] @pytest.mark.asyncio async def test_open_video_task_no_suggested_input_props_defaults_empty( db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch ) -> None: await _seed(db_session) _enable(monkeypatch) engine = video_engine_module.VideoEngine(db_session) task = await engine.open_video_task( occasion="spotlight x", script="s", platforms=["x"], brief="b" ) assert task is not None draft = markers.get_video_draft(task) assert draft is not None assert draft["suggested_input_props"] == {} @pytest.mark.asyncio async def test_open_video_task_acceptance_criteria_has_design_bar_line( db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch ) -> None: await _seed(db_session) _enable(monkeypatch) engine = video_engine_module.VideoEngine(db_session) task = await engine.open_video_task( occasion="release v9.9.9", script="s", platforms=["x"], brief="b" ) assert task is not None assert len(task.acceptance_criteria) == FOUR assert "motion/README.md" in task.acceptance_criteria[2] assert "panel-demo" in task.acceptance_criteria[2] assert "request_render" in task.acceptance_criteria[3] @pytest.mark.asyncio async def test_draft_release_video_dedupes_same_version( db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch ) -> None: await _seed(db_session) _enable(monkeypatch, video_on_release=True) _mock_local_model(monkeypatch, "shipped!") engine = video_engine_module.VideoEngine(db_session) first = await engine.draft_release_video(version="1.0.0", changelog=_CHANGELOG) second = await engine.draft_release_video(version="1.0.0", changelog=_CHANGELOG) assert first is not None assert second is None # --------------------------------------------------------------------------- # # rerender — CEO-triggered clear of the render idempotency keys # --------------------------------------------------------------------------- # @pytest.mark.asyncio async def test_rerender_clears_render_state_keeps_the_rest( db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch ) -> None: await _seed(db_session) _enable(monkeypatch) engine = video_engine_module.VideoEngine(db_session) task = await engine.open_video_task( occasion="rerender-me", script="s", platforms=["x"], brief="b" ) assert task is not None draft = markers.get_video_draft(task) or {} markers.set_video_draft( task, { **draft, "composition_id": "Intro", "render_status": "failed", "render_attempts": THREE, "render_error": "sidecar timeout", }, ) task.status = TS.COMPLETED await db_session.flush() result = await engine.rerender(cast("UUID", task.id)) assert result is not None cleared = markers.get_video_draft(result) assert cleared is not None assert "render_status" not in cleared assert "render_attempts" not in cleared assert "render_error" not in cleared assert cleared["composition_id"] == "Intro" # everything else preserved @pytest.mark.asyncio async def test_rerender_none_for_non_completed_task( db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch ) -> None: await _seed(db_session) _enable(monkeypatch) engine = video_engine_module.VideoEngine(db_session) task = await engine.open_video_task( occasion="not-yet-done", script="s", platforms=["x"], brief="b" ) assert task is not None draft = markers.get_video_draft(task) or {} markers.set_video_draft(task, {**draft, "composition_id": "Intro"}) await db_session.flush() # still PENDING, not COMPLETED result = await engine.rerender(cast("UUID", task.id)) assert result is None @pytest.mark.asyncio async def test_rerender_none_without_composition_id( db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch ) -> None: await _seed(db_session) _enable(monkeypatch) engine = video_engine_module.VideoEngine(db_session) task = await engine.open_video_task( occasion="no-composition-yet", script="s", platforms=["x"], brief="b" ) assert task is not None task.status = TS.COMPLETED await db_session.flush() # no propose_video call yet -> no composition_id result = await engine.rerender(cast("UUID", task.id)) assert result is None @pytest.mark.asyncio async def test_rerender_none_for_missing_task(db_session: AsyncSession) -> None: engine = video_engine_module.VideoEngine(db_session) result = await engine.rerender(uuid4()) assert result is None open_tasks = await get_task_service(db_session).list_open_video_posts() assert open_tasks == [] # --------------------------------------------------------------------------- # # reauthor_from_rejection — CEO reject feedback loop # --------------------------------------------------------------------------- # @pytest.mark.asyncio async def test_reauthor_from_rejection_opens_revision_with_reason_and_pointer( db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch ) -> None: await _seed(db_session) _enable(monkeypatch) engine = video_engine_module.VideoEngine(db_session) source_task = await engine.open_video_task( occasion="release v1.0.0", script="Here's what shipped", platforms=["x", "tiktok"], brief="Announce the release", ) assert source_task is not None draft = markers.get_video_draft(source_task) or {} markers.set_video_draft(source_task, {**draft, "composition_id": "ReleaseIntro"}) # Mirrors production: the render loop only fires once the authoring task # has gone through the full delivery lifecycle to COMPLETED — an open # source task with the same occasion would otherwise dedupe the revision # reauthor is about to open right back against itself. source_task.status = TS.COMPLETED await db_session.flush() post_task = await engine._originate_video_post( source_task=source_task, mp4_paths={"vertical": "a.mp4", "square": "b.mp4"}, captions={"x": "cap", "tiktok": "cap2"}, platforms=["x", "tiktok"], ) # Mirrors VideoPostService.reject: the draft is already CANCELLED by the # time reauthor_from_rejection runs, so open_video_task's own-occasion # dedup (which only scans OPEN drafts) doesn't block against it. post_task.status = TS.CANCELLED await db_session.flush() revision = await engine.reauthor_from_rejection( post_task, "Logo is cut off in the second scene" ) assert revision is not None assert revision.id != post_task.id assert revision.source == VIDEO_SOURCE revision_draft = markers.get_video_draft(revision) assert revision_draft is not None assert revision_draft["occasion"] == "release v1.0.0" # SAME occasion assert revision_draft["platforms"] == ["x", "tiktok"] assert "Logo is cut off in the second scene" in revision_draft["brief"] assert "motion/compositions/ReleaseIntro/" in revision_draft["brief"] assert "do not start a new composition" in revision_draft["brief"] @pytest.mark.asyncio async def test_reauthor_from_rejection_missing_draft_returns_none( db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch ) -> None: await _seed(db_session) _enable(monkeypatch) engine = video_engine_module.VideoEngine(db_session) task = await engine.open_video_task( occasion="no-draft", script="s", platforms=["x"], brief="b" ) assert task is not None task.orchestration_markers = {} # strip the video_draft marker await db_session.flush() result = await engine.reauthor_from_rejection(task, "some reason") assert result is None # --------------------------------------------------------------------------- # # scene criterion — a brief-named feature list becomes its own gate-checkable # AC (delegation detail-fidelity: prose feature counts used to pass gates # even when a dev shipped fewer scenes than the brief named) # --------------------------------------------------------------------------- # @pytest.mark.asyncio async def test_draft_release_video_carries_scene_criterion_for_highlights( db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch ) -> None: await _seed(db_session) _enable(monkeypatch, video_on_release=True) _mock_local_model(monkeypatch, "shipped!") engine = video_engine_module.VideoEngine(db_session) task = await engine.draft_release_video(version="1.0.0", changelog=_CHANGELOG) assert task is not None assert len(task.acceptance_criteria) == FIVE scene_ac = task.acceptance_criteria[FOUR] assert scene_ac.startswith("Every brief-named feature appears as its own") assert "a huge new release" in scene_ac @pytest.mark.asyncio async def test_open_video_task_no_highlights_keeps_ac_count_unchanged( db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch ) -> None: await _seed(db_session) _enable(monkeypatch) engine = video_engine_module.VideoEngine(db_session) task = await engine.open_video_task( occasion="spotlight no-highlights", script="s", platforms=["x"], brief="b", suggested_input_props={"version": "1.0.0"}, # no "highlights" key ) assert task is not None assert len(task.acceptance_criteria) == FOUR # no scene criterion appended @pytest.mark.asyncio async def test_reauthor_from_rejection_with_highlights_regenerates_scene_criterion( db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch ) -> None: await _seed(db_session) _enable(monkeypatch) engine = video_engine_module.VideoEngine(db_session) source_task = await engine.open_video_task( occasion="release v3.0.0", script="s", platforms=["x"], brief="b", suggested_input_props={ "version": "3.0.0", "highlights": ["Feature A", "Feature B"], }, ) assert source_task is not None draft = markers.get_video_draft(source_task) or {} # Simulate propose_video: the dev's real input_props carries the same # highlights forward onto the marker. markers.set_video_draft( source_task, { **draft, "composition_id": "ReleaseIntro", "input_props": { "version": "3.0.0", "highlights": ["Feature A", "Feature B"], }, }, ) source_task.status = TS.COMPLETED await db_session.flush() post_task = await engine._originate_video_post( source_task=source_task, mp4_paths={"vertical": "a.mp4", "square": "b.mp4"}, captions={"x": "cap"}, platforms=["x"], ) post_task.status = TS.CANCELLED await db_session.flush() revision = await engine.reauthor_from_rejection(post_task, "Logo too small") assert revision is not None assert len(revision.acceptance_criteria) == FIVE scene_ac = revision.acceptance_criteria[FOUR] assert "Feature A" in scene_ac assert "Feature B" in scene_ac @pytest.mark.asyncio async def test_reauthor_from_rejection_without_highlights_adds_feedback_criterion( db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch ) -> None: await _seed(db_session) _enable(monkeypatch) engine = video_engine_module.VideoEngine(db_session) source_task = await engine.open_video_task( occasion="release v4.0.0", script="s", platforms=["x"], brief="b" ) assert source_task is not None draft = markers.get_video_draft(source_task) or {} markers.set_video_draft(source_task, {**draft, "composition_id": "Intro"}) source_task.status = TS.COMPLETED await db_session.flush() post_task = await engine._originate_video_post( source_task=source_task, mp4_paths={"vertical": "a.mp4", "square": "b.mp4"}, captions={"x": "cap"}, platforms=["x"], ) post_task.status = TS.CANCELLED await db_session.flush() revision = await engine.reauthor_from_rejection(post_task, "Logo is cut off") assert revision is not None assert len(revision.acceptance_criteria) == FIVE assert revision.acceptance_criteria[FOUR] == ( "Every point in the CEO rejection feedback is visibly addressed in " "the rendered cut" ) @pytest.mark.asyncio async def test_open_video_task_scene_criterion_truncates_pathological_feature_list( db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch ) -> None: await _seed(db_session) _enable(monkeypatch) engine = video_engine_module.VideoEngine(db_session) features = [f"Feature number {i}" for i in range(50)] task = await engine.open_video_task( occasion="release pathological", script="s", platforms=["x"], brief="b", suggested_input_props={"highlights": features}, ) assert task is not None assert len(task.acceptance_criteria) <= SEVEN scene_ac = task.acceptance_criteria[-1] assert len(scene_ac) <= _AC_ITEM_CHAR_CAP assert "more)" in scene_ac # truncated, not silently dropped