From 7852c618ac9f3e2bc051a65b4c26f2f854b0d622 Mon Sep 17 00:00:00 2001 From: Ben Sima Date: Fri, 21 Nov 2025 00:41:03 -0500 Subject: task: sync database --- .tasks/tasks.jsonl | 1 + 1 file changed, 1 insertion(+) diff --git a/.tasks/tasks.jsonl b/.tasks/tasks.jsonl index d21d7fa..46a183c 100644 --- a/.tasks/tasks.jsonl +++ b/.tasks/tasks.jsonl @@ -158,3 +158,4 @@ {"taskCreatedAt":"2025-11-20T21:41:20.048368004Z","taskDependencies":[],"taskId":"t-1ne7VoO","taskNamespace":"Biz/Que/Host.hs","taskParent":"t-1f9QP23","taskPriority":"P2","taskStatus":"Open","taskTitle":"Revive authkey authentication in Que/Host","taskType":"WorkTask","taskUpdatedAt":"2025-11-20T21:41:20.048368004Z"} {"taskCreatedAt":"2025-11-20T21:41:20.067644599Z","taskDependencies":[],"taskId":"t-1ne80pJ","taskNamespace":"Biz/Dragons.hs","taskParent":"t-1f9QP23","taskPriority":"P2","taskStatus":"Done","taskTitle":"Store generated JWK in persistent file","taskType":"WorkTask","taskUpdatedAt":"2025-11-20T22:54:17.655700806Z"} {"taskCreatedAt":"2025-11-21T04:30:05.792313193Z","taskDependencies":[],"taskId":"t-rWacMb1av","taskNamespace":"Omni/Task.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Open","taskTitle":"Make task IDs case-insensitive","taskType":"WorkTask","taskUpdatedAt":"2025-11-21T04:30:05.792313193Z"} +{"taskCreatedAt":"2025-11-21T05:28:31.973657907Z","taskDependencies":[],"taskId":"t-rWagBsB6w","taskNamespace":"Biz/PodcastItLater.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Add error handling tests for Worker","taskType":"WorkTask","taskUpdatedAt":"2025-11-21T05:40:59.255645021Z"} -- cgit v1.2.3 From e82ec6759702ef9eac7b78df17ed5cb3f51c4541 Mon Sep 17 00:00:00 2001 From: Ben Sima Date: Fri, 21 Nov 2025 00:41:11 -0500 Subject: feat: implement t-1neWD8r --- Biz/PodcastItLater/Worker.py | 113 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) diff --git a/Biz/PodcastItLater/Worker.py b/Biz/PodcastItLater/Worker.py index 5203490..e7b2ffc 100644 --- a/Biz/PodcastItLater/Worker.py +++ b/Biz/PodcastItLater/Worker.py @@ -2039,6 +2039,117 @@ class TestJobProcessing(Test.TestCase): mock_update.assert_not_called() +class TestWorkerErrorHandling(Test.TestCase): + """Test worker error handling and recovery.""" + + def setUp(self) -> None: + """Set up test environment.""" + Core.Database.init_db() + self.user_id, _ = Core.Database.create_user("test@example.com") + self.job_id = Core.Database.add_to_queue( + "https://example.com", + "test@example.com", + self.user_id, + ) + self.shutdown_handler = ShutdownHandler() + self.processor = ArticleProcessor(self.shutdown_handler) + + @staticmethod + def tearDown() -> None: + """Clean up.""" + Core.Database.teardown() + + def test_process_pending_jobs_exception_handling(self) -> None: + """Test that process_pending_jobs handles exceptions.""" + + def side_effect(job: dict[str, Any]) -> None: + # Simulate process_job starting and setting status to processing + Core.Database.update_job_status(job["id"], "processing") + msg = "Unexpected Error" + raise ValueError(msg) + + with ( + unittest.mock.patch.object( + self.processor, + "process_job", + side_effect=side_effect, + ), + unittest.mock.patch( + "Biz.PodcastItLater.Core.Database.update_job_status", + side_effect=Core.Database.update_job_status, + ) as _mock_update, + ): + process_pending_jobs(self.processor) + + # Job should be marked as error + job = Core.Database.get_job_by_id(self.job_id) + self.assertIsNotNone(job) + if job: + self.assertEqual(job["status"], "error") + self.assertIn("Unexpected Error", job["error_message"]) + + def test_process_retryable_jobs_success(self) -> None: + """Test processing of retryable jobs.""" + # Set up a retryable job + Core.Database.update_job_status(self.job_id, "error", "Fail 1") + + # Modify created_at to be in the past to satisfy backoff + with Core.Database.get_connection() as conn: + conn.execute( + "UPDATE queue SET created_at = ? WHERE id = ?", + ( + ( + datetime.now(tz=timezone.utc) - timedelta(minutes=5) + ).isoformat(), + self.job_id, + ), + ) + conn.commit() + + process_retryable_jobs() + + job = Core.Database.get_job_by_id(self.job_id) + self.assertIsNotNone(job) + if job: + self.assertEqual(job["status"], "pending") + + def test_process_retryable_jobs_not_ready(self) -> None: + """Test that jobs are not retried before backoff period.""" + # Set up a retryable job that just failed + Core.Database.update_job_status(self.job_id, "error", "Fail 1") + + # created_at is now, so backoff should prevent retry + process_retryable_jobs() + + job = Core.Database.get_job_by_id(self.job_id) + self.assertIsNotNone(job) + if job: + self.assertEqual(job["status"], "error") + + +class TestTextChunking(Test.TestCase): + """Test text chunking edge cases.""" + + def test_split_text_single_long_word(self) -> None: + """Handle text with a single word exceeding limit.""" + long_word = "a" * 4000 + chunks = split_text_into_chunks(long_word, max_chars=3000) + + # Should keep it as one chunk or split? + # The current implementation does not split words + self.assertEqual(len(chunks), 1) + self.assertEqual(len(chunks[0]), 4000) + + def test_split_text_no_sentence_boundaries(self) -> None: + """Handle long text with no sentence boundaries.""" + text = "word " * 1000 # 5000 chars + chunks = split_text_into_chunks(text, max_chars=3000) + + # Should keep it as one chunk as it can't split by ". " + self.assertEqual(len(chunks), 1) + self.assertGreater(len(chunks[0]), 3000) + + def test() -> None: """Run the tests.""" Test.run( @@ -2048,6 +2159,8 @@ def test() -> None: TestTextToSpeech, TestMemoryEfficiency, TestJobProcessing, + TestWorkerErrorHandling, + TestTextChunking, ], ) -- cgit v1.2.3