collab-runtime 0.2.9__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 (82) hide show
  1. collab/__init__.py +77 -0
  2. collab/__main__.py +11 -0
  3. collab_runtime-0.2.9.dist-info/METADATA +218 -0
  4. collab_runtime-0.2.9.dist-info/RECORD +82 -0
  5. collab_runtime-0.2.9.dist-info/WHEEL +5 -0
  6. collab_runtime-0.2.9.dist-info/entry_points.txt +3 -0
  7. collab_runtime-0.2.9.dist-info/licenses/LICENSE +21 -0
  8. collab_runtime-0.2.9.dist-info/top_level.txt +10 -0
  9. scripts/cleanup.py +395 -0
  10. scripts/collab_git_hook.py +190 -0
  11. scripts/format_code.py +594 -0
  12. scripts/generate_tests.py +560 -0
  13. scripts/validate_code.py +1397 -0
  14. src/__init__.py +4 -0
  15. src/dashboard/index.html +1131 -0
  16. src/live_locks_watcher.py +1982 -0
  17. src/lock_client.py +4268 -0
  18. src/logging_config.py +259 -0
  19. src/main.py +436 -0
  20. tests/backend/__init__.py +0 -0
  21. tests/backend/functional/__init__.py +0 -0
  22. tests/backend/functional/test_package_imports.py +43 -0
  23. tests/backend/integration/__init__.py +0 -0
  24. tests/backend/integration/test_cli_contract_parity.py +220 -0
  25. tests/backend/performance/__init__.py +0 -0
  26. tests/backend/reliability/__init__.py +0 -0
  27. tests/backend/security/__init__.py +0 -0
  28. tests/backend/unit/live_locks_watcher/__init__.py +5 -0
  29. tests/backend/unit/live_locks_watcher/_helpers.py +123 -0
  30. tests/backend/unit/live_locks_watcher/conftest.py +18 -0
  31. tests/backend/unit/live_locks_watcher/test_live_locks_watcher_dashboard.py +188 -0
  32. tests/backend/unit/live_locks_watcher/test_live_locks_watcher_developer.py +56 -0
  33. tests/backend/unit/live_locks_watcher/test_live_locks_watcher_graceful_shutdown.py +459 -0
  34. tests/backend/unit/live_locks_watcher/test_live_locks_watcher_main.py +1925 -0
  35. tests/backend/unit/live_locks_watcher/test_live_locks_watcher_module.py +187 -0
  36. tests/backend/unit/live_locks_watcher/test_live_locks_watcher_multi_session.py +320 -0
  37. tests/backend/unit/live_locks_watcher/test_live_locks_watcher_notify.py +67 -0
  38. tests/backend/unit/live_locks_watcher/test_live_locks_watcher_parsing.py +155 -0
  39. tests/backend/unit/live_locks_watcher/test_live_locks_watcher_process_helpers.py +684 -0
  40. tests/backend/unit/live_locks_watcher/test_live_locks_watcher_processing.py +173 -0
  41. tests/backend/unit/live_locks_watcher/test_live_locks_watcher_prompt_abort.py +71 -0
  42. tests/backend/unit/live_locks_watcher/test_live_locks_watcher_reconcile.py +516 -0
  43. tests/backend/unit/live_locks_watcher/test_live_locks_watcher_scan.py +296 -0
  44. tests/backend/unit/lock_client/__init__.py +1 -0
  45. tests/backend/unit/lock_client/_helpers.py +132 -0
  46. tests/backend/unit/lock_client/test_lock_client_acquire.py +214 -0
  47. tests/backend/unit/lock_client/test_lock_client_active.py +104 -0
  48. tests/backend/unit/lock_client/test_lock_client_api.py +63 -0
  49. tests/backend/unit/lock_client/test_lock_client_cli.py +682 -0
  50. tests/backend/unit/lock_client/test_lock_client_daemon.py +3730 -0
  51. tests/backend/unit/lock_client/test_lock_client_dashboard.py +438 -0
  52. tests/backend/unit/lock_client/test_lock_client_discover.py +241 -0
  53. tests/backend/unit/lock_client/test_lock_client_force_release.py +354 -0
  54. tests/backend/unit/lock_client/test_lock_client_helper_branches.py +1890 -0
  55. tests/backend/unit/lock_client/test_lock_client_history.py +301 -0
  56. tests/backend/unit/lock_client/test_lock_client_isolation.py +316 -0
  57. tests/backend/unit/lock_client/test_lock_client_pid.py +75 -0
  58. tests/backend/unit/lock_client/test_lock_client_reconcile.py +464 -0
  59. tests/backend/unit/lock_client/test_lock_client_release.py +77 -0
  60. tests/backend/unit/lock_client/test_lock_client_shutdown.py +1110 -0
  61. tests/backend/unit/lock_client/test_lock_client_utils.py +474 -0
  62. tests/backend/unit/lock_client/test_lock_client_watch.py +866 -0
  63. tests/backend/unit/scripts/__init__.py +1 -0
  64. tests/backend/unit/scripts/_helpers.py +42 -0
  65. tests/backend/unit/scripts/test_cleanup.py +285 -0
  66. tests/backend/unit/scripts/test_collab_git_hook.py +280 -0
  67. tests/backend/unit/scripts/test_collab_git_hook_ported.py +50 -0
  68. tests/backend/unit/scripts/test_format_code.py +368 -0
  69. tests/backend/unit/scripts/test_format_code_ported.py +177 -0
  70. tests/backend/unit/scripts/test_generate_tests.py +305 -0
  71. tests/backend/unit/scripts/test_hook_templates.py +357 -0
  72. tests/backend/unit/scripts/test_setup_hook_overlay.py +95 -0
  73. tests/backend/unit/scripts/test_validate_code.py +867 -0
  74. tests/backend/unit/scripts/test_validate_code_ported.py +237 -0
  75. tests/backend/unit/test_entrypoints_main_run.py +83 -0
  76. tests/backend/unit/test_logging_config.py +529 -0
  77. tests/backend/unit/test_main_watch_pid_file.py +278 -0
  78. tests/conftest.py +167 -0
  79. tests/frontend/__init__.py +0 -0
  80. tests/frontend/jest/__init__.py +0 -0
  81. tests/frontend/playwright/__init__.py +0 -0
  82. tests/packaging/test_smoke_install.py +76 -0
