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,516 @@
1
+ """Reconciliation startup tests for live_locks_watcher."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+
7
+ from ._helpers import load_watcher_module
8
+
9
+ # ---- Auto-migrated from migrated_remaining ----
10
+
11
+
12
+ def test_reconcile_readopts_dirty_locked_file(monkeypatch):
13
+ """§8c: Dirty file with existing lock is re-adopted, no acquire RPC.
14
+
15
+ When startup reconciliation finds a file that is dirty AND already locked by this
16
+ developer (same session token or no token), it should re-adopt the lock without
17
+ calling acquire_lock RPC.
18
+ """
19
+ mod = load_watcher_module()
20
+ monkeypatch.setattr(watcher, "DEVELOPER_ID", "alice")
21
+ monkeypatch.setattr(watcher, "_is_ephemeral_dev", lambda d: False)
22
+
23
+ # Clean up state
24
+ mod._local_owned_locks.clear()
25
+ mod._active_conflicts.clear()
26
+
27
+ # Mock git status: src/app.py is dirty
28
+ monkeypatch.setattr(watcher, "_run_git_status_porcelain", lambda: {"src/app.py"})
29
+ monkeypatch.setattr(watcher, "_get_current_branch", lambda: "main")
30
+
31
+ # Existing lock for src/app.py with matching SESSION_TOKEN
32
+ current_token = mod.SESSION_TOKEN
33
+
34
+ class FakeResponse:
35
+ data = [
36
+ {
37
+ "file_path": "src/app.py",
38
+ "developer_id": "alice",
39
+ "lock_token": current_token,
40
+ "branch_name": "main",
41
+ }
42
+ ]
43
+
44
+ rpc_called = []
45
+
46
+ class FakeClient:
47
+ def table(self, name):
48
+ return self
49
+
50
+ def select(self, *args):
51
+ return self
52
+
53
+ def eq(self, *args):
54
+ return self
55
+
56
+ def execute(self):
57
+ return FakeResponse()
58
+
59
+ def rpc(self, name, params):
60
+ rpc_called.append(name)
61
+ return self
62
+
63
+ client = FakeClient()
64
+ mod._reconcile_on_startup(client)
65
+
66
+ # File should be re-adopted (in _local_owned_locks)
67
+ assert "src/app.py" in mod._local_owned_locks
68
+ # acquire_lock RPC should NOT have been called
69
+ assert "acquire_lock" not in rpc_called
70
+
71
+ # Clean up
72
+ mod._local_owned_locks.clear()
73
+
74
+
75
+ def test_reconcile_releases_stale_clean_lock(monkeypatch):
76
+ """§8d: Locked file that is now clean is released as stale.
77
+
78
+ When startup reconciliation finds a file locked by this developer but the file is
79
+ NOT in git status (clean), it should delete the lock.
80
+ """
81
+ mod = load_watcher_module()
82
+ monkeypatch.setattr(watcher, "DEVELOPER_ID", "alice")
83
+ monkeypatch.setattr(watcher, "_is_ephemeral_dev", lambda d: False)
84
+
85
+ mod._local_owned_locks.clear()
86
+ mod._active_conflicts.clear()
87
+
88
+ # Mock git status: NO dirty files
89
+ monkeypatch.setattr(watcher, "_run_git_status_porcelain", lambda: set())
90
+ monkeypatch.setattr(watcher, "_get_current_branch", lambda: "main")
91
+
92
+ # Existing lock for src/old.py (stale)
93
+ class FakeSelectResponse:
94
+ data = [
95
+ {
96
+ "file_path": "src/old.py",
97
+ "developer_id": "alice",
98
+ "lock_token": "old-token",
99
+ "branch_name": "main",
100
+ }
101
+ ]
102
+
103
+ deleted_files = []
104
+
105
+ class FakeTable:
106
+ def __init__(self):
107
+ self._file_path = None
108
+
109
+ def select(self, *args):
110
+ return self
111
+
112
+ def delete(self):
113
+ return self
114
+
115
+ def eq(self, field, value):
116
+ if field == "file_path":
117
+ self._file_path = value
118
+ return self
119
+
120
+ def execute(self):
121
+ if self._file_path:
122
+ deleted_files.append(self._file_path)
123
+ return None
124
+ return FakeSelectResponse()
125
+
126
+ class FakeClient:
127
+ def table(self, name):
128
+ return FakeTable()
129
+
130
+ client = FakeClient()
131
+ mod._reconcile_on_startup(client)
132
+
133
+ # src/old.py should have been released
134
+ assert "src/old.py" in deleted_files
135
+ # Should NOT be in _local_owned_locks
136
+ assert "src/old.py" not in mod._local_owned_locks
137
+
138
+ mod._local_owned_locks.clear()
139
+
140
+
141
+ def test_reconcile_acquires_lock_for_new_dirty_file(monkeypatch):
142
+ """§8e: Dirty file with no existing lock is acquired at startup.
143
+
144
+ When startup reconciliation finds a dirty file that has no existing lock in
145
+ Supabase, it should call acquire_lock RPC to lock it.
146
+ """
147
+ mod = load_watcher_module()
148
+ monkeypatch.setattr(watcher, "DEVELOPER_ID", "alice")
149
+ monkeypatch.setattr(watcher, "_is_ephemeral_dev", lambda d: False)
150
+
151
+ mod._local_owned_locks.clear()
152
+ mod._active_conflicts.clear()
153
+
154
+ # Mock git status: src/new.py is dirty
155
+ monkeypatch.setattr(watcher, "_run_git_status_porcelain", lambda: {"src/new.py"})
156
+ monkeypatch.setattr(watcher, "_get_current_branch", lambda: "main")
157
+ monkeypatch.setattr(watcher, "_should_ignore_path", lambda p: False)
158
+
159
+ # No existing locks
160
+ class FakeSelectResponse:
161
+ data = []
162
+
163
+ class FakeRPCResponse:
164
+ data = [{"status": "ok"}]
165
+
166
+ rpc_calls = []
167
+
168
+ class FakeRPCChain:
169
+ def execute(self):
170
+ return FakeRPCResponse()
171
+
172
+ class FakeClient:
173
+ def table(self, name):
174
+ return self
175
+
176
+ def select(self, *args):
177
+ return self
178
+
179
+ def eq(self, *args):
180
+ return self
181
+
182
+ def execute(self):
183
+ return FakeSelectResponse()
184
+
185
+ def rpc(self, name, params):
186
+ rpc_calls.append({"name": name, "params": params})
187
+ return FakeRPCChain()
188
+
189
+ client = FakeClient()
190
+ mod._reconcile_on_startup(client)
191
+
192
+ # acquire_lock RPC should have been called for src/new.py
193
+ assert len(rpc_calls) == 1
194
+ assert rpc_calls[0]["name"] == "acquire_lock"
195
+ assert rpc_calls[0]["params"]["p_file_path"] == "src/new.py"
196
+ # File should now be in _local_owned_locks
197
+ assert "src/new.py" in mod._local_owned_locks
198
+
199
+ mod._local_owned_locks.clear()
200
+
201
+
202
+ def test_reconcile_post_restart_conflict_non_interactive(monkeypatch):
203
+ """§8f: Post-restart conflict in non-TTY defaults to continue.
204
+
205
+ When a dirty file is locked by another developer and stdin is not a TTY, the watcher
206
+ should add the file to _active_conflicts and continue running (no interactive
207
+ prompt).
208
+ """
209
+ mod = load_watcher_module()
210
+ monkeypatch.setattr(watcher, "DEVELOPER_ID", "alice")
211
+ monkeypatch.setattr(watcher, "_is_ephemeral_dev", lambda d: False)
212
+
213
+ mod._local_owned_locks.clear()
214
+ mod._active_conflicts.clear()
215
+
216
+ # Mock git status: src/shared.py is dirty
217
+ monkeypatch.setattr(watcher, "_run_git_status_porcelain", lambda: {"src/shared.py"})
218
+ monkeypatch.setattr(watcher, "_get_current_branch", lambda: "main")
219
+ monkeypatch.setattr(watcher, "_should_ignore_path", lambda p: False)
220
+ # Non-interactive
221
+ monkeypatch.setattr(sys, "stdin", type("F", (), {"isatty": lambda s: False})())
222
+
223
+ # No existing locks for alice
224
+ class FakeSelectResponse:
225
+ data = []
226
+
227
+ # RPC returns conflict
228
+ class FakeConflictResponse:
229
+ data = [{"status": "conflict", "owner": "bob"}]
230
+
231
+ class FakeRPCChain:
232
+ def execute(self):
233
+ return FakeConflictResponse()
234
+
235
+ notify_calls = []
236
+ monkeypatch.setattr(watcher, "_notify", lambda t, m: notify_calls.append((t, m)))
237
+
238
+ class FakeClient:
239
+ def table(self, name):
240
+ return self
241
+
242
+ def select(self, *args):
243
+ return self
244
+
245
+ def eq(self, *args):
246
+ return self
247
+
248
+ def execute(self):
249
+ return FakeSelectResponse()
250
+
251
+ def rpc(self, name, params):
252
+ return FakeRPCChain()
253
+
254
+ client = FakeClient()
255
+ mod._reconcile_on_startup(client)
256
+
257
+ # File should be in _active_conflicts
258
+ assert "src/shared.py" in mod._active_conflicts
259
+ # Should NOT be in _local_owned_locks (conflict)
260
+ assert "src/shared.py" not in mod._local_owned_locks
261
+ # Notification should have been sent
262
+ assert any("Post-restart" in t for t, m in notify_calls)
263
+
264
+ mod._active_conflicts.clear()
265
+
266
+
267
+ def test_reconcile_multi_session_different_token_non_interactive(monkeypatch):
268
+ """§8g: Different session token in non-TTY defaults to leave lock.
269
+
270
+ When startup reconciliation finds a dirty file locked by this developer but with a
271
+ different session token, and stdin is not a TTY, it should leave the lock untouched
272
+ (safe default).
273
+ """
274
+ mod = load_watcher_module()
275
+ monkeypatch.setattr(watcher, "DEVELOPER_ID", "alice")
276
+ monkeypatch.setattr(watcher, "_is_ephemeral_dev", lambda d: False)
277
+
278
+ mod._local_owned_locks.clear()
279
+ mod._active_conflicts.clear()
280
+
281
+ # Mock git status: src/multi.py is dirty
282
+ monkeypatch.setattr(watcher, "_run_git_status_porcelain", lambda: {"src/multi.py"})
283
+ monkeypatch.setattr(watcher, "_get_current_branch", lambda: "main")
284
+ # Non-interactive
285
+ monkeypatch.setattr(sys, "stdin", type("F", (), {"isatty": lambda s: False})())
286
+
287
+ # Existing lock with DIFFERENT token
288
+ class FakeSelectResponse:
289
+ data = [
290
+ {
291
+ "file_path": "src/multi.py",
292
+ "developer_id": "alice",
293
+ "lock_token": "other-machine-token-12345",
294
+ "branch_name": "main",
295
+ }
296
+ ]
297
+
298
+ rpc_calls = []
299
+
300
+ class FakeClient:
301
+ def table(self, name):
302
+ return self
303
+
304
+ def select(self, *args):
305
+ return self
306
+
307
+ def eq(self, *args):
308
+ return self
309
+
310
+ def execute(self):
311
+ return FakeSelectResponse()
312
+
313
+ def rpc(self, name, params):
314
+ rpc_calls.append(name)
315
+ return self
316
+
317
+ client = FakeClient()
318
+ mod._reconcile_on_startup(client)
319
+
320
+ # Lock should NOT be re-adopted (different token, non-interactive → leave)
321
+ assert "src/multi.py" not in mod._local_owned_locks
322
+ assert "acquire_lock" not in rpc_calls
323
+
324
+ mod._local_owned_locks.clear()
325
+
326
+
327
+ watcher = load_watcher_module()
328
+
329
+
330
+ def test_reconcile_stale_release_exception_and_acquire_exception(monkeypatch):
331
+ """Reconcile should continue when stale-release or acquire raises exceptions."""
332
+ mod = load_watcher_module()
333
+ monkeypatch.setattr(mod, "DEVELOPER_ID", "alice")
334
+ monkeypatch.setattr(mod, "_is_ephemeral_dev", lambda d: False)
335
+ monkeypatch.setattr(mod, "_get_current_branch", lambda: "main")
336
+ monkeypatch.setattr(mod, "_run_git_status_porcelain", lambda: {"src/new.py"})
337
+ monkeypatch.setattr(mod, "_should_ignore_path", lambda p: False)
338
+
339
+ # Existing stale lock is clean (not in dirty set), and unlocking it raises.
340
+ existing = [{"file_path": "src/stale.py", "lock_token": "x"}]
341
+
342
+ class FakeClient:
343
+ def __init__(self):
344
+ self._mode = "select"
345
+
346
+ def table(self, name):
347
+ return self
348
+
349
+ def select(self, *args):
350
+ self._mode = "select"
351
+ return self
352
+
353
+ def update(self, *args, **kwargs):
354
+ self._mode = "update"
355
+ return self
356
+
357
+ def delete(self):
358
+ self._mode = "delete"
359
+ return self
360
+
361
+ def eq(self, *args):
362
+ return self
363
+
364
+ def execute(self):
365
+ if self._mode == "select":
366
+ return type("R", (), {"data": existing})()
367
+ raise RuntimeError("db failure")
368
+
369
+ def rpc(self, *args, **kwargs):
370
+ return self
371
+
372
+ # Should swallow both stale-release and acquire exceptions.
373
+ mod._reconcile_on_startup(FakeClient())
374
+
375
+
376
+ def test_reconcile_skips_ignored_unlocked_dirty_files(monkeypatch):
377
+ """Ignored dirty files should not trigger acquire_lock RPC."""
378
+ mod = load_watcher_module()
379
+ monkeypatch.setattr(mod, "DEVELOPER_ID", "alice")
380
+ monkeypatch.setattr(mod, "_is_ephemeral_dev", lambda d: False)
381
+ monkeypatch.setattr(mod, "_get_current_branch", lambda: "main")
382
+ monkeypatch.setattr(mod, "_run_git_status_porcelain", lambda: {".git/config"})
383
+ monkeypatch.setattr(mod, "_should_ignore_path", lambda p: True)
384
+
385
+ rpc_calls = []
386
+
387
+ class FakeClient:
388
+ def table(self, name):
389
+ return self
390
+
391
+ def select(self, *args):
392
+ return self
393
+
394
+ def eq(self, *args):
395
+ return self
396
+
397
+ def execute(self):
398
+ return type("R", (), {"data": []})()
399
+
400
+ def rpc(self, *args, **kwargs):
401
+ rpc_calls.append(True)
402
+ return self
403
+
404
+ mod._reconcile_on_startup(FakeClient())
405
+ assert rpc_calls == []
406
+
407
+
408
+ def test_get_modified_and_unpushed_files_status_exception(monkeypatch):
409
+ """If git status fails, helper should continue and return best-effort set."""
410
+ mod = load_watcher_module()
411
+
412
+ def _check_output(cmd, *args, **kwargs):
413
+ if cmd[:3] == ["git", "status", "--porcelain"]:
414
+ raise RuntimeError("status fail")
415
+ if cmd[:2] == ["git", "rev-parse"]:
416
+ return b"origin/main\n"
417
+ if cmd[:3] == ["git", "diff", "--name-only"]:
418
+ return b"src/from_diff.py\n"
419
+ return b""
420
+
421
+ monkeypatch.setattr(mod.subprocess, "check_output", _check_output)
422
+ monkeypatch.setattr(mod, "_normalize_path", lambda p, root: p)
423
+ monkeypatch.setattr(mod, "_should_ignore_path", lambda p: False)
424
+ out = mod._get_modified_and_unpushed_files()
425
+ assert "src/from_diff.py" in out
426
+
427
+
428
+ def test_reconcile_on_startup_git_status_exception(monkeypatch):
429
+ """_reconcile_on_startup exits cleanly when git status fails."""
430
+ mod = load_watcher_module()
431
+ monkeypatch.setattr(
432
+ mod,
433
+ "_run_git_status_porcelain",
434
+ lambda: (_ for _ in ()).throw(RuntimeError("git fail")),
435
+ )
436
+
437
+ class _Client:
438
+ def table(self, _name):
439
+ return self
440
+
441
+ def select(self, *_a, **_k):
442
+ return self
443
+
444
+ def eq(self, *_a, **_k):
445
+ return self
446
+
447
+ def execute(self):
448
+ return type("R", (), {"data": []})()
449
+
450
+ mod._reconcile_on_startup(_Client()) # should not raise
451
+
452
+
453
+ def test_reconcile_ephemeral_developer_short_circuit(monkeypatch):
454
+ """Ephemeral developers should skip startup reconciliation entirely."""
455
+ mod = load_watcher_module()
456
+ monkeypatch.setattr(mod, "DEVELOPER_ID", "test_dev_1")
457
+ monkeypatch.setattr(mod, "_is_ephemeral_dev", lambda d: True)
458
+
459
+ class FakeClient:
460
+ def table(self, name):
461
+ raise AssertionError("should not query DB for ephemeral dev")
462
+
463
+ mod._reconcile_on_startup(FakeClient())
464
+
465
+
466
+ def test_reconcile_same_machine_re_adopt_update_exception(monkeypatch):
467
+ """Same-machine token mismatch should re-adopt even if token update fails."""
468
+ mod = load_watcher_module()
469
+ monkeypatch.setattr(mod, "DEVELOPER_ID", "alice")
470
+ monkeypatch.setattr(mod, "_is_ephemeral_dev", lambda d: False)
471
+ monkeypatch.setattr(mod, "_run_git_status_porcelain", lambda: {"src/file.py"})
472
+ monkeypatch.setattr(mod, "_get_current_branch", lambda: "main")
473
+ monkeypatch.setattr(mod, "_is_same_machine_token", lambda token: True)
474
+
475
+ mod._local_owned_locks.clear()
476
+
477
+ class FakeClient:
478
+ def __init__(self):
479
+ self._mode = "select"
480
+
481
+ def table(self, name):
482
+ return self
483
+
484
+ def select(self, *args):
485
+ self._mode = "select"
486
+ return self
487
+
488
+ def update(self, *args, **kwargs):
489
+ self._mode = "update"
490
+ return self
491
+
492
+ def eq(self, *args):
493
+ return self
494
+
495
+ def execute(self):
496
+ if self._mode == "select":
497
+ return type(
498
+ "R",
499
+ (),
500
+ {
501
+ "data": [
502
+ {
503
+ "file_path": "src/file.py",
504
+ "developer_id": "alice",
505
+ "lock_token": "other-token",
506
+ }
507
+ ]
508
+ },
509
+ )()
510
+ raise RuntimeError("update failed")
511
+
512
+ def rpc(self, *args, **kwargs):
513
+ return self
514
+
515
+ mod._reconcile_on_startup(FakeClient())
516
+ assert "src/file.py" in mod._local_owned_locks