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,474 @@
1
+ import os
2
+ import sys
3
+ import types
4
+ from datetime import datetime as _real_datetime
5
+ from pathlib import Path
6
+
7
+ from ._helpers import FakeClient, FakeResponse, load_lock_client_module
8
+
9
+ mod = load_lock_client_module()
10
+
11
+
12
+ def test_safe_now_typeerror_uses_class_now(monkeypatch):
13
+ """Cover _safe_now TypeError path that calls class-level now()."""
14
+
15
+ class _OddDate:
16
+ # Bound call on instance raises TypeError; class-level call works.
17
+ def now():
18
+ return _real_datetime(2026, 5, 2, 10, 0, 0)
19
+
20
+ monkeypatch.setattr(mod, "datetime", _OddDate(), raising=False)
21
+ got = mod._safe_now()
22
+ assert isinstance(got, _real_datetime)
23
+ assert got.year == 2026
24
+
25
+
26
+ def test_safe_now_typeerror_falls_back_to_real_datetime(monkeypatch):
27
+ """Cover _safe_now final fallback when class-level now() also errors."""
28
+
29
+ class _BadDate:
30
+ # Instance call triggers TypeError, class-level call also TypeError.
31
+ def now(self):
32
+ return _real_datetime(2026, 5, 2, 10, 0, 0)
33
+
34
+ monkeypatch.setattr(mod, "datetime", _BadDate(), raising=False)
35
+ got = mod._safe_now()
36
+ assert isinstance(got, _real_datetime)
37
+
38
+
39
+ def test_get_create_client_preloaded_module_importerror_exits(monkeypatch):
40
+ """Cover _get_create_client branch where preloaded supabase import fails."""
41
+ import builtins
42
+
43
+ mod._supabase_create_client = None
44
+ fake = types.SimpleNamespace(create_client=lambda *_a, **_k: None)
45
+ fake.__spec__ = types.SimpleNamespace(origin="/usr/lib/python/supabase/__init__.py")
46
+ monkeypatch.setitem(sys.modules, "supabase", fake)
47
+
48
+ real_import = builtins.__import__
49
+
50
+ def _fake_import(name, *args, **kwargs):
51
+ if name == "supabase":
52
+ raise ImportError("forced missing supabase")
53
+ return real_import(name, *args, **kwargs)
54
+
55
+ monkeypatch.setattr(builtins, "__import__", _fake_import)
56
+ try:
57
+ import pytest
58
+
59
+ with pytest.raises(SystemExit):
60
+ mod._get_create_client()
61
+ finally:
62
+ mod._supabase_create_client = None
63
+
64
+
65
+ def test_resolve_executable_path_which_raises_in_test_mode(monkeypatch):
66
+ """Which() errors should fall back to command name in test mode."""
67
+
68
+ def _boom(_name):
69
+ raise AttributeError("platform mismatch")
70
+
71
+ monkeypatch.setattr(mod.shutil, "which", _boom)
72
+ monkeypatch.setenv("COLLAB_TEST_MODE", "1")
73
+ assert mod._resolve_executable_path("tasklist") == "tasklist"
74
+
75
+
76
+ def test_resolve_executable_path_which_raises_outside_test_mode(monkeypatch):
77
+ """Which() errors should return None outside test mode."""
78
+
79
+ def _boom(_name):
80
+ raise OSError("lookup failed")
81
+
82
+ monkeypatch.setattr(mod.shutil, "which", _boom)
83
+ monkeypatch.delenv("COLLAB_TEST_MODE", raising=False)
84
+ monkeypatch.delenv("TESTING", raising=False)
85
+ monkeypatch.delenv("PYTEST_CURRENT_TEST", raising=False)
86
+ assert mod._resolve_executable_path("taskkill") is None
87
+
88
+
89
+ def test_get_create_client_preloaded_shadowed_module_exits(monkeypatch):
90
+ """Cover local-shadow detection branch for preloaded supabase module."""
91
+ mod._supabase_create_client = None
92
+ fake = types.SimpleNamespace(create_client=lambda *_a, **_k: None)
93
+ fake.__spec__ = types.SimpleNamespace(
94
+ origin=os.path.join(mod._COLLAB_ROOT, "supabase.py")
95
+ )
96
+ monkeypatch.setitem(sys.modules, "supabase", fake)
97
+
98
+ import pytest
99
+
100
+ with pytest.raises(SystemExit):
101
+ mod._get_create_client()
102
+ mod._supabase_create_client = None
103
+
104
+
105
+ def test_get_create_client_preloaded_missing_create_client_exits(monkeypatch):
106
+ """Cover _get_create_client branch where module lacks create_client."""
107
+ mod._supabase_create_client = None
108
+ fake = types.SimpleNamespace()
109
+ fake.__spec__ = types.SimpleNamespace(origin="/usr/lib/python/supabase/__init__.py")
110
+ monkeypatch.setitem(sys.modules, "supabase", fake)
111
+
112
+ import pytest
113
+
114
+ with pytest.raises(SystemExit):
115
+ mod._get_create_client()
116
+ mod._supabase_create_client = None
117
+
118
+
119
+ def test_get_create_client_import_missing_exits(monkeypatch):
120
+ """Cover fallback import branch when supabase package is unavailable."""
121
+ import builtins
122
+
123
+ mod._supabase_create_client = None
124
+ monkeypatch.delitem(sys.modules, "supabase", raising=False)
125
+
126
+ real_import = builtins.__import__
127
+
128
+ def _fake_import(name, *args, **kwargs):
129
+ if name == "supabase":
130
+ raise ImportError("forced missing supabase")
131
+ return real_import(name, *args, **kwargs)
132
+
133
+ monkeypatch.setattr(builtins, "__import__", _fake_import)
134
+
135
+ import pytest
136
+
137
+ with pytest.raises(SystemExit):
138
+ mod._get_create_client()
139
+ mod._supabase_create_client = None
140
+
141
+
142
+ def test_is_same_machine_token_returns_false_with_duplicates(monkeypatch):
143
+ """Cover duplicate-seed continue path and final False return."""
144
+ c = mod.LockClient(local_only=True)
145
+ c.developer_id = "alice"
146
+ monkeypatch.setattr(
147
+ mod.LockClient, "_get_git_username", staticmethod(lambda: "alice")
148
+ )
149
+ monkeypatch.setenv("USERNAME", "alice")
150
+ monkeypatch.setattr(mod.socket, "gethostname", lambda: "host-a")
151
+ monkeypatch.setattr(mod.os.path, "abspath", lambda _p: "C:/repo")
152
+
153
+ assert c._is_same_machine_token("deadbeefdeadbeef") is False
154
+
155
+
156
+ def test_get_cmdline_for_pid_windows_powershell_failure_returns_none(monkeypatch):
157
+ """Cover Windows fallback branch where PowerShell cmdline query fails."""
158
+ monkeypatch.setattr(mod.sys, "platform", "win32")
159
+ monkeypatch.setattr(mod.shutil, "which", lambda _exe: None)
160
+
161
+ def _check_output(*_a, **_k):
162
+ raise RuntimeError("powershell failure")
163
+
164
+ monkeypatch.setattr(mod.subprocess, "check_output", _check_output)
165
+ assert mod.LockClient._get_cmdline_for_pid(12345) is None
166
+
167
+
168
+ def test_get_cmdline_for_pid_unix_proc_empty_and_exception(monkeypatch, tmp_path):
169
+ """Cover /proc cmdline empty-data and exception fallback branches."""
170
+ monkeypatch.setattr(mod.sys, "platform", "linux")
171
+
172
+ # Empty /proc content -> None
173
+ monkeypatch.setattr(mod.os.path, "exists", lambda _p: True)
174
+
175
+ class _Empty:
176
+ def __enter__(self):
177
+ return self
178
+
179
+ def __exit__(self, *_a):
180
+ return False
181
+
182
+ def read(self):
183
+ return b""
184
+
185
+ import builtins
186
+
187
+ monkeypatch.setattr(builtins, "open", lambda *_a, **_k: _Empty())
188
+ assert mod.LockClient._get_cmdline_for_pid(8888) is None
189
+
190
+ # Exception while reading /proc -> None
191
+ def _raise_open(*_a, **_k):
192
+ raise OSError("proc read failed")
193
+
194
+ monkeypatch.setattr(builtins, "open", _raise_open)
195
+ assert mod.LockClient._get_cmdline_for_pid(9999) is None
196
+
197
+
198
+ def test_init_ephemeral_guard_handles_bad_developer_id(monkeypatch):
199
+ """Cover __init__ branch where developer_id.startswith raises."""
200
+
201
+ class _BadDev:
202
+ def startswith(self, _p):
203
+ raise RuntimeError("bad developer id")
204
+
205
+ c = mod.LockClient(developer_id=_BadDev(), local_only=True)
206
+ assert c._is_ephemeral is False
207
+
208
+
209
+ def test_normalize_file_path_abs_dotprefix_and_exception_fallback(monkeypatch):
210
+ """Cover normalize branches: abs relpath, './' trim, and exception fallback."""
211
+ c = mod.LockClient(local_only=True)
212
+
213
+ # Absolute path branch via relpath.
214
+ abs_fp = os.path.join(mod._PROJECT_ROOT, "src", "x.py")
215
+ out = c._normalize_file_path(abs_fp)
216
+ assert out.endswith("src/x.py")
217
+
218
+ # './' trimming branch.
219
+ monkeypatch.setattr(mod.os.path, "isabs", lambda _p: False)
220
+ assert c._normalize_file_path("./src/y.py") == "src/y.py"
221
+
222
+ # Exception fallback branch.
223
+ monkeypatch.setattr(
224
+ mod.os.path, "isabs", lambda _p: (_ for _ in ()).throw(RuntimeError("boom"))
225
+ )
226
+ assert c._normalize_file_path("a\\b.py") == "a/b.py"
227
+
228
+
229
+ def test_force_release_all_exception_returns_zero(monkeypatch):
230
+ """Cover force_release_all outer exception handler (lines 944-946)."""
231
+ monkeypatch.setenv("SUPABASE_URL", "https://test.supabase.co")
232
+ monkeypatch.setenv("SUPABASE_ANON_KEY", "test_key")
233
+ monkeypatch.setenv("SUPABASE_SERVICE_ROLE_KEY", "service-key")
234
+
235
+ c = mod.LockClient(local_only=True)
236
+ c._is_admin = True
237
+
238
+ class _BoomClient:
239
+ def table(self, _name):
240
+ raise RuntimeError("db fail")
241
+
242
+ c._client = _BoomClient()
243
+ assert c.force_release_all() == 0
244
+
245
+
246
+ def test_get_state_dir_env(tmp_path, monkeypatch):
247
+ mod = load_lock_client_module()
248
+ monkeypatch.setenv("COLLAB_STATE_DIR", str(tmp_path))
249
+ got = mod._get_state_dir()
250
+ assert os.path.abspath(got) == os.path.abspath(str(tmp_path))
251
+
252
+
253
+ def test_normalize_and_parse_paths(tmp_path):
254
+ mod = load_lock_client_module()
255
+ # Absolute path -> relative
256
+ p = os.path.join(mod._PROJECT_ROOT, "src", "file.py")
257
+ out = mod.LockClient(local_only=True)._normalize_file_path(p)
258
+ assert out.startswith("src/")
259
+
260
+ # collab/ stays collab/
261
+ s = "collab/foo.txt"
262
+ out = mod.LockClient(local_only=True)._normalize_file_path(s)
263
+ assert out.startswith("collab/")
264
+
265
+ # parse git rename
266
+ line = "R old/path -> new/path"
267
+ assert mod.LockClient._parse_git_status_path(line) == "new/path"
268
+
269
+
270
+ def test_should_ignore_path():
271
+ mod = load_lock_client_module()
272
+ assert mod.LockClient._should_ignore_path(".git/HEAD")
273
+ assert mod.LockClient._should_ignore_path(".collab/.startup_summary.json")
274
+ assert not mod.LockClient._should_ignore_path("src/main.py")
275
+
276
+
277
+ def test_session_token_consistent():
278
+ mod = load_lock_client_module()
279
+ c1 = mod.LockClient(developer_id="dev_x", local_only=True)
280
+ c2 = mod.LockClient(developer_id="dev_x", local_only=True)
281
+ t1 = c1._get_session_token()
282
+ t2 = c2._get_session_token()
283
+ assert t1 == t2
284
+ assert c1._is_same_machine_token(t1)
285
+
286
+
287
+ def test_get_create_client_uses_sys_modules_with_safe_origin(monkeypatch):
288
+ # Force reload of cached create client
289
+ mod._supabase_create_client = None
290
+
291
+ fake = types.SimpleNamespace()
292
+
293
+ def fake_create(url, key):
294
+ return FakeClient(FakeResponse(status=200, data=[]))
295
+
296
+ fake.create_client = fake_create
297
+ # ensure origin not inside repo to avoid shadow detection
298
+ fake.__spec__ = types.SimpleNamespace(
299
+ origin="/usr/lib/python3/dist-packages/supabase/__init__.py"
300
+ )
301
+ monkeypatch.setitem(sys.modules, "supabase", fake)
302
+ fn = mod._get_create_client()
303
+ assert callable(fn)
304
+ c = fn("u", "k")
305
+ assert isinstance(c, FakeClient)
306
+
307
+
308
+ def test_get_create_client_allows_project_venv_site_packages(monkeypatch):
309
+ """Installed packages inside a repo-local virtualenv are not local shadows."""
310
+ mod._supabase_create_client = None
311
+
312
+ fake = types.SimpleNamespace()
313
+ fake.create_client = lambda *_a, **_k: None
314
+ fake.__spec__ = types.SimpleNamespace(
315
+ origin=os.path.join(
316
+ mod._PROJECT_ROOT,
317
+ ".venv",
318
+ "Lib",
319
+ "site-packages",
320
+ "supabase",
321
+ "__init__.py",
322
+ )
323
+ )
324
+ monkeypatch.setitem(sys.modules, "supabase", fake)
325
+
326
+ fn = mod._get_create_client()
327
+ assert callable(fn)
328
+
329
+
330
+ def test_parse_response_success_and_error_and_dict():
331
+ # success object-like
332
+ class ResObj:
333
+ def __init__(self):
334
+ self.status_code = 200
335
+ self.data = {"key": "value"}
336
+ self.error = None
337
+
338
+ status, data, error = mod.LockClient._parse_response(ResObj())
339
+ assert status == 200 and data == {"key": "value"}
340
+
341
+ # error response
342
+ class FakeResp:
343
+ def __init__(self, status=200, data=None, error=None):
344
+ self.status = status
345
+ self.data = data
346
+ self.error = error
347
+
348
+ status2, data2, error2 = mod.LockClient._parse_response(
349
+ FakeResp(status=400, data=None, error="Bad request")
350
+ )
351
+ assert status2 == 400 and error2 == "Bad request"
352
+
353
+ # dict input
354
+ resp = {"status": 200, "data": [{"file": "test"}], "error": None}
355
+ status3, data3, error3 = mod.LockClient._parse_response(resp)
356
+ assert status3 == 200 and data3 == [{"file": "test"}]
357
+
358
+
359
+ def test_mark_missing_lines_coverage_helper():
360
+ base = Path(__file__).resolve().parents[4]
361
+ # Keep this lightweight helper deterministic across environments by
362
+ # asserting the migrated runtime modules exist in the repository.
363
+ assert (base / "src" / "lock_client.py").exists()
364
+ assert (base / "src" / "live_locks_watcher.py").exists()
365
+
366
+
367
+ def test_safe_now_returns_datetime(monkeypatch):
368
+ from datetime import datetime as dt
369
+
370
+ now = dt(2026, 4, 27, 12, 0, 0)
371
+ monkeypatch.setattr(mod, "_safe_now", lambda: now)
372
+ assert mod._safe_now() == now
373
+
374
+
375
+ def test_get_state_dir_env_var(monkeypatch, tmp_path):
376
+ monkeypatch.setenv("COLLAB_STATE_DIR", str(tmp_path))
377
+ sd = mod._get_state_dir()
378
+ assert sd == str(tmp_path)
379
+
380
+
381
+ def test_get_state_dir_default_creates_dir(monkeypatch):
382
+ monkeypatch.delenv("COLLAB_STATE_DIR", raising=False)
383
+ sd = mod._get_state_dir()
384
+ assert os.path.exists(sd)
385
+ assert "collab_runtime_" in sd or "mockcmms_collab_" in sd
386
+
387
+
388
+ def test_state_path(monkeypatch, tmp_path):
389
+ monkeypatch.setenv("COLLAB_STATE_DIR", str(tmp_path))
390
+ p = mod._state_path("test.marker")
391
+ assert p == os.path.join(str(tmp_path), "test.marker")
392
+
393
+
394
+ def test_quiet_console_loggers_restores_levels(monkeypatch):
395
+ import logging
396
+
397
+ test_logger = logging.getLogger("httpx")
398
+ original = test_logger.level
399
+ with mod._quiet_console_loggers(names=["httpx"]):
400
+ assert test_logger.level == logging.WARNING
401
+ assert test_logger.level == original
402
+
403
+
404
+ def test_quiet_console_loggers_restores_collab_propagation(monkeypatch):
405
+ import logging
406
+
407
+ collab_logger = logging.getLogger("collab")
408
+ collab_logger.propagate = True
409
+ with mod._quiet_console_loggers():
410
+ assert collab_logger.propagate is False
411
+ assert collab_logger.propagate is True
412
+
413
+
414
+ def test_quiet_console_loggers_default_names(monkeypatch):
415
+ with mod._quiet_console_loggers():
416
+ pass
417
+
418
+
419
+ def test_validate_credentials_ok(monkeypatch):
420
+ monkeypatch.setattr(mod, "SUPABASE_URL", "https://test.supabase.co")
421
+ monkeypatch.setattr(mod, "SUPABASE_ANON_KEY", "test_key")
422
+ mod._validate_credentials() # Should not raise
423
+
424
+
425
+ def test_parse_response_and_retry(monkeypatch):
426
+ bad = FakeResponse(status=500, data=None, error="oops")
427
+ ok = FakeResponse(status=200, data=[{"file_path": "src/a.py"}], error=None)
428
+
429
+ class FlakyClient(FakeClient):
430
+ def __init__(self):
431
+ super().__init__(bad)
432
+ self._calls = 0
433
+
434
+ def execute(self):
435
+ self._calls += 1
436
+ if self._calls == 1:
437
+ return bad
438
+ return ok
439
+
440
+ client = FlakyClient()
441
+ _, data, _ = mod.LockClient._parse_response(client.execute())
442
+ if not data:
443
+ _, data, _ = mod.LockClient._parse_response(client.execute())
444
+
445
+ assert isinstance(data, list)
446
+ assert data[0]["file_path"] == "src/a.py"
447
+
448
+
449
+ def test_state_dir_and_normalize(monkeypatch, tmp_path):
450
+ monkeypatch.setenv("COLLAB_STATE_DIR", str(tmp_path / "state"))
451
+ state_dir = mod._get_state_dir()
452
+ assert os.path.isdir(state_dir)
453
+
454
+ c = mod.LockClient(local_only=True)
455
+ abs_path = os.path.join(mod._PROJECT_ROOT, "src", "routes", "main.py")
456
+ normalized = c._normalize_file_path(abs_path)
457
+ assert normalized == "src/routes/main.py"
458
+
459
+
460
+ def test_session_token_and_git_helpers(monkeypatch):
461
+ monkeypatch.setattr(
462
+ mod.LockClient, "_get_git_username", staticmethod(lambda: "devx")
463
+ )
464
+ c1 = mod.LockClient(local_only=True)
465
+ c2 = mod.LockClient(local_only=True)
466
+
467
+ t1 = c1._get_session_token()
468
+ t2 = c2._get_session_token()
469
+ assert t1 == t2
470
+ assert c1._is_same_machine_token(t1)
471
+
472
+
473
+ # (Both test_parse_response_dict and test_parse_response_error removed as duplicates
474
+ # of test_parse_response_success_and_error_and_dict above)