Files
ECC/tests/test_taste_mint3d.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

111 lines
4.4 KiB
Python

"""Mocked minting regressions; no optional renderer or provider is needed."""
import importlib.util
import io
import tempfile
import unittest
from contextlib import redirect_stdout
from pathlib import Path
from types import ModuleType, SimpleNamespace
from unittest.mock import Mock, patch
SCRIPT = (
Path(__file__).resolve().parents[1] / "skills/taste-application/scripts/mint3d.py"
)
class MintPreservationTests(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.addCleanup(self.tmp.cleanup)
self.root = Path(self.tmp.name)
self.props = self.root / "props"
self.props.mkdir()
self.fal = Mock()
self.fal.FalError = RuntimeError
self.fal.ENDPOINTS = dict.fromkeys(
("image_to_3d", "text_to_3d", "retopology", "part_split"), "model"
)
self.fal.is_dry_run.return_value = False
self.fal.text_to_3d.return_value = "https://v3.fal.media/fullpbr.glb"
self.fal.retopologize.return_value = "https://v3.fal.media/proxy.glb"
self.fal.download.side_effect = lambda url, dest: Path(dest).write_text(url)
pack = SimpleNamespace(
dir=self.root, spec_path=self.root / "spec.json", read_json=lambda _: {}
)
self.loader = Mock(return_value=pack)
self.renderer = Mock()
self.renderer.turntable.return_value = (["frame.png"], "mock")
fake = ModuleType("taste")
fake.falapi = self.fal
fake.pack = SimpleNamespace(load=self.loader)
fake.render3d = self.renderer
spec = importlib.util.spec_from_file_location("mint_preservation_test", SCRIPT)
self.module = importlib.util.module_from_spec(spec)
with patch.dict("sys.modules", {"taste": fake}):
spec.loader.exec_module(self.module)
quiet = redirect_stdout(io.StringIO())
quiet.__enter__()
self.addCleanup(quiet.__exit__, None, None, None)
def test_raw_retained_before_remesh_and_rendered(self):
def remesh(url, **kwargs):
self.assertEqual((self.props / "visor.glb").read_text(), url)
return "https://v3.fal.media/proxy.glb"
self.fal.retopologize.side_effect = remesh
record = self.module.mint3d(
"genre", prompt="chrome visor", name="visor", retopo=True
)
self.assertEqual(
(self.props / "visor.glb").read_text(), "https://v3.fal.media/fullpbr.glb"
)
self.assertEqual(
(self.props / "visor_retopo.glb").read_text(),
"https://v3.fal.media/proxy.glb",
)
self.assertEqual(record["mesh"], str(self.props / "visor.glb"))
self.assertEqual(record["retopo_mesh"], str(self.props / "visor_retopo.glb"))
self.assertEqual(
self.renderer.turntable.call_args.args[0], self.props / "visor.glb"
)
def test_existing_artifacts_refused_before_generation(self):
for relative in (
"props/visor.glb",
"props/visor_plate.png",
"props/visor_retopo.glb",
"props/visor.json",
"props/visor_part00.glb",
"turntables/visor.mp4",
"turntables/visor",
):
with self.subTest(relative=relative):
artifact = self.root / relative
artifact.parent.mkdir(parents=True, exist_ok=True)
artifact.write_bytes(b"original")
with self.assertRaises(FileExistsError):
self.module.mint3d(
"genre", prompt="chrome visor", name="visor", retopo=True
)
self.assertEqual(artifact.read_bytes(), b"original")
artifact.unlink()
self.fal.text_to_3d.assert_not_called()
self.fal.text_to_image.assert_not_called()
self.fal.image_to_3d.assert_not_called()
def test_remesh_failure_keeps_original_renderable(self):
self.fal.retopologize.side_effect = RuntimeError("mock failure")
record = self.module.mint3d("genre", prompt="visor", name="visor", retopo=True)
self.assertEqual(
(self.props / "visor.glb").read_text(), "https://v3.fal.media/fullpbr.glb"
)
self.assertNotIn("retopo_mesh", record)
self.assertEqual(
self.renderer.turntable.call_args.args[0], self.props / "visor.glb"
)
if __name__ == "__main__":
unittest.main()