@optima-chat/optima-agent 0.8.95 → 0.8.97

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/.claude/skills/gen/SKILL.md +1 -1
  2. package/.claude/skills/video-gen/SKILL.md +449 -0
  3. package/.claude/skills/video-gen/templates/lifestyle-scene.md +18 -0
  4. package/.claude/skills/video-gen/templates/pdp-360-showcase.md +17 -0
  5. package/.claude/skills/video-gen/templates/pdp-feature-highlight.md +18 -0
  6. package/.claude/skills/video-gen/templates/tiktok-before-after.md +17 -0
  7. package/.claude/skills/video-gen/templates/tiktok-product-reveal.md +17 -0
  8. package/.claude/skills/video-gen/templates/tiktok-unboxing.md +18 -0
  9. package/package.json +1 -1
  10. package/.claude/skills/video-clone/SKILL.md +0 -199
  11. package/.claude/skills/video-clone/assets/phase-state-template.json +0 -11
  12. package/.claude/skills/video-clone/references/ffmpeg-commands.md +0 -42
  13. package/.claude/skills/video-clone/references/gate-enforcement.md +0 -144
  14. package/.claude/skills/video-clone/references/kling-api.md +0 -85
  15. package/.claude/skills/video-clone/references/prompt-template.md +0 -71
  16. package/.claude/skills/video-clone/references/url-parsing.md +0 -32
  17. package/.claude/skills/video-clone/references/workflow-system.md +0 -92
  18. package/.claude/skills/video-clone/scripts/_confirm.py +0 -96
  19. package/.claude/skills/video-clone/scripts/_confirm_test.py +0 -125
  20. package/.claude/skills/video-clone/scripts/_gate.py +0 -162
  21. package/.claude/skills/video-clone/scripts/_gate_e2e_test.py +0 -226
  22. package/.claude/skills/video-clone/scripts/_gate_test.py +0 -148
  23. package/.claude/skills/video-clone/scripts/_project.py +0 -56
  24. package/.claude/skills/video-clone/scripts/analyze_source.py +0 -113
  25. package/.claude/skills/video-clone/scripts/analyze_source_test.py +0 -52
  26. package/.claude/skills/video-clone/scripts/assemble.py +0 -106
  27. package/.claude/skills/video-clone/scripts/confirm.py +0 -12
  28. package/.claude/skills/video-clone/scripts/edit_first_frame.py +0 -66
  29. package/.claude/skills/video-clone/scripts/extract_frames.py +0 -108
  30. package/.claude/skills/video-clone/scripts/gen_video.py +0 -59
  31. package/.claude/skills/video-clone/scripts/init_project.py +0 -103
  32. package/.claude/skills/video-clone/scripts/init_project_test.py +0 -106
  33. package/.claude/skills/video-clone/scripts/kling_generate.py +0 -262
  34. package/.claude/skills/video-clone/scripts/kling_generate_test.py +0 -191
  35. package/.claude/skills/video-clone/scripts/preflight.py +0 -102
  36. package/.claude/skills/video-clone/scripts/preview.py +0 -208
  37. package/.claude/skills/video-clone/scripts/preview_test.py +0 -169
  38. package/.claude/skills/video-clone/scripts/save_workflow.py +0 -129
  39. package/.claude/skills/video-clone/scripts/save_workflow_test.py +0 -106
  40. package/.claude/skills/video-clone/scripts/status.py +0 -202
  41. package/.claude/skills/video-clone/scripts/status_test.py +0 -174
