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,354 @@
|
|
|
1
|
+
"""Force-release tests for LockClient (admin & non-admin flows)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ._helpers import (
|
|
6
|
+
FakeClient,
|
|
7
|
+
FakeResponse,
|
|
8
|
+
load_lock_client_module,
|
|
9
|
+
make_create_client,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
mod = load_lock_client_module()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_force_release(monkeypatch):
|
|
16
|
+
"""Test force-releasing a lock (admin operation)."""
|
|
17
|
+
monkeypatch.setenv("SUPABASE_URL", "https://test.supabase.co")
|
|
18
|
+
monkeypatch.setenv("SUPABASE_SERVICE_ROLE_KEY", "service_key")
|
|
19
|
+
|
|
20
|
+
response = FakeResponse(status=200, data=[{"file_path": "src/app.py"}])
|
|
21
|
+
monkeypatch.setattr(mod, "_get_create_client", lambda: make_create_client(response))
|
|
22
|
+
|
|
23
|
+
lc = mod.LockClient(developer_id="admin")
|
|
24
|
+
ok, msg = lc.force_release("src/app.py")
|
|
25
|
+
assert isinstance(ok, bool)
|
|
26
|
+
assert isinstance(msg, str)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_force_release_api_exception(monkeypatch):
|
|
30
|
+
"""Test force_release when API call raises an exception."""
|
|
31
|
+
monkeypatch.setenv("SUPABASE_URL", "https://test.supabase.co")
|
|
32
|
+
monkeypatch.setenv("SUPABASE_ANON_KEY", "test_key")
|
|
33
|
+
|
|
34
|
+
class ExplodingClient(FakeClient):
|
|
35
|
+
def execute(self):
|
|
36
|
+
raise RuntimeError("Network timeout error")
|
|
37
|
+
|
|
38
|
+
monkeypatch.setattr(
|
|
39
|
+
mod,
|
|
40
|
+
"_get_create_client",
|
|
41
|
+
lambda: (lambda url, key: ExplodingClient(FakeResponse())),
|
|
42
|
+
)
|
|
43
|
+
monkeypatch.setattr(mod.time, "sleep", lambda x: None)
|
|
44
|
+
|
|
45
|
+
lc = mod.LockClient(developer_id="admin")
|
|
46
|
+
ok, msg = lc.force_release("src/app.py")
|
|
47
|
+
assert ok is False
|
|
48
|
+
assert "API Error" in msg
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_force_release_with_error(monkeypatch):
|
|
52
|
+
"""Test force_release when response has error."""
|
|
53
|
+
monkeypatch.setenv("SUPABASE_URL", "https://test.supabase.co")
|
|
54
|
+
monkeypatch.setenv("SUPABASE_ANON_KEY", "test_key")
|
|
55
|
+
|
|
56
|
+
response = FakeResponse(status=500, data=None, error="Server error")
|
|
57
|
+
monkeypatch.setattr(mod, "_get_create_client", lambda: make_create_client(response))
|
|
58
|
+
|
|
59
|
+
lc = mod.LockClient(developer_id="admin")
|
|
60
|
+
ok, msg = lc.force_release("src/app.py")
|
|
61
|
+
assert ok is False
|
|
62
|
+
assert "API Error" in msg
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_force_release_no_lock(monkeypatch):
|
|
66
|
+
"""Test force_release when no lock exists to remove."""
|
|
67
|
+
monkeypatch.setenv("SUPABASE_URL", "https://test.supabase.co")
|
|
68
|
+
monkeypatch.setenv("SUPABASE_ANON_KEY", "test_key")
|
|
69
|
+
|
|
70
|
+
# Need data to be truly None (not []) to hit the "No lock removed" path
|
|
71
|
+
class NullDataResponse:
|
|
72
|
+
status = 200
|
|
73
|
+
data = None
|
|
74
|
+
error = None
|
|
75
|
+
|
|
76
|
+
monkeypatch.setattr(
|
|
77
|
+
mod, "_get_create_client", lambda: make_create_client(NullDataResponse())
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
lc = mod.LockClient(developer_id="admin")
|
|
81
|
+
ok, msg = lc.force_release("src/app.py")
|
|
82
|
+
assert ok is False
|
|
83
|
+
assert "No lock removed" in msg
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_force_release_nonadmin_own_lock(monkeypatch):
|
|
87
|
+
"""Non-admin user can force-release their own lock."""
|
|
88
|
+
monkeypatch.setenv("SUPABASE_URL", "https://test.supabase.co")
|
|
89
|
+
monkeypatch.setenv("SUPABASE_ANON_KEY", "test_key")
|
|
90
|
+
monkeypatch.delenv("SUPABASE_SERVICE_ROLE_KEY", raising=False)
|
|
91
|
+
monkeypatch.setattr(mod, "SUPABASE_SERVICE_ROLE_KEY", None)
|
|
92
|
+
|
|
93
|
+
status_resp = FakeResponse(
|
|
94
|
+
status=200,
|
|
95
|
+
data=[
|
|
96
|
+
{
|
|
97
|
+
"file_path": "src/app.py",
|
|
98
|
+
"developer_id": "alice",
|
|
99
|
+
"expires_at": "2099-01-01T00:00:00Z",
|
|
100
|
+
}
|
|
101
|
+
],
|
|
102
|
+
)
|
|
103
|
+
delete_resp = FakeResponse(status=200, data=[{"file_path": "src/app.py"}])
|
|
104
|
+
|
|
105
|
+
call_log = []
|
|
106
|
+
|
|
107
|
+
class TrackingClient(FakeClient):
|
|
108
|
+
"""Tracks method calls to verify developer_id filter is applied."""
|
|
109
|
+
|
|
110
|
+
def __init__(self, resp):
|
|
111
|
+
super().__init__(resp)
|
|
112
|
+
self._current_resp = resp
|
|
113
|
+
|
|
114
|
+
def select(self, *args, **kwargs):
|
|
115
|
+
self._current_resp = status_resp
|
|
116
|
+
return self
|
|
117
|
+
|
|
118
|
+
def delete(self, *args, **kwargs):
|
|
119
|
+
self._current_resp = delete_resp
|
|
120
|
+
call_log.append("delete")
|
|
121
|
+
return self
|
|
122
|
+
|
|
123
|
+
def eq(self, col, val):
|
|
124
|
+
call_log.append(("eq", col, val))
|
|
125
|
+
return self
|
|
126
|
+
|
|
127
|
+
def execute(self):
|
|
128
|
+
return self._current_resp
|
|
129
|
+
|
|
130
|
+
monkeypatch.setattr(
|
|
131
|
+
mod,
|
|
132
|
+
"_get_create_client",
|
|
133
|
+
lambda: (lambda url, key: TrackingClient(status_resp)),
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
lc = mod.LockClient(developer_id="alice")
|
|
137
|
+
ok, msg = lc.force_release("src/app.py")
|
|
138
|
+
assert ok is True
|
|
139
|
+
eq_calls = [c for c in call_log if isinstance(c, tuple) and c[0] == "eq"]
|
|
140
|
+
dev_id_filters = [c for c in eq_calls if c[1] == "developer_id"]
|
|
141
|
+
assert len(dev_id_filters) > 0, "Non-admin should filter by developer_id"
|
|
142
|
+
assert dev_id_filters[-1][2] == "alice"
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def test_force_release_nonadmin_other_dev_lock_denied(monkeypatch):
|
|
146
|
+
"""Non-admin user cannot force-release another developer's lock."""
|
|
147
|
+
monkeypatch.setenv("SUPABASE_URL", "https://test.supabase.co")
|
|
148
|
+
monkeypatch.setenv("SUPABASE_ANON_KEY", "test_key")
|
|
149
|
+
monkeypatch.delenv("SUPABASE_SERVICE_ROLE_KEY", raising=False)
|
|
150
|
+
monkeypatch.setattr(mod, "SUPABASE_SERVICE_ROLE_KEY", None)
|
|
151
|
+
|
|
152
|
+
status_resp = FakeResponse(
|
|
153
|
+
status=200,
|
|
154
|
+
data=[
|
|
155
|
+
{
|
|
156
|
+
"file_path": "src/app.py",
|
|
157
|
+
"developer_id": "bob",
|
|
158
|
+
"expires_at": "2099-01-01T00:00:00Z",
|
|
159
|
+
}
|
|
160
|
+
],
|
|
161
|
+
)
|
|
162
|
+
monkeypatch.setattr(
|
|
163
|
+
mod, "_get_create_client", lambda: make_create_client(status_resp)
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
lc = mod.LockClient(developer_id="alice")
|
|
167
|
+
ok, msg = lc.force_release("src/app.py")
|
|
168
|
+
assert ok is False
|
|
169
|
+
assert "Permission denied" in msg
|
|
170
|
+
assert "@bob" in msg
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def test_force_release_admin_other_dev_lock_succeeds(monkeypatch):
|
|
174
|
+
"""Admin user can force-release any developer's lock."""
|
|
175
|
+
monkeypatch.setenv("SUPABASE_URL", "https://test.supabase.co")
|
|
176
|
+
monkeypatch.setenv("SUPABASE_SERVICE_ROLE_KEY", "service_key")
|
|
177
|
+
monkeypatch.setattr(mod, "SUPABASE_SERVICE_ROLE_KEY", "service_key")
|
|
178
|
+
|
|
179
|
+
delete_resp = FakeResponse(status=200, data=[{"file_path": "src/app.py"}])
|
|
180
|
+
|
|
181
|
+
call_log = []
|
|
182
|
+
|
|
183
|
+
class TrackingClient(FakeClient):
|
|
184
|
+
def delete(self, *args, **kwargs):
|
|
185
|
+
call_log.append("delete")
|
|
186
|
+
return self
|
|
187
|
+
|
|
188
|
+
def eq(self, col, val):
|
|
189
|
+
call_log.append(("eq", col, val))
|
|
190
|
+
return self
|
|
191
|
+
|
|
192
|
+
monkeypatch.setattr(
|
|
193
|
+
mod,
|
|
194
|
+
"_get_create_client",
|
|
195
|
+
lambda: (lambda url, key: TrackingClient(delete_resp)),
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
lc = mod.LockClient(developer_id="admin_user")
|
|
199
|
+
ok, msg = lc.force_release("src/app.py")
|
|
200
|
+
assert ok is True
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
# ---------------------------------------------------------------------------
|
|
204
|
+
# force_release_all tests
|
|
205
|
+
# ---------------------------------------------------------------------------
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def test_force_release_all_non_admin_returns_zero(monkeypatch):
|
|
209
|
+
"""force_release_all returns 0 immediately when user is not admin."""
|
|
210
|
+
monkeypatch.setenv("SUPABASE_URL", "https://test.supabase.co")
|
|
211
|
+
monkeypatch.setenv("SUPABASE_ANON_KEY", "test_key")
|
|
212
|
+
monkeypatch.delenv("SUPABASE_SERVICE_ROLE_KEY", raising=False)
|
|
213
|
+
monkeypatch.setattr(mod, "SUPABASE_SERVICE_ROLE_KEY", None)
|
|
214
|
+
|
|
215
|
+
lc = mod.LockClient(developer_id="alice")
|
|
216
|
+
assert not lc._is_admin
|
|
217
|
+
result = lc.force_release_all()
|
|
218
|
+
assert result == 0
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def test_force_release_all_empty_locks_returns_zero(monkeypatch):
|
|
222
|
+
"""force_release_all returns 0 when there are no active locks."""
|
|
223
|
+
monkeypatch.setenv("SUPABASE_URL", "https://test.supabase.co")
|
|
224
|
+
monkeypatch.setenv("SUPABASE_ANON_KEY", "test_key")
|
|
225
|
+
|
|
226
|
+
lc = mod.LockClient(local_only=True, developer_id="root")
|
|
227
|
+
lc._is_admin = True
|
|
228
|
+
monkeypatch.setattr(lc, "active", lambda: [])
|
|
229
|
+
|
|
230
|
+
result = lc.force_release_all()
|
|
231
|
+
assert result == 0
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def test_force_release_all_counts_deleted_rows(monkeypatch):
|
|
235
|
+
"""force_release_all returns count from API-returned deleted rows."""
|
|
236
|
+
monkeypatch.setenv("SUPABASE_URL", "https://test.supabase.co")
|
|
237
|
+
monkeypatch.setenv("SUPABASE_ANON_KEY", "test_key")
|
|
238
|
+
|
|
239
|
+
lc = mod.LockClient(local_only=True, developer_id="root")
|
|
240
|
+
lc._is_admin = True
|
|
241
|
+
|
|
242
|
+
locks = [{"file_path": f"src/file{i}.py"} for i in range(3)]
|
|
243
|
+
monkeypatch.setattr(lc, "active", lambda: locks)
|
|
244
|
+
|
|
245
|
+
class RespWithData:
|
|
246
|
+
status = 200
|
|
247
|
+
data = [
|
|
248
|
+
{"file_path": "src/file0.py"},
|
|
249
|
+
{"file_path": "src/file1.py"},
|
|
250
|
+
{"file_path": "src/file2.py"},
|
|
251
|
+
]
|
|
252
|
+
error = None
|
|
253
|
+
|
|
254
|
+
class FakeDeleteClient:
|
|
255
|
+
def table(self, *a, **k):
|
|
256
|
+
return self
|
|
257
|
+
|
|
258
|
+
def delete(self, *a, **k):
|
|
259
|
+
return self
|
|
260
|
+
|
|
261
|
+
def in_(self, *a, **k):
|
|
262
|
+
return self
|
|
263
|
+
|
|
264
|
+
def execute(self):
|
|
265
|
+
return RespWithData()
|
|
266
|
+
|
|
267
|
+
lc._client = FakeDeleteClient()
|
|
268
|
+
result = lc.force_release_all()
|
|
269
|
+
assert result == 3
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def test_force_release_all_chunk_exception_returns_partial(monkeypatch):
|
|
273
|
+
"""force_release_all returns partial count when chunk delete raises."""
|
|
274
|
+
monkeypatch.setenv("SUPABASE_URL", "https://test.supabase.co")
|
|
275
|
+
monkeypatch.setenv("SUPABASE_ANON_KEY", "test_key")
|
|
276
|
+
|
|
277
|
+
lc = mod.LockClient(local_only=True, developer_id="root")
|
|
278
|
+
lc._is_admin = True
|
|
279
|
+
|
|
280
|
+
locks = [{"file_path": f"src/file{i}.py"} for i in range(5)]
|
|
281
|
+
monkeypatch.setattr(lc, "active", lambda: locks)
|
|
282
|
+
|
|
283
|
+
class ExplodingClient:
|
|
284
|
+
def table(self, *a, **k):
|
|
285
|
+
return self
|
|
286
|
+
|
|
287
|
+
def delete(self, *a, **k):
|
|
288
|
+
return self
|
|
289
|
+
|
|
290
|
+
def in_(self, *a, **k):
|
|
291
|
+
return self
|
|
292
|
+
|
|
293
|
+
def execute(self):
|
|
294
|
+
raise RuntimeError("network error")
|
|
295
|
+
|
|
296
|
+
lc._client = ExplodingClient()
|
|
297
|
+
# Should return 0 (no chunks completed) without raising
|
|
298
|
+
monkeypatch.setattr(mod.time, "sleep", lambda x: None)
|
|
299
|
+
result = lc.force_release_all()
|
|
300
|
+
assert result == 0 # first chunk already fails
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def test_force_release_all_api_error_returns_partial(monkeypatch):
|
|
304
|
+
"""force_release_all returns partial count when chunk returns error."""
|
|
305
|
+
monkeypatch.setenv("SUPABASE_URL", "https://test.supabase.co")
|
|
306
|
+
monkeypatch.setenv("SUPABASE_ANON_KEY", "test_key")
|
|
307
|
+
|
|
308
|
+
lc = mod.LockClient(local_only=True, developer_id="root")
|
|
309
|
+
lc._is_admin = True
|
|
310
|
+
|
|
311
|
+
locks = [{"file_path": f"src/file{i}.py"} for i in range(3)]
|
|
312
|
+
monkeypatch.setattr(lc, "active", lambda: locks)
|
|
313
|
+
|
|
314
|
+
class ErrorResp:
|
|
315
|
+
status = 500
|
|
316
|
+
data = None
|
|
317
|
+
error = "Server error"
|
|
318
|
+
|
|
319
|
+
class ErrorClient:
|
|
320
|
+
def table(self, *a, **k):
|
|
321
|
+
return self
|
|
322
|
+
|
|
323
|
+
def delete(self, *a, **k):
|
|
324
|
+
return self
|
|
325
|
+
|
|
326
|
+
def in_(self, *a, **k):
|
|
327
|
+
return self
|
|
328
|
+
|
|
329
|
+
def execute(self):
|
|
330
|
+
return ErrorResp()
|
|
331
|
+
|
|
332
|
+
lc._client = ErrorClient()
|
|
333
|
+
result = lc.force_release_all()
|
|
334
|
+
assert result == 0 # first chunk fails, 0 deleted so far
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def test_force_release_all_locks_with_no_path_skipped(monkeypatch):
|
|
338
|
+
"""force_release_all skips lock entries that have empty or missing file_path."""
|
|
339
|
+
monkeypatch.setenv("SUPABASE_URL", "https://test.supabase.co")
|
|
340
|
+
monkeypatch.setenv("SUPABASE_ANON_KEY", "test_key")
|
|
341
|
+
|
|
342
|
+
lc = mod.LockClient(local_only=True, developer_id="root")
|
|
343
|
+
lc._is_admin = True
|
|
344
|
+
|
|
345
|
+
# Only locks with empty/missing file_path - should result in 0 paths
|
|
346
|
+
locks = [{"file_path": ""}, {"developer_id": "root"}, {"file_path": None}]
|
|
347
|
+
monkeypatch.setattr(lc, "active", lambda: locks)
|
|
348
|
+
|
|
349
|
+
result = lc.force_release_all()
|
|
350
|
+
assert result == 0 # No valid paths -> count == 0 -> early return
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
# (test_force_release_permission_denied removed: duplicate of
|
|
354
|
+
# test_force_release_nonadmin_other_dev_lock_denied above)
|