@@ -0,0 +1,1110 @@
1
+ """Graceful shutdown tests for LockClient._graceful_shutdown()."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import os
7
+ from unittest import mock
8
+
9
+ import pytest
10
+
11
+ from ._helpers import FakeResponse, load_lock_client_module, make_create_client
12
+
13
+ mod = load_lock_client_module()
14
+
15
+
16
+ def test_graceful_shutdown_git_fallback(monkeypatch, tmp_path):
17
+ """Test _graceful_shutdown falls back to release_all on git error."""
18
+ monkeypatch.setenv("SUPABASE_URL", "https://test.supabase.co")
19
+ monkeypatch.setenv("SUPABASE_ANON_KEY", "test_key")
20
+
21
+ pid_file = tmp_path / "daemon.pid"
22
+ pid_file.write_text("12345")
23
+ monkeypatch.setattr(mod, "PID_FILE", str(pid_file))
24
+ monkeypatch.setenv("COLLAB_STATE_DIR", str(tmp_path))
25
+ monkeypatch.setenv("COLLAB_TEST_MODE", "0")
26
+ monkeypatch.setattr(
27
+ mod, "_get_create_client", lambda: make_create_client(FakeResponse())
28
+ )
29
+
30
+ lc = mod.LockClient(developer_id="test_user")
31
+
32
+ def broken_git():
33
+ raise RuntimeError("git failed")
34
+
35
+ monkeypatch.setattr(lc, "_run_git_status", broken_git)
36
+ monkeypatch.setattr(lc, "release_all", mock.Mock(return_value=2))
37
+
38
+ lc._graceful_shutdown()
39
+ assert not pid_file.exists()
40
+ # Now verifies behavior: PRESERVE locks on shutdown
41
+ lc.release_all.assert_not_called()
42
+
43
+
44
+ def test_graceful_shutdown_smart_release(monkeypatch, tmp_path):
45
+ """Test _graceful_shutdown selectively releases clean files."""
46
+ monkeypatch.setenv("SUPABASE_URL", "https://test.supabase.co")
47
+ monkeypatch.setenv("SUPABASE_ANON_KEY", "test_key")
48
+
49
+ pid_file = tmp_path / "daemon.pid"
50
+ pid_file.write_text("12345")
51
+ monkeypatch.setattr(mod, "PID_FILE", str(pid_file))
52
+ monkeypatch.setenv("COLLAB_STATE_DIR", str(tmp_path))
53
+ monkeypatch.setenv("COLLAB_TEST_MODE", "0")
54
+ monkeypatch.setattr(
55
+ mod, "_get_create_client", lambda: make_create_client(FakeResponse())
56
+ )
57
+
58
+ lc = mod.LockClient(developer_id="test_user")
59
+
60
+ monkeypatch.setattr(lc, "_run_git_status", lambda: " M src/dirty.py\n")
61
+
62
+ locks = [
63
+ {"developer_id": "test_user", "file_path": "src/dirty.py"},
64
+ {"developer_id": "test_user", "file_path": "src/clean.py"},
65
+ {"developer_id": "test_user", "file_path": ""},
66
+ {"developer_id": "other_user", "file_path": "src/other.py"},
67
+ ]
68
+ monkeypatch.setattr(lc, "active", mock.Mock(return_value=locks))
69
+
70
+ release_mock = mock.Mock(return_value=(True, None))
71
+ monkeypatch.setattr(lc, "release", release_mock)
72
+
73
+ lc._graceful_shutdown()
74
+
75
+ # Now verifies behavior: PRESERVE locks on shutdown
76
+ release_mock.assert_not_called()
77
+ assert not pid_file.exists()
78
+
79
+
80
+ def test_graceful_shutdown_with_exception(monkeypatch, tmp_path):
81
+ """Test _graceful_shutdown handles release errors gracefully."""
82
+ monkeypatch.setenv("SUPABASE_URL", "https://test.supabase.co")
83
+ monkeypatch.setenv("SUPABASE_ANON_KEY", "test_key")
84
+
85
+ pid_file = tmp_path / "daemon.pid"
86
+ pid_file.write_text("12345")
87
+ monkeypatch.setattr(mod, "PID_FILE", str(pid_file))
88
+ monkeypatch.setenv("COLLAB_STATE_DIR", str(tmp_path))
89
+ monkeypatch.setenv("COLLAB_TEST_MODE", "0")
90
+ monkeypatch.setattr(
91
+ mod, "_get_create_client", lambda: make_create_client(FakeResponse())
92
+ )
93
+
94
+ lc = mod.LockClient(developer_id="test_user")
95
+
96
+ # Force the fallback path and make it fail
97
+ def fail_git():
98
+ raise RuntimeError("git fail")
99
+
100
+ monkeypatch.setattr(lc, "_run_git_status", fail_git)
101
+ monkeypatch.setattr(lc, "release_all", mock.Mock(side_effect=RuntimeError("fail")))
102
+
103
+ lc._graceful_shutdown() # Should not raise
104
+
105
+
106
+ # RESTORED: test_graceful_shutdown_releases_locks
107
+ def test_graceful_shutdown_releases_locks(monkeypatch, tmp_path):
108
+ """Test _graceful_shutdown logs when locks are released (restored).
109
+
110
+ This covers the case where a lock owned by the current developer is present and the
111
+ graceful shutdown path should attempt to release it.
112
+ """
113
+ monkeypatch.setenv("SUPABASE_URL", "https://test.supabase.co")
114
+ monkeypatch.setenv("SUPABASE_ANON_KEY", "test_key")
115
+
116
+ pid_file = tmp_path / "daemon.pid"
117
+ pid_file.write_text("12345")
118
+ monkeypatch.setattr(mod, "PID_FILE", str(pid_file))
119
+ monkeypatch.setenv("COLLAB_STATE_DIR", str(tmp_path))
120
+ monkeypatch.setenv("COLLAB_TEST_MODE", "0")
121
+
122
+ # Return locks to release
123
+ locks_data = [
124
+ {"file_path": "src/app.py", "developer_id": "test_user"},
125
+ ]
126
+ response = FakeResponse(status=200, data=locks_data)
127
+ monkeypatch.setattr(mod, "_get_create_client", lambda: make_create_client(response))
128
+
129
+ lc = mod.LockClient(developer_id="test_user")
130
+ lc._graceful_shutdown()
131
+
132
+
133
+ # ---------------------------------------------------------------------------
134
+ # Additional graceful_shutdown coverage tests
135
+ # ---------------------------------------------------------------------------
136
+
137
+
138
+ def test_graceful_shutdown_test_mode_returns_early(monkeypatch, tmp_path):
139
+ """_graceful_shutdown exits early in COLLAB_TEST_MODE=1."""
140
+ monkeypatch.setenv("SUPABASE_URL", "https://test.supabase.co")
141
+ monkeypatch.setenv("SUPABASE_ANON_KEY", "test_key")
142
+ monkeypatch.setenv("COLLAB_TEST_MODE", "1")
143
+ monkeypatch.setattr(
144
+ mod, "_get_create_client", lambda: make_create_client(FakeResponse())
145
+ )
146
+
147
+ pid_file = tmp_path / "daemon.pid"
148
+ pid_file.write_text("12345")
149
+ monkeypatch.setattr(mod, "PID_FILE", str(pid_file))
150
+ monkeypatch.setenv("COLLAB_STATE_DIR", str(tmp_path))
151
+
152
+ lc = mod.LockClient(developer_id="test_user")
153
+ lc._graceful_shutdown()
154
+ # PID file should still exist (early return before removal)
155
+ assert pid_file.exists()
156
+
157
+
158
+ def test_graceful_shutdown_double_call_noop(monkeypatch, tmp_path):
159
+ """Second call to _graceful_shutdown is a no-op (_shutdown_done guard)."""
160
+ monkeypatch.setenv("SUPABASE_URL", "https://test.supabase.co")
161
+ monkeypatch.setenv("SUPABASE_ANON_KEY", "test_key")
162
+ monkeypatch.setenv("COLLAB_TEST_MODE", "0")
163
+ monkeypatch.setattr(
164
+ mod, "_get_create_client", lambda: make_create_client(FakeResponse())
165
+ )
166
+
167
+ pid_file = tmp_path / "daemon.pid"
168
+ pid_file.write_text("12345")
169
+ monkeypatch.setattr(mod, "PID_FILE", str(pid_file))
170
+ monkeypatch.setenv("COLLAB_STATE_DIR", str(tmp_path))
171
+ monkeypatch.setattr(mod.time, "sleep", lambda x: None)
172
+
173
+ lc = mod.LockClient(developer_id="test_user")
174
+ lc._graceful_shutdown()
175
+ # Second call should be a no-op
176
+ lc._graceful_shutdown() # Should not raise
177
+
178
+
179
+ def test_graceful_shutdown_writes_shutdown_marker(monkeypatch, tmp_path):
180
+ """_graceful_shutdown writes .shutdown_complete marker file."""
181
+ monkeypatch.setenv("SUPABASE_URL", "https://test.supabase.co")
182
+ monkeypatch.setenv("SUPABASE_ANON_KEY", "test_key")
183
+ monkeypatch.setenv("COLLAB_TEST_MODE", "0")
184
+ monkeypatch.setattr(
185
+ mod, "_get_create_client", lambda: make_create_client(FakeResponse())
186
+ )
187
+
188
+ pid_file = tmp_path / "daemon.pid"
189
+ pid_file.write_text("12345")
190
+ monkeypatch.setattr(mod, "PID_FILE", str(pid_file))
191
+ monkeypatch.setenv("COLLAB_STATE_DIR", str(tmp_path))
192
+ monkeypatch.setattr(mod.time, "sleep", lambda x: None)
193
+
194
+ lc = mod.LockClient(developer_id="test_user")
195
+ lc._graceful_shutdown()
196
+
197
+ shutdown_file = tmp_path / ".shutdown_complete"
198
+ assert shutdown_file.exists()
199
+
200
+
201
+ def test_graceful_shutdown_writes_shutdown_marker_deep(monkeypatch, tmp_path):
202
+ """_graceful_shutdown writes the .shutdown_complete marker file (deep path with
203
+ _make_client)."""
204
+ state_dir = str(tmp_path)
205
+ monkeypatch.setenv("COLLAB_STATE_DIR", state_dir)
206
+ lc = _make_client(monkeypatch, tmp_path)
207
+ lc.active = mock.Mock(return_value=[])
208
+ lc._graceful_shutdown()
209
+ # Marker should exist in state dir
210
+ marker = tmp_path / ".shutdown_complete"
211
+ assert marker.exists()
212
+
213
+
214
+ def test_graceful_shutdown_preserves_locks(monkeypatch, tmp_path):
215
+ """_graceful_shutdown does NOT release any locks (preserves them)."""
216
+ monkeypatch.setenv("SUPABASE_URL", "https://test.supabase.co")
217
+ monkeypatch.setenv("SUPABASE_ANON_KEY", "test_key")
218
+ monkeypatch.setenv("COLLAB_TEST_MODE", "0")
219
+
220
+ locks_data = [
221
+ {"file_path": "src/app.py", "developer_id": "test_user"},
222
+ {"file_path": "src/utils.py", "developer_id": "test_user"},
223
+ ]
224
+ resp = FakeResponse(status=200, data=locks_data)
225
+ monkeypatch.setattr(mod, "_get_create_client", lambda: make_create_client(resp))
226
+
227
+ pid_file = tmp_path / "daemon.pid"
228
+ pid_file.write_text("12345")
229
+ monkeypatch.setattr(mod, "PID_FILE", str(pid_file))
230
+ monkeypatch.setenv("COLLAB_STATE_DIR", str(tmp_path))
231
+ monkeypatch.setattr(mod.time, "sleep", lambda x: None)
232
+
233
+ lc = mod.LockClient(developer_id="test_user")
234
+ release_mock = mock.Mock()
235
+ monkeypatch.setattr(lc, "release", release_mock)
236
+ monkeypatch.setattr(lc, "release_all", mock.Mock())
237
+
238
+ lc._graceful_shutdown()
239
+
240
+ release_mock.assert_not_called()
241
+
242
+
243
+ def test_graceful_shutdown_removes_pid_file(monkeypatch, tmp_path):
244
+ """_graceful_shutdown removes the PID file."""
245
+ monkeypatch.setenv("SUPABASE_URL", "https://test.supabase.co")
246
+ monkeypatch.setenv("SUPABASE_ANON_KEY", "test_key")
247
+ monkeypatch.setenv("COLLAB_TEST_MODE", "0")
248
+ monkeypatch.setattr(
249
+ mod, "_get_create_client", lambda: make_create_client(FakeResponse())
250
+ )
251
+
252
+ pid_file = tmp_path / "daemon.pid"
253
+ pid_file.write_text("12345")
254
+ monkeypatch.setattr(mod, "PID_FILE", str(pid_file))
255
+ monkeypatch.setenv("COLLAB_STATE_DIR", str(tmp_path))
256
+ monkeypatch.setattr(mod.time, "sleep", lambda x: None)
257
+
258
+ lc = mod.LockClient(developer_id="test_user")
259
+ lc._graceful_shutdown()
260
+
261
+ assert not pid_file.exists()
262
+
263
+
264
+ def test_graceful_shutdown_with_reason(monkeypatch, tmp_path):
265
+ """_graceful_shutdown logs the reason when provided."""
266
+ monkeypatch.setenv("SUPABASE_URL", "https://test.supabase.co")
267
+ monkeypatch.setenv("SUPABASE_ANON_KEY", "test_key")
268
+ monkeypatch.setenv("COLLAB_TEST_MODE", "0")
269
+ monkeypatch.setattr(
270
+ mod, "_get_create_client", lambda: make_create_client(FakeResponse())
271
+ )
272
+
273
+ pid_file = tmp_path / "daemon.pid"
274
+ pid_file.write_text("12345")
275
+ monkeypatch.setattr(mod, "PID_FILE", str(pid_file))
276
+ monkeypatch.setenv("COLLAB_STATE_DIR", str(tmp_path))
277
+ monkeypatch.setattr(mod.time, "sleep", lambda x: None)
278
+
279
+ lc = mod.LockClient(developer_id="test_user")
280
+ # Should not raise
281
+ lc._graceful_shutdown(reason="stop_requested")
282
+
283
+
284
+ def test_graceful_shutdown_active_raises(monkeypatch, tmp_path):
285
+ """_graceful_shutdown handles exception in active() gracefully."""
286
+ monkeypatch.setenv("SUPABASE_URL", "https://test.supabase.co")
287
+ monkeypatch.setenv("SUPABASE_ANON_KEY", "test_key")
288
+ monkeypatch.setenv("COLLAB_TEST_MODE", "0")
289
+ monkeypatch.setattr(
290
+ mod, "_get_create_client", lambda: make_create_client(FakeResponse())
291
+ )
292
+
293
+ pid_file = tmp_path / "daemon.pid"
294
+ pid_file.write_text("12345")
295
+ monkeypatch.setattr(mod, "PID_FILE", str(pid_file))
296
+ monkeypatch.setenv("COLLAB_STATE_DIR", str(tmp_path))
297
+ monkeypatch.setattr(mod.time, "sleep", lambda x: None)
298
+
299
+ lc = mod.LockClient(developer_id="test_user")
300
+ monkeypatch.setattr(lc, "active", mock.Mock(side_effect=RuntimeError("API down")))
301
+
302
+ lc._graceful_shutdown() # Should not raise
303
+
304
+
305
+ # ---------------------------------------------------------------------------
306
+ # Deep shutdown / signal-handler / parent-monitor branch coverage tests
307
+ # ---------------------------------------------------------------------------
308
+
309
+
310
+ def _make_client(monkeypatch, tmp_path):
311
+ monkeypatch.setenv("SUPABASE_URL", "https://test.supabase.co")
312
+ monkeypatch.setenv("SUPABASE_ANON_KEY", "test_key")
313
+ monkeypatch.setenv("COLLAB_STATE_DIR", str(tmp_path))
314
+ monkeypatch.setenv("COLLAB_TEST_MODE", "0")
315
+ monkeypatch.setattr(
316
+ mod, "_get_create_client", lambda: make_create_client(FakeResponse())
317
+ )
318
+ pid_file = tmp_path / "daemon.pid"
319
+ monkeypatch.setattr(mod, "PID_FILE", str(pid_file))
320
+ return mod.LockClient(developer_id="test_user")
321
+
322
+
323
+ # ---------------------------------------------------------------------------
324
+ # _graceful_shutdown — no-reason branch (line 2521-2522 region)
325
+ # ---------------------------------------------------------------------------
326
+
327
+
328
+ def test_graceful_shutdown_no_reason_logs_generic_message(monkeypatch, tmp_path):
329
+ """_graceful_shutdown with no reason logs the generic message."""
330
+ lc = _make_client(monkeypatch, tmp_path)
331
+ lc.active = mock.Mock(return_value=[])
332
+ lc._graceful_shutdown(reason=None) # covers else branch at 2521-2522
333
+
334
+
335
+ def test_graceful_shutdown_with_reason_logs_specific_message(monkeypatch, tmp_path):
336
+ """_graceful_shutdown with a reason logs the reason-specific message."""
337
+ lc = _make_client(monkeypatch, tmp_path)
338
+ lc.active = mock.Mock(return_value=[])
339
+ lc._graceful_shutdown(reason="test_reason") # covers if branch at 2504
340
+
341
+
342
+ def test_graceful_shutdown_active_raises_logs_error(monkeypatch, tmp_path):
343
+ """Active() exception during shutdown is caught and logged."""
344
+ lc = _make_client(monkeypatch, tmp_path)
345
+ lc.active = mock.Mock(side_effect=RuntimeError("db down"))
346
+ lc._graceful_shutdown() # should not raise
347
+
348
+
349
+ def test_graceful_shutdown_active_has_my_locks(monkeypatch, tmp_path):
350
+ """Active() returns locks owned by developer — logs preserved message."""
351
+ lc = _make_client(monkeypatch, tmp_path)
352
+ lc.active = mock.Mock(
353
+ return_value=[
354
+ {"developer_id": "test_user", "file_path": "src/foo.py"},
355
+ {"developer_id": "other_user", "file_path": "src/bar.py"},
356
+ ]
357
+ )
358
+ lc._graceful_shutdown() # covers lines where n_kept is incremented
359
+
360
+
361
+ def test_graceful_shutdown_writes_shutdown_marker_deep_alt(monkeypatch, tmp_path):
362
+ """_graceful_shutdown writes the .shutdown_complete marker file."""
363
+ state_dir = str(tmp_path)
364
+ monkeypatch.setenv("COLLAB_STATE_DIR", state_dir)
365
+ lc = _make_client(monkeypatch, tmp_path)
366
+ lc.active = mock.Mock(return_value=[])
367
+ lc._graceful_shutdown()
368
+ # Marker should exist in state dir
369
+ marker = tmp_path / ".shutdown_complete"
370
+ assert marker.exists()
371
+
372
+
373
+ def test_graceful_shutdown_shutdown_marker_open_fails(monkeypatch, tmp_path):
374
+ """If writing shutdown marker raises, _graceful_shutdown doesn't propagate."""
375
+ import builtins
376
+
377
+ state_dir = str(tmp_path)
378
+ monkeypatch.setenv("COLLAB_STATE_DIR", state_dir)
379
+ lc = _make_client(monkeypatch, tmp_path)
380
+ lc.active = mock.Mock(return_value=[])
381
+
382
+ real_open = builtins.open
383
+
384
+ def fail_open(path, *args, **kwargs):
385
+ if ".shutdown_complete" in str(path):
386
+ raise OSError("disk full")
387
+ return real_open(path, *args, **kwargs)
388
+
389
+ monkeypatch.setattr(builtins, "open", fail_open)
390
+ lc._graceful_shutdown() # should not raise
391
+
392
+
393
+ def test_graceful_shutdown_pid_removal_retries(monkeypatch, tmp_path):
394
+ """PID file removal retries on OSError then succeeds on third attempt."""
395
+ lc = _make_client(monkeypatch, tmp_path)
396
+ pid_file = tmp_path / "daemon.pid"
397
+ pid_file.write_text("99999")
398
+ monkeypatch.setattr(mod, "PID_FILE", str(pid_file))
399
+ lc.active = mock.Mock(return_value=[])
400
+
401
+ call_count = [0]
402
+ real_remove = os.remove
403
+
404
+ def flaky_remove(path):
405
+ call_count[0] += 1
406
+ if call_count[0] < 3 and ".pid" in str(path) and "shutdown" not in str(path):
407
+ raise OSError("busy")
408
+ real_remove(path)
409
+
410
+ monkeypatch.setattr(mod.os, "remove", flaky_remove)
411
+ monkeypatch.setattr(mod.time, "sleep", lambda x: None)
412
+ lc._graceful_shutdown()
413
+
414
+
415
+ def test_graceful_shutdown_flush_handler_raises(monkeypatch, tmp_path):
416
+ """Handlers that raise during flush() are silently swallowed."""
417
+ lc = _make_client(monkeypatch, tmp_path)
418
+ lc.active = mock.Mock(return_value=[])
419
+
420
+ bad_handler = mock.MagicMock()
421
+ bad_handler.flush.side_effect = RuntimeError("broken")
422
+
423
+ with mock.patch("logging.getLogger") as mock_get_logger:
424
+ fake_logger = mock.MagicMock()
425
+ fake_logger.handlers = [bad_handler]
426
+ mock_get_logger.return_value = fake_logger
427
+ lc._graceful_shutdown() # should not raise
428
+
429
+
430
+ # ---------------------------------------------------------------------------
431
+ # _register_signal_handlers — SIGTERM / SIGBREAK / console handler
432
+ # ---------------------------------------------------------------------------
433
+
434
+
435
+ def test_register_signal_handlers_non_win32(monkeypatch, tmp_path):
436
+ """On non-win32, SIGTERM + SIGINT are registered without error."""
437
+ lc = _make_client(monkeypatch, tmp_path)
438
+ monkeypatch.setenv("COLLAB_TEST_MODE", "0")
439
+
440
+ signals_set = []
441
+ monkeypatch.setattr(
442
+ mod.signal, "signal", lambda sig, handler: signals_set.append(sig)
443
+ )
444
+ monkeypatch.setattr(mod.sys, "platform", "linux")
445
+
446
+ lc._register_signal_handlers()
447
+ assert mod.signal.SIGINT in signals_set or len(signals_set) >= 1
448
+
449
+
450
+ def test_register_signal_handlers_test_mode_skips_atexit(monkeypatch, tmp_path):
451
+ """In COLLAB_TEST_MODE=1, atexit is not registered."""
452
+ lc = _make_client(monkeypatch, tmp_path)
453
+ monkeypatch.setenv("COLLAB_TEST_MODE", "1")
454
+
455
+ atexit_registered = []
456
+ monkeypatch.setattr(mod.atexit, "register", lambda fn: atexit_registered.append(fn))
457
+ monkeypatch.setattr(mod.signal, "signal", lambda *a: None)
458
+
459
+ lc._register_signal_handlers()
460
+ assert len(atexit_registered) == 0
461
+
462
+
463
+ def test_register_signal_handlers_win32_sigbreak(monkeypatch, tmp_path):
464
+ """On win32 with SIGBREAK, the handler is registered."""
465
+ lc = _make_client(monkeypatch, tmp_path)
466
+ monkeypatch.setenv("COLLAB_TEST_MODE", "1")
467
+
468
+ signals_set = []
469
+ monkeypatch.setattr(
470
+ mod.signal, "signal", lambda sig, handler: signals_set.append(sig)
471
+ )
472
+ monkeypatch.setattr(mod.sys, "platform", "win32")
473
+ monkeypatch.setattr(mod.signal, "SIGBREAK", 21, raising=False)
474
+
475
+ lc._register_signal_handlers()
476
+ assert 21 in signals_set
477
+
478
+
479
+ def test_register_signal_handlers_win32_sigbreak_exception(monkeypatch, tmp_path):
480
+ """SIGBREAK registration failure is silently caught."""
481
+ lc = _make_client(monkeypatch, tmp_path)
482
+ monkeypatch.setenv("COLLAB_TEST_MODE", "1")
483
+ monkeypatch.setattr(mod.sys, "platform", "win32")
484
+
485
+ def raising_signal(sig, handler):
486
+ if sig == 21: # SIGBREAK
487
+ raise OSError("not permitted")
488
+
489
+ monkeypatch.setattr(mod.signal, "signal", raising_signal)
490
+ monkeypatch.setattr(mod.signal, "SIGBREAK", 21, raising=False)
491
+
492
+ lc._register_signal_handlers() # should not raise
493
+
494
+
495
+ def test_register_signal_handlers_win32_console_handler_exception(
496
+ monkeypatch, tmp_path
497
+ ):
498
+ """If SetConsoleCtrlHandler import/call fails, handler is skipped silently."""
499
+ lc = _make_client(monkeypatch, tmp_path)
500
+ monkeypatch.setenv("COLLAB_TEST_MODE", "1")
501
+ monkeypatch.setattr(mod.sys, "platform", "win32")
502
+ monkeypatch.setattr(mod.signal, "signal", lambda *a: None)
503
+ monkeypatch.setattr(mod.signal, "SIGBREAK", 21, raising=False)
504
+
505
+ import builtins
506
+
507
+ real_import = builtins.__import__
508
+
509
+ def fail_ctypes_import(name, *args, **kwargs):
510
+ if name == "ctypes":
511
+ raise ImportError("no ctypes")
512
+ return real_import(name, *args, **kwargs)
513
+
514
+ monkeypatch.setattr(builtins, "__import__", fail_ctypes_import)
515
+ lc._register_signal_handlers() # should not raise
516
+
517
+
518
+ # ---------------------------------------------------------------------------
519
+ # _start_parent_monitor_thread — branches on non-win32 / no parent / failure
520
+ # ---------------------------------------------------------------------------
521
+
522
+
523
+ def test_start_parent_monitor_thread_non_win32_returns_early(monkeypatch, tmp_path):
524
+ """On non-win32, returns immediately without doing anything."""
525
+ lc = _make_client(monkeypatch, tmp_path)
526
+ monkeypatch.setattr(mod.sys, "platform", "linux")
527
+ lc._parent_pid = 9999
528
+ lc._start_parent_monitor_thread()
529
+ assert not lc._parent_monitor_started
530
+
531
+
532
+ def test_start_parent_monitor_thread_no_parent_returns_early(monkeypatch, tmp_path):
533
+ """Without a parent PID, returns early."""
534
+ lc = _make_client(monkeypatch, tmp_path)
535
+ monkeypatch.setattr(mod.sys, "platform", "win32")
536
+ lc._parent_pid = None
537
+ lc._start_parent_monitor_thread()
538
+ assert not lc._parent_monitor_started
539
+
540
+
541
+ def test_start_parent_monitor_thread_openprocess_fails(monkeypatch, tmp_path):
542
+ """If OpenProcess returns 0 (failure), monitor is not started."""
543
+ lc = _make_client(monkeypatch, tmp_path)
544
+ monkeypatch.setattr(mod.sys, "platform", "win32")
545
+ lc._parent_pid = 9999
546
+
547
+ fake_ctypes = mock.MagicMock()
548
+ fake_ctypes.windll.kernel32.OpenProcess.return_value = 0
549
+ fake_ctypes.windll.kernel32.GetLastError.return_value = 5
550
+ monkeypatch.setattr(mod, "ctypes", fake_ctypes, raising=False)
551
+
552
+ import builtins
553
+
554
+ real_import = builtins.__import__
555
+
556
+ def mock_import(name, *args, **kwargs):
557
+ if name == "ctypes":
558
+ return fake_ctypes
559
+ return real_import(name, *args, **kwargs)
560
+
561
+ monkeypatch.setattr(builtins, "__import__", mock_import)
562
+ lc._start_parent_monitor_thread()
563
+ assert not lc._parent_monitor_started
564
+
565
+
566
+ def test_start_parent_monitor_thread_import_exception(monkeypatch, tmp_path):
567
+ """If ctypes import fails, the exception is caught and monitor marked not
568
+ started."""
569
+ lc = _make_client(monkeypatch, tmp_path)
570
+ monkeypatch.setattr(mod.sys, "platform", "win32")
571
+ lc._parent_pid = 9999
572
+
573
+ import builtins
574
+
575
+ real_import = builtins.__import__
576
+
577
+ def fail_import(name, *args, **kwargs):
578
+ if name == "ctypes":
579
+ raise ImportError("no ctypes")
580
+ return real_import(name, *args, **kwargs)
581
+
582
+ monkeypatch.setattr(builtins, "__import__", fail_import)
583
+ lc._start_parent_monitor_thread()
584
+ assert not lc._parent_monitor_started
585
+
586
+
587
+ def test_start_parent_monitor_thread_getlasterror_raises(monkeypatch, tmp_path):
588
+ """If GetLastError() raises, the exception is swallowed; monitor still not
589
+ started."""
590
+ lc = _make_client(monkeypatch, tmp_path)
591
+ monkeypatch.setattr(mod.sys, "platform", "win32")
592
+ lc._parent_pid = 9999
593
+
594
+ import builtins
595
+
596
+ real_import = builtins.__import__
597
+
598
+ fake_ctypes = mock.MagicMock()
599
+ fake_ctypes.windll.kernel32.OpenProcess.return_value = 0
600
+ fake_ctypes.windll.kernel32.GetLastError.side_effect = OSError("fail")
601
+ fake_ctypes.WINFUNCTYPE = mock.MagicMock(return_value=mock.MagicMock())
602
+ fake_ctypes.wintypes = mock.MagicMock()
603
+
604
+ def mock_import(name, *args, **kwargs):
605
+ if name == "ctypes":
606
+ return fake_ctypes
607
+ if name in ("ctypes.wintypes", "wintypes"):
608
+ return fake_ctypes.wintypes
609
+ return real_import(name, *args, **kwargs)
610
+
611
+ monkeypatch.setattr(builtins, "__import__", mock_import)
612
+ lc._start_parent_monitor_thread() # should not raise
613
+ assert not lc._parent_monitor_started
614
+
615
+
616
+ # ---------------------------------------------------------------------------
617
+ # _graceful_shutdown — stray marker removal paths (lines 2578-2583)
618
+ # ---------------------------------------------------------------------------
619
+
620
+
621
+ def test_graceful_shutdown_removes_stray_markers(monkeypatch, tmp_path):
622
+ """Stray repo markers are removed during shutdown."""
623
+ state_dir = str(tmp_path)
624
+ monkeypatch.setenv("COLLAB_STATE_DIR", state_dir)
625
+ lc = _make_client(monkeypatch, tmp_path)
626
+ lc.active = mock.Mock(return_value=[])
627
+
628
+ # Create stray marker files in COLLAB_ROOT
629
+ collab_root = getattr(mod, "_COLLAB_ROOT", str(tmp_path))
630
+ stray_shutdown = os.path.join(collab_root, ".shutdown_complete")
631
+ stray_summary = os.path.join(collab_root, ".startup_summary.json")
632
+ for p in (stray_shutdown, stray_summary):
633
+ try:
634
+ with open(p, "w") as f:
635
+ f.write("stray")
636
+ except OSError:
637
+ pass
638
+
639
+ lc._graceful_shutdown() # should not raise
640
+
641
+
642
+ def test_graceful_shutdown_stray_marker_remove_fails(monkeypatch, tmp_path):
643
+ """If stray marker removal raises, shutdown continues without error."""
644
+ state_dir = str(tmp_path)
645
+ monkeypatch.setenv("COLLAB_STATE_DIR", state_dir)
646
+ lc = _make_client(monkeypatch, tmp_path)
647
+ lc.active = mock.Mock(return_value=[])
648
+
649
+ real_remove = os.remove
650
+
651
+ def raising_remove(path):
652
+ if ".shutdown_complete" in str(path) or ".startup_summary" in str(path):
653
+ raise OSError("permission denied")
654
+ real_remove(path)
655
+
656
+ monkeypatch.setattr(mod.os, "remove", raising_remove)
657
+ monkeypatch.setattr(mod.os.path, "exists", lambda p: True)
658
+ lc._graceful_shutdown() # should not raise
659
+
660
+
661
+ # ---------------------------------------------------------------------------
662
+ # _graceful_shutdown — fsync paths for log handlers (lines 2604-2660)
663
+ # ---------------------------------------------------------------------------
664
+
665
+
666
+ def test_graceful_shutdown_fsync_on_file_handler(monkeypatch, tmp_path):
667
+ """Fsync is attempted on file-backed log handlers during shutdown."""
668
+ state_dir = str(tmp_path)
669
+ monkeypatch.setenv("COLLAB_STATE_DIR", state_dir)
670
+ lc = _make_client(monkeypatch, tmp_path)
671
+ lc.active = mock.Mock(return_value=[])
672
+
673
+ # Create a real file handler so fsync path is exercised
674
+ log_file = tmp_path / "test.log"
675
+ fh = logging.FileHandler(str(log_file))
676
+ collab_logger = logging.getLogger("collab.test_fsync")
677
+ collab_logger.addHandler(fh)
678
+ try:
679
+ lc._graceful_shutdown()
680
+ finally:
681
+ collab_logger.removeHandler(fh)
682
+ fh.close()
683
+
684
+
685
+ def test_graceful_shutdown_fsync_raises_still_completes(monkeypatch, tmp_path):
686
+ """If fsync raises, shutdown still completes."""
687
+ state_dir = str(tmp_path)
688
+ monkeypatch.setenv("COLLAB_STATE_DIR", state_dir)
689
+ lc = _make_client(monkeypatch, tmp_path)
690
+ lc.active = mock.Mock(return_value=[])
691
+
692
+ monkeypatch.setattr(mod.os, "fsync", mock.Mock(side_effect=OSError("fsync fail")))
693
+ lc._graceful_shutdown() # should not raise
694
+
695
+
696
+ def test_graceful_shutdown_logging_shutdown_raises(monkeypatch, tmp_path):
697
+ """If logging.shutdown() raises, graceful_shutdown still completes."""
698
+ state_dir = str(tmp_path)
699
+ monkeypatch.setenv("COLLAB_STATE_DIR", state_dir)
700
+ lc = _make_client(monkeypatch, tmp_path)
701
+ lc.active = mock.Mock(return_value=[])
702
+
703
+ monkeypatch.setattr(
704
+ mod.logging, "shutdown", mock.Mock(side_effect=RuntimeError("boom"))
705
+ )
706
+ lc._graceful_shutdown() # should not raise
707
+
708
+
709
+ def test_graceful_shutdown_print_raises_is_swallowed(monkeypatch, tmp_path):
710
+ """Print() failure in shutdown marker output path is swallowed."""
711
+ import builtins
712
+
713
+ lc = _make_client(monkeypatch, tmp_path)
714
+ lc.active = mock.Mock(return_value=[])
715
+ real_print = builtins.print
716
+
717
+ def flaky_print(*args, **kwargs):
718
+ raise OSError("stdout unavailable")
719
+
720
+ monkeypatch.setattr(builtins, "print", flaky_print)
721
+ lc._graceful_shutdown()
722
+ monkeypatch.setattr(builtins, "print", real_print)
723
+
724
+
725
+ def test_register_signal_handlers_calls_exception_logging(monkeypatch, tmp_path):
726
+ """Signal callback exceptions in graceful shutdown are caught and logged."""
727
+ lc = _make_client(monkeypatch, tmp_path)
728
+ monkeypatch.setenv("COLLAB_TEST_MODE", "1")
729
+ monkeypatch.setattr(mod.sys, "platform", "linux")
730
+
731
+ handlers = {}
732
+
733
+ def capture_signal(sig, fn):
734
+ handlers[sig] = fn
735
+
736
+ monkeypatch.setattr(mod.signal, "signal", capture_signal)
737
+ monkeypatch.setattr(
738
+ lc, "_graceful_shutdown", mock.Mock(side_effect=RuntimeError("fail"))
739
+ )
740
+ monkeypatch.setattr(mod.sys, "exit", mock.Mock(side_effect=SystemExit(0)))
741
+
742
+ lc._register_signal_handlers()
743
+ with pytest.raises(SystemExit):
744
+ handlers[mod.signal.SIGINT](2, None)
745
+
746
+
747
+ def test_register_signal_handlers_console_handler_graceful_shutdown_exception(
748
+ monkeypatch, tmp_path
749
+ ):
750
+ """Windows console handler swallows graceful-shutdown exceptions."""
751
+ lc = _make_client(monkeypatch, tmp_path)
752
+ monkeypatch.setenv("COLLAB_TEST_MODE", "1")
753
+ monkeypatch.setattr(mod.sys, "platform", "win32")
754
+ monkeypatch.setattr(mod.signal, "signal", lambda *a: None)
755
+ monkeypatch.setattr(mod.signal, "SIGBREAK", 21, raising=False)
756
+ monkeypatch.setattr(
757
+ lc, "_graceful_shutdown", mock.Mock(side_effect=RuntimeError("boom"))
758
+ )
759
+
760
+ captured_console_handler = {"fn": None}
761
+
762
+ class _K32:
763
+ @staticmethod
764
+ def SetConsoleCtrlHandler(fn, enable):
765
+ captured_console_handler["fn"] = fn
766
+ return True
767
+
768
+ fake_ctypes = mock.MagicMock()
769
+ fake_ctypes.windll.kernel32 = _K32()
770
+ fake_ctypes.WINFUNCTYPE = lambda *a, **k: (lambda f: f)
771
+ fake_wintypes = mock.MagicMock()
772
+ fake_wintypes.BOOL = int
773
+ fake_wintypes.DWORD = int
774
+
775
+ import builtins
776
+
777
+ real_import = builtins.__import__
778
+
779
+ def mock_import(name, *args, **kwargs):
780
+ if name == "ctypes":
781
+ return fake_ctypes
782
+ if name == "ctypes.wintypes":
783
+ return fake_wintypes
784
+ return real_import(name, *args, **kwargs)
785
+
786
+ monkeypatch.setattr(builtins, "__import__", mock_import)
787
+
788
+ lc._register_signal_handlers()
789
+ assert captured_console_handler["fn"] is not None
790
+ captured_console_handler["fn"](2)
791
+
792
+
793
+ def test_start_parent_monitor_waiter_closes_handle_and_swallows_assign_exceptions(
794
+ monkeypatch, tmp_path
795
+ ):
796
+ """Waiter path covers close-handle failure and guarded attribute assignments."""
797
+ lc = _make_client(monkeypatch, tmp_path)
798
+ monkeypatch.setattr(mod.sys, "platform", "win32")
799
+ lc._parent_pid = 4242
800
+
801
+ monkeypatch.setattr(
802
+ lc, "_graceful_shutdown", mock.Mock(side_effect=RuntimeError("shutdown error"))
803
+ )
804
+
805
+ class _K32:
806
+ @staticmethod
807
+ def OpenProcess(access, inherit, pid):
808
+ return 999
809
+
810
+ @staticmethod
811
+ def WaitForSingleObject(hndl, timeout):
812
+ return 0
813
+
814
+ @staticmethod
815
+ def CloseHandle(hndl):
816
+ raise OSError("close failed")
817
+
818
+ fake_ctypes = mock.MagicMock()
819
+ fake_ctypes.windll.kernel32 = _K32()
820
+
821
+ class _ImmediateThread:
822
+ def __init__(self, target, args=(), daemon=None):
823
+ self._target = target
824
+ self._args = args
825
+
826
+ def start(self):
827
+ self._target(*self._args)
828
+
829
+ import builtins
830
+
831
+ real_import = builtins.__import__
832
+
833
+ def mock_import(name, *args, **kwargs):
834
+ if name == "ctypes":
835
+ return fake_ctypes
836
+ return real_import(name, *args, **kwargs)
837
+
838
+ monkeypatch.setattr(builtins, "__import__", mock_import)
839
+ monkeypatch.setattr(mod.threading, "Thread", _ImmediateThread)
840
+ lc._start_parent_monitor_thread()
841
+
842
+
843
+ def test_start_parent_monitor_thread_thread_construction_failure(monkeypatch, tmp_path):
844
+ """Thread construction failure triggers outer exception guard in monitor startup."""
845
+ lc = _make_client(monkeypatch, tmp_path)
846
+ monkeypatch.setattr(mod.sys, "platform", "win32")
847
+ lc._parent_pid = 8888
848
+
849
+ class _K32:
850
+ @staticmethod
851
+ def OpenProcess(access, inherit, pid):
852
+ return 111
853
+
854
+ fake_ctypes = mock.MagicMock()
855
+ fake_ctypes.windll.kernel32 = _K32()
856
+
857
+ import builtins
858
+
859
+ real_import = builtins.__import__
860
+
861
+ def mock_import(name, *args, **kwargs):
862
+ if name == "ctypes":
863
+ return fake_ctypes
864
+ return real_import(name, *args, **kwargs)
865
+
866
+ monkeypatch.setattr(builtins, "__import__", mock_import)
867
+ monkeypatch.setattr(
868
+ mod.threading, "Thread", mock.Mock(side_effect=RuntimeError("thread fail"))
869
+ )
870
+ lc._start_parent_monitor_thread()
871
+ assert lc._parent_monitor_started is False
872
+
873
+
874
+ def test_register_signal_handlers_console_handler_outer_exception(
875
+ monkeypatch, tmp_path
876
+ ):
877
+ """Console handler outer-except branch is exercised when debug logging fails."""
878
+ lc = _make_client(monkeypatch, tmp_path)
879
+ monkeypatch.setenv("COLLAB_TEST_MODE", "1")
880
+ monkeypatch.setattr(mod.sys, "platform", "win32")
881
+ monkeypatch.setattr(mod.signal, "signal", lambda *a: None)
882
+ monkeypatch.setattr(mod.signal, "SIGBREAK", 21, raising=False)
883
+
884
+ captured_console_handler = {"fn": None}
885
+
886
+ class _K32:
887
+ @staticmethod
888
+ def SetConsoleCtrlHandler(fn, enable):
889
+ captured_console_handler["fn"] = fn
890
+ return True
891
+
892
+ fake_ctypes = mock.MagicMock()
893
+ fake_ctypes.windll.kernel32 = _K32()
894
+ fake_ctypes.WINFUNCTYPE = lambda *a, **k: (lambda f: f)
895
+ fake_wintypes = mock.MagicMock()
896
+ fake_wintypes.BOOL = int
897
+ fake_wintypes.DWORD = int
898
+
899
+ import builtins
900
+
901
+ real_import = builtins.__import__
902
+
903
+ def mock_import(name, *args, **kwargs):
904
+ if name == "ctypes":
905
+ return fake_ctypes
906
+ if name == "ctypes.wintypes":
907
+ return fake_wintypes
908
+ return real_import(name, *args, **kwargs)
909
+
910
+ monkeypatch.setattr(builtins, "__import__", mock_import)
911
+
912
+ real_debug = mod.logger.debug
913
+
914
+ def flaky_debug(msg, *args, **kwargs):
915
+ if isinstance(msg, str) and "Console control event" in msg:
916
+ raise RuntimeError("debug fail")
917
+ return real_debug(msg, *args, **kwargs)
918
+
919
+ monkeypatch.setattr(mod.logger, "debug", flaky_debug)
920
+
921
+ lc._register_signal_handlers()
922
+ assert captured_console_handler["fn"] is not None
923
+ captured_console_handler["fn"](9)
924
+
925
+
926
+ def test_start_parent_monitor_waiter_assignment_guards(monkeypatch, tmp_path):
927
+ """Waiter assignment guard except blocks are hit when attribute sets fail in
928
+ waiter."""
929
+ lc = _make_client(monkeypatch, tmp_path)
930
+ monkeypatch.setattr(mod.sys, "platform", "win32")
931
+ lc._parent_pid = 5151
932
+
933
+ base_setattr = object.__setattr__
934
+ base_setattr(lc, "_in_waiter", False)
935
+
936
+ def guarded_setattr(self, name, value):
937
+ if getattr(self, "_in_waiter", False) and name in {
938
+ "_parent_monitor_started",
939
+ "_parent_monitor_handle",
940
+ "_parent_monitor_thread",
941
+ }:
942
+ raise RuntimeError("blocked in waiter")
943
+ base_setattr(self, name, value)
944
+
945
+ monkeypatch.setattr(type(lc), "__setattr__", guarded_setattr, raising=False)
946
+ monkeypatch.setattr(lc, "_graceful_shutdown", lambda *a, **k: None)
947
+
948
+ class _K32:
949
+ @staticmethod
950
+ def OpenProcess(access, inherit, pid):
951
+ return 222
952
+
953
+ @staticmethod
954
+ def WaitForSingleObject(hndl, timeout):
955
+ return 0
956
+
957
+ @staticmethod
958
+ def CloseHandle(hndl):
959
+ return True
960
+
961
+ fake_ctypes = mock.MagicMock()
962
+ fake_ctypes.windll.kernel32 = _K32()
963
+
964
+ class _ImmediateThread:
965
+ def __init__(self, target, args=(), daemon=None):
966
+ self._target = target
967
+ self._args = args
968
+
969
+ def start(self):
970
+ base_setattr(lc, "_in_waiter", True)
971
+ try:
972
+ self._target(*self._args)
973
+ finally:
974
+ base_setattr(lc, "_in_waiter", False)
975
+
976
+ import builtins
977
+
978
+ real_import = builtins.__import__
979
+
980
+ def mock_import(name, *args, **kwargs):
981
+ if name == "ctypes":
982
+ return fake_ctypes
983
+ return real_import(name, *args, **kwargs)
984
+
985
+ monkeypatch.setattr(builtins, "__import__", mock_import)
986
+ monkeypatch.setattr(mod.threading, "Thread", _ImmediateThread)
987
+ lc._start_parent_monitor_thread()
988
+
989
+
990
+ def test_start_parent_monitor_waiter_outer_exception(monkeypatch, tmp_path):
991
+ """Waiter outer exception branch is hit when WaitForSingleObject raises."""
992
+ lc = _make_client(monkeypatch, tmp_path)
993
+ monkeypatch.setattr(mod.sys, "platform", "win32")
994
+ lc._parent_pid = 6161
995
+
996
+ class _K32:
997
+ @staticmethod
998
+ def OpenProcess(access, inherit, pid):
999
+ return 333
1000
+
1001
+ @staticmethod
1002
+ def WaitForSingleObject(hndl, timeout):
1003
+ raise RuntimeError("wait fail")
1004
+
1005
+ @staticmethod
1006
+ def CloseHandle(hndl):
1007
+ return True
1008
+
1009
+ fake_ctypes = mock.MagicMock()
1010
+ fake_ctypes.windll.kernel32 = _K32()
1011
+
1012
+ class _ImmediateThread:
1013
+ def __init__(self, target, args=(), daemon=None):
1014
+ self._target = target
1015
+ self._args = args
1016
+
1017
+ def start(self):
1018
+ self._target(*self._args)
1019
+
1020
+ import builtins
1021
+
1022
+ real_import = builtins.__import__
1023
+
1024
+ def mock_import(name, *args, **kwargs):
1025
+ if name == "ctypes":
1026
+ return fake_ctypes
1027
+ return real_import(name, *args, **kwargs)
1028
+
1029
+ monkeypatch.setattr(builtins, "__import__", mock_import)
1030
+ monkeypatch.setattr(mod.threading, "Thread", _ImmediateThread)
1031
+ lc._start_parent_monitor_thread()
1032
+
1033
+
1034
+ def test_reconcile_git_failure_returns_current_user_locks(monkeypatch, tmp_path):
1035
+ """Reconcile on git failure returns current user's active locks set."""
1036
+ lc = _make_client(monkeypatch, tmp_path)
1037
+ monkeypatch.setattr(
1038
+ lc,
1039
+ "_get_modified_and_unpushed_files",
1040
+ mock.Mock(side_effect=RuntimeError("git fail")),
1041
+ )
1042
+ monkeypatch.setattr(
1043
+ lc,
1044
+ "active",
1045
+ mock.Mock(
1046
+ return_value=[
1047
+ {"developer_id": "test_user", "file_path": "src/a.py"},
1048
+ {"developer_id": "other", "file_path": "src/b.py"},
1049
+ ]
1050
+ ),
1051
+ )
1052
+
1053
+ out = lc._reconcile()
1054
+ assert out == {"src/a.py"}
1055
+
1056
+
1057
+ def test_reconcile_still_valid_same_machine_token_is_resumed(monkeypatch, tmp_path):
1058
+ """still_valid lock with different token but same machine enters resumed list
1059
+ path."""
1060
+ lc = _make_client(monkeypatch, tmp_path)
1061
+ monkeypatch.setattr(lc, "_get_modified_and_unpushed_files", lambda: ["src/a.py"])
1062
+ monkeypatch.setattr(
1063
+ lc,
1064
+ "active",
1065
+ lambda: [
1066
+ {
1067
+ "developer_id": "test_user",
1068
+ "file_path": "src/a.py",
1069
+ "lock_token": "old-token",
1070
+ }
1071
+ ],
1072
+ )
1073
+ monkeypatch.setattr(lc, "_get_session_token", lambda: "new-token")
1074
+ monkeypatch.setattr(lc, "_is_same_machine_token", lambda tok: True)
1075
+ monkeypatch.setattr(lc, "release_multiple", lambda x: None)
1076
+ monkeypatch.setattr(lc, "acquire_multiple", lambda *a, **k: (True, [], ""))
1077
+
1078
+ class _FakeTable:
1079
+ def update(self, *a, **k):
1080
+ return self
1081
+
1082
+ def eq(self, *a, **k):
1083
+ return self
1084
+
1085
+ def execute(self):
1086
+ return None
1087
+
1088
+ lc._client = mock.MagicMock()
1089
+ lc._client.table.return_value = _FakeTable()
1090
+ monkeypatch.setattr(mod, "_state_path", lambda name: str(tmp_path / name))
1091
+ monkeypatch.setattr(mod.time, "time", lambda: 0)
1092
+
1093
+ out = lc._reconcile()
1094
+ assert "src/a.py" in out
1095
+
1096
+
1097
+ def test_reconcile_summary_outer_guards_swallow_exceptions(monkeypatch, tmp_path):
1098
+ """Summary writing/cleanup outer exception guards are exercised."""
1099
+ lc = _make_client(monkeypatch, tmp_path)
1100
+ monkeypatch.setattr(lc, "_get_modified_and_unpushed_files", lambda: [])
1101
+ monkeypatch.setattr(lc, "active", lambda: [])
1102
+ monkeypatch.setattr(lc, "_get_session_token", lambda: "tok")
1103
+ monkeypatch.setattr(mod, "_state_path", lambda name: str(tmp_path / name))
1104
+
1105
+ # Force Thread() creation in cleanup helper to fail (2871-2872 guard)
1106
+ monkeypatch.setattr(
1107
+ mod.threading, "Thread", mock.Mock(side_effect=RuntimeError("thread ctor fail"))
1108
+ )
1109
+ out = lc._reconcile()
1110
+ assert out == set()