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.
- collab/__init__.py +77 -0
- collab/__main__.py +11 -0
- collab_runtime-0.2.9.dist-info/METADATA +218 -0
- collab_runtime-0.2.9.dist-info/RECORD +82 -0
- collab_runtime-0.2.9.dist-info/WHEEL +5 -0
- collab_runtime-0.2.9.dist-info/entry_points.txt +3 -0
- collab_runtime-0.2.9.dist-info/licenses/LICENSE +21 -0
- collab_runtime-0.2.9.dist-info/top_level.txt +10 -0
- scripts/cleanup.py +395 -0
- scripts/collab_git_hook.py +190 -0
- scripts/format_code.py +594 -0
- scripts/generate_tests.py +560 -0
- scripts/validate_code.py +1397 -0
- src/__init__.py +4 -0
- src/dashboard/index.html +1131 -0
- src/live_locks_watcher.py +1982 -0
- src/lock_client.py +4268 -0
- src/logging_config.py +259 -0
- src/main.py +436 -0
- tests/backend/__init__.py +0 -0
- tests/backend/functional/__init__.py +0 -0
- tests/backend/functional/test_package_imports.py +43 -0
- tests/backend/integration/__init__.py +0 -0
- tests/backend/integration/test_cli_contract_parity.py +220 -0
- tests/backend/performance/__init__.py +0 -0
- tests/backend/reliability/__init__.py +0 -0
- tests/backend/security/__init__.py +0 -0
- tests/backend/unit/live_locks_watcher/__init__.py +5 -0
- tests/backend/unit/live_locks_watcher/_helpers.py +123 -0
- tests/backend/unit/live_locks_watcher/conftest.py +18 -0
- tests/backend/unit/live_locks_watcher/test_live_locks_watcher_dashboard.py +188 -0
- tests/backend/unit/live_locks_watcher/test_live_locks_watcher_developer.py +56 -0
- tests/backend/unit/live_locks_watcher/test_live_locks_watcher_graceful_shutdown.py +459 -0
- tests/backend/unit/live_locks_watcher/test_live_locks_watcher_main.py +1925 -0
- tests/backend/unit/live_locks_watcher/test_live_locks_watcher_module.py +187 -0
- tests/backend/unit/live_locks_watcher/test_live_locks_watcher_multi_session.py +320 -0
- tests/backend/unit/live_locks_watcher/test_live_locks_watcher_notify.py +67 -0
- tests/backend/unit/live_locks_watcher/test_live_locks_watcher_parsing.py +155 -0
- tests/backend/unit/live_locks_watcher/test_live_locks_watcher_process_helpers.py +684 -0
- tests/backend/unit/live_locks_watcher/test_live_locks_watcher_processing.py +173 -0
- tests/backend/unit/live_locks_watcher/test_live_locks_watcher_prompt_abort.py +71 -0
- tests/backend/unit/live_locks_watcher/test_live_locks_watcher_reconcile.py +516 -0
- tests/backend/unit/live_locks_watcher/test_live_locks_watcher_scan.py +296 -0
- tests/backend/unit/lock_client/__init__.py +1 -0
- tests/backend/unit/lock_client/_helpers.py +132 -0
- tests/backend/unit/lock_client/test_lock_client_acquire.py +214 -0
- tests/backend/unit/lock_client/test_lock_client_active.py +104 -0
- tests/backend/unit/lock_client/test_lock_client_api.py +63 -0
- tests/backend/unit/lock_client/test_lock_client_cli.py +682 -0
- tests/backend/unit/lock_client/test_lock_client_daemon.py +3730 -0
- tests/backend/unit/lock_client/test_lock_client_dashboard.py +438 -0
- tests/backend/unit/lock_client/test_lock_client_discover.py +241 -0
- tests/backend/unit/lock_client/test_lock_client_force_release.py +354 -0
- tests/backend/unit/lock_client/test_lock_client_helper_branches.py +1890 -0
- tests/backend/unit/lock_client/test_lock_client_history.py +301 -0
- tests/backend/unit/lock_client/test_lock_client_isolation.py +316 -0
- tests/backend/unit/lock_client/test_lock_client_pid.py +75 -0
- tests/backend/unit/lock_client/test_lock_client_reconcile.py +464 -0
- tests/backend/unit/lock_client/test_lock_client_release.py +77 -0
- tests/backend/unit/lock_client/test_lock_client_shutdown.py +1110 -0
- tests/backend/unit/lock_client/test_lock_client_utils.py +474 -0
- tests/backend/unit/lock_client/test_lock_client_watch.py +866 -0
- tests/backend/unit/scripts/__init__.py +1 -0
- tests/backend/unit/scripts/_helpers.py +42 -0
- tests/backend/unit/scripts/test_cleanup.py +285 -0
- tests/backend/unit/scripts/test_collab_git_hook.py +280 -0
- tests/backend/unit/scripts/test_collab_git_hook_ported.py +50 -0
- tests/backend/unit/scripts/test_format_code.py +368 -0
- tests/backend/unit/scripts/test_format_code_ported.py +177 -0
- tests/backend/unit/scripts/test_generate_tests.py +305 -0
- tests/backend/unit/scripts/test_hook_templates.py +357 -0
- tests/backend/unit/scripts/test_setup_hook_overlay.py +95 -0
- tests/backend/unit/scripts/test_validate_code.py +867 -0
- tests/backend/unit/scripts/test_validate_code_ported.py +237 -0
- tests/backend/unit/test_entrypoints_main_run.py +83 -0
- tests/backend/unit/test_logging_config.py +529 -0
- tests/backend/unit/test_main_watch_pid_file.py +278 -0
- tests/conftest.py +167 -0
- tests/frontend/__init__.py +0 -0
- tests/frontend/jest/__init__.py +0 -0
- tests/frontend/playwright/__init__.py +0 -0
- 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)
|