karaoke-gen 0.90.1__py3-none-any.whl → 0.99.3__py3-none-any.whl

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 (197) hide show
  1. backend/.coveragerc +20 -0
  2. backend/.gitignore +37 -0
  3. backend/Dockerfile +43 -0
  4. backend/Dockerfile.base +74 -0
  5. backend/README.md +242 -0
  6. backend/__init__.py +0 -0
  7. backend/api/__init__.py +0 -0
  8. backend/api/dependencies.py +457 -0
  9. backend/api/routes/__init__.py +0 -0
  10. backend/api/routes/admin.py +835 -0
  11. backend/api/routes/audio_search.py +913 -0
  12. backend/api/routes/auth.py +348 -0
  13. backend/api/routes/file_upload.py +2112 -0
  14. backend/api/routes/health.py +409 -0
  15. backend/api/routes/internal.py +435 -0
  16. backend/api/routes/jobs.py +1629 -0
  17. backend/api/routes/review.py +652 -0
  18. backend/api/routes/themes.py +162 -0
  19. backend/api/routes/users.py +1513 -0
  20. backend/config.py +172 -0
  21. backend/main.py +157 -0
  22. backend/middleware/__init__.py +5 -0
  23. backend/middleware/audit_logging.py +124 -0
  24. backend/models/__init__.py +0 -0
  25. backend/models/job.py +519 -0
  26. backend/models/requests.py +123 -0
  27. backend/models/theme.py +153 -0
  28. backend/models/user.py +254 -0
  29. backend/models/worker_log.py +164 -0
  30. backend/pyproject.toml +29 -0
  31. backend/quick-check.sh +93 -0
  32. backend/requirements.txt +29 -0
  33. backend/run_tests.sh +60 -0
  34. backend/services/__init__.py +0 -0
  35. backend/services/audio_analysis_service.py +243 -0
  36. backend/services/audio_editing_service.py +278 -0
  37. backend/services/audio_search_service.py +702 -0
  38. backend/services/auth_service.py +630 -0
  39. backend/services/credential_manager.py +792 -0
  40. backend/services/discord_service.py +172 -0
  41. backend/services/dropbox_service.py +301 -0
  42. backend/services/email_service.py +1093 -0
  43. backend/services/encoding_interface.py +454 -0
  44. backend/services/encoding_service.py +502 -0
  45. backend/services/firestore_service.py +512 -0
  46. backend/services/flacfetch_client.py +573 -0
  47. backend/services/gce_encoding/README.md +72 -0
  48. backend/services/gce_encoding/__init__.py +22 -0
  49. backend/services/gce_encoding/main.py +589 -0
  50. backend/services/gce_encoding/requirements.txt +16 -0
  51. backend/services/gdrive_service.py +356 -0
  52. backend/services/job_logging.py +258 -0
  53. backend/services/job_manager.py +853 -0
  54. backend/services/job_notification_service.py +271 -0
  55. backend/services/langfuse_preloader.py +98 -0
  56. backend/services/local_encoding_service.py +590 -0
  57. backend/services/local_preview_encoding_service.py +407 -0
  58. backend/services/lyrics_cache_service.py +216 -0
  59. backend/services/metrics.py +413 -0
  60. backend/services/nltk_preloader.py +122 -0
  61. backend/services/packaging_service.py +287 -0
  62. backend/services/rclone_service.py +106 -0
  63. backend/services/spacy_preloader.py +65 -0
  64. backend/services/storage_service.py +209 -0
  65. backend/services/stripe_service.py +371 -0
  66. backend/services/structured_logging.py +254 -0
  67. backend/services/template_service.py +330 -0
  68. backend/services/theme_service.py +469 -0
  69. backend/services/tracing.py +543 -0
  70. backend/services/user_service.py +721 -0
  71. backend/services/worker_service.py +558 -0
  72. backend/services/youtube_service.py +112 -0
  73. backend/services/youtube_upload_service.py +445 -0
  74. backend/tests/__init__.py +4 -0
  75. backend/tests/conftest.py +224 -0
  76. backend/tests/emulator/__init__.py +7 -0
  77. backend/tests/emulator/conftest.py +109 -0
  78. backend/tests/emulator/test_e2e_cli_backend.py +1053 -0
  79. backend/tests/emulator/test_emulator_integration.py +356 -0
  80. backend/tests/emulator/test_style_loading_direct.py +436 -0
  81. backend/tests/emulator/test_worker_logs_direct.py +229 -0
  82. backend/tests/emulator/test_worker_logs_subcollection.py +443 -0
  83. backend/tests/requirements-test.txt +10 -0
  84. backend/tests/requirements.txt +6 -0
  85. backend/tests/test_admin_email_endpoints.py +411 -0
  86. backend/tests/test_api_integration.py +460 -0
  87. backend/tests/test_api_routes.py +93 -0
  88. backend/tests/test_audio_analysis_service.py +294 -0
  89. backend/tests/test_audio_editing_service.py +386 -0
  90. backend/tests/test_audio_search.py +1398 -0
  91. backend/tests/test_audio_services.py +378 -0
  92. backend/tests/test_auth_firestore.py +231 -0
  93. backend/tests/test_config_extended.py +68 -0
  94. backend/tests/test_credential_manager.py +377 -0
  95. backend/tests/test_dependencies.py +54 -0
  96. backend/tests/test_discord_service.py +244 -0
  97. backend/tests/test_distribution_services.py +820 -0
  98. backend/tests/test_dropbox_service.py +472 -0
  99. backend/tests/test_email_service.py +492 -0
  100. backend/tests/test_emulator_integration.py +322 -0
  101. backend/tests/test_encoding_interface.py +412 -0
  102. backend/tests/test_file_upload.py +1739 -0
  103. backend/tests/test_flacfetch_client.py +632 -0
  104. backend/tests/test_gdrive_service.py +524 -0
  105. backend/tests/test_instrumental_api.py +431 -0
  106. backend/tests/test_internal_api.py +343 -0
  107. backend/tests/test_job_creation_regression.py +583 -0
  108. backend/tests/test_job_manager.py +356 -0
  109. backend/tests/test_job_manager_notifications.py +329 -0
  110. backend/tests/test_job_notification_service.py +443 -0
  111. backend/tests/test_jobs_api.py +283 -0
  112. backend/tests/test_local_encoding_service.py +423 -0
  113. backend/tests/test_local_preview_encoding_service.py +567 -0
  114. backend/tests/test_main.py +87 -0
  115. backend/tests/test_models.py +918 -0
  116. backend/tests/test_packaging_service.py +382 -0
  117. backend/tests/test_requests.py +201 -0
  118. backend/tests/test_routes_jobs.py +282 -0
  119. backend/tests/test_routes_review.py +337 -0
  120. backend/tests/test_services.py +556 -0
  121. backend/tests/test_services_extended.py +112 -0
  122. backend/tests/test_spacy_preloader.py +119 -0
  123. backend/tests/test_storage_service.py +448 -0
  124. backend/tests/test_style_upload.py +261 -0
  125. backend/tests/test_template_service.py +295 -0
  126. backend/tests/test_theme_service.py +516 -0
  127. backend/tests/test_unicode_sanitization.py +522 -0
  128. backend/tests/test_upload_api.py +256 -0
  129. backend/tests/test_validate.py +156 -0
  130. backend/tests/test_video_worker_orchestrator.py +847 -0
  131. backend/tests/test_worker_log_subcollection.py +509 -0
  132. backend/tests/test_worker_logging.py +365 -0
  133. backend/tests/test_workers.py +1116 -0
  134. backend/tests/test_workers_extended.py +178 -0
  135. backend/tests/test_youtube_service.py +247 -0
  136. backend/tests/test_youtube_upload_service.py +568 -0
  137. backend/utils/test_data.py +27 -0
  138. backend/validate.py +173 -0
  139. backend/version.py +27 -0
  140. backend/workers/README.md +597 -0
  141. backend/workers/__init__.py +11 -0
  142. backend/workers/audio_worker.py +618 -0
  143. backend/workers/lyrics_worker.py +683 -0
  144. backend/workers/render_video_worker.py +483 -0
  145. backend/workers/screens_worker.py +535 -0
  146. backend/workers/style_helper.py +198 -0
  147. backend/workers/video_worker.py +1277 -0
  148. backend/workers/video_worker_orchestrator.py +701 -0
  149. backend/workers/worker_logging.py +278 -0
  150. karaoke_gen/instrumental_review/static/index.html +7 -4
  151. karaoke_gen/karaoke_finalise/karaoke_finalise.py +6 -1
  152. karaoke_gen/utils/__init__.py +163 -8
  153. karaoke_gen/video_background_processor.py +9 -4
  154. {karaoke_gen-0.90.1.dist-info → karaoke_gen-0.99.3.dist-info}/METADATA +1 -1
  155. {karaoke_gen-0.90.1.dist-info → karaoke_gen-0.99.3.dist-info}/RECORD +196 -46
  156. lyrics_transcriber/correction/agentic/agent.py +17 -6
  157. lyrics_transcriber/correction/agentic/providers/config.py +9 -5
  158. lyrics_transcriber/correction/agentic/providers/langchain_bridge.py +96 -93
  159. lyrics_transcriber/correction/agentic/providers/model_factory.py +27 -6
  160. lyrics_transcriber/correction/anchor_sequence.py +151 -37
  161. lyrics_transcriber/correction/corrector.py +192 -130
  162. lyrics_transcriber/correction/handlers/syllables_match.py +44 -2
  163. lyrics_transcriber/correction/operations.py +24 -9
  164. lyrics_transcriber/correction/phrase_analyzer.py +18 -0
  165. lyrics_transcriber/frontend/package-lock.json +2 -2
  166. lyrics_transcriber/frontend/package.json +1 -1
  167. lyrics_transcriber/frontend/src/components/AIFeedbackModal.tsx +1 -1
  168. lyrics_transcriber/frontend/src/components/CorrectedWordWithActions.tsx +11 -7
  169. lyrics_transcriber/frontend/src/components/EditActionBar.tsx +31 -5
  170. lyrics_transcriber/frontend/src/components/EditModal.tsx +28 -10
  171. lyrics_transcriber/frontend/src/components/EditTimelineSection.tsx +123 -27
  172. lyrics_transcriber/frontend/src/components/EditWordList.tsx +112 -60
  173. lyrics_transcriber/frontend/src/components/Header.tsx +90 -76
  174. lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +53 -31
  175. lyrics_transcriber/frontend/src/components/LyricsSynchronizer/SyncControls.tsx +44 -13
  176. lyrics_transcriber/frontend/src/components/LyricsSynchronizer/TimelineCanvas.tsx +66 -50
  177. lyrics_transcriber/frontend/src/components/LyricsSynchronizer/index.tsx +124 -30
  178. lyrics_transcriber/frontend/src/components/ReferenceView.tsx +1 -1
  179. lyrics_transcriber/frontend/src/components/TimelineEditor.tsx +12 -5
  180. lyrics_transcriber/frontend/src/components/TimingOffsetModal.tsx +3 -3
  181. lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +1 -1
  182. lyrics_transcriber/frontend/src/components/WordDivider.tsx +11 -7
  183. lyrics_transcriber/frontend/src/components/shared/components/Word.tsx +4 -2
  184. lyrics_transcriber/frontend/src/hooks/useManualSync.ts +103 -1
  185. lyrics_transcriber/frontend/src/theme.ts +42 -15
  186. lyrics_transcriber/frontend/tsconfig.tsbuildinfo +1 -1
  187. lyrics_transcriber/frontend/vite.config.js +5 -0
  188. lyrics_transcriber/frontend/web_assets/assets/{index-BECn1o8Q.js → index-BSMgOq4Z.js} +6959 -5782
  189. lyrics_transcriber/frontend/web_assets/assets/index-BSMgOq4Z.js.map +1 -0
  190. lyrics_transcriber/frontend/web_assets/index.html +6 -2
  191. lyrics_transcriber/frontend/web_assets/nomad-karaoke-logo.svg +5 -0
  192. lyrics_transcriber/output/generator.py +17 -3
  193. lyrics_transcriber/output/video.py +60 -95
  194. lyrics_transcriber/frontend/web_assets/assets/index-BECn1o8Q.js.map +0 -1
  195. {karaoke_gen-0.90.1.dist-info → karaoke_gen-0.99.3.dist-info}/WHEEL +0 -0
  196. {karaoke_gen-0.90.1.dist-info → karaoke_gen-0.99.3.dist-info}/entry_points.txt +0 -0
  197. {karaoke_gen-0.90.1.dist-info → karaoke_gen-0.99.3.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,343 @@
1
+ """
2
+ Unit tests for internal.py API routes using FastAPI TestClient.
3
+
4
+ Internal routes are used for worker communication within Cloud Run.
5
+ These endpoints require admin authentication.
6
+
7
+ With Cloud Tasks integration, these endpoints include idempotency checks
8
+ to prevent duplicate processing on retries.
9
+ """
10
+ import os
11
+ import pytest
12
+ from datetime import datetime, UTC
13
+ from unittest.mock import MagicMock, AsyncMock, patch
14
+ from fastapi.testclient import TestClient
15
+
16
+ from backend.models.job import Job, JobStatus
17
+
18
+
19
+ # Set test admin token for auth
20
+ os.environ.setdefault('ADMIN_TOKENS', 'test-admin-token')
21
+
22
+
23
+ def _create_mock_job(job_id: str = "test123") -> Job:
24
+ """Create a mock Job object for testing."""
25
+ return Job(
26
+ job_id=job_id,
27
+ status=JobStatus.PROCESSING,
28
+ created_at=datetime.now(UTC),
29
+ updated_at=datetime.now(UTC),
30
+ state_data={}, # Empty state_data - no worker progress yet
31
+ )
32
+
33
+
34
+ @pytest.fixture
35
+ def client():
36
+ """Create TestClient with mocked workers and job manager."""
37
+ mock_creds = MagicMock()
38
+ mock_creds.universe_domain = 'googleapis.com'
39
+
40
+ # Mock the worker functions at the internal routes module level
41
+ mock_audio = AsyncMock()
42
+ mock_lyrics = AsyncMock()
43
+ mock_screens = AsyncMock()
44
+ mock_video = AsyncMock()
45
+ mock_render = AsyncMock()
46
+
47
+ # Mock JobManager for idempotency checks
48
+ mock_job_manager = MagicMock()
49
+ mock_job_manager.get_job.return_value = _create_mock_job()
50
+ mock_job_manager.update_state_data.return_value = None
51
+
52
+ with patch('backend.api.routes.internal.process_audio_separation', mock_audio), \
53
+ patch('backend.api.routes.internal.process_lyrics_transcription', mock_lyrics), \
54
+ patch('backend.api.routes.internal.generate_screens', mock_screens), \
55
+ patch('backend.api.routes.internal.generate_video', mock_video), \
56
+ patch('backend.api.routes.internal.process_render_video', mock_render), \
57
+ patch('backend.api.routes.internal.JobManager', return_value=mock_job_manager), \
58
+ patch('backend.services.firestore_service.firestore'), \
59
+ patch('backend.services.storage_service.storage'), \
60
+ patch('google.auth.default', return_value=(mock_creds, 'test-project')):
61
+ from backend.main import app
62
+ yield TestClient(app)
63
+
64
+
65
+ @pytest.fixture
66
+ def auth_headers():
67
+ """Auth headers for internal API testing."""
68
+ return {"Authorization": "Bearer test-admin-token"}
69
+
70
+
71
+ class TestAudioWorkerEndpoint:
72
+ """Tests for POST /api/internal/workers/audio."""
73
+
74
+ def test_audio_worker_returns_200(self, client, auth_headers):
75
+ """Test audio worker endpoint returns 200 with auth."""
76
+ response = client.post(
77
+ "/api/internal/workers/audio",
78
+ headers=auth_headers,
79
+ json={"job_id": "test123"}
80
+ )
81
+ assert response.status_code == 200
82
+
83
+ def test_audio_worker_accepts_job_id(self, client, auth_headers):
84
+ """Test audio worker accepts job_id in body."""
85
+ response = client.post(
86
+ "/api/internal/workers/audio",
87
+ headers=auth_headers,
88
+ json={"job_id": "any-job-id"}
89
+ )
90
+ assert response.status_code == 200
91
+
92
+ def test_audio_worker_requires_job_id(self, client, auth_headers):
93
+ """Test audio worker requires job_id."""
94
+ response = client.post(
95
+ "/api/internal/workers/audio",
96
+ headers=auth_headers,
97
+ json={}
98
+ )
99
+ assert response.status_code == 422
100
+
101
+ def test_audio_worker_requires_auth(self, client, auth_headers):
102
+ """Test audio worker requires authentication."""
103
+ response = client.post(
104
+ "/api/internal/workers/audio",
105
+ json={"job_id": "test123"}
106
+ )
107
+ assert response.status_code == 401
108
+
109
+
110
+ class TestLyricsWorkerEndpoint:
111
+ """Tests for POST /api/internal/workers/lyrics."""
112
+
113
+ def test_lyrics_worker_returns_200(self, client, auth_headers):
114
+ """Test lyrics worker endpoint returns 200 with auth."""
115
+ response = client.post(
116
+ "/api/internal/workers/lyrics",
117
+ headers=auth_headers,
118
+ json={"job_id": "test123"}
119
+ )
120
+ assert response.status_code == 200
121
+
122
+ def test_lyrics_worker_requires_job_id(self, client, auth_headers):
123
+ """Test lyrics worker requires job_id."""
124
+ response = client.post(
125
+ "/api/internal/workers/lyrics",
126
+ headers=auth_headers,
127
+ json={}
128
+ )
129
+ assert response.status_code == 422
130
+
131
+ def test_lyrics_worker_requires_auth(self, client, auth_headers):
132
+ """Test lyrics worker requires authentication."""
133
+ response = client.post(
134
+ "/api/internal/workers/lyrics",
135
+ json={"job_id": "test123"}
136
+ )
137
+ assert response.status_code == 401
138
+
139
+
140
+ class TestScreensWorkerEndpoint:
141
+ """Tests for POST /api/internal/workers/screens."""
142
+
143
+ def test_screens_worker_returns_200(self, client, auth_headers):
144
+ """Test screens worker endpoint returns 200 with auth."""
145
+ response = client.post(
146
+ "/api/internal/workers/screens",
147
+ headers=auth_headers,
148
+ json={"job_id": "test123"}
149
+ )
150
+ assert response.status_code == 200
151
+
152
+ def test_screens_worker_requires_job_id(self, client, auth_headers):
153
+ """Test screens worker requires job_id."""
154
+ response = client.post(
155
+ "/api/internal/workers/screens",
156
+ headers=auth_headers,
157
+ json={}
158
+ )
159
+ assert response.status_code == 422
160
+
161
+ def test_screens_worker_requires_auth(self, client, auth_headers):
162
+ """Test screens worker requires authentication."""
163
+ response = client.post(
164
+ "/api/internal/workers/screens",
165
+ json={"job_id": "test123"}
166
+ )
167
+ assert response.status_code == 401
168
+
169
+
170
+ class TestVideoWorkerEndpoint:
171
+ """Tests for POST /api/internal/workers/video."""
172
+
173
+ def test_video_worker_returns_200(self, client, auth_headers):
174
+ """Test video worker endpoint returns 200 with auth."""
175
+ response = client.post(
176
+ "/api/internal/workers/video",
177
+ headers=auth_headers,
178
+ json={"job_id": "test123"}
179
+ )
180
+ assert response.status_code == 200
181
+
182
+ def test_video_worker_requires_job_id(self, client, auth_headers):
183
+ """Test video worker requires job_id."""
184
+ response = client.post(
185
+ "/api/internal/workers/video",
186
+ headers=auth_headers,
187
+ json={}
188
+ )
189
+ assert response.status_code == 422
190
+
191
+ def test_video_worker_requires_auth(self, client, auth_headers):
192
+ """Test video worker requires authentication."""
193
+ response = client.post(
194
+ "/api/internal/workers/video",
195
+ json={"job_id": "test123"}
196
+ )
197
+ assert response.status_code == 401
198
+
199
+
200
+ class TestRenderVideoWorkerEndpoint:
201
+ """Tests for POST /api/internal/workers/render-video."""
202
+
203
+ def test_render_video_worker_returns_200(self, client, auth_headers):
204
+ """Test render-video worker endpoint returns 200 with auth."""
205
+ response = client.post(
206
+ "/api/internal/workers/render-video",
207
+ headers=auth_headers,
208
+ json={"job_id": "test123"}
209
+ )
210
+ assert response.status_code == 200
211
+
212
+ def test_render_video_worker_requires_job_id(self, client, auth_headers):
213
+ """Test render-video worker requires job_id."""
214
+ response = client.post(
215
+ "/api/internal/workers/render-video",
216
+ headers=auth_headers,
217
+ json={}
218
+ )
219
+ assert response.status_code == 422
220
+
221
+ def test_render_video_worker_requires_auth(self, client, auth_headers):
222
+ """Test render-video worker requires authentication."""
223
+ response = client.post(
224
+ "/api/internal/workers/render-video",
225
+ json={"job_id": "test123"}
226
+ )
227
+ assert response.status_code == 401
228
+
229
+
230
+ class TestWorkerResponseFormat:
231
+ """Tests for worker response format."""
232
+
233
+ def test_audio_worker_response_contains_status(self, client, auth_headers):
234
+ """Test audio worker response contains status."""
235
+ response = client.post(
236
+ "/api/internal/workers/audio",
237
+ headers=auth_headers,
238
+ json={"job_id": "test123"}
239
+ )
240
+ data = response.json()
241
+ assert "status" in data or "message" in data
242
+
243
+ def test_lyrics_worker_response_contains_status(self, client, auth_headers):
244
+ """Test lyrics worker response contains status."""
245
+ response = client.post(
246
+ "/api/internal/workers/lyrics",
247
+ headers=auth_headers,
248
+ json={"job_id": "test123"}
249
+ )
250
+ data = response.json()
251
+ assert "status" in data or "message" in data
252
+
253
+
254
+ class TestWorkerIdempotency:
255
+ """Tests for worker idempotency checks (Cloud Tasks retry support)."""
256
+
257
+ def test_audio_worker_returns_already_running_when_in_progress(self, auth_headers):
258
+ """Test audio worker returns already_running if worker is already running."""
259
+ mock_creds = MagicMock()
260
+ mock_creds.universe_domain = 'googleapis.com'
261
+
262
+ # Create job with audio already running
263
+ mock_job = _create_mock_job()
264
+ mock_job.state_data = {'audio_progress': {'stage': 'running'}}
265
+
266
+ mock_job_manager = MagicMock()
267
+ mock_job_manager.get_job.return_value = mock_job
268
+
269
+ with patch('backend.api.routes.internal.process_audio_separation', AsyncMock()), \
270
+ patch('backend.api.routes.internal.JobManager', return_value=mock_job_manager), \
271
+ patch('backend.services.firestore_service.firestore'), \
272
+ patch('backend.services.storage_service.storage'), \
273
+ patch('google.auth.default', return_value=(mock_creds, 'test-project')):
274
+ from backend.main import app
275
+ client = TestClient(app)
276
+
277
+ response = client.post(
278
+ "/api/internal/workers/audio",
279
+ headers=auth_headers,
280
+ json={"job_id": "test123"}
281
+ )
282
+
283
+ assert response.status_code == 200
284
+ data = response.json()
285
+ assert data["status"] == "already_running"
286
+
287
+ def test_audio_worker_returns_already_complete_when_finished(self, auth_headers):
288
+ """Test audio worker returns already_complete if worker already finished."""
289
+ mock_creds = MagicMock()
290
+ mock_creds.universe_domain = 'googleapis.com'
291
+
292
+ # Create job with audio already complete
293
+ mock_job = _create_mock_job()
294
+ mock_job.state_data = {'audio_progress': {'stage': 'complete'}}
295
+
296
+ mock_job_manager = MagicMock()
297
+ mock_job_manager.get_job.return_value = mock_job
298
+
299
+ with patch('backend.api.routes.internal.process_audio_separation', AsyncMock()), \
300
+ patch('backend.api.routes.internal.JobManager', return_value=mock_job_manager), \
301
+ patch('backend.services.firestore_service.firestore'), \
302
+ patch('backend.services.storage_service.storage'), \
303
+ patch('google.auth.default', return_value=(mock_creds, 'test-project')):
304
+ from backend.main import app
305
+ client = TestClient(app)
306
+
307
+ response = client.post(
308
+ "/api/internal/workers/audio",
309
+ headers=auth_headers,
310
+ json={"job_id": "test123"}
311
+ )
312
+
313
+ assert response.status_code == 200
314
+ data = response.json()
315
+ assert data["status"] == "already_complete"
316
+
317
+ def test_audio_worker_returns_not_found_when_job_missing(self, auth_headers):
318
+ """Test audio worker returns not_found if job doesn't exist."""
319
+ mock_creds = MagicMock()
320
+ mock_creds.universe_domain = 'googleapis.com'
321
+
322
+ # Return None for job not found
323
+ mock_job_manager = MagicMock()
324
+ mock_job_manager.get_job.return_value = None
325
+
326
+ with patch('backend.api.routes.internal.process_audio_separation', AsyncMock()), \
327
+ patch('backend.api.routes.internal.JobManager', return_value=mock_job_manager), \
328
+ patch('backend.services.firestore_service.firestore'), \
329
+ patch('backend.services.storage_service.storage'), \
330
+ patch('google.auth.default', return_value=(mock_creds, 'test-project')):
331
+ from backend.main import app
332
+ client = TestClient(app)
333
+
334
+ response = client.post(
335
+ "/api/internal/workers/audio",
336
+ headers=auth_headers,
337
+ json={"job_id": "nonexistent"}
338
+ )
339
+
340
+ assert response.status_code == 200
341
+ data = response.json()
342
+ assert data["status"] == "not_found"
343
+