Files
ECC/skills/taste-application/tests/test_assets.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

177 lines
8.2 KiB
Python

"""Local asset handoff rejects unsupported provenance and changed files."""
import json
import struct
import tempfile
import unittest
from pathlib import Path
from tasteforge.assets import ingest_assets, validate_assets
class AssetTests(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.addCleanup(self.tmp.cleanup)
self.root = Path(self.tmp.name).resolve()
(self.root / 'clip.mp4').write_bytes(b'existing media')
self.config = self.root / 'input.json'
self.receipt = self.root / 'receipt.json'
self.asset = dict(id='clip', modality='video', path='clip.mp4',
origin='local_passthrough')
def ingest(self, assets=None, **extra):
self.config.write_text(json.dumps(dict(assets=assets or [self.asset], **extra)))
return ingest_assets(self.config, self.receipt)
def test_roundtrip_and_lineage(self):
(self.root / 'genre.json').write_text('{}')
result = self.ingest(input_artifacts=['genre.json'], genre_spec='genre.json')
self.assertEqual(validate_assets(self.receipt), result)
self.assertEqual(result['provider_calls'], 0)
self.assertIs(result['provider_execution'], False)
self.assertEqual(result['assets'][0]['bytes'], 14)
self.assertEqual(result['genre_spec']['sha256'], result['input_artifacts'][0]['sha256'])
def test_external_result_requires_evidence_and_identifier(self):
self.asset['origin'] = 'external_result'
with self.assertRaises(ValueError):
self.ingest()
(self.root / 'provider.json').write_text('{"status":"completed"}')
self.asset['provider_provenance'] = dict(provider='fal', request_id='abc',
evidence_path='provider.json')
result = self.ingest()
self.assertEqual(result['assets'][0]['provider_provenance']['request_id'], 'abc')
self.assertFalse(result['provider_execution'])
(self.root / 'provider.json').write_text('{}')
with self.assertRaises(ValueError):
validate_assets(self.receipt)
def test_recovered_does_not_infer_provider(self):
self.asset['origin'] = 'recovered_unverified'
result = self.ingest()
self.assertNotIn('provider_provenance', result['assets'][0])
def test_changed_media_and_lineage_rejected(self):
self.ingest()
(self.root / 'clip.mp4').write_bytes(b'changed')
with self.assertRaises(ValueError):
validate_assets(self.receipt)
def test_glb_header(self):
path = self.root / 'model.glb'
path.write_bytes(struct.pack('<4sII', b'glTF', 2, 12))
self.asset.update(modality='3d_asset', path='model.glb')
self.ingest()
self.receipt.unlink()
for header in [(b'xxxx', 2, 12), (b'glTF', 1, 12), (b'glTF', 2, 99)]:
path.write_bytes(struct.pack('<4sII', *header))
with self.assertRaises(ValueError):
self.ingest()
def test_duplicate_invalid_remote_empty_and_special(self):
with self.assertRaises(ValueError):
self.ingest([self.asset, self.asset])
for update in [dict(modality='audio'), dict(path='https://example.org/a.mp4'),
dict(path='.'), dict(id=''), dict(origin='generated')]:
with self.assertRaises(ValueError):
self.ingest([{**self.asset, **update}])
def test_symlink_and_symlink_parent(self):
(self.root / 'link.mp4').symlink_to(self.root / 'clip.mp4')
with self.assertRaises(ValueError):
self.ingest([{**self.asset, 'path': 'link.mp4'}])
(self.root / 'alias').symlink_to(self.root, target_is_directory=True)
with self.assertRaises(ValueError):
self.ingest([{**self.asset, 'path': 'alias/clip.mp4'}])
def test_no_overwrites_or_collisions(self):
self.ingest()
original = self.receipt.read_bytes()
with self.assertRaises(ValueError):
self.ingest()
self.assertEqual(original, self.receipt.read_bytes())
with self.assertRaises(ValueError):
ingest_assets(self.config, self.config)
def test_tampered_receipt_operation_and_duplicates(self):
result = self.ingest()
for patch in [dict(provider_calls=1), dict(provider_calls=False),
dict(provider_execution=True), dict(assets=result['assets'] * 2)]:
self.receipt.write_text(json.dumps({**result, **patch}))
with self.assertRaises(ValueError):
validate_assets(self.receipt)
def test_bundle_binds_exact_request_and_revalidates(self):
from tasteforge.workflow import run_workflow
workflow = self.root / 'workflow.json'
workflow.write_text(json.dumps(dict(
schema_version=1, run_id='assets-test', seed=42,
genres=[dict(number=1, slug='flash', label='Flash', references=['clip.mp4'],
signature=dict(materials=['chrome'], motion=['orbit'],
composition=['center'], avoid=['mud']))])))
bundle = self.root / 'bundle'
run_workflow(workflow, bundle, probe=lambda path: dict(
duration=6.0, width=1920, height=1080, fps=24.0, codec='fixture',
sample_times=[0.75, 2.25, 3.75], scene_changes=[0.75, 2.25]))
self.asset['request_id'] = 'assets-test-1-video'
result = self.ingest(bundle_dir='bundle')
self.assertEqual(result['assets'][0]['genre_slug'], 'flash')
self.assertEqual(validate_assets(self.receipt), result)
self.receipt.unlink()
self.asset['request_id'] = 'assets-test-1-image'
with self.assertRaises(ValueError):
self.ingest(bundle_dir='bundle')
self.asset['request_id'] = 'assets-test-1-video'
self.asset['genre_slug'] = 'invented'
with self.assertRaises(ValueError):
self.ingest(bundle_dir='bundle')
result['assets'][0]['style_fingerprint'] = 'fabricated'
self.receipt.write_text(json.dumps(result))
with self.assertRaises(ValueError):
validate_assets(self.receipt)
def test_invalid_provenance_and_lineage_shapes(self):
for provenance in [None, {}, dict(provider='fal', evidence_path='clip.mp4'),
dict(provider='fal', request_id='', evidence_path='clip.mp4'),
dict(provider='fal', request_id='id', evidence_path='missing.json')]:
with self.assertRaises(ValueError):
self.ingest([{**self.asset, 'origin': 'external_result',
'provider_provenance': provenance}])
with self.assertRaises(ValueError):
self.ingest(input_artifacts='clip.mp4')
with self.assertRaises(ValueError):
self.ingest([{**self.asset, 'provider_provenance': {'provider': 'fal'}}])
with self.assertRaises(ValueError):
self.ingest([{**self.asset, 'genre_slug': 'invented'}])
def test_special_file_and_missing_output_directory(self):
import os
os.mkfifo(self.root / 'pipe')
with self.assertRaises(ValueError):
self.ingest([{**self.asset, 'path': 'pipe'}])
self.receipt = self.root / 'missing' / 'receipt.json'
with self.assertRaises(ValueError):
self.ingest()
def test_mutated_lineage_and_invalid_receipt_bindings(self):
(self.root / 'genre.json').write_text('{}')
result = self.ingest(genre_spec='genre.json')
(self.root / 'genre.json').write_text('{"changed":true}')
with self.assertRaises(ValueError):
validate_assets(self.receipt)
for patch in [dict(schema='wrong'), dict(input_artifacts=None),
dict(genre_spec=None)]:
self.receipt.write_text(json.dumps({**result, **patch}))
with self.assertRaises(ValueError):
validate_assets(self.receipt)
def test_invalid_json_shapes(self):
for value in [[], {}, {'assets': []}, {'assets': [None]}]:
self.config.write_text(json.dumps(value))
with self.assertRaises(ValueError):
ingest_assets(self.config, self.receipt)
if __name__ == '__main__':
unittest.main()