Files
ECC/tests/test_taste_blender.py
T
928c1dea72 feat(tasteforge): package reusable workflows and preserve native edits (#3033)
* feat: bundle standalone taste distillation and application workflows

* docs: fix imported taste skill markdown lint

* docs: align Turkish agent catalog with taste skills

* refactor: make ECC the canonical reusable video engine

* fix: preserve video duration when applying image overlays

* fix: preserve background colors in image compositing

* fix: report best-effort duration targets and shortfalls

* feat: ship verified Fusion presets with compatibility provenance

* feat(tasteforge): preserve native edits in application bundles

* feat(tasteforge): compile local preservation without hosted input

* fix: update js-yaml to patched 4.3.2

* test: report bounded Stop wrapper failure diagnostics

* fix(tasteforge): fail closed on unsafe output names, missing overlays and cadence

- cli: default report and spec paths are derived from pack name and profile
  genre; require the manifest's name pattern before using either as a
  filename part so a traversal string cannot write outside cwd/out.
- apply_local: a pack without cadence.json, or with no measured shots and
  no explicit mean_shot, raises instead of silently planning 1.0s shots and
  reporting a measured cadence.
- legacy apply: a missing overlay aborts before any paid upload; forge()
  would have rejected it after every take was generated.
- requirements-live: pin fal-client>=0.13.0, the first release whose
  subscribe() accepts client_timeout.

Addresses the five P1 findings from the independent review of #3033.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015fxHRsydPqEcYngGbqkgt1

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 15:31:36 +01:00

90 lines
3.5 KiB
Python

"""Original Blender workflow boundary tests without importing bpy."""
import importlib.util
import tempfile
import unittest
from pathlib import Path
from types import SimpleNamespace
SCRIPT = (
Path(__file__).resolve().parents[1]
/ "skills/taste-application/scripts/blender_prop.py"
)
spec = importlib.util.spec_from_file_location("blender_prop", SCRIPT)
prop = importlib.util.module_from_spec(spec)
spec.loader.exec_module(prop)
class BlenderPropTests(unittest.TestCase):
def test_frame_geometry_validation(self):
for frames, width, height, fps in [
(0, 640, 480, 30),
(48, 0, 480, 30),
(48, 640, 480, float("nan")),
(True, 640, 480, 30),
(48, 640, 480, 0),
]:
with self.subTest(frames=frames, fps=fps), self.assertRaises(ValueError):
prop.validate_settings(frames, width, height, fps)
prop.validate_settings(48, 1920, 1080, 29.97)
def test_legacy_and_layered_fcurves(self):
curve = SimpleNamespace(
keyframe_points=[SimpleNamespace(interpolation="BEZIER")]
)
old = SimpleNamespace(fcurves=[curve])
prop.linearize_action(old)
self.assertEqual(curve.keyframe_points[0].interpolation, "LINEAR")
curve.keyframe_points[0].interpolation = "BEZIER"
bag = SimpleNamespace(fcurves=[curve])
strip = SimpleNamespace(channelbags=[bag])
new = SimpleNamespace(layers=[SimpleNamespace(strips=[strip])])
prop.linearize_action(new)
self.assertEqual(curve.keyframe_points[0].interpolation, "LINEAR")
def test_output_cannot_overwrite_or_follow_symlinks(self):
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary).resolve()
target = root / "scene.blend"
prop.validate_output(target)
target.touch()
with self.assertRaises(ValueError):
prop.validate_output(target)
link = root / "link.blend"
link.symlink_to(target)
with self.assertRaises(ValueError):
prop.validate_output(link)
def test_geometry_rejects_nonfinite_and_empty(self):
for lower, upper in [((0, 0, 0), (0, 0, 0)), ((0, 0, 0), (float("inf"), 1, 1))]:
with self.assertRaises(ValueError):
prop.validate_bounds(lower, upper)
prop.validate_bounds((-1, -1, -1), (1, 1, 1))
def test_landscape_camera_preserves_sphere_fit(self):
self.assertAlmostEqual(
prop.camera_distance(1, 1024, 1024), (3.2**2 + 0.8**2) ** 0.5
)
self.assertGreater(
prop.camera_distance(1, 1920, 1080), prop.camera_distance(1, 1024, 1024)
)
def test_render_receipt_requires_every_frame_and_finished_status(self):
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
with self.assertRaises(RuntimeError):
prop.verify_render({"FINISHED"}, root, 2)
for frame in (1, 2):
(root / f"turn_{frame:04d}.png").write_bytes(b"png")
with self.assertRaises(RuntimeError):
prop.verify_render({"CANCELLED"}, root, 2)
prop.verify_render({"FINISHED"}, root, 2)
(root / "turn_0002.png").write_bytes(b"")
with self.assertRaises(RuntimeError):
prop.verify_render({"FINISHED"}, root, 2)
def test_lab_neutral_white(self):
self.assertTrue(
all(0.99 <= value <= 1 for value in prop._lab_to_linear_srgb(100, 0, 0))
)