refactor (untested)

This commit is contained in:
Mariano Gabriel
2026-06-28 21:12:13 -03:00
parent 5ea05eb553
commit cc64544d50
26 changed files with 540 additions and 340 deletions

29
tests/__init__.py Normal file
View File

@@ -0,0 +1,29 @@
"""Smoke + focused tests for meetus.
Run from the repo root:
python -m unittest discover -s tests -t . -v
This package's import makes the repo importable and provides a `cv2` stub (only
cv2 is missing in minimal envs; numpy/PIL are real and not shadowed).
"""
import os
import sys
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
STUBS_DIR = Path(__file__).resolve().parent / "_stubs"
for _p in (str(STUBS_DIR), str(REPO_ROOT)):
if _p not in sys.path:
sys.path.insert(0, _p)
def subprocess_env(extra=None):
"""Env for subprocess tests: repo root + cv2 stub on PYTHONPATH."""
env = dict(os.environ)
pp = env.get("PYTHONPATH", "")
parts = [str(REPO_ROOT), str(STUBS_DIR)] + ([pp] if pp else [])
env["PYTHONPATH"] = os.pathsep.join(parts)
if extra:
env.update(extra)
return env

2
tests/_stubs/cv2.py Normal file
View File

@@ -0,0 +1,2 @@
# Empty stub so meetus.frame_extractor (which does `import cv2`) imports in test
# environments without OpenCV. Tests never exercise cv2 functionality.

109
tests/test_batch.py Normal file
View File

@@ -0,0 +1,109 @@
"""Deeper tests for ctrl/batch.sh: recursive + space-safe discovery, audio
inclusion, structure mirroring, flag forwarding, failure resilience, and the
stdin-isolation fix (a greedy child must not stall the batch)."""
import os
import subprocess
import tempfile
import unittest
from pathlib import Path
from tests import REPO_ROOT
BATCH = REPO_ROOT / "ctrl" / "batch.sh"
def run_batch(args, env_extra=None):
env = dict(os.environ)
if env_extra:
env.update(env_extra)
return subprocess.run(["bash", str(BATCH), *args], cwd=REPO_ROOT, env=env,
capture_output=True, text=True)
def stub(d, body):
p = d / "pystub"
p.write_text("#!/bin/bash\n" + body + "\n")
p.chmod(0o755)
return str(p)
class TestBatchDry(unittest.TestCase):
def test_recursive_spaces_audio_and_mirror(self):
with tempfile.TemporaryDirectory() as tmp:
tmp = Path(tmp)
ind = tmp / "in"
(ind / "team a" / "sub one").mkdir(parents=True)
(ind / "team a" / "sub one" / "session 1.mkv").touch()
(ind / "clip.MP4").touch() # case-insensitive
(ind / "radio.ogg").touch() # audio
(ind / "notes.txt").touch() # ignored
r = run_batch(["-i", str(ind), "-o", str(tmp / "out"), "-n"])
self.assertEqual(r.returncode, 0, r.stderr)
out = r.stdout
self.assertIn("session 1.mkv", out)
self.assertIn("clip.MP4", out)
self.assertIn("radio.ogg", out)
self.assertNotIn("notes.txt", out)
self.assertIn("Found 3 video", out)
self.assertIn(str(tmp / "out" / "team a" / "sub one"), out)
class TestBatchRun(unittest.TestCase):
def test_forward_flags_and_mirror(self):
with tempfile.TemporaryDirectory() as tmp:
tmp = Path(tmp)
ind = tmp / "in" / "team a"
ind.mkdir(parents=True)
(ind / "session 1.mkv").touch()
r = run_batch(["-i", str(tmp / "in"), "-o", str(tmp / "out"),
"--", "--embed-images", "--diarize"],
env_extra={"PYTHON": "echo"})
self.assertEqual(r.returncode, 0, r.stderr)
line = next(l for l in r.stdout.splitlines() if "process_meeting.py" in l)
self.assertIn("--output-dir", line)
self.assertIn(str(tmp / "out" / "team a"), line)
self.assertIn("--embed-images", line)
self.assertIn("session 1.mkv", line)
def test_empty_forward_does_not_crash(self):
# set -u + empty FORWARD array guard.
with tempfile.TemporaryDirectory() as tmp:
tmp = Path(tmp)
ind = tmp / "in"
ind.mkdir()
(ind / "x.mp4").touch()
r = run_batch(["-i", str(ind), "-o", str(tmp / "out")],
env_extra={"PYTHON": "echo"})
self.assertEqual(r.returncode, 0, r.stderr)
self.assertIn("process_meeting.py", r.stdout)
def test_continues_past_failures(self):
with tempfile.TemporaryDirectory() as tmp:
tmp = Path(tmp)
ind = tmp / "in"
ind.mkdir()
(ind / "a.mp4").touch()
(ind / "b.mp4").touch()
r = run_batch(["-i", str(ind), "-o", str(tmp / "out")],
env_extra={"PYTHON": stub(tmp, "exit 1")})
self.assertNotEqual(r.returncode, 0) # nonzero when any failed
self.assertIn("0 ok, 2 failed", r.stdout)
self.assertIn("FAILED", r.stderr)
def test_stdin_isolation_processes_all_items(self):
# A child that drains stdin (like ffmpeg) must NOT eat the file list.
with tempfile.TemporaryDirectory() as tmp:
tmp = Path(tmp)
ind = tmp / "in"
ind.mkdir()
for n in ("a.mp4", "b.mp4", "c.mp4"):
(ind / n).touch()
greedy = stub(tmp, 'shift; cat >/dev/null; echo "DID $1"')
r = run_batch(["-i", str(ind), "-o", str(tmp / "out")],
env_extra={"PYTHON": greedy})
self.assertEqual(r.returncode, 0, r.stderr)
self.assertEqual(r.stdout.count("DID "), 3)
if __name__ == "__main__":
unittest.main()

