mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
* 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>
238 lines
7.9 KiB
Python
238 lines
7.9 KiB
Python
"""Tier 1 — tracing Requirement enum + check_requirements scaffolding."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
from roboco.foundation.policy import tracing
|
|
|
|
|
|
def test_requirement_enum_has_canonical_values() -> None:
|
|
"""Required-set vocabulary mirrors the pre-Phase-2 tracing_gate.py
|
|
PLUS the new pre-gateway parity additions."""
|
|
expected = {
|
|
"plan",
|
|
"commits>=1",
|
|
"pr_open",
|
|
"progress>=1",
|
|
"journal:reflect",
|
|
"journal:decision",
|
|
"journal:learning",
|
|
"journal:struggle",
|
|
"journal:note_at_claim",
|
|
"journal:decision_at_claim",
|
|
"journal:during_work>=1",
|
|
"acceptance_criteria_addressed",
|
|
"qa_notes>=min",
|
|
"qa_evidence_inspected",
|
|
"docs_notes>=min",
|
|
"docs_files_non_empty",
|
|
"self_verified",
|
|
"notes>=min",
|
|
"subtasks_terminal",
|
|
"dev_notes>=min",
|
|
"pr_reviewer_notes>=min",
|
|
"quick_context>=min",
|
|
"findings_addressed",
|
|
"render_verified",
|
|
}
|
|
actual = {r.value for r in tracing.Requirement}
|
|
assert actual == expected, f"Requirement drift: {actual ^ expected}"
|
|
|
|
|
|
def test_gate_context_has_journal_presence_flags() -> None:
|
|
ctx = tracing.GateContext()
|
|
assert ctx.journal_reflect_present is False
|
|
assert ctx.journal_decision_present is False
|
|
assert ctx.journal_learning_present is False
|
|
assert ctx.journal_struggle_present is False
|
|
assert ctx.journal_note_at_claim_present is False
|
|
assert ctx.journal_during_work_count == 0
|
|
|
|
|
|
def test_gate_result_has_passed_and_missing() -> None:
|
|
result = tracing.GateResult(passed=True)
|
|
assert result.passed is True
|
|
assert result.missing == []
|
|
|
|
|
|
def test_docs_notes_checker_reads_doc_notes_not_dev_notes() -> None:
|
|
"""Regression: _check_docs_notes_min_chars must read ``doc_notes`` (the
|
|
documenter's section), not ``dev_notes`` (the developer's)."""
|
|
ctx = tracing.GateContext(docs_notes_min_chars=20)
|
|
# A long dev_notes must NOT satisfy the docs requirement.
|
|
only_dev = SimpleNamespace(dev_notes="x" * 50, doc_notes="")
|
|
assert tracing._check_docs_notes_min_chars(only_dev, ctx) == ["docs_notes>=min"]
|
|
# A long doc_notes satisfies it.
|
|
has_doc = SimpleNamespace(dev_notes="", doc_notes="y" * 25)
|
|
assert tracing._check_docs_notes_min_chars(has_doc, ctx) == []
|
|
|
|
|
|
def test_note_section_checkers_read_their_own_fields() -> None:
|
|
ctx = tracing.GateContext() # defaults: dev 40, pr_reviewer 40, quick_context 30
|
|
assert (
|
|
tracing._check_dev_notes_min_chars(SimpleNamespace(dev_notes="z" * 40), ctx)
|
|
== []
|
|
)
|
|
assert tracing._check_dev_notes_min_chars(
|
|
SimpleNamespace(dev_notes="z" * 39), ctx
|
|
) == ["dev_notes>=min"]
|
|
assert (
|
|
tracing._check_pr_reviewer_notes_min_chars(
|
|
SimpleNamespace(pr_reviewer_notes="z" * 40), ctx
|
|
)
|
|
== []
|
|
)
|
|
assert tracing._check_quick_context_min_chars(
|
|
SimpleNamespace(quick_context="z" * 29), ctx
|
|
) == ["quick_context>=min"]
|
|
|
|
|
|
def test_note_section_obligations_wired_to_verbs() -> None:
|
|
assert tracing.Requirement.DEV_NOTES_MIN_CHARS in tracing.requirements_for(
|
|
"i_am_done"
|
|
)
|
|
assert tracing.Requirement.QUICK_CONTEXT_MIN_CHARS in tracing.requirements_for(
|
|
"delegate"
|
|
)
|
|
for verb in ("pr_pass", "pr_fail", "post_pr_review"):
|
|
assert tracing.Requirement.PR_REVIEWER_NOTES_MIN_CHARS in (
|
|
tracing.requirements_for(verb)
|
|
)
|
|
|
|
|
|
def test_check_requirements_passes_when_all_satisfied() -> None:
|
|
task = SimpleNamespace(
|
|
plan={"x": 1},
|
|
commits=[{"sha": "abc"}],
|
|
pr_number=42,
|
|
progress_updates=[{"message": "x"}],
|
|
acceptance_criteria=[],
|
|
acceptance_criteria_status=[],
|
|
)
|
|
ctx = tracing.GateContext(journal_reflect_present=True)
|
|
result = tracing.check_requirements(
|
|
task=task,
|
|
requirements=[
|
|
tracing.Requirement.PLAN,
|
|
tracing.Requirement.COMMITS_AT_LEAST_ONE,
|
|
tracing.Requirement.PR_OPEN,
|
|
tracing.Requirement.JOURNAL_REFLECT,
|
|
],
|
|
ctx=ctx,
|
|
)
|
|
assert result.passed is True
|
|
|
|
|
|
def test_check_requirements_returns_missing_keys_on_failure() -> None:
|
|
task = SimpleNamespace(
|
|
plan=None,
|
|
commits=[],
|
|
pr_number=None,
|
|
progress_updates=[],
|
|
acceptance_criteria=[],
|
|
acceptance_criteria_status=[],
|
|
)
|
|
result = tracing.check_requirements(
|
|
task=task,
|
|
requirements=[
|
|
tracing.Requirement.PLAN,
|
|
tracing.Requirement.COMMITS_AT_LEAST_ONE,
|
|
tracing.Requirement.PR_OPEN,
|
|
tracing.Requirement.JOURNAL_REFLECT,
|
|
],
|
|
)
|
|
assert result.passed is False
|
|
assert "plan" in result.missing
|
|
assert "commits>=1" in result.missing
|
|
assert "pr_open" in result.missing
|
|
assert "journal:reflect" in result.missing
|
|
|
|
|
|
def test_acceptance_criteria_check_treats_reflect_note_as_addressing_artifact() -> None:
|
|
"""Spec §9 item 1: reflect-note clears the criteria gate."""
|
|
task = SimpleNamespace(
|
|
acceptance_criteria=["AC1", "AC2"],
|
|
acceptance_criteria_status=[],
|
|
)
|
|
ctx = tracing.GateContext(journal_reflect_present=True)
|
|
result = tracing.check_requirements(
|
|
task=task,
|
|
requirements=[tracing.Requirement.ACCEPTANCE_CRITERIA_ADDRESSED],
|
|
ctx=ctx,
|
|
)
|
|
assert result.passed is True
|
|
|
|
|
|
def test_during_work_count_satisfies_requirement() -> None:
|
|
task = SimpleNamespace()
|
|
ctx = tracing.GateContext(journal_during_work_count=1)
|
|
result = tracing.check_requirements(
|
|
task=task,
|
|
requirements=[tracing.Requirement.JOURNAL_DURING_WORK_AT_LEAST_ONE],
|
|
ctx=ctx,
|
|
)
|
|
assert result.passed is True
|
|
|
|
|
|
def test_verb_requirements_covers_pm_decision_chain() -> None:
|
|
"""The 6 inline journal:decision callsites' verbs all require it."""
|
|
for verb in (
|
|
"submit_up",
|
|
"complete",
|
|
"unblock",
|
|
"escalate_up",
|
|
"escalate_to_ceo",
|
|
"delegate",
|
|
):
|
|
reqs = tracing.requirements_for(verb)
|
|
assert tracing.Requirement.JOURNAL_DECISION in reqs, (
|
|
f"{verb} should require journal:decision per spec §11"
|
|
)
|
|
|
|
|
|
def test_verb_requirements_covers_dev_completion_chain() -> None:
|
|
reqs = tracing.requirements_for("i_am_done")
|
|
assert tracing.Requirement.COMMITS_AT_LEAST_ONE in reqs
|
|
assert tracing.Requirement.PR_OPEN in reqs
|
|
assert tracing.Requirement.PROGRESS_AT_LEAST_ONE in reqs
|
|
assert tracing.Requirement.JOURNAL_REFLECT in reqs
|
|
assert tracing.Requirement.JOURNAL_DURING_WORK_AT_LEAST_ONE in reqs
|
|
assert tracing.Requirement.ACCEPTANCE_CRITERIA_ADDRESSED in reqs
|
|
|
|
|
|
def test_verb_requirements_includes_pre_gateway_parity_at_claim() -> None:
|
|
assert tracing.Requirement.JOURNAL_NOTE_AT_CLAIM in tracing.requirements_for(
|
|
"i_will_work_on"
|
|
)
|
|
assert tracing.Requirement.JOURNAL_DECISION_AT_CLAIM in tracing.requirements_for(
|
|
"i_will_plan"
|
|
)
|
|
|
|
|
|
def test_pm_complete_requires_both_decision_and_reflect() -> None:
|
|
"""Pre-gateway parity P4: PMs wrote both before complete."""
|
|
reqs = tracing.requirements_for("complete")
|
|
assert tracing.Requirement.JOURNAL_DECISION in reqs
|
|
assert tracing.Requirement.JOURNAL_REFLECT in reqs
|
|
|
|
|
|
def test_qa_pass_review_requires_learning() -> None:
|
|
reqs = tracing.requirements_for("pass_review")
|
|
assert tracing.Requirement.QA_NOTES_MIN_CHARS in reqs
|
|
assert tracing.Requirement.QA_EVIDENCE_INSPECTED in reqs
|
|
assert tracing.Requirement.JOURNAL_LEARNING in reqs
|
|
|
|
|
|
def test_i_am_blocked_requires_struggle_journal() -> None:
|
|
"""Lifts JOURNAL_STRUGGLE out of dangling-enum status."""
|
|
assert tracing.Requirement.JOURNAL_STRUGGLE in tracing.requirements_for(
|
|
"i_am_blocked"
|
|
)
|
|
|
|
|
|
def test_requirements_for_unknown_verb_raises_key_error() -> None:
|
|
with pytest.raises(KeyError):
|
|
tracing.requirements_for("not_a_real_verb")
|