mirror of
https://github.com/calesthio/OpenMontage.git
synced 2026-08-26 01:52:30 +08:00
Merge remote-tracking branch 'origin/main' into codex/repair-pr-442
# Conflicts: # .env.example
This commit is contained in:
31
tests/contracts/test_agent_instruction_integrity.py
Normal file
31
tests/contracts/test_agent_instruction_integrity.py
Normal file
@@ -0,0 +1,31 @@
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
def _read(relative_path: str) -> str:
|
||||
return (REPO_ROOT / relative_path).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_music_plans_discover_all_music_capabilities() -> None:
|
||||
instruction_files = [
|
||||
"AGENT_GUIDE.md",
|
||||
"skills/pipelines/cinematic/idea-director.md",
|
||||
"skills/pipelines/cinematic/proposal-director.md",
|
||||
"skills/pipelines/documentary-montage/idea-director.md",
|
||||
"skills/pipelines/explainer/proposal-director.md",
|
||||
]
|
||||
|
||||
for relative_path in instruction_files:
|
||||
text = _read(relative_path)
|
||||
for capability in ("music_library", "music_search", "music_generation"):
|
||||
assert f'get_by_capability("{capability}")' in text, (
|
||||
f"{relative_path} omits the {capability!r} music source"
|
||||
)
|
||||
|
||||
|
||||
def test_explainer_directors_do_not_reference_fictitious_submit_functions() -> None:
|
||||
for stage in ("idea", "script", "scene"):
|
||||
text = _read(f"skills/pipelines/explainer/{stage}-director.md")
|
||||
assert "handle_explainer_" not in text
|
||||
@@ -6,6 +6,7 @@ import json
|
||||
import pytest
|
||||
|
||||
from lib.checkpoint import (
|
||||
CANONICAL_STAGE_ARTIFACTS,
|
||||
CheckpointValidationError,
|
||||
HISTORY_DIRNAME,
|
||||
PROJECT_MARKER_FILENAME,
|
||||
@@ -27,6 +28,22 @@ def _minimal_script() -> dict:
|
||||
}
|
||||
|
||||
|
||||
def _approve_predecessors(tmp_path, project_id, pipeline_type, *stages) -> None:
|
||||
from tests.contracts.test_phase0_contracts import sample_artifact
|
||||
|
||||
for stage in stages:
|
||||
artifact_name = CANONICAL_STAGE_ARTIFACTS[stage]
|
||||
write_checkpoint(
|
||||
tmp_path,
|
||||
project_id,
|
||||
stage,
|
||||
"completed",
|
||||
artifacts={artifact_name: sample_artifact(artifact_name)},
|
||||
pipeline_type=pipeline_type,
|
||||
human_approved=True,
|
||||
)
|
||||
|
||||
|
||||
class TestGateEnforcement:
|
||||
"""GI-4: gated stages cannot be completed without approval evidence."""
|
||||
|
||||
@@ -39,6 +56,9 @@ class TestGateEnforcement:
|
||||
)
|
||||
|
||||
def test_awaiting_human_is_the_correct_gate_state(self, tmp_path):
|
||||
_approve_predecessors(
|
||||
tmp_path, "proj", "animated-explainer", "research", "proposal"
|
||||
)
|
||||
path = write_checkpoint(
|
||||
tmp_path, "proj", "script", "awaiting_human",
|
||||
artifacts={"script": _minimal_script()},
|
||||
@@ -51,6 +71,9 @@ class TestGateEnforcement:
|
||||
assert cp["human_approval_required"] is True
|
||||
|
||||
def test_completed_with_approval_passes(self, tmp_path):
|
||||
_approve_predecessors(
|
||||
tmp_path, "proj", "animated-explainer", "research", "proposal"
|
||||
)
|
||||
path = write_checkpoint(
|
||||
tmp_path, "proj", "script", "completed",
|
||||
artifacts={"script": _minimal_script()},
|
||||
@@ -84,6 +107,9 @@ class TestCheckpointHistory:
|
||||
"""Superseded checkpoints are archived, not destroyed."""
|
||||
|
||||
def test_overwrite_archives_previous(self, tmp_path):
|
||||
_approve_predecessors(
|
||||
tmp_path, "proj", "animated-explainer", "research", "proposal"
|
||||
)
|
||||
write_checkpoint(
|
||||
tmp_path, "proj", "script", "awaiting_human",
|
||||
artifacts={"script": _minimal_script()},
|
||||
|
||||
19
tests/contracts/test_env_example.py
Normal file
19
tests/contracts/test_env_example.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from pathlib import Path
|
||||
|
||||
from dotenv import dotenv_values
|
||||
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
def test_env_example_does_not_turn_comments_into_credentials() -> None:
|
||||
"""A fresh copy of .env.example must leave every documented key unset."""
|
||||
|
||||
values = dotenv_values(REPO_ROOT / ".env.example")
|
||||
false_credentials = {
|
||||
key: value
|
||||
for key, value in values.items()
|
||||
if isinstance(value, str) and value.lstrip().startswith("#")
|
||||
}
|
||||
|
||||
assert false_credentials == {}
|
||||
@@ -174,10 +174,14 @@ class TestPhase2ErrorHandling:
|
||||
# Either succeeds (provider available) or fails gracefully
|
||||
assert isinstance(r, ToolResult)
|
||||
|
||||
def test_diagram_gen_empty_boxes(self):
|
||||
def test_diagram_gen_empty_boxes(self, tmp_path):
|
||||
tool = DiagramGen()
|
||||
if tool.get_status() == ToolStatus.AVAILABLE:
|
||||
r = tool.execute({"diagram_type": "boxes", "boxes": []})
|
||||
r = tool.execute({
|
||||
"diagram_type": "boxes",
|
||||
"boxes": [],
|
||||
"output_path": str(tmp_path / "empty-boxes.png"),
|
||||
})
|
||||
assert isinstance(r, ToolResult)
|
||||
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import builtins
|
||||
import base64
|
||||
import os
|
||||
import shutil
|
||||
from types import SimpleNamespace
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
@@ -571,16 +572,26 @@ class TestVeoVideo:
|
||||
called_kwargs = mock_client.models.generate_videos.call_args[1]
|
||||
assert called_kwargs["image"] is not None
|
||||
|
||||
def test_vertex_ai_mode_rejection(self):
|
||||
def test_vertex_ai_mode_requires_inline_video_bytes(self):
|
||||
tool = VeoVideo()
|
||||
mock_client = MagicMock()
|
||||
mock_client.vertexai = True
|
||||
if hasattr(mock_client, "_api_client"):
|
||||
delattr(mock_client, "_api_client")
|
||||
mock_client.models.generate_videos.return_value = SimpleNamespace(
|
||||
done=True,
|
||||
error=None,
|
||||
response=SimpleNamespace(
|
||||
generated_videos=[
|
||||
SimpleNamespace(video=SimpleNamespace(video_bytes=None))
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
with (
|
||||
patch.dict(os.environ, {"GEMINI_API_KEY": "test_key"}),
|
||||
patch("google.genai.Client", return_value=mock_client),
|
||||
patch(
|
||||
"tools.google_credentials.get_genai_client",
|
||||
return_value=mock_client,
|
||||
),
|
||||
):
|
||||
inputs = {
|
||||
"prompt": "cinematic shot",
|
||||
@@ -589,7 +600,7 @@ class TestVeoVideo:
|
||||
res = tool.execute(inputs)
|
||||
assert res.success is False
|
||||
assert res.error is not None
|
||||
assert "only supported using the Gemini Developer API" in res.error
|
||||
assert "without inline bytes" in res.error
|
||||
|
||||
def test_missing_local_image_paths(self):
|
||||
tool = VeoVideo()
|
||||
|
||||
9
tests/contracts/test_pipeline_manifest_categories.py
Normal file
9
tests/contracts/test_pipeline_manifest_categories.py
Normal file
@@ -0,0 +1,9 @@
|
||||
"""Contracts for the category vocabulary used by shipped pipelines."""
|
||||
|
||||
from lib.pipeline_loader import load_pipeline
|
||||
|
||||
|
||||
def test_documentary_pipeline_uses_a_schema_supported_category() -> None:
|
||||
manifest = load_pipeline("documentary-montage")
|
||||
|
||||
assert manifest["category"] == "documentary"
|
||||
28
tests/contracts/test_remotion_video_transition_contract.py
Normal file
28
tests/contracts/test_remotion_video_transition_contract.py
Normal file
@@ -0,0 +1,28 @@
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
def test_video_scene_honors_hard_cut_tokens_and_backing_color() -> None:
|
||||
source = (REPO_ROOT / "remotion-composer/src/Explainer.tsx").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
|
||||
assert '["cut", "none"].includes((transitionIn || "").toLowerCase())' in source
|
||||
assert '["cut", "none"].includes((transitionOut || "").toLowerCase())' in source
|
||||
assert "transitionIn={cut.transition_in}" in source
|
||||
assert "transitionOut={cut.transition_out}" in source
|
||||
assert "sceneDurationSeconds={cut.out_seconds - cut.in_seconds}" in source
|
||||
assert "Math.round(sceneDurationSeconds * fps)" in source
|
||||
assert "durationInFrames - transitionFrames" in source
|
||||
assert "backgroundColor={cut.backgroundColor}" in source
|
||||
|
||||
|
||||
def test_cinematic_fades_are_bounded_by_each_scene_duration() -> None:
|
||||
source = (REPO_ROOT / "remotion-composer/src/CinematicRenderer.tsx").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
|
||||
assert "Math.round(scene.durationSeconds * fps)" in source
|
||||
assert "durationInFrames - fadeOutFrames" in source
|
||||
30
tests/contracts/test_utf8_file_io.py
Normal file
30
tests/contracts/test_utf8_file_io.py
Normal file
@@ -0,0 +1,30 @@
|
||||
import ast
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
RUNTIME_FILES = [
|
||||
"lib/checkpoint.py",
|
||||
"lib/pipeline_loader.py",
|
||||
"schemas/artifacts/__init__.py",
|
||||
"styles/playbook_loader.py",
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("relative_path", RUNTIME_FILES)
|
||||
def test_pipeline_contract_files_use_explicit_utf8(relative_path: str) -> None:
|
||||
path = REPO_ROOT / relative_path
|
||||
tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
|
||||
|
||||
bare_opens = []
|
||||
for node in ast.walk(tree):
|
||||
if not isinstance(node, ast.Call):
|
||||
continue
|
||||
if not isinstance(node.func, ast.Name) or node.func.id != "open":
|
||||
continue
|
||||
if not any(keyword.arg == "encoding" for keyword in node.keywords):
|
||||
bare_opens.append(node.lineno)
|
||||
|
||||
assert bare_opens == [], f"bare open() calls at lines {bare_opens}"
|
||||
Reference in New Issue
Block a user