77
tests/test_config.py Normal file
View File

@@ -0,0 +1,77 @@
"""Deeper tests for WorkflowConfig — the --transcript-formats parsing and the
post-cleanup shape (embed-images only; deprecated analysis attrs gone)."""
import unittest
from meetus.workflow import WorkflowConfig
def cfg(**kw):
kw.setdefault("video", "sample.mp4")
return WorkflowConfig(**kw)
class TestTranscriptFormats(unittest.TestCase):
def test_default_is_json_only(self):
self.assertEqual(cfg().transcript_formats, ["json"])
def test_srt_implies_json(self):
# User shouldn't have to list json; it's always the base.
self.assertEqual(cfg(transcript_formats="srt").transcript_formats,
["json", "srt"])
def test_multiple(self):
self.assertEqual(cfg(transcript_formats="srt,txt").transcript_formats,
["json", "srt", "txt"])
def test_all_expands(self):
self.assertEqual(set(cfg(transcript_formats="all").transcript_formats),
{"json", "srt", "vtt", "txt", "tsv"})
def test_dedup_and_json_kept(self):
self.assertEqual(cfg(transcript_formats="json,srt,srt").transcript_formats,
["json", "srt"])
def test_whitespace_and_case(self):
self.assertEqual(cfg(transcript_formats=" SRT , Txt ").transcript_formats,
["json", "srt", "txt"])
def test_invalid_raises(self):
with self.assertRaises(ValueError):
cfg(transcript_formats="srt,bogus")
class TestDefaults(unittest.TestCase):
def test_scalar_defaults(self):
c = cfg()
self.assertEqual(c.embed_quality, 80)
self.assertEqual(c.format, "detailed")
self.assertEqual(c.whisper_model, "medium")
self.assertEqual(c.scene_threshold, 15.0)
def test_to_dict_analysis_is_embed_images(self):
self.assertEqual(cfg().to_dict()["analysis"]["method"], "embed-images")
def test_to_dict_whisper_has_diarize_and_formats(self):
d = cfg(diarize=True, transcript_formats="srt").to_dict()
self.assertTrue(d["whisper"]["diarize"])
self.assertEqual(d["whisper"]["transcript_formats"], ["json", "srt"])
class TestDeprecatedRemoved(unittest.TestCase):
"""The OCR/vision/hybrid surface was removed in the cleanup."""
def test_no_deprecated_attrs(self):
c = cfg()
for attr in ("use_vision", "use_hybrid", "hybrid_llm_cleanup",
"hybrid_llm_model", "vision_model", "vision_context",
"ocr_engine", "no_deduplicate"):
self.assertFalse(hasattr(c, attr), f"{attr} should be removed")
def test_extra_kwargs_are_ignored(self):
# Stray kwargs (e.g. an old flag) must not crash construction.
c = cfg(use_vision=True, ocr_engine="tesseract")
self.assertFalse(hasattr(c, "use_vision"))
if __name__ == "__main__":
unittest.main()

55
tests/test_makefile.py Normal file
View File

