2026-07-05 13:37:17 +02:00
|
|
|
"""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
|
|
|
|
|
|
2026-07-11 07:39:17 +02:00
|
|
|
from typing import TYPE_CHECKING, cast
|
2026-07-05 13:37:17 +02:00
|
|
|
from unittest.mock import AsyncMock
|
2026-07-11 07:39:17 +02:00
|
|
|
from uuid import UUID, uuid4
|
2026-07-05 13:37:17 +02:00
|
|
|
|
|
|
|
|
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
|
2026-07-09 08:31:08 +02:00
|
|
|
from roboco.services.company_goals import get_company_goals_service
|
2026-07-05 13:37:17 +02:00
|
|
|
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
|
2026-07-09 08:31:08 +02:00
|
|
|
THREE = 3
|
2026-07-05 13:37:17 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
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,
|
2026-07-07 10:09:23 +02:00
|
|
|
video_engine_enabled=True,
|
2026-07-05 13:37:17 +02:00
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
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
|
2026-07-09 08:31:08 +02:00
|
|
|
# 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]
|
2026-07-05 13:37:17 +02:00
|
|
|
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"]
|
2026-07-09 08:31:08 +02:00
|
|
|
# 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"]
|
2026-07-05 13:37:17 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@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() == []
|
|
|
|
|
|
|
|
|
|
|
2026-07-07 10:09:23 +02:00
|
|
|
@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() == []
|
|
|
|
|
|
|
|
|
|
|
2026-07-05 13:37:17 +02:00
|
|
|
@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,
|
2026-07-07 10:09:23 +02:00
|
|
|
video_engine_enabled=True,
|
2026-07-05 13:37:17 +02:00
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
2026-07-11 07:39:17 +02:00
|
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
# 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
|
|
|
|
|
|
|
|
|
|
|
2026-07-05 13:37:17 +02:00
|
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
# _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."
|
2026-07-09 08:31:08 +02:00
|
|
|
# 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"],
|
|
|
|
|
}
|
2026-07-05 13:37:17 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@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"]
|
2026-07-09 08:31:08 +02:00
|
|
|
# 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"]
|
2026-07-05 13:37:17 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@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."
|
2026-07-09 08:31:08 +02:00
|
|
|
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]
|
2026-07-05 13:37:17 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@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
|
2026-07-11 07:39:17 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
# 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
|
2026-07-05 13:37:17 +02:00
|
|
|
open_tasks = await get_task_service(db_session).list_open_video_posts()
|
2026-07-11 07:39:17 +02:00
|
|
|
assert open_tasks == []
|