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,682 @@
1
+ """CLI-focused tests for LockClient moved from the canonical file.
2
+
3
+ These tests use the shared helpers in `_helpers.py` to load the module and re-use the
4
+ FakeResponse/FakeClient factories.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import os
10
+ import subprocess
11
+ import sys
12
+ import tempfile
13
+
14
+ import pytest
15
+
16
+ from ._helpers import FakeResponse, load_lock_client_module, make_create_client
17
+
18
+ mod = load_lock_client_module()
19
+
20
+
21
+ def test_cli_history_partial_match_hint(monkeypatch, capsys):
22
+ """Cover history fallback hint when first row path differs from query path."""
23
+ monkeypatch.setenv("SUPABASE_URL", "https://test.supabase.co")
24
+ monkeypatch.setenv("SUPABASE_ANON_KEY", "test_key")
25
+ monkeypatch.setattr(
26
+ mod, "_get_create_client", lambda: make_create_client(FakeResponse())
27
+ )
28
+
29
+ rows = [
30
+ {
31
+ "file_path": "src/other/app.py",
32
+ "acquired_at": "2026-01-01T10:00:00+00:00",
33
+ "released_at": "2026-01-01T11:00:00+00:00",
34
+ "developer_id": "alice",
35
+ "branch_name": "feat/x",
36
+ "outcome": "released",
37
+ }
38
+ ]
39
+
40
+ monkeypatch.setattr(mod.LockClient, "history", lambda self, fp, limit=20: rows)
41
+ monkeypatch.setattr(
42
+ sys,
43
+ "argv",
44
+ ["lock_client.py", "history", "src/requested.py"],
45
+ )
46
+
47
+ mod._run_cli()
48
+ out = capsys.readouterr().out
49
+ assert "no exact match" in out.lower()
50
+ assert "partial matches" in out.lower()
51
+
52
+
53
+ def test_main_unhandled_exception_exits_with_fatal(monkeypatch, capsys):
54
+ """Cover main() unhandled-exception logging and fatal stderr message."""
55
+ import src.main as main_module
56
+
57
+ monkeypatch.setattr(
58
+ main_module, "_run_cli", lambda: (_ for _ in ()).throw(RuntimeError("boom"))
59
+ )
60
+
61
+ with pytest.raises(SystemExit):
62
+ mod.main()
63
+
64
+ err = capsys.readouterr().err
65
+ assert "fatal: lock_client crashed" in err.lower()
66
+
67
+
68
+ def test_cli_history_prune_success(monkeypatch, capsys):
69
+ """History-prune should print success message when prune succeeds."""
70
+ monkeypatch.setenv("SUPABASE_URL", "https://test.supabase.co")
71
+ monkeypatch.setenv("SUPABASE_ANON_KEY", "test_key")
72
+ monkeypatch.setattr(
73
+ mod, "_get_create_client", lambda: make_create_client(FakeResponse())
74
+ )
75
+ monkeypatch.setattr(
76
+ mod.LockClient,
77
+ "prune_history",
78
+ lambda self, retention_days=30: (True, 5, "history-pruned"),
79
+ )
80
+ monkeypatch.setattr(
81
+ sys, "argv", ["lock_client.py", "history-prune", "--days", "30"]
82
+ )
83
+
84
+ mod._run_cli()
85
+ out = capsys.readouterr().out
86
+ assert "pruned 5 lock history row(s)" in out.lower()
87
+
88
+
89
+ def test_cli_history_prune_failure(monkeypatch, capsys):
90
+ """History-prune should exit non-zero and print failure details on error."""
91
+ monkeypatch.setenv("SUPABASE_URL", "https://test.supabase.co")
92
+ monkeypatch.setenv("SUPABASE_ANON_KEY", "test_key")
93
+ monkeypatch.setattr(
94
+ mod, "_get_create_client", lambda: make_create_client(FakeResponse())
95
+ )
96
+ monkeypatch.setattr(
97
+ mod.LockClient,
98
+ "prune_history",
99
+ lambda self, retention_days=30: (False, 0, "bad-request"),
100
+ )
101
+ monkeypatch.setattr(
102
+ sys, "argv", ["lock_client.py", "history-prune", "--days", "30"]
103
+ )
104
+
105
+ with pytest.raises(SystemExit):
106
+ mod._run_cli()
107
+
108
+ out = capsys.readouterr().out
109
+ assert "failed to prune lock history" in out.lower()
110
+
111
+
112
+ def test_cli_acquire(monkeypatch, tmp_path, capsys):
113
+ monkeypatch.setenv("SUPABASE_URL", "https://test.supabase.co")
114
+ monkeypatch.setenv("SUPABASE_ANON_KEY", "test_key")
115
+
116
+ test_file = tmp_path / "src" / "app.py"
117
+ test_file.parent.mkdir(parents=True)
118
+ test_file.write_text("# code")
119
+
120
+ response = FakeResponse(status=200, data=[{"status": "ok"}])
121
+ monkeypatch.setattr(mod, "_get_create_client", lambda: make_create_client(response))
122
+ monkeypatch.setattr(mod, "_PROJECT_ROOT", str(tmp_path))
123
+ monkeypatch.setattr(sys, "argv", ["lock_client.py", "acquire", str(test_file)])
124
+
125
+ try:
126
+ mod._run_cli()
127
+ except SystemExit:
128
+ pass
129
+ captured = capsys.readouterr()
130
+ assert "locked" in captured.out.lower() or "✓" in captured.out
131
+
132
+
133
+ def test_cli_release(monkeypatch, capsys):
134
+ monkeypatch.setenv("SUPABASE_URL", "https://test.supabase.co")
135
+ monkeypatch.setenv("SUPABASE_ANON_KEY", "test_key")
136
+
137
+ response = FakeResponse(status=200, data=[{"file_path": "src/app.py"}])
138
+ monkeypatch.setattr(mod, "_get_create_client", lambda: make_create_client(response))
139
+ monkeypatch.setattr(sys, "argv", ["lock_client.py", "release", "src/app.py"])
140
+
141
+ mod._run_cli()
142
+ captured = capsys.readouterr()
143
+ assert "released" in captured.out.lower() or "✓" in captured.out
144
+
145
+
146
+ def test_cli_active_no_locks(monkeypatch, capsys):
147
+ monkeypatch.setenv("SUPABASE_URL", "https://test.supabase.co")
148
+ monkeypatch.setenv("SUPABASE_ANON_KEY", "test_key")
149
+
150
+ response = FakeResponse(status=200, data=[])
151
+ monkeypatch.setattr(mod, "_get_create_client", lambda: make_create_client(response))
152
+ monkeypatch.setattr(sys, "argv", ["lock_client.py", "active"])
153
+
154
+ mod._run_cli()
155
+ captured = capsys.readouterr()
156
+ assert "no active" in captured.out.lower()
157
+
158
+
159
+ def test_cli_active_with_locks(monkeypatch, capsys):
160
+ monkeypatch.setenv("SUPABASE_URL", "https://test.supabase.co")
161
+ monkeypatch.setenv("SUPABASE_ANON_KEY", "test_key")
162
+
163
+ response = FakeResponse(
164
+ status=200,
165
+ data=[
166
+ {
167
+ "file_path": "src/app.py",
168
+ "developer_id": "user1",
169
+ "branch_name": "main",
170
+ "reason": "testing",
171
+ }
172
+ ],
173
+ )
174
+ monkeypatch.setattr(mod, "_get_create_client", lambda: make_create_client(response))
175
+ monkeypatch.setattr(sys, "argv", ["lock_client.py", "active"])
176
+
177
+ mod._run_cli()
178
+ captured = capsys.readouterr()
179
+ assert "src/app.py" in captured.out
180
+
181
+
182
+ def test_cli_status_locked(monkeypatch, capsys):
183
+ monkeypatch.setenv("SUPABASE_URL", "https://test.supabase.co")
184
+ monkeypatch.setenv("SUPABASE_ANON_KEY", "test_key")
185
+
186
+ from datetime import datetime, timedelta, timezone
187
+
188
+ future = (datetime.now(timezone.utc) + timedelta(hours=8)).isoformat()
189
+ response = FakeResponse(
190
+ status=200,
191
+ data=[
192
+ {
193
+ "file_path": "src/app.py",
194
+ "developer_id": "user1",
195
+ "acquired_at": "2025-01-01T10:00:00+00:00",
196
+ "expires_at": future,
197
+ }
198
+ ],
199
+ )
200
+ monkeypatch.setattr(mod, "_get_create_client", lambda: make_create_client(response))
201
+ monkeypatch.setattr(sys, "argv", ["lock_client.py", "status", "src/app.py"])
202
+
203
+ mod._run_cli()
204
+ captured = capsys.readouterr()
205
+ assert "locked" in captured.out.lower() or "🔒" in captured.out
206
+
207
+
208
+ def test_cli_status_unlocked(monkeypatch, capsys):
209
+ monkeypatch.setenv("SUPABASE_URL", "https://test.supabase.co")
210
+ monkeypatch.setenv("SUPABASE_ANON_KEY", "test_key")
211
+
212
+ response = FakeResponse(status=200, data=[])
213
+ monkeypatch.setattr(mod, "_get_create_client", lambda: make_create_client(response))
214
+ monkeypatch.setattr(sys, "argv", ["lock_client.py", "status", "src/app.py"])
215
+
216
+ mod._run_cli()
217
+ captured = capsys.readouterr()
218
+ assert "unlocked" in captured.out.lower() or "🔓" in captured.out
219
+
220
+
221
+ def test_cli_release_all(monkeypatch, capsys):
222
+ monkeypatch.setenv("SUPABASE_URL", "https://test.supabase.co")
223
+ monkeypatch.setenv("SUPABASE_ANON_KEY", "test_key")
224
+
225
+ response = FakeResponse(status=200, data=[])
226
+ monkeypatch.setattr(mod, "_get_create_client", lambda: make_create_client(response))
227
+ monkeypatch.setattr(sys, "argv", ["lock_client.py", "release-all"])
228
+
229
+ mod._run_cli()
230
+ captured = capsys.readouterr()
231
+ assert "released" in captured.out.lower()
232
+
233
+
234
+ def test_cli_force_release(monkeypatch, capsys):
235
+ monkeypatch.setenv("SUPABASE_URL", "https://test.supabase.co")
236
+ monkeypatch.setenv("SUPABASE_ANON_KEY", "test_key")
237
+
238
+ response = FakeResponse(status=200, data=[{"file_path": "src/app.py"}])
239
+ monkeypatch.setattr(mod, "_get_create_client", lambda: make_create_client(response))
240
+ monkeypatch.setattr(sys, "argv", ["lock_client.py", "force-release", "src/app.py"])
241
+
242
+ mod._run_cli()
243
+ captured = capsys.readouterr()
244
+ assert "✓" in captured.out or "✗" in captured.out
245
+
246
+
247
+ def test_cli_force_release_all_requires_admin(monkeypatch, capsys):
248
+ """Force-release-all exits with permission message for non-admin client."""
249
+ monkeypatch.setenv("SUPABASE_URL", "https://test.supabase.co")
250
+ monkeypatch.setenv("SUPABASE_ANON_KEY", "test_key")
251
+
252
+ response = FakeResponse(status=200, data=[])
253
+ monkeypatch.setattr(mod, "_get_create_client", lambda: make_create_client(response))
254
+ monkeypatch.delenv("SUPABASE_SERVICE_ROLE_KEY", raising=False)
255
+ monkeypatch.setattr(
256
+ mod.LockClient, "is_admin", property(lambda self: False), raising=False
257
+ )
258
+ monkeypatch.setattr(sys, "argv", ["lock_client.py", "force-release-all"])
259
+
260
+ with pytest.raises(SystemExit):
261
+ mod._run_cli()
262
+ captured = capsys.readouterr()
263
+ assert "permission denied" in captured.out.lower()
264
+
265
+
266
+ def test_cli_acquire_batch(monkeypatch, tmp_path, capsys):
267
+ monkeypatch.setenv("SUPABASE_URL", "https://test.supabase.co")
268
+ monkeypatch.setenv("SUPABASE_ANON_KEY", "test_key")
269
+
270
+ file1 = tmp_path / "src" / "a.py"
271
+ file2 = tmp_path / "src" / "b.py"
272
+ file1.parent.mkdir(parents=True)
273
+ file1.write_text("# a")
274
+ file2.write_text("# b")
275
+
276
+ response = FakeResponse(status=200, data=[{"status": "ok"}])
277
+ monkeypatch.setattr(mod, "_get_create_client", lambda: make_create_client(response))
278
+ monkeypatch.setattr(mod, "_PROJECT_ROOT", str(tmp_path))
279
+ monkeypatch.setattr(
280
+ sys, "argv", ["lock_client.py", "acquire-batch", str(file1), str(file2)]
281
+ )
282
+
283
+ try:
284
+ mod._run_cli()
285
+ except SystemExit:
286
+ pass
287
+ captured = capsys.readouterr()
288
+ assert "locked" in captured.out.lower() or "✓" in captured.out
289
+
290
+
291
+ def test_cli_acquire_batch_conflict(monkeypatch, tmp_path, capsys):
292
+ monkeypatch.setenv("SUPABASE_URL", "https://test.supabase.co")
293
+ monkeypatch.setenv("SUPABASE_ANON_KEY", "test_key")
294
+
295
+ file1 = tmp_path / "src" / "a.py"
296
+ file1.parent.mkdir(parents=True)
297
+ file1.write_text("# a")
298
+
299
+ response = FakeResponse(status=200, data=[{"status": "conflict", "owner": "other"}])
300
+ monkeypatch.setattr(mod, "_get_create_client", lambda: make_create_client(response))
301
+ monkeypatch.setattr(mod, "_PROJECT_ROOT", str(tmp_path))
302
+ monkeypatch.setattr(sys, "argv", ["lock_client.py", "acquire-batch", str(file1)])
303
+
304
+ with pytest.raises(SystemExit):
305
+ mod._run_cli()
306
+
307
+
308
+ def test_cli_release_batch(monkeypatch, capsys):
309
+ monkeypatch.setenv("SUPABASE_URL", "https://test.supabase.co")
310
+ monkeypatch.setenv("SUPABASE_ANON_KEY", "test_key")
311
+
312
+ response = FakeResponse(status=200, data=[{"file_path": "src/app.py"}])
313
+ monkeypatch.setattr(mod, "_get_create_client", lambda: make_create_client(response))
314
+ monkeypatch.setattr(
315
+ sys, "argv", ["lock_client.py", "release-batch", "src/a.py", "src/b.py"]
316
+ )
317
+
318
+ mod._run_cli()
319
+ captured = capsys.readouterr()
320
+ assert "released" in captured.out.lower()
321
+
322
+
323
+ def test_cli_daemon_start(monkeypatch, tmp_path, capsys):
324
+ monkeypatch.setenv("SUPABASE_URL", "https://test.supabase.co")
325
+ monkeypatch.setenv("SUPABASE_ANON_KEY", "test_key")
326
+
327
+ pid_file = tmp_path / "daemon.pid"
328
+ monkeypatch.setattr(mod, "PID_FILE", str(pid_file))
329
+ monkeypatch.setattr(
330
+ mod, "_get_create_client", lambda: make_create_client(FakeResponse())
331
+ )
332
+
333
+ class FakeProc:
334
+ pid = 12345
335
+
336
+ called_popen = []
337
+ read_pid_calls = [0]
338
+
339
+ def mock_read_pid():
340
+ read_pid_calls[0] += 1
341
+ if read_pid_calls[0] <= 1:
342
+ return None
343
+ return 67891
344
+
345
+ def mock_popen_wrap(*a, **k):
346
+ called_popen.append(True)
347
+ return FakeProc()
348
+
349
+ class LocalLockClient(mod.LockClient):
350
+ @staticmethod
351
+ def _read_pid():
352
+ return mock_read_pid()
353
+
354
+ monkeypatch.setattr(mod, "LockClient", LocalLockClient)
355
+ # Ensure we don't rely on a real process check in tests
356
+ is_alive = staticmethod(lambda pid: True)
357
+ monkeypatch.setattr(mod.LockClient, "_is_process_alive", is_alive)
358
+ monkeypatch.setattr(subprocess, "Popen", mock_popen_wrap)
359
+ monkeypatch.setattr(sys, "argv", ["lock_client.py", "daemon-start"])
360
+
361
+ try:
362
+ mod._run_cli()
363
+ except SystemExit:
364
+ pass
365
+ captured = capsys.readouterr()
366
+ assert "started" in captured.out.lower()
367
+
368
+
369
+ def test_cli_daemon_stop(monkeypatch, tmp_path, capsys):
370
+ monkeypatch.setenv("SUPABASE_URL", "https://test.supabase.co")
371
+ monkeypatch.setenv("SUPABASE_ANON_KEY", "test_key")
372
+
373
+ pid_file = tmp_path / "daemon.pid"
374
+ monkeypatch.setattr(mod, "PID_FILE", str(pid_file))
375
+ monkeypatch.setattr(
376
+ mod, "_get_create_client", lambda: make_create_client(FakeResponse())
377
+ )
378
+ monkeypatch.setattr(sys, "argv", ["lock_client.py", "daemon-stop"])
379
+
380
+ mod._run_cli()
381
+ captured = capsys.readouterr()
382
+ assert "no running" in captured.out.lower() or "stop" in captured.out.lower()
383
+
384
+
385
+ def test_cli_daemon_status(monkeypatch, tmp_path, capsys):
386
+ monkeypatch.setenv("SUPABASE_URL", "https://test.supabase.co")
387
+ monkeypatch.setenv("SUPABASE_ANON_KEY", "test_key")
388
+
389
+ pid_file = tmp_path / "daemon.pid"
390
+ monkeypatch.setattr(mod, "PID_FILE", str(pid_file))
391
+ monkeypatch.setattr(
392
+ mod, "_get_create_client", lambda: make_create_client(FakeResponse())
393
+ )
394
+ monkeypatch.setattr(sys, "argv", ["lock_client.py", "daemon-status"])
395
+
396
+ try:
397
+ mod._run_cli()
398
+ except SystemExit:
399
+ pass
400
+ captured = capsys.readouterr()
401
+ assert "not running" in captured.out.lower()
402
+
403
+
404
+ def test_cli_reconcile(monkeypatch, tmp_path, capsys):
405
+ monkeypatch.setenv("SUPABASE_URL", "https://test.supabase.co")
406
+ monkeypatch.setenv("SUPABASE_ANON_KEY", "test_key")
407
+
408
+ monkeypatch.setattr(
409
+ mod, "_get_create_client", lambda: make_create_client(FakeResponse())
410
+ )
411
+ monkeypatch.setattr(mod.LockClient, "_run_git_status", staticmethod(lambda: ""))
412
+ monkeypatch.setattr(sys, "argv", ["lock_client.py", "reconcile"])
413
+
414
+ mod._run_cli()
415
+
416
+
417
+ def test_cli_history(monkeypatch, capsys):
418
+ monkeypatch.setenv("SUPABASE_URL", "https://test.supabase.co")
419
+ monkeypatch.setenv("SUPABASE_ANON_KEY", "test_key")
420
+
421
+ response = FakeResponse(status=200, data=[])
422
+ monkeypatch.setattr(mod, "_get_create_client", lambda: make_create_client(response))
423
+ monkeypatch.setattr(sys, "argv", ["lock_client.py", "history"])
424
+
425
+ mod._run_cli()
426
+ captured = capsys.readouterr()
427
+ assert "no lock history" in captured.out.lower()
428
+
429
+
430
+ def test_cli_history_json_flag(monkeypatch, capsys):
431
+ monkeypatch.setenv("SUPABASE_URL", "https://test.supabase.co")
432
+ monkeypatch.setenv("SUPABASE_ANON_KEY", "test_key")
433
+
434
+ records = [{"id": 1, "file_path": "src/app.py", "developer_id": "alice"}]
435
+ response = FakeResponse(status=200, data=records)
436
+ monkeypatch.setattr(mod, "_get_create_client", lambda: make_create_client(response))
437
+ monkeypatch.setattr(sys, "argv", ["lock_client.py", "history", "--json"])
438
+
439
+ mod._run_cli()
440
+ captured = capsys.readouterr()
441
+ assert '"file_path"' in captured.out
442
+ assert '"src/app.py"' in captured.out
443
+
444
+
445
+ def test_cli_history_no_match_with_file(monkeypatch, capsys):
446
+ monkeypatch.setenv("SUPABASE_URL", "https://test.supabase.co")
447
+ monkeypatch.setenv("SUPABASE_ANON_KEY", "test_key")
448
+
449
+ response = FakeResponse(status=200, data=[])
450
+ monkeypatch.setattr(mod, "_get_create_client", lambda: make_create_client(response))
451
+ monkeypatch.setattr(sys, "argv", ["lock_client.py", "history", "nonexistent.py"])
452
+
453
+ mod._run_cli()
454
+ captured = capsys.readouterr()
455
+ assert "no history found" in captured.out.lower()
456
+ assert "tip" in captured.out.lower()
457
+
458
+
459
+ def test_cli_history_formatted_output(monkeypatch, capsys):
460
+ monkeypatch.setenv("SUPABASE_URL", "https://test.supabase.co")
461
+ monkeypatch.setenv("SUPABASE_ANON_KEY", "test_key")
462
+
463
+ records = [
464
+ {
465
+ "id": 1,
466
+ "file_path": "src/app.py",
467
+ "developer_id": "alice",
468
+ "acquired_at": "2026-04-03T22:00:00+00:00",
469
+ "released_at": "2026-04-03T22:30:00+00:00",
470
+ "branch_name": "main",
471
+ "outcome": "released",
472
+ }
473
+ ]
474
+ response = FakeResponse(status=200, data=records)
475
+ monkeypatch.setattr(mod, "_get_create_client", lambda: make_create_client(response))
476
+ monkeypatch.setattr(sys, "argv", ["lock_client.py", "history"])
477
+
478
+ mod._run_cli()
479
+ captured = capsys.readouterr()
480
+ assert "src/app.py" in captured.out
481
+ assert "@alice" in captured.out
482
+ assert "released" in captured.out
483
+ assert "branch:main" in captured.out
484
+
485
+
486
+ def test_cli_history_partial_match_output(monkeypatch, capsys):
487
+ monkeypatch.setenv("SUPABASE_URL", "https://test.supabase.co")
488
+ monkeypatch.setenv("SUPABASE_ANON_KEY", "test_key")
489
+
490
+ fallback_records = [
491
+ {
492
+ "id": 1,
493
+ "file_path": "collab/README.md",
494
+ "developer_id": "alice",
495
+ "acquired_at": "2026-04-03T22:00:00+00:00",
496
+ "released_at": "2026-04-03T22:30:00+00:00",
497
+ "branch_name": "main",
498
+ "outcome": "released",
499
+ }
500
+ ]
501
+ call_count = [0]
502
+
503
+ class FallbackClient:
504
+ def table(self, *a, **k):
505
+ return self
506
+
507
+ def select(self, *a, **k):
508
+ return self
509
+
510
+ def eq(self, *a, **k):
511
+ return self
512
+
513
+ def ilike(self, *a, **k):
514
+ return self
515
+
516
+ def order(self, *a, **k):
517
+ return self
518
+
519
+ def limit(self, *a, **k):
520
+ return self
521
+
522
+ def execute(self):
523
+ call_count[0] += 1
524
+ if call_count[0] == 1:
525
+ return FakeResponse(data=[])
526
+ return FakeResponse(data=fallback_records)
527
+
528
+ monkeypatch.setattr(mod, "SUPABASE_URL", "https://test.supabase.co")
529
+ monkeypatch.setattr(mod, "SUPABASE_ANON_KEY", "test_key")
530
+ monkeypatch.setattr(
531
+ mod, "_get_create_client", lambda: (lambda url, key: FallbackClient())
532
+ )
533
+ lc = mod.LockClient(developer_id="test_user")
534
+ result = lc.history(file_path="README.md")
535
+ assert result == fallback_records
536
+ assert call_count[0] == 2
537
+
538
+ # RESTORED: test_validate_credentials_missing_url
539
+ def test_validate_credentials_missing_url(monkeypatch):
540
+ monkeypatch.setattr(mod, "SUPABASE_URL", "")
541
+ monkeypatch.setattr(mod, "SUPABASE_ANON_KEY", "test_key")
542
+
543
+ with pytest.raises(SystemExit):
544
+ mod._validate_credentials()
545
+
546
+ # RESTORED: test_validate_credentials_missing_key
547
+ def test_validate_credentials_missing_key(monkeypatch):
548
+ monkeypatch.setattr(mod, "SUPABASE_URL", "https://test.supabase.co")
549
+ monkeypatch.setattr(mod, "SUPABASE_ANON_KEY", "")
550
+
551
+ with pytest.raises(SystemExit):
552
+ mod._validate_credentials()
553
+
554
+
555
+ def test_cli_dashboard(monkeypatch, capsys):
556
+ monkeypatch.setenv("SUPABASE_URL", "https://test.supabase.co")
557
+ monkeypatch.setenv("SUPABASE_ANON_KEY", "test_key")
558
+
559
+ monkeypatch.setattr(
560
+ mod, "_get_create_client", lambda: make_create_client(FakeResponse())
561
+ )
562
+
563
+ def mock_prepare(self):
564
+ _tmp = os.path.join(tempfile.gettempdir(), "dash.html")
565
+ return "http://127.0.0.1:9999/dash.html", _tmp
566
+
567
+ monkeypatch.setattr(mod.LockClient, "_prepare_dashboard_server", mock_prepare)
568
+
569
+ import webbrowser
570
+
571
+ monkeypatch.setattr(webbrowser, "open", lambda url: None)
572
+ monkeypatch.setattr(
573
+ mod.time, "sleep", lambda x: (_ for _ in ()).throw(KeyboardInterrupt())
574
+ )
575
+ monkeypatch.setattr(sys, "argv", ["lock_client.py", "dashboard"])
576
+
577
+ try:
578
+ mod._run_cli()
579
+ except KeyboardInterrupt:
580
+ pass
581
+
582
+
583
+ def test_cli_watch(monkeypatch, tmp_path, capsys):
584
+ monkeypatch.setenv("SUPABASE_URL", "https://test.supabase.co")
585
+ monkeypatch.setenv("SUPABASE_ANON_KEY", "test_key")
586
+
587
+ pid_file = tmp_path / "daemon.pid"
588
+ monkeypatch.setattr(mod, "PID_FILE", str(pid_file))
589
+ monkeypatch.setattr(
590
+ mod, "_get_create_client", lambda: make_create_client(FakeResponse())
591
+ )
592
+ monkeypatch.setattr(mod.LockClient, "_run_git_status", staticmethod(lambda: ""))
593
+ monkeypatch.setattr(mod.LockClient, "_reconcile", lambda self: set())
594
+ monkeypatch.setattr(
595
+ mod.time, "sleep", lambda *a, **k: (_ for _ in ()).throw(KeyboardInterrupt())
596
+ )
597
+ monkeypatch.setattr(sys, "argv", ["lock_client.py", "watch"])
598
+
599
+ mod._run_cli()
600
+
601
+
602
+ def test_cli_no_command(monkeypatch, capsys):
603
+ monkeypatch.setenv("SUPABASE_URL", "https://test.supabase.co")
604
+ monkeypatch.setenv("SUPABASE_ANON_KEY", "test_key")
605
+
606
+ monkeypatch.setattr(
607
+ mod, "_get_create_client", lambda: make_create_client(FakeResponse())
608
+ )
609
+ monkeypatch.setattr(sys, "argv", ["lock_client.py"])
610
+
611
+ mod._run_cli()
612
+ capsys.readouterr()
613
+
614
+
615
+ def test_main_entry_point(monkeypatch, capsys):
616
+ monkeypatch.setenv("SUPABASE_URL", "https://test.supabase.co")
617
+ monkeypatch.setenv("SUPABASE_ANON_KEY", "test_key")
618
+
619
+ monkeypatch.setattr(
620
+ mod, "_get_create_client", lambda: make_create_client(FakeResponse())
621
+ )
622
+ monkeypatch.setattr(sys, "argv", ["lock_client.py"])
623
+
624
+ mod.main()
625
+
626
+
627
+ def test_cli_daemon_start_with_auto_open_env(monkeypatch, tmp_path, capsys):
628
+ monkeypatch.setenv("SUPABASE_URL", "https://test.supabase.co")
629
+ monkeypatch.setenv("SUPABASE_ANON_KEY", "test_key")
630
+ monkeypatch.setenv("AUTO_OPEN_DASHBOARD", "1")
631
+
632
+ pid_file = tmp_path / "daemon.pid"
633
+ monkeypatch.setattr(mod, "PID_FILE", str(pid_file))
634
+ monkeypatch.setattr(
635
+ mod, "_get_create_client", lambda: make_create_client(FakeResponse())
636
+ )
637
+
638
+ class FakeProc:
639
+ pid = 12345
640
+
641
+ popen_cmds = []
642
+ read_pid_calls = [0]
643
+
644
+ def mock_popen(cmd, **kwargs):
645
+ popen_cmds.append(cmd)
646
+ return FakeProc()
647
+
648
+ def mock_read_pid():
649
+ read_pid_calls[0] += 1
650
+ if read_pid_calls[0] <= 1:
651
+ return None
652
+ return 67892
653
+
654
+ class LocalLockClient(mod.LockClient):
655
+ @staticmethod
656
+ def _read_pid():
657
+ return mock_read_pid()
658
+
659
+ monkeypatch.setattr(mod, "LockClient", LocalLockClient)
660
+ # Mock Popen so we capture the child command, and stub process liveness
661
+ monkeypatch.setattr(subprocess, "Popen", mock_popen)
662
+ is_alive = staticmethod(lambda pid: True)
663
+ monkeypatch.setattr(mod.LockClient, "_is_process_alive", is_alive)
664
+ monkeypatch.setattr(sys, "argv", ["lock_client.py", "daemon-start"])
665
+
666
+ mod._run_cli()
667
+ assert any("--open-dashboard" in str(cmd) for cmd in popen_cmds)
668
+
669
+
670
+ def test_cli_acquire_failure(monkeypatch, capsys):
671
+ monkeypatch.setenv("SUPABASE_URL", "https://test.supabase.co")
672
+ monkeypatch.setenv("SUPABASE_ANON_KEY", "test_key")
673
+
674
+ monkeypatch.setattr(
675
+ mod, "_get_create_client", lambda: make_create_client(FakeResponse())
676
+ )
677
+ monkeypatch.setattr(
678
+ sys, "argv", ["lock_client.py", "acquire", "nonexistent/file.py"]
679
+ )
680
+
681
+ with pytest.raises(SystemExit):
682
+ mod._run_cli()