@@ -1,96 +0,0 @@
1
- """Shared implementation for confirm.py.
2
-
3
- run_confirm() handles:
4
- - argparse with --project / --quote / --force
5
- - quote validation (non-empty)
6
- - negation heuristic (refuses quotes that sound like corrections unless --force)
7
- - resolving the project directory via GEN_OUTPUT_ROOT
8
- - automatically passing caller=Path(sys.argv[0]).name to _gate.set_gate()
9
- """
10
- from __future__ import annotations
11
-
12
- import argparse
13
- import os
14
- import sys
15
- from pathlib import Path
16
-
17
- import _gate
18
-
19
- NEGATION_MARKERS = (
20
- "不", "别", "改", "调整", "不行", "不对", "再", "换",
21
- "no", "not", "don't", "dont", "modify", "change", "different", "wrong",
22
- )
23
-
24
- GATE_NAME = "preview_confirmed"
25
-
26
-
27
- def _gen_output_root() -> Path:
28
- override = os.environ.get("GEN_OUTPUT_ROOT")
29
- if override:
30
- return Path(override)
31
- return Path.cwd() / "gen-output"
32
-
33
-
34
- def _looks_like_negation(quote: str) -> bool:
35
- low = quote.lower()
36
- return any(m in low for m in NEGATION_MARKERS)
37
-
38
-
39
- def run_confirm() -> int:
40
- ap = argparse.ArgumentParser(
41
- description=f"Record user confirmation for {GATE_NAME}."
42
- )
43
- ap.add_argument("--project", required=True, help="Project name under video-clone/")
44
- ap.add_argument(
45
- "--quote", required=True,
46
- help="The user's actual confirmation words (verbatim). Required.",
47
- )
48
- ap.add_argument(
49
- "--force", action="store_true",
50
- help="Required if the quote contains negation markers "
51
- "(used when user said something like 'no audio, rest is fine').",
52
- )
53
- args = ap.parse_args()
54
-
55
- quote = args.quote.strip()
56
- if not quote:
57
- print(
58
- "ERROR: --quote is empty. You must pass the user's actual "
59
- "confirmation words.",
60
- file=sys.stderr,
61
- )
62
- return 1
63
-
64
- if _looks_like_negation(quote) and not args.force:
65
- print(
66
- f"ERROR: quote looks like a correction / negation: {quote!r}\n"
67
- f"If the user really confirmed (e.g. 'no audio, rest is fine'), "
68
- f"re-run with --force. Otherwise, go back and get a cleaner "
69
- f"confirmation from the user.",
70
- file=sys.stderr,
71
- )
72
- return 1
73
-
74
- project_dir = _gen_output_root() / "video-clone" / args.project
75
- if not project_dir.is_dir():
76
- print(
77
- f"ERROR: project directory not found at {project_dir}\n"
78
- f"Run init_project.py first.",
79
- file=sys.stderr,
80
- )
81
- return 1
82
-
83
- # IMPORTANT: caller is sys.argv[0] (the entry script), not __file__.
84
- # We want the audit to record "confirm.py", not "_confirm.py".
85
- caller = Path(sys.argv[0]).name if sys.argv and sys.argv[0] else "<unknown>"
86
-
87
- try:
88
- _gate.set_gate(project_dir, GATE_NAME, quote, caller=caller)
89
- except (ValueError, FileNotFoundError, KeyError) as e:
90
- print(f"ERROR: {e}", file=sys.stderr)
91
- return 1
92
-
93
- print(f"OK: {GATE_NAME} recorded for project {args.project}")
94
- print(f" quote: {quote}")
95
- print(f" caller: {caller}")
96
- return 0
@@ -1,125 +0,0 @@
1
- """Unit tests for _confirm.run_confirm() — caller injection + quote validation."""
2
- from __future__ import annotations
3
-
4
- import json
5
- import os
6
- import sys
7
- import tempfile
8
- from pathlib import Path
9
- from unittest.mock import patch
10
-
11
- SCRIPTS_DIR = Path(__file__).parent
12
- sys.path.insert(0, str(SCRIPTS_DIR))
13
-
14
- import _confirm # noqa: E402
15
- import _gate # noqa: E402
16
-
17
- TEMPLATE = json.loads(
18
- (SCRIPTS_DIR.parent / "assets" / "phase-state-template.json").read_text(encoding="utf-8")
19
- )
20
-
21
-
22
- def _make_project(root: Path, name: str = "p1") -> Path:
23
- proj = root / "video-clone" / name
24
- (proj / ".state").mkdir(parents=True)
25
- state = json.loads(json.dumps(TEMPLATE))
26
- state["project"] = name
27
- state["task_type"] = "video_clone"
28
- (proj / ".state" / "phase.json").write_text(json.dumps(state), encoding="utf-8")
29
- return proj
30
-
31
-
32
- def _run_with_argv(argv, env=None):
33
- old_argv = sys.argv
34
- old_env = dict(os.environ)
35
- try:
36
- sys.argv = argv
37
- if env:
38
- os.environ.update(env)
39
- return _confirm.run_confirm()
40
- finally:
41
- sys.argv = old_argv
42
- os.environ.clear()
43
- os.environ.update(old_env)
44
-
45
-
46
- def test_run_confirm_injects_caller_from_argv0():
47
- with tempfile.TemporaryDirectory() as td:
48
- _make_project(Path(td))
49
- rc = _run_with_argv(
50
- ["confirm.py", "--project", "p1", "--quote", "OK 开始"],
51
- env={"GEN_OUTPUT_ROOT": td},
52
- )
53
- assert rc == 0
54
- state = _gate.load_state(Path(td) / "video-clone" / "p1")
55
- history = state["history"]
56
- assert history[-1]["caller"] == "confirm.py"
57
- assert history[-1]["user_quote"] == "OK 开始"
58
- print("PASS test_run_confirm_injects_caller_from_argv0")
59
-
60
-
61
- def test_run_confirm_empty_quote_returns_one():
62
- with tempfile.TemporaryDirectory() as td:
63
- _make_project(Path(td))
64
- rc = _run_with_argv(
65
- ["confirm.py", "--project", "p1", "--quote", " "],
66
- env={"GEN_OUTPUT_ROOT": td},
67
- )
68
- assert rc == 1
69
- print("PASS test_run_confirm_empty_quote_returns_one")
70
-
71
-
72
- def test_run_confirm_negation_without_force_returns_one():
73
- with tempfile.TemporaryDirectory() as td:
74
- _make_project(Path(td))
75
- rc = _run_with_argv(
76
- ["confirm.py", "--project", "p1", "--quote", "不要这样改"],
77
- env={"GEN_OUTPUT_ROOT": td},
78
- )
79
- assert rc == 1
80
- print("PASS test_run_confirm_negation_without_force_returns_one")
81
-
82
-
83
- def test_run_confirm_negation_with_force_returns_zero():
84
- with tempfile.TemporaryDirectory() as td:
85
- _make_project(Path(td))
86
- rc = _run_with_argv(
87
- ["confirm.py", "--project", "p1", "--quote", "no audio, rest is fine", "--force"],
88
- env={"GEN_OUTPUT_ROOT": td},
89
- )
90
- assert rc == 0
91
- print("PASS test_run_confirm_negation_with_force_returns_zero")
92
-
93
-
94
- def test_run_confirm_missing_project_returns_one():
95
- with tempfile.TemporaryDirectory() as td:
96
- rc = _run_with_argv(
97
- ["confirm.py", "--project", "does-not-exist", "--quote", "ok"],
98
- env={"GEN_OUTPUT_ROOT": td},
99
- )
100
- assert rc == 1
101
- print("PASS test_run_confirm_missing_project_returns_one")
102
-
103
-
104
- def test_run_confirm_success_writes_history_with_caller():
105
- with tempfile.TemporaryDirectory() as td:
106
- _make_project(Path(td), "p2")
107
- rc = _run_with_argv(
108
- ["confirm.py", "--project", "p2", "--quote", "好的,开始"],
109
- env={"GEN_OUTPUT_ROOT": td},
110
- )
111
- assert rc == 0
112
- state = _gate.load_state(Path(td) / "video-clone" / "p2")
113
- assert state["gates"]["preview_confirmed"]["status"] is True
114
- assert state["history"][-1]["caller"] == "confirm.py"
115
- print("PASS test_run_confirm_success_writes_history_with_caller")
116
-
117
-
118
- if __name__ == "__main__":
119
- test_run_confirm_injects_caller_from_argv0()
120
- test_run_confirm_empty_quote_returns_one()
121
- test_run_confirm_negation_without_force_returns_one()
122
- test_run_confirm_negation_with_force_returns_zero()
123
- test_run_confirm_missing_project_returns_one()
124
- test_run_confirm_success_writes_history_with_caller()
125
- print("All 6 _confirm tests passed.")
@@ -1,162 +0,0 @@
1
- """Phase-state machine for the video-clone skill.
2
-
3
- Every executable script that spends money or writes durable artifacts calls
4
- `require_gate()` at startup. If the required gate is not yet confirmed, the
5
- script exits with code 1 and prints a [HARD-GATE BLOCKED] message describing
6
- exactly how to unblock it.
7
-
8
- State lives in `<project_dir>/.state/phase.json` (one file per project).
9
- The schema is defined in `assets/phase-state-template.json`.
10
-
11
- This module is stdlib-only. Import it from sibling scripts with:
12
-
13
- from _gate import require_gate
14
- require_gate(project_dir, "preview_confirmed")
15
-
16
- Gates (single):
17
- preview_confirmed — user reviewed the preview bundle and confirmed
18
- they want video generation to proceed.
19
- """
20
- from __future__ import annotations
21
-
22
- import json
23
- import sys
24
- from datetime import datetime, timezone
25
- from pathlib import Path
26
-
27
- GATE_NAMES = ("preview_confirmed",)
28
- STATE_REL = Path(".state") / "phase.json"
29
-
30
-
31
- def _state_path(project_dir: Path | str) -> Path:
32
- return Path(project_dir) / STATE_REL
33
-
34
-
35
- def load_state(project_dir: Path | str) -> dict:
36
- """Read and parse .state/phase.json for the given project directory."""
37
- path = _state_path(project_dir)
38
- if not path.exists():
39
- raise FileNotFoundError(
40
- f"No phase state at {path}. Run init_project.py first."
41
- )
42
- return json.loads(path.read_text(encoding="utf-8"))
43
-
44
-
45
- def save_state(project_dir: Path | str, state: dict) -> None:
46
- """Atomically overwrite .state/phase.json."""
47
- path = _state_path(project_dir)
48
- path.parent.mkdir(parents=True, exist_ok=True)
49
- tmp = path.with_suffix(".json.tmp")
50
- tmp.write_text(json.dumps(state, indent=2, ensure_ascii=False), encoding="utf-8")
51
- tmp.replace(path)
52
-
53
-
54
- def _now_iso() -> str:
55
- return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
56
-
57
-
58
- def append_history(state: dict, event: str, **extra) -> None:
59
- entry = {"event": event, "at": _now_iso()}
60
- entry.update(extra)
61
- state.setdefault("history", []).append(entry)
62
-
63
-
64
- def set_gate(
65
- project_dir: Path | str,
66
- gate_name: str,
67
- user_quote: str,
68
- *,
69
- caller: str,
70
- ) -> None:
71
- """Flip a gate from false → true and log the user's confirmation quote.
72
-
73
- Raises ValueError if gate_name is unknown, user_quote is empty, or
74
- caller is empty/whitespace. caller is keyword-only and required so
75
- bypassing _confirm.run_confirm() either fails or leaves a visible
76
- `caller` field in history[].
77
- """
78
- if gate_name not in GATE_NAMES:
79
- raise ValueError(
80
- f"unknown gate: {gate_name!r}. valid: {', '.join(GATE_NAMES)}"
81
- )
82
- if not user_quote or not user_quote.strip():
83
- raise ValueError("user_quote is required and must be non-empty")
84
- if not caller or not caller.strip():
85
- raise ValueError("caller is required and must be non-empty")
86
-
87
- state = load_state(project_dir)
88
- if gate_name not in state.get("gates", {}):
89
- raise KeyError(
90
- f"gate {gate_name!r} not found in state — likely an old "
91
- f"3-gate schema. Either complete this project with the "
92
- f"previous version of the scripts (PR #65), or start a new "
93
- f"project with init_project.py."
94
- )
95
- now = _now_iso()
96
- state["gates"][gate_name] = {
97
- "status": True,
98
- "confirmed_at": now,
99
- "user_quote": user_quote,
100
- }
101
- append_history(state, gate_name, user_quote=user_quote, caller=caller)
102
- save_state(project_dir, state)
103
-
104
-
105
- def require_gate(project_dir: Path | str, gate_name: str) -> None:
106
- """Assert the given gate is confirmed. Exits with code 1 on failure.
107
-
108
- This is the mechanical HARD-GATE enforcement. Do not catch SystemExit
109
- to "handle" the failure — the entire point is that Claude cannot
110
- bypass it.
111
- """
112
- if gate_name not in GATE_NAMES:
113
- _fail(
114
- f"require_gate called with unknown gate {gate_name!r}. "
115
- f"valid: {', '.join(GATE_NAMES)}"
116
- )
117
-
118
- try:
119
- state = load_state(project_dir)
120
- except FileNotFoundError as e:
121
- _fail(str(e))
122
-
123
- gates = state.get("gates", {})
124
- if gate_name not in gates:
125
- _fail(
126
- f"\n[HARD-GATE BLOCKED] {Path(sys.argv[0]).name} expected "
127
- f"gate {gate_name!r} but it is not present in {project_dir}/"
128
- f".state/phase.json.\n"
129
- f"This is most likely an old 3-gate schema project. Either:\n"
130
- f" 1. Complete this project with the previous version of "
131
- f"the scripts (PR #65), or\n"
132
- f" 2. Start a new project with: "
133
- f"python scripts/init_project.py --name <new-name> "
134
- f"--task-type <video_clone|video_gen>\n"
135
- )
136
-
137
- gate = gates.get(gate_name, {})
138
- if gate.get("status") is True:
139
- return # gate satisfied, continue
140
-
141
- script_name = Path(sys.argv[0]).name if sys.argv and sys.argv[0] else "<script>"
142
- msg = (
143
- f"\n[HARD-GATE BLOCKED] {script_name} needs {gate_name}=True\n"
144
- f"Current state: {gate_name}={gate.get('status', False)}\n"
145
- f"Project: {Path(project_dir).resolve()}\n"
146
- f"\n"
147
- f"To proceed:\n"
148
- f" 1. Show the preview bundle to the user and wait for their confirmation.\n"
149
- f" 2. Run: python scripts/confirm.py "
150
- f"--project <name> --quote \"<user's actual words>\"\n"
151
- f" 3. Retry this command.\n"
152
- f"\n"
153
- f"Claude: do NOT rationalize past this. The gate exists because "
154
- f"text instructions alone did not stop prior bypass attempts. "
155
- f"Go get the real user confirmation.\n"
156
- )
157
- _fail(msg)
158
-
159
-
160
- def _fail(message: str) -> None:
161
- print(message, file=sys.stderr)
162
- sys.exit(1)
@@ -1,226 +0,0 @@
1
- """E2E gate-enforcement tests for single-gate model.
2
-
3
- 7 cases:
4
- 1. analyze_source.py runs WITHOUT gate (no gate requirement)
5
- 2. extract_frames.py runs WITHOUT gate (no gate requirement)
6
- 3. edit_first_frame.py runs WITHOUT gate (no gate requirement)
7
- 4. kling_generate.py is BLOCKED by missing preview_confirmed gate
8
- 5. gen_video.py is BLOCKED by missing preview_confirmed gate
9
- 6. save_workflow.py is BLOCKED by missing preview_confirmed gate
10
- 7. Old 3-gate schema is REJECTED by require_gate with informative message
11
-
12
- Run: python scripts/_gate_e2e_test.py
13
- """
14
- from __future__ import annotations
15
-
16
- import json
17
- import os
18
- import subprocess
19
- import sys
20
- import tempfile
21
- from pathlib import Path
22
-
23
- SCRIPTS_DIR = Path(__file__).parent
24
-
25
-
26
- def _make_project(tmp: Path, gate: bool = False,
27
- old_schema: bool = False) -> Path:
28
- proj = tmp / "video-clone" / "e2e-proj"
29
- for sub in ("source", "frames", "videos", ".state"):
30
- (proj / sub).mkdir(parents=True)
31
-
32
- if old_schema:
33
- state = {
34
- "schema_version": 0,
35
- "project": "e2e-proj",
36
- "task_type": "video_clone",
37
- "created_at": "2025-01-01T00:00:00Z",
38
- "current_phase": 0,
39
- "gates": {
40
- "plan_confirmed": {"status": True, "confirmed_at": "x", "user_quote": "y"},
41
- "prompt_confirmed": {"status": True, "confirmed_at": "x", "user_quote": "y"},
42
- "frame_confirmed": {"status": True, "confirmed_at": "x", "user_quote": "y"},
43
- },
44
- "history": [],
45
- }
46
- else:
47
- state = {
48
- "schema_version": 1,
49
- "project": "e2e-proj",
50
- "task_type": "video_clone",
51
- "created_at": "2025-01-01T00:00:00Z",
52
- "current_phase": 0,
53
- "gates": {
54
- "preview_confirmed": {
55
- "status": gate,
56
- "confirmed_at": "2025-01-01T00:01:00Z" if gate else None,
57
- "user_quote": "go" if gate else None,
58
- }
59
- },
60
- "history": [],
61
- }
62
- (proj / ".state" / "phase.json").write_text(
63
- json.dumps(state, indent=2), encoding="utf-8"
64
- )
65
- return proj
66
-
67
-
68
- def _run(tmp: Path, script: str, extra: list[str] | None = None,
69
- extra_env: dict | None = None) -> subprocess.CompletedProcess:
70
- env = os.environ.copy()
71
- env["GEN_OUTPUT_ROOT"] = str(tmp)
72
- if extra_env:
73
- env.update(extra_env)
74
- cmd = [sys.executable, str(SCRIPTS_DIR / script),
75
- "--project", "e2e-proj"]
76
- if extra:
77
- cmd += extra
78
- return subprocess.run(cmd, capture_output=True, text=True, env=env)
79
-
80
-
81
- # ------------------------------------------------------------------
82
- # Case 1: analyze_source.py runs without gate
83
- # ------------------------------------------------------------------
84
- def test_analyze_source_runs_without_gate():
85
- """analyze_source.py must not call require_gate; a missing source file
86
- is the expected failure (not a gate block)."""
87
- with tempfile.TemporaryDirectory() as td:
88
- tmp = Path(td)
89
- _make_project(tmp, gate=False)
90
- result = _run(tmp, "analyze_source.py", ["--source", "nonexistent.mp4"])
91
- assert "HARD-GATE BLOCKED" not in result.stderr, (
92
- f"analyze_source must not gate-block. stderr: {result.stderr}"
93
- )
94
- assert result.returncode != 0 # fails due to missing file, not gate
95
- print("PASS test_analyze_source_runs_without_gate")
96
-
97
-
98
- # ------------------------------------------------------------------
99
- # Case 2: extract_frames.py runs without gate
100
- # ------------------------------------------------------------------
101
- def test_extract_frames_runs_without_gate():
102
- with tempfile.TemporaryDirectory() as td:
103
- tmp = Path(td)
104
- _make_project(tmp, gate=False)
105
- result = _run(tmp, "extract_frames.py", ["--source", "nonexistent.mp4"])
106
- assert "HARD-GATE BLOCKED" not in result.stderr, (
107
- f"extract_frames must not gate-block. stderr: {result.stderr}"
108
- )
109
- print("PASS test_extract_frames_runs_without_gate")
110
-
111
-
112
- # ------------------------------------------------------------------
113
- # Case 3: edit_first_frame.py runs without gate
114
- # ------------------------------------------------------------------
115
- def test_edit_first_frame_runs_without_gate():
116
- with tempfile.TemporaryDirectory() as td:
117
- tmp = Path(td)
118
- _make_project(tmp, gate=False)
119
- result = _run(tmp, "edit_first_frame.py", ["--image", "nonexistent.png"])
120
- assert "HARD-GATE BLOCKED" not in result.stderr, (
121
- f"edit_first_frame must not gate-block. stderr: {result.stderr}"
122
- )
123
- print("PASS test_edit_first_frame_runs_without_gate")
124
-
125
-
126
- # ------------------------------------------------------------------
127
- # Case 4: kling_generate.py is BLOCKED without preview_confirmed
128
- # ------------------------------------------------------------------
129
- def test_kling_generate_blocked_without_gate():
130
- with tempfile.TemporaryDirectory() as td:
131
- tmp = Path(td)
132
- _make_project(tmp, gate=False)
133
- result = _run(tmp, "kling_generate.py",
134
- ["--frame", "frames/frame_v1.png"],
135
- extra_env={"PIAPI_KEY": "dummy"})
136
- assert result.returncode == 1, (
137
- f"Expected exit 1\nstdout: {result.stdout}\nstderr: {result.stderr}"
138
- )
139
- assert "HARD-GATE BLOCKED" in result.stderr, (
140
- f"Expected HARD-GATE BLOCKED in stderr:\n{result.stderr}"
141
- )
142
- print("PASS test_kling_generate_blocked_without_gate")
143
-
144
-
145
- # ------------------------------------------------------------------
146
- # Case 5: gen_video.py is BLOCKED without preview_confirmed
147
- # ------------------------------------------------------------------
148
- def test_gen_video_blocked_without_gate():
149
- with tempfile.TemporaryDirectory() as td:
150
- tmp = Path(td)
151
- _make_project(tmp, gate=False)
152
- result = _run(tmp, "gen_video.py", ["--frame", "frames/frame_v1.png"])
153
- assert result.returncode == 1, (
154
- f"Expected exit 1\nstdout: {result.stdout}\nstderr: {result.stderr}"
155
- )
156
- assert "HARD-GATE BLOCKED" in result.stderr, (
157
- f"Expected HARD-GATE BLOCKED in stderr:\n{result.stderr}"
158
- )
159
- print("PASS test_gen_video_blocked_without_gate")
160
-
161
-
162
- # ------------------------------------------------------------------
163
- # Case 6: save_workflow.py is BLOCKED without preview_confirmed
164
- # ------------------------------------------------------------------
165
- def test_save_workflow_blocked_without_gate():
166
- with tempfile.TemporaryDirectory() as td:
167
- tmp = Path(td)
168
- _make_project(tmp, gate=False)
169
- result = _run(tmp, "save_workflow.py",
170
- ["--name", "wf1", "--scene", "test",
171
- "--rating", "3", "--strategy", "test strat"])
172
- assert result.returncode == 1, (
173
- f"Expected exit 1\nstdout: {result.stdout}\nstderr: {result.stderr}"
174
- )
175
- assert "HARD-GATE BLOCKED" in result.stderr, (
176
- f"Expected HARD-GATE BLOCKED in stderr:\n{result.stderr}"
177
- )
178
- print("PASS test_save_workflow_blocked_without_gate")
179
-
180
-
181
- # ------------------------------------------------------------------
182
- # Case 7: Old 3-gate schema is REJECTED with informative message
183
- # ------------------------------------------------------------------
184
- def test_old_3gate_schema_rejected_at_gate_check():
185
- """When any gated script encounters an old 3-gate schema, require_gate
186
- must exit 1 with a message mentioning the old schema."""
187
- with tempfile.TemporaryDirectory() as td:
188
- tmp = Path(td)
189
- _make_project(tmp, old_schema=True)
190
- result = _run(tmp, "kling_generate.py",
191
- ["--frame", "frames/frame_v1.png"],
192
- extra_env={"PIAPI_KEY": "dummy"})
193
- assert result.returncode == 1
194
- stderr = result.stderr
195
- assert any(kw in stderr for kw in (
196
- "old 3-gate", "schema", "plan_confirmed", "HARD-GATE BLOCKED"
197
- )), f"Expected old-schema rejection message in stderr:\n{stderr}"
198
- print("PASS test_old_3gate_schema_rejected_at_gate_check")
199
-
200
-
201
- TESTS = [
202
- ("analyze_source runs without gate", test_analyze_source_runs_without_gate),
203
- ("extract_frames runs without gate", test_extract_frames_runs_without_gate),
204
- ("edit_first_frame runs without gate", test_edit_first_frame_runs_without_gate),
205
- ("kling_generate blocked without gate", test_kling_generate_blocked_without_gate),
206
- ("gen_video blocked without gate", test_gen_video_blocked_without_gate),
207
- ("save_workflow blocked without gate", test_save_workflow_blocked_without_gate),
208
- ("old 3-gate schema rejected", test_old_3gate_schema_rejected_at_gate_check),
209
- ]
210
-
211
-
212
- def main() -> int:
213
- failed = 0
214
- for name, fn in TESTS:
215
- try:
216
- fn()
217
- print(f"PASS {name}")
218
- except Exception as e:
219
- failed += 1
220
- print(f"FAIL {name}: {type(e).__name__}: {e}")
221
- print(f"\n{len(TESTS) - failed}/{len(TESTS)} passed")
222
- return 0 if failed == 0 else 1
223
-
224
-
225
- if __name__ == "__main__":
226
- sys.exit(main())