import json import sys import tempfile import unittest from pathlib import Path from unittest.mock import MagicMock, call, patch sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src")) import processing_logger from data_export import DataExporter from result_updater import ResultUpdater from scratch_manager import ScratchManager class Story06ProcessingLoggerTests(unittest.TestCase): def test_insert_log_executes_correct_sql(self): cursor = MagicMock() processing_logger.insert_log( cursor, video_id=42, model_version="v1.0.0", frame_count=8, confidence_score=0.82, routing_decision="MATCH", frame_confidences=[0.80, 0.82, 0.85], ) cursor.execute.assert_called_once() sql, params = cursor.execute.call_args[0] self.assertIn("INSERT INTO processing_logs", sql) self.assertEqual(params[0], 42) # video_id self.assertEqual(params[1], "v1.0.0") # model_version self.assertEqual(params[2], 8) # frame_count self.assertAlmostEqual(params[3], 0.82) # confidence_score scores = json.loads(params[4]) # confidence_scores JSON self.assertEqual(scores, [0.80, 0.82, 0.85]) self.assertEqual(params[5], "MATCH") # routing_decision def test_insert_log_null_frame_confidences(self): cursor = MagicMock() processing_logger.insert_log(cursor, 1, "v0", 0, None, "SKIP") _, params = cursor.execute.call_args[0] self.assertIsNone(params[4]) # confidence_scores column class Story06ResultUpdaterTests(unittest.TestCase): def _make_db(self, rowcount=1): cursor = MagicMock() cursor.rowcount = rowcount conn = MagicMock() conn.cursor.return_value = cursor db = MagicMock() db.transaction.return_value.__enter__ = MagicMock(return_value=conn) db.transaction.return_value.__exit__ = MagicMock(return_value=False) return db, cursor def test_persist_returns_true_when_update_succeeds(self): db, cursor = self._make_db(rowcount=1) updater = ResultUpdater(db, model_version="v1.0.0") result = updater.persist( video_id=7, frame_count=5, confidence=0.9, routing="MATCH", frame_confidences=[0.9], ) self.assertTrue(result) def test_persist_returns_false_on_state_guard_miss(self): db, cursor = self._make_db(rowcount=0) updater = ResultUpdater(db, model_version="v1.0.0") result = updater.persist( video_id=7, frame_count=5, confidence=0.9, routing="MATCH", frame_confidences=[0.9], ) self.assertFalse(result) def test_persist_calls_insert_log_after_update(self): db, cursor = self._make_db(rowcount=1) updater = ResultUpdater(db, model_version="v1.0.0") updater.persist(video_id=7, frame_count=5, confidence=0.9, routing="MATCH", frame_confidences=[0.9]) # cursor.execute called twice: UPDATE videos + INSERT processing_logs self.assertEqual(cursor.execute.call_count, 2) class Story06DataExporterTests(unittest.TestCase): def test_jsonl_flush_writes_valid_records(self): with tempfile.TemporaryDirectory() as tmpdir: exporter = DataExporter( output_path=tmpdir, model_version="v1", export_format="jsonl", batch_size=100, ) exporter.add({"video_id": 1, "routing": "MATCH", "video_confidence": 0.9, "confidence_scores": [0.9], "sample_count": 1, "file_path": "/a.mp4", "processed_at": "2026-01-01T00:00:00+00:00"}) path = exporter.flush() self.assertIsNotNone(path) lines = Path(path).read_text(encoding="utf-8").strip().split("\n") self.assertEqual(len(lines), 1) record = json.loads(lines[0]) self.assertEqual(record["routing"], "MATCH") self.assertAlmostEqual(record["video_confidence"], 0.9) def test_auto_flush_at_batch_size(self): with tempfile.TemporaryDirectory() as tmpdir: exporter = DataExporter( output_path=tmpdir, model_version="v1", export_format="jsonl", batch_size=2, ) exporter.add({"video_id": 1, "routing": "SKIP", "video_confidence": 0.1, "confidence_scores": [], "sample_count": 0, "file_path": "/a.mp4", "processed_at": ""}) exporter.add({"video_id": 2, "routing": "MATCH", "video_confidence": 0.9, "confidence_scores": [0.9], "sample_count": 1, "file_path": "/b.mp4", "processed_at": ""}) # batch_size=2 → auto-flush triggered on second add self.assertEqual(exporter._buffer, []) def test_exclude_frame_confidences_when_disabled(self): with tempfile.TemporaryDirectory() as tmpdir: exporter = DataExporter( output_path=tmpdir, model_version="v1", export_format="jsonl", batch_size=100, include_frame_confidences=False, ) exporter.add({"video_id": 1, "routing": "SKIP", "video_confidence": 0.1, "confidence_scores": [0.1, 0.2], "sample_count": 2, "file_path": "/a.mp4", "processed_at": ""}) path = exporter.flush() record = json.loads(Path(path).read_text()) self.assertNotIn("confidence_scores", record) class Story06ScratchManagerCleanupAllTests(unittest.TestCase): def test_cleanup_all_removes_entire_video_directory(self): with tempfile.TemporaryDirectory() as tmpdir: manager = ScratchManager(base_path=tmpdir, video_id="v42", auto_cleanup=True) frame_dir = manager.ensure_frame_dir() crops_dir = Path(tmpdir) / "v42" / "crops" crops_dir.mkdir(parents=True) (frame_dir / "frame.jpg").write_bytes(b"f") (crops_dir / "crop.jpg").write_bytes(b"c") manager.cleanup_all() self.assertFalse((Path(tmpdir) / "v42").exists()) if __name__ == "__main__": unittest.main()