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

167 lines
6.9 KiB
Python

"""Offline graph contracts; no provider or network access."""
import importlib.util
import json
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
SCRIPT = (
Path(__file__).resolve().parents[1]
/ "skills/taste-application/scripts/workflow_graphs.py"
)
spec = importlib.util.spec_from_file_location("workflow_graphs", SCRIPT)
graphs = importlib.util.module_from_spec(spec)
spec.loader.exec_module(graphs)
class WorkflowGraphTests(unittest.TestCase):
def test_style_is_compiled_and_neutral_grade_is_explicit(self):
cfg = {
"brief": "A dancer",
"style_steer": "wireframe motion",
"source_video": "https://example.org/own.mp4",
}
original = dict(cfg)
first = graphs.compile_application_input(cfg)
second = graphs.compile_application_input(
{**cfg, "style_steer": "handheld motion"}
)
self.assertNotEqual(first["compiled_prompt"], second["compiled_prompt"])
self.assertIn("WHAT", first["compiled_prompt"])
self.assertIn("HOW", first["compiled_prompt"])
self.assertIn("Render neutral", first["compiled_prompt"])
self.assertEqual(cfg, original)
self.assertEqual(
set(first), set(graphs.load_graph("apply")["contents"]["schema"]["input"])
)
def test_templates_wired_and_blank(self):
for kind in ("apply", "apply-motion", "distill", "prop3d"):
graph = graphs.load_graph(kind)
graphs.validate_graph(graph)
self.assertEqual(set(graph), {"name", "title", "contents"})
for field in graph["contents"]["schema"]["input"].values():
self.assertEqual(field["defaultValue"], "")
self.assertTrue(field["required"])
nodes = graphs.load_graph("apply")["contents"]["nodes"]
self.assertEqual(nodes["node-merge"]["input"]["target_fps"], 30)
for name in ("node-gen1", "node-gen2", "node-gen3"):
self.assertEqual(nodes[name]["input"]["prompt"], "$input.compiled_prompt")
self.assertEqual(nodes[name]["input"]["audio_urls"], [])
self.assertIs(nodes[name]["input"]["generate_audio"], False)
def test_distill_supplied_grounding_only(self):
cfg = {
"genre": "Industrial",
"measured_grounding": "Measured source: local/report.json; cadence 0.6 seconds.",
"references": [
"https://example.org/a",
"https://example.org/b",
"https://example.org/c",
],
}
data = graphs.prepare_distillation_input(cfg)
self.assertIn(cfg["measured_grounding"], data["measured_grounding"])
self.assertIn("Industrial", data["measured_grounding"])
self.assertEqual(
set(data), set(graphs.load_graph("distill")["contents"]["schema"]["input"])
)
rendered = json.dumps(graphs.load_graph("distill"))
for inherited in (
"190 sampled",
"FlashEthereal",
"hunyuan",
"model_glb",
"77 cuts",
):
self.assertNotIn(inherited, rendered)
for bad in (
{**cfg, "genre": ""},
{**cfg, "measured_grounding": ""},
{**cfg, "references": cfg["references"][:2]},
):
with self.assertRaises(ValueError):
graphs.prepare_distillation_input(bad)
def test_optional_motion_variant_preserves_application_contract(self):
still = graphs.load_graph('apply')
motion = graphs.load_graph('apply-motion')
self.assertEqual(still['contents']['schema'], motion['contents']['schema'])
self.assertNotEqual(still['name'], motion['name'])
for name in ('node-gen1', 'node-gen2', 'node-gen3'):
original = still['contents']['nodes'][name]['input']
variant = motion['contents']['nodes'][name]['input']
self.assertNotIn('video_urls', original)
self.assertEqual(variant['video_urls'], ['$input.source_video'])
self.assertEqual({k: v for k, v in variant.items() if k != 'video_urls'}, original)
self.assertEqual(motion['contents']['nodes']['node-merge']['input']['target_fps'], 30)
payload = graphs.compile_application_input({'brief': 'a', 'style_steer': 'b',
'source_video': 'https://example.org/own.mp4'})
self.assertEqual(set(payload), set(motion['contents']['schema']['input']))
def test_bad_inputs_fail(self):
for source in ("file:///tmp/private", "http://example.org/a", "", 42):
with self.assertRaises(ValueError):
graphs.compile_application_input(
{"brief": "a", "style_steer": "b", "source_video": source}
)
def test_validator_rejects_disconnected_inputs_and_cycles(self):
graph = graphs.load_graph("apply")
graph["contents"]["schema"]["input"]["unused"] = {"required": True}
with self.assertRaises(ValueError):
graphs.validate_graph(graph)
graph = graphs.load_graph("apply")
graph["contents"]["nodes"]["node-xfirst"]["depends"].append("node-merge")
with self.assertRaises(ValueError):
graphs.validate_graph(graph)
def test_validator_rejects_unknown_output_dependency(self):
graph = graphs.load_graph("apply")
graph["contents"]["output"]["unexpected"] = "$missing-node.video"
with self.assertRaises(ValueError):
graphs.validate_graph(graph)
graph = graphs.load_graph("apply")
graph["contents"]["nodes"]["output"]["fields"]["unexpected"] = (
"$node-xlast.images"
)
graph["contents"]["nodes"]["output"]["depends"] = ["node-gen1"]
with self.assertRaises(ValueError):
graphs.validate_graph(graph)
def test_cli_no_overwrite(self):
with tempfile.TemporaryDirectory() as folder:
config, out = Path(folder) / "config.json", Path(folder) / "out.json"
config.write_text(
json.dumps(
{
"brief": "a",
"style_steer": "b",
"source_video": "https://example.org/own.mp4",
}
)
)
command = [
sys.executable,
str(SCRIPT),
"--kind",
"apply",
"--config",
str(config),
"--out",
str(out),
]
self.assertEqual(subprocess.run(command, capture_output=True).returncode, 0)
before = out.read_bytes()
self.assertNotEqual(
subprocess.run(command, capture_output=True).returncode, 0
)
self.assertEqual(out.read_bytes(), before)
if __name__ == "__main__":
unittest.main()