mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
Feature/video artifact verification (#537)
* fix(release): CI wait polls the prod rung; escape the header tooltip apostrophe get_latest_ci_conclusion defaults to the ladder's head rung, so wait_for_ci searched slave for a release commit that lives on master and timed out after 40 minutes with the run already green. The wait now passes the prod branch explicitly. Also fixes the react/no-unescaped-entities error that turned master's Panel CI red. * fix(panel,video): dead dialog triggers behind tooltips; dotted composition ids render HelpTip nested inside a Dialog/AlertDialog trigger puts the trigger's click handler on the Tooltip root, which renders no DOM — the agents Spawn item and the KB Reindex-All / Delete-index confirms were dead. Tooltips now wrap the triggers. The video renderer accepts interior single dots in composition ids (release-0.25.0) with '..' still unrepresentable, and propose_video refuses an unrenderable id at authoring time. * fix(dispatch): restart-safe PM review turns A leaf task in awaiting_pm_review had no periodic pickup: the closure dispatcher bailed on childless tasks and skipped PR-bearing review tasks as already-promoted, assuming the submit-time PM session was still alive — an assumption every restart breaks. Proven live on the docs-sync leaf after the 0.25.0 redeploy, which also dependency-blocked its sibling dev task. Childless awaiting_pm_review tasks now flow to the PM's review turn, and the merge turn respawns its PM when none is active. * feat(video): verify the rendered artifact, not the source The 14s release-0.25.0 cut shipped with only one of four scenes visibly registering: the dev authored DOM, the smoke asserted DOM, QA read code — nobody consumed the rendered MP4 before the CEO did. Close that loop, and the reject loop behind it: - sidecar frames mode: POST /render with frames=1..32 renders the cut, ffprobes the REAL duration, extracts midpoint-sampled keyframe PNGs (timestamps in filenames), streams a tar.gz back with X-Video-Duration - request_render do-verb (developer/QA, request_sandbox's shape): renders the caller's ACTUAL composition — dev's own worktree (head_sha/dirty provenance), QA a read-only git-archive export of the assembled branch — extracts frames to the container-shared .previews/ path, stamps the render_preview marker, returns the paths as envelope evidence - gate: i_am_done on a source=video task refuses without a stamped render_preview (Requirement.RENDER_VERIFIED; canonical source string moved to foundation as markers.VIDEO_TASK_SOURCE; mirrored in the possibilities-matrix fast path so it cannot bypass the check) - QA claim_review evidence carries video_context (composition id, the dev's preview, a re-render instruction) so review checks output - dev spawn prompt block + a 4th authoring AC order Read-every-frame verification before submitting - reject -> re-author: a CEO reject with a reason opens a fresh authoring task carrying the verbatim feedback + a revise-in-place pointer at the existing composition (best-effort, never fails the reject) — rejection feedback no longer dies on the cancelled draft E2E: rendered the committed release-0.25.0 composition through the new frames mode locally — the returned keyframes show exactly the reported failure (blank frame at 5.8s, only 'Env ladder' by 12.8s), the check the fleet was missing. --------- Co-authored-by: Renn F <rennf93@users.noreply.github.com>
This commit is contained in:
@@ -35,6 +35,7 @@ SLUG = "roboco"
|
||||
ONE = 1
|
||||
TWO = 2
|
||||
THREE = 3
|
||||
FOUR = 4
|
||||
|
||||
|
||||
async def _seed(session: AsyncSession) -> None:
|
||||
@@ -138,9 +139,11 @@ async def test_open_video_task_creates_assigned_authoring_task(
|
||||
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 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
|
||||
@@ -154,6 +157,7 @@ async def test_open_video_task_creates_assigned_authoring_task(
|
||||
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"]
|
||||
@@ -715,9 +719,10 @@ async def test_open_video_task_acceptance_criteria_has_design_bar_line(
|
||||
occasion="release v9.9.9", script="s", platforms=["x"], brief="b"
|
||||
)
|
||||
assert task is not None
|
||||
assert len(task.acceptance_criteria) == THREE
|
||||
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
|
||||
@@ -818,3 +823,76 @@ async def test_rerender_none_for_missing_task(db_session: AsyncSession) -> None:
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user