@@ -0,0 +1,55 @@
"""Smoke for the Makefile wrapper: OUT defaults to IN, the baked-in FLAGS, and
the AUDIO_LANG knob (named to avoid the LANG locale env var)."""
import os
import shutil
import subprocess
import tempfile
import unittest
from pathlib import Path
from tests import REPO_ROOT
def make(target, **vars):
args = ["make", target] + [f"{k}={v}" for k, v in vars.items()]
return subprocess.run(args, cwd=REPO_ROOT, capture_output=True, text=True,
env=dict(os.environ))
@unittest.skipUnless(shutil.which("make"), "make not installed")
class TestMakefile(unittest.TestCase):
def test_dry_out_defaults_to_in(self):
with tempfile.TemporaryDirectory() as tmp:
ind = Path(tmp) / "in"
ind.mkdir()
(ind / "x.mp4").touch()
r = make("dry", IN=str(ind))
self.assertEqual(r.returncode, 0, r.stderr)
self.assertIn(f"Output: {ind}", r.stdout)
self.assertIn("Found 1 video", r.stdout)
def test_batch_baked_in_flags(self):
with tempfile.TemporaryDirectory() as tmp:
ind = Path(tmp) / "in"
ind.mkdir()
(ind / "x.mp4").touch()
r = make("batch", IN=str(ind), PYTHON="echo")
self.assertEqual(r.returncode, 0, r.stderr)
line = next(l for l in r.stdout.splitlines() if "process_meeting.py" in l)
self.assertIn("--scene-threshold 10", line)
self.assertIn("--transcript-formats srt", line)
self.assertIn("--embed-images", line)
self.assertNotIn("--language", line) # no AUDIO_LANG -> no --language
def test_audio_lang_appends_language(self):
with tempfile.TemporaryDirectory() as tmp:
ind = Path(tmp) / "in"
ind.mkdir()
(ind / "x.mp4").touch()
r = make("batch", IN=str(ind), PYTHON="echo", AUDIO_LANG="es")
self.assertEqual(r.returncode, 0, r.stderr)
self.assertIn("--language es", r.stdout)
if __name__ == "__main__":
unittest.main()

57
tests/test_smoke.py Normal file
View File

@@ -0,0 +1,57 @@
"""Smoke: the wired package imports, deprecated stays unwired, and the cleaned
CLI surface is what we expect."""
import subprocess
import sys
import unittest
from tests import REPO_ROOT, subprocess_env
class TestImports(unittest.TestCase):
def test_core_modules_import(self):
import meetus # noqa: F401
import meetus.workflow # noqa: F401 (frame_extractor → cv2 stub)
import meetus.frame_extractor # noqa: F401
import meetus.transcript_merger # noqa: F401
import meetus.output_manager # noqa: F401
import meetus.cache_manager # noqa: F401
def test_deprecated_importable_for_reference(self):
import meetus.deprecated.ocr_processor # noqa: F401
import meetus.deprecated.vision_processor # noqa: F401
def test_workflow_does_not_import_deprecated(self):
src = (REPO_ROOT / "meetus" / "workflow.py").read_text()
self.assertNotIn("from .deprecated", src)
self.assertNotIn("import deprecated", src)
class TestCliHelp(unittest.TestCase):
def _help(self, rel):
return subprocess.run(
[sys.executable, str(REPO_ROOT / rel), "--help"],
cwd=REPO_ROOT, env=subprocess_env(), capture_output=True, text=True,
)
def test_process_meeting_help_is_clean(self):
r = self._help("process_meeting.py")
self.assertEqual(r.returncode, 0, r.stderr)
out = r.stdout
for kept in ("--embed-images", "--diarize", "--transcript-formats",
"--scene-detection", "--skip-cache-frames", "--extract-only"):
self.assertIn(kept, out, f"{kept} should still be offered")
for gone in ("--use-vision", "--use-hybrid", "--ocr-engine",
"--hybrid-llm", "--vision-model", "--vision-context",
"--no-deduplicate", "--skip-cache-analysis"):
self.assertNotIn(gone, out, f"{gone} should be removed from the CLI")
def test_summarizers_offer_output_dir(self):
for rel in ("ctrl/summarize/summarize_simple.py",
"ctrl/summarize/compile_meeting.py"):
r = self._help(rel)
self.assertEqual(r.returncode, 0, r.stderr)
self.assertIn("--output-dir", r.stdout, rel)
if __name__ == "__main__":
unittest.main()

46
tests/test_summarize.py Normal file
View File

@@ -0,0 +1,46 @@
"""Deeper test for the summarizer --output-dir work: compile_meeting.default_output
(strips _enhanced, honors a base dir)."""
import importlib.util
import unittest
from pathlib import Path
from tests import REPO_ROOT
def _load(rel, name):
spec = importlib.util.spec_from_file_location(name, REPO_ROOT / rel)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod
class TestCompileDefaultOutput(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.cm = _load("ctrl/summarize/compile_meeting.py", "compile_meeting")
def test_next_to_transcript_strips_enhanced(self):
t = Path("/x/20260101-001-foo/foo_enhanced.txt")
self.assertEqual(self.cm.default_output(t, "reference"),
Path("/x/20260101-001-foo/foo_reference.md"))
def test_base_dir_override(self):
t = Path("/x/20260101-001-foo/foo_enhanced.txt")
self.assertEqual(self.cm.default_output(t, "reference", Path("/out")),
Path("/out/foo_reference.md"))
def test_no_enhanced_suffix(self):
t = Path("/x/run/foo.txt")
self.assertEqual(self.cm.default_output(t, "reference"),
Path("/x/run/foo_reference.md"))
def test_run_folder_mirroring(self):
# How main() builds base_dir for --output-dir: <out>/<transcript's run dir>.
t = Path("/runs/20260101-001-foo/foo_enhanced.txt")
base = Path("/out") / t.parent.name
self.assertEqual(self.cm.default_output(t, "reference", base),
Path("/out/20260101-001-foo/foo_reference.md"))
if __name__ == "__main__":
unittest.main()