Files
roboco/tests/unit/services/test_video_engine.py
T

821 lines
30 KiB
Python
Raw Normal View History

"""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
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.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
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
# --------------------------------------------------------------------------- #
# 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) == THREE
assert "motion/README.md" in task.acceptance_criteria[2]
assert "panel-demo" in task.acceptance_criteria[2]
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 "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_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_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"]
# --------------------------------------------------------------------------- #
# 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) == THREE
assert "motion/README.md" in task.acceptance_criteria[2]
assert "panel-demo" in task.acceptance_criteria[2]
@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 == []