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,187 @@
|
|
|
1
|
+
"""Module-level tests for live_locks_watcher."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib
|
|
6
|
+
import importlib.util
|
|
7
|
+
import sys
|
|
8
|
+
import types
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
|
|
13
|
+
from ._helpers import load_watcher_module
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _repo_root() -> Path:
|
|
17
|
+
here = Path(__file__).resolve()
|
|
18
|
+
for parent in here.parents:
|
|
19
|
+
if (parent / "pyproject.toml").exists() and (parent / "src").exists():
|
|
20
|
+
return parent
|
|
21
|
+
raise FileNotFoundError("Could not locate repository root")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_main_function_exists():
|
|
25
|
+
mod = load_watcher_module()
|
|
26
|
+
assert hasattr(mod, "main") and callable(mod.main)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_module_imports():
|
|
30
|
+
mod = load_watcher_module()
|
|
31
|
+
assert hasattr(mod, "main")
|
|
32
|
+
assert hasattr(mod, "_parse_git_status_path")
|
|
33
|
+
assert hasattr(mod, "_should_ignore_path")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_main_block_present():
|
|
37
|
+
module_file = _repo_root().joinpath("src/live_locks_watcher.py")
|
|
38
|
+
src = module_file.read_text(encoding="utf-8")
|
|
39
|
+
assert '__name__ == "__main__"' in src or "__name__ == '__main__'" in src
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_reload_watcher_with_colorama_and_plyer(monkeypatch):
|
|
43
|
+
"""Reload the watcher module with fake colorama and plyer to exercise optional-
|
|
44
|
+
import branches executed at module import time."""
|
|
45
|
+
fake_colorama = types.SimpleNamespace(
|
|
46
|
+
Fore=types.SimpleNamespace(GREEN="G", YELLOW="Y", CYAN="C", MAGENTA="M"),
|
|
47
|
+
Style=types.SimpleNamespace(RESET_ALL="R"),
|
|
48
|
+
init=lambda: None,
|
|
49
|
+
)
|
|
50
|
+
fake_plyer = types.SimpleNamespace(
|
|
51
|
+
notification=types.SimpleNamespace(
|
|
52
|
+
notify=lambda **k: None,
|
|
53
|
+
),
|
|
54
|
+
)
|
|
55
|
+
fake_supa = types.SimpleNamespace(create_client=lambda url, key: object())
|
|
56
|
+
|
|
57
|
+
# Inject into sys.modules and monkeypatch find_spec so importlib sees them
|
|
58
|
+
sys.modules["colorama"] = fake_colorama
|
|
59
|
+
sys.modules["plyer"] = types.SimpleNamespace(notification=fake_plyer.notification)
|
|
60
|
+
sys.modules["supabase"] = fake_supa
|
|
61
|
+
orig_find_spec = importlib.util.find_spec
|
|
62
|
+
monkeypatch.setattr(importlib.util, "find_spec", lambda name: object())
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
spec = importlib.util.spec_from_file_location(
|
|
66
|
+
"tmp_watcher",
|
|
67
|
+
_repo_root().joinpath("src/live_locks_watcher.py"),
|
|
68
|
+
)
|
|
69
|
+
assert spec and spec.loader
|
|
70
|
+
mod = importlib.util.module_from_spec(spec)
|
|
71
|
+
spec.loader.exec_module(mod) # type: ignore[arg-defined]
|
|
72
|
+
|
|
73
|
+
# Basic smoke checks on functions that depend on the optional imports
|
|
74
|
+
assert callable(mod._color)
|
|
75
|
+
# _notify should use our fake notification without raising
|
|
76
|
+
mod._notify("T", "M")
|
|
77
|
+
finally:
|
|
78
|
+
for name in ("colorama", "plyer", "supabase"):
|
|
79
|
+
try:
|
|
80
|
+
del sys.modules[name]
|
|
81
|
+
except KeyError:
|
|
82
|
+
pass
|
|
83
|
+
import importlib as _importlib
|
|
84
|
+
|
|
85
|
+
monkeypatch.setattr(_importlib.util, "find_spec", orig_find_spec)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_color_without_colorama():
|
|
89
|
+
mod = load_watcher_module()
|
|
90
|
+
mod._HAS_COLORAMA = False
|
|
91
|
+
out = mod._color("hello", "X")
|
|
92
|
+
assert out == "hello"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_setup_collab_logging_fallback_to_basic_config(monkeypatch):
|
|
96
|
+
"""setup_collab_logging falls back to basicConfig when _setup_collab_logging_obj is
|
|
97
|
+
None."""
|
|
98
|
+
mod = load_watcher_module()
|
|
99
|
+
monkeypatch.setattr(mod, "_setup_collab_logging_obj", None)
|
|
100
|
+
called = []
|
|
101
|
+
monkeypatch.setattr(
|
|
102
|
+
mod.logging, "basicConfig", lambda **kwargs: called.append(kwargs)
|
|
103
|
+
)
|
|
104
|
+
mod.setup_collab_logging(collab_dir="/fake/collab")
|
|
105
|
+
assert called, "basicConfig should be called when _setup_collab_logging_obj is None"
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def test_reload_watcher_handles_find_spec_exceptions(monkeypatch):
|
|
109
|
+
"""Import-time optional dependency probes should tolerate find_spec errors."""
|
|
110
|
+
module_path = _repo_root().joinpath("src/live_locks_watcher.py")
|
|
111
|
+
|
|
112
|
+
def _raising_find_spec(_name):
|
|
113
|
+
raise RuntimeError("probe failed")
|
|
114
|
+
|
|
115
|
+
monkeypatch.setattr(importlib.util, "find_spec", _raising_find_spec)
|
|
116
|
+
|
|
117
|
+
spec = importlib.util.spec_from_file_location("tmp_watcher_probe_fail", module_path)
|
|
118
|
+
assert spec and spec.loader
|
|
119
|
+
mod = importlib.util.module_from_spec(spec)
|
|
120
|
+
spec.loader.exec_module(mod) # type: ignore[arg-defined]
|
|
121
|
+
|
|
122
|
+
# Module should still import with optional dependencies disabled.
|
|
123
|
+
assert mod._HAS_COLORAMA is False
|
|
124
|
+
assert mod.create_client is None
|
|
125
|
+
assert mod.desktop_notify is None
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def test_reload_watcher_exits_on_local_supabase_shadow(monkeypatch):
|
|
129
|
+
"""Watcher should abort when a local .collab/supabase module shadows package."""
|
|
130
|
+
module_path = _repo_root().joinpath("src/live_locks_watcher.py")
|
|
131
|
+
# Force a deterministic runtime collab root so shadow detection doesn't
|
|
132
|
+
# depend on the process cwd/environment.
|
|
133
|
+
monkeypatch.setenv("COLLAB_HOME", str(module_path.parent))
|
|
134
|
+
fake_supa_spec = types.SimpleNamespace(
|
|
135
|
+
origin=str(module_path.parent / "supabase.py")
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
def _find_spec(name):
|
|
139
|
+
if name == "supabase":
|
|
140
|
+
return fake_supa_spec
|
|
141
|
+
return None
|
|
142
|
+
|
|
143
|
+
monkeypatch.setattr(importlib.util, "find_spec", _find_spec)
|
|
144
|
+
|
|
145
|
+
spec = importlib.util.spec_from_file_location(
|
|
146
|
+
"tmp_watcher_shadowed_supa", module_path
|
|
147
|
+
)
|
|
148
|
+
assert spec and spec.loader
|
|
149
|
+
mod = importlib.util.module_from_spec(spec)
|
|
150
|
+
|
|
151
|
+
with pytest.raises(SystemExit):
|
|
152
|
+
spec.loader.exec_module(mod) # type: ignore[arg-defined]
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def test_watcher_allows_project_venv_site_packages_origin(monkeypatch):
|
|
156
|
+
"""Watcher import should allow installed packages from a repo-local virtualenv."""
|
|
157
|
+
module_path = _repo_root().joinpath("src/live_locks_watcher.py")
|
|
158
|
+
project_root = module_path.parents[2]
|
|
159
|
+
monkeypatch.delenv("COLLAB_HOME", raising=False)
|
|
160
|
+
monkeypatch.setenv("COLLAB_PROJECT_ROOT", str(project_root))
|
|
161
|
+
fake_supa_spec = types.SimpleNamespace(
|
|
162
|
+
origin=str(
|
|
163
|
+
project_root
|
|
164
|
+
/ ".venv"
|
|
165
|
+
/ "Lib"
|
|
166
|
+
/ "site-packages"
|
|
167
|
+
/ "supabase"
|
|
168
|
+
/ "__init__.py"
|
|
169
|
+
)
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
def _find_spec(name):
|
|
173
|
+
if name == "supabase":
|
|
174
|
+
return fake_supa_spec
|
|
175
|
+
return None
|
|
176
|
+
|
|
177
|
+
fake_supa = types.SimpleNamespace(create_client=lambda *_a, **_k: object())
|
|
178
|
+
monkeypatch.setitem(sys.modules, "supabase", fake_supa)
|
|
179
|
+
monkeypatch.setattr(importlib.util, "find_spec", _find_spec)
|
|
180
|
+
|
|
181
|
+
spec = importlib.util.spec_from_file_location(
|
|
182
|
+
"tmp_watcher_site_packages_supa", module_path
|
|
183
|
+
)
|
|
184
|
+
assert spec and spec.loader
|
|
185
|
+
mod = importlib.util.module_from_spec(spec)
|
|
186
|
+
spec.loader.exec_module(mod) # type: ignore[arg-defined]
|
|
187
|
+
assert callable(mod.create_client)
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
"""Multi-session and post-restart conflict handling tests for live_locks_watcher."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from ._helpers import load_watcher_module
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_handle_multi_session_interactive_readopt_choice_1(monkeypatch):
|
|
13
|
+
"""Test interactive multi-session lock re-adopt (choice 1)."""
|
|
14
|
+
mod = load_watcher_module()
|
|
15
|
+
monkeypatch.setattr(mod, "DEVELOPER_ID", "alice")
|
|
16
|
+
monkeypatch.setattr(sys, "stdin", type("F", (), {"isatty": lambda s: True})())
|
|
17
|
+
|
|
18
|
+
import builtins
|
|
19
|
+
|
|
20
|
+
monkeypatch.setattr(builtins, "input", lambda p: "1")
|
|
21
|
+
|
|
22
|
+
update_called = []
|
|
23
|
+
|
|
24
|
+
class FakeTable:
|
|
25
|
+
def update(self, *args):
|
|
26
|
+
update_called.append("update")
|
|
27
|
+
return self
|
|
28
|
+
|
|
29
|
+
def eq(self, *args):
|
|
30
|
+
return self
|
|
31
|
+
|
|
32
|
+
def execute(self):
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
class FakeClient:
|
|
36
|
+
def table(self, name):
|
|
37
|
+
return FakeTable()
|
|
38
|
+
|
|
39
|
+
client = FakeClient()
|
|
40
|
+
mod._local_owned_locks.clear()
|
|
41
|
+
|
|
42
|
+
mod._handle_multi_session_lock(client, "src/multi.py", "old-token")
|
|
43
|
+
|
|
44
|
+
assert "update" in update_called
|
|
45
|
+
assert "src/multi.py" in mod._local_owned_locks
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_handle_multi_session_interactive_release_choice_3(monkeypatch):
|
|
49
|
+
"""Test interactive multi-session lock release (choice 3)."""
|
|
50
|
+
mod = load_watcher_module()
|
|
51
|
+
monkeypatch.setattr(mod, "DEVELOPER_ID", "alice")
|
|
52
|
+
monkeypatch.setattr(sys, "stdin", type("F", (), {"isatty": lambda s: True})())
|
|
53
|
+
|
|
54
|
+
import builtins
|
|
55
|
+
|
|
56
|
+
monkeypatch.setattr(builtins, "input", lambda p: "3")
|
|
57
|
+
|
|
58
|
+
delete_called = []
|
|
59
|
+
|
|
60
|
+
class FakeTable:
|
|
61
|
+
def delete(self):
|
|
62
|
+
delete_called.append("delete")
|
|
63
|
+
return self
|
|
64
|
+
|
|
65
|
+
def eq(self, *args):
|
|
66
|
+
return self
|
|
67
|
+
|
|
68
|
+
def execute(self):
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
class FakeClient:
|
|
72
|
+
def table(self, name):
|
|
73
|
+
return FakeTable()
|
|
74
|
+
|
|
75
|
+
client = FakeClient()
|
|
76
|
+
mod._local_owned_locks.clear()
|
|
77
|
+
|
|
78
|
+
mod._handle_multi_session_lock(client, "src/multi.py", "old-token")
|
|
79
|
+
|
|
80
|
+
assert "delete" in delete_called
|
|
81
|
+
assert "src/multi.py" not in mod._local_owned_locks
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_handle_multi_session_interactive_leave_choice_2(monkeypatch):
|
|
85
|
+
"""Test interactive multi-session lock leave (choice 2)."""
|
|
86
|
+
mod = load_watcher_module()
|
|
87
|
+
monkeypatch.setattr(mod, "DEVELOPER_ID", "alice")
|
|
88
|
+
monkeypatch.setattr(sys, "stdin", type("F", (), {"isatty": lambda s: True})())
|
|
89
|
+
|
|
90
|
+
import builtins
|
|
91
|
+
|
|
92
|
+
monkeypatch.setattr(builtins, "input", lambda p: "2")
|
|
93
|
+
|
|
94
|
+
touched_db = []
|
|
95
|
+
|
|
96
|
+
class FakeTable:
|
|
97
|
+
def update(self, *args):
|
|
98
|
+
touched_db.append(True)
|
|
99
|
+
return self
|
|
100
|
+
|
|
101
|
+
def delete(self):
|
|
102
|
+
touched_db.append(True)
|
|
103
|
+
return self
|
|
104
|
+
|
|
105
|
+
def eq(self, *args):
|
|
106
|
+
return self
|
|
107
|
+
|
|
108
|
+
def execute(self):
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
class FakeClient:
|
|
112
|
+
def table(self, name):
|
|
113
|
+
return FakeTable()
|
|
114
|
+
|
|
115
|
+
client = FakeClient()
|
|
116
|
+
mod._local_owned_locks.clear()
|
|
117
|
+
|
|
118
|
+
mod._handle_multi_session_lock(client, "src/multi.py", "old-token")
|
|
119
|
+
|
|
120
|
+
assert not touched_db
|
|
121
|
+
assert "src/multi.py" not in mod._local_owned_locks
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def test_handle_post_restart_conflict_interactive_abort_choice_4(monkeypatch):
|
|
125
|
+
"""Test interactive post-restart conflict aborts on choice 4."""
|
|
126
|
+
mod = load_watcher_module()
|
|
127
|
+
monkeypatch.setattr(mod, "DEVELOPER_ID", "alice")
|
|
128
|
+
monkeypatch.setattr(sys, "stdin", type("F", (), {"isatty": lambda s: True})())
|
|
129
|
+
|
|
130
|
+
inputs = iter(["4"])
|
|
131
|
+
import builtins
|
|
132
|
+
|
|
133
|
+
monkeypatch.setattr(builtins, "input", lambda p: next(inputs))
|
|
134
|
+
|
|
135
|
+
exit_called = []
|
|
136
|
+
|
|
137
|
+
def mock_exit(code):
|
|
138
|
+
exit_called.append(code)
|
|
139
|
+
raise SystemExit(code)
|
|
140
|
+
|
|
141
|
+
monkeypatch.setattr(sys, "exit", mock_exit)
|
|
142
|
+
|
|
143
|
+
shutdown_called = []
|
|
144
|
+
monkeypatch.setattr(mod, "_graceful_shutdown", lambda: shutdown_called.append(True))
|
|
145
|
+
monkeypatch.setattr(mod, "_notify", lambda t, m: None)
|
|
146
|
+
|
|
147
|
+
mod._active_conflicts.clear()
|
|
148
|
+
|
|
149
|
+
with pytest.raises(SystemExit):
|
|
150
|
+
mod._handle_post_restart_conflict(None, "src/conflict.py", {"owner": "bob"})
|
|
151
|
+
|
|
152
|
+
assert shutdown_called
|
|
153
|
+
assert exit_called == [1]
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def test_handle_post_restart_conflict_interactive_show_diff_then_continue(monkeypatch):
|
|
157
|
+
"""Choice 2 shows git diff, then choice 1 continues and tracks conflict."""
|
|
158
|
+
mod = load_watcher_module()
|
|
159
|
+
monkeypatch.setattr(mod, "DEVELOPER_ID", "alice")
|
|
160
|
+
monkeypatch.setattr(sys, "stdin", type("F", (), {"isatty": lambda s: True})())
|
|
161
|
+
|
|
162
|
+
# First select "show diff", then continue
|
|
163
|
+
inputs = iter(["2", "1"])
|
|
164
|
+
import builtins
|
|
165
|
+
|
|
166
|
+
monkeypatch.setattr(builtins, "input", lambda p: next(inputs))
|
|
167
|
+
monkeypatch.setattr(
|
|
168
|
+
mod.subprocess,
|
|
169
|
+
"check_output",
|
|
170
|
+
lambda *a, **k: b"diff --git a/src/conflict.py b/src/conflict.py\n",
|
|
171
|
+
)
|
|
172
|
+
monkeypatch.setattr(mod, "_notify", lambda t, m: None)
|
|
173
|
+
|
|
174
|
+
mod._active_conflicts.clear()
|
|
175
|
+
mod._handle_post_restart_conflict(
|
|
176
|
+
None,
|
|
177
|
+
"src/conflict.py",
|
|
178
|
+
{"owner": "bob", "branch": "main", "reason": "test"},
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
assert "src/conflict.py" in mod._active_conflicts
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def test_handle_post_restart_conflict_interactive_diff_failure_then_continue(
|
|
185
|
+
monkeypatch,
|
|
186
|
+
):
|
|
187
|
+
"""Choice 2 handles git diff failure and still allows continue."""
|
|
188
|
+
mod = load_watcher_module()
|
|
189
|
+
monkeypatch.setattr(mod, "DEVELOPER_ID", "alice")
|
|
190
|
+
monkeypatch.setattr(sys, "stdin", type("F", (), {"isatty": lambda s: True})())
|
|
191
|
+
|
|
192
|
+
# First select "show diff" (fails), then continue
|
|
193
|
+
inputs = iter(["2", "1"])
|
|
194
|
+
import builtins
|
|
195
|
+
|
|
196
|
+
monkeypatch.setattr(builtins, "input", lambda p: next(inputs))
|
|
197
|
+
|
|
198
|
+
def _boom(*args, **kwargs):
|
|
199
|
+
raise RuntimeError("git diff unavailable")
|
|
200
|
+
|
|
201
|
+
monkeypatch.setattr(mod.subprocess, "check_output", _boom)
|
|
202
|
+
monkeypatch.setattr(mod, "_notify", lambda t, m: None)
|
|
203
|
+
|
|
204
|
+
mod._active_conflicts.clear()
|
|
205
|
+
mod._handle_post_restart_conflict(None, "src/conflict.py", {"owner": "bob"})
|
|
206
|
+
|
|
207
|
+
assert "src/conflict.py" in mod._active_conflicts
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def test_handle_post_restart_conflict_tty_input_eof_defaults_continue(monkeypatch):
|
|
211
|
+
"""EOF/interrupt in prompt defaults to choice 1 (continue)."""
|
|
212
|
+
mod = load_watcher_module()
|
|
213
|
+
monkeypatch.setattr(sys, "stdin", type("F", (), {"isatty": lambda s: True})())
|
|
214
|
+
|
|
215
|
+
import builtins
|
|
216
|
+
|
|
217
|
+
def _raise_eof(prompt):
|
|
218
|
+
raise EOFError()
|
|
219
|
+
|
|
220
|
+
monkeypatch.setattr(builtins, "input", _raise_eof)
|
|
221
|
+
monkeypatch.setattr(mod, "_notify", lambda t, m: None)
|
|
222
|
+
|
|
223
|
+
mod._active_conflicts.clear()
|
|
224
|
+
mod._handle_post_restart_conflict(None, "src/eof_conflict.py", {"owner": "bob"})
|
|
225
|
+
|
|
226
|
+
assert "src/eof_conflict.py" in mod._active_conflicts
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def test_handle_multi_session_interactive_eof_defaults_leave(monkeypatch):
|
|
230
|
+
"""EOF in interactive prompt should fall back to leave-lock option."""
|
|
231
|
+
mod = load_watcher_module()
|
|
232
|
+
monkeypatch.setattr(mod, "DEVELOPER_ID", "alice")
|
|
233
|
+
monkeypatch.setattr(sys, "stdin", type("F", (), {"isatty": lambda s: True})())
|
|
234
|
+
|
|
235
|
+
import builtins
|
|
236
|
+
|
|
237
|
+
def _raise_eof(prompt):
|
|
238
|
+
raise EOFError()
|
|
239
|
+
|
|
240
|
+
monkeypatch.setattr(builtins, "input", _raise_eof)
|
|
241
|
+
touched = []
|
|
242
|
+
|
|
243
|
+
class FakeTable:
|
|
244
|
+
def update(self, *args):
|
|
245
|
+
touched.append("update")
|
|
246
|
+
return self
|
|
247
|
+
|
|
248
|
+
def delete(self):
|
|
249
|
+
touched.append("delete")
|
|
250
|
+
return self
|
|
251
|
+
|
|
252
|
+
def eq(self, *args):
|
|
253
|
+
return self
|
|
254
|
+
|
|
255
|
+
def execute(self):
|
|
256
|
+
return None
|
|
257
|
+
|
|
258
|
+
class FakeClient:
|
|
259
|
+
def table(self, name):
|
|
260
|
+
return FakeTable()
|
|
261
|
+
|
|
262
|
+
mod._handle_multi_session_lock(FakeClient(), "src/multi.py", "old-token")
|
|
263
|
+
assert touched == []
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def test_handle_multi_session_choice1_update_exception(monkeypatch):
|
|
267
|
+
"""Update failure on choice 1 should be caught and still re-adopt locally."""
|
|
268
|
+
mod = load_watcher_module()
|
|
269
|
+
monkeypatch.setattr(mod, "DEVELOPER_ID", "alice")
|
|
270
|
+
monkeypatch.setattr(sys, "stdin", type("F", (), {"isatty": lambda s: True})())
|
|
271
|
+
|
|
272
|
+
import builtins
|
|
273
|
+
|
|
274
|
+
monkeypatch.setattr(builtins, "input", lambda p: "1")
|
|
275
|
+
mod._local_owned_locks.clear()
|
|
276
|
+
|
|
277
|
+
class FakeTable:
|
|
278
|
+
def update(self, *args):
|
|
279
|
+
return self
|
|
280
|
+
|
|
281
|
+
def eq(self, *args):
|
|
282
|
+
return self
|
|
283
|
+
|
|
284
|
+
def execute(self):
|
|
285
|
+
raise RuntimeError("db down")
|
|
286
|
+
|
|
287
|
+
class FakeClient:
|
|
288
|
+
def table(self, name):
|
|
289
|
+
return FakeTable()
|
|
290
|
+
|
|
291
|
+
mod._handle_multi_session_lock(FakeClient(), "src/err_update.py", "old-token")
|
|
292
|
+
assert "src/err_update.py" in mod._local_owned_locks
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def test_handle_multi_session_choice3_delete_exception(monkeypatch):
|
|
296
|
+
"""Delete failure on choice 3 should be caught without crashing."""
|
|
297
|
+
mod = load_watcher_module()
|
|
298
|
+
monkeypatch.setattr(mod, "DEVELOPER_ID", "alice")
|
|
299
|
+
monkeypatch.setattr(sys, "stdin", type("F", (), {"isatty": lambda s: True})())
|
|
300
|
+
|
|
301
|
+
import builtins
|
|
302
|
+
|
|
303
|
+
monkeypatch.setattr(builtins, "input", lambda p: "3")
|
|
304
|
+
|
|
305
|
+
class FakeTable:
|
|
306
|
+
def delete(self):
|
|
307
|
+
return self
|
|
308
|
+
|
|
309
|
+
def eq(self, *args):
|
|
310
|
+
return self
|
|
311
|
+
|
|
312
|
+
def execute(self):
|
|
313
|
+
raise RuntimeError("db down")
|
|
314
|
+
|
|
315
|
+
class FakeClient:
|
|
316
|
+
def table(self, name):
|
|
317
|
+
return FakeTable()
|
|
318
|
+
|
|
319
|
+
# no raise expected
|
|
320
|
+
mod._handle_multi_session_lock(FakeClient(), "src/err_delete.py", "old-token")
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Notification-related tests for live_locks_watcher."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ._helpers import load_watcher_module
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_notify_uses_desktop_notify_if_available(monkeypatch, caplog):
|
|
9
|
+
mod = load_watcher_module()
|
|
10
|
+
called = {}
|
|
11
|
+
|
|
12
|
+
class FakeDesktop:
|
|
13
|
+
def notify(self, title=None, message=None, app_name=None, timeout=None):
|
|
14
|
+
called["title"] = title
|
|
15
|
+
called["msg"] = message
|
|
16
|
+
|
|
17
|
+
monkeypatch.setattr(mod, "desktop_notify", FakeDesktop())
|
|
18
|
+
monkeypatch.setenv("COLLAB_TEST_MODE", "0")
|
|
19
|
+
mod._notify("T", "M")
|
|
20
|
+
assert called.get("title") == "T"
|
|
21
|
+
assert "M" in called.get("msg")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_notify_with_title_and_message(monkeypatch):
|
|
25
|
+
mod = load_watcher_module()
|
|
26
|
+
notify_called = []
|
|
27
|
+
|
|
28
|
+
def mock_notify(title, message, app_name=None):
|
|
29
|
+
notify_called.append((title, message, app_name))
|
|
30
|
+
|
|
31
|
+
monkeypatch.setattr(mod, "_desktop_notify", mock_notify, raising=False)
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
mod._notify("Test Title", "Test Message")
|
|
35
|
+
except Exception:
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
if notify_called:
|
|
39
|
+
assert notify_called[0][0] == "Test Title"
|
|
40
|
+
assert notify_called[0][1] == "Test Message"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_notify_fallback_no_desktop_notify(monkeypatch, caplog):
|
|
44
|
+
mod = load_watcher_module()
|
|
45
|
+
monkeypatch.setattr(mod, "desktop_notify", None)
|
|
46
|
+
import logging
|
|
47
|
+
|
|
48
|
+
with caplog.at_level(logging.INFO):
|
|
49
|
+
monkeypatch.setenv("COLLAB_TEST_MODE", "0")
|
|
50
|
+
mod._notify("Test Title", "Test Message")
|
|
51
|
+
assert "Test Title" in caplog.text or "Test Message" in caplog.text
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_notify_desktop_notify_exception(monkeypatch, caplog):
|
|
55
|
+
mod = load_watcher_module()
|
|
56
|
+
|
|
57
|
+
class FailingNotify:
|
|
58
|
+
def notify(self, **kwargs):
|
|
59
|
+
raise RuntimeError("notify failed")
|
|
60
|
+
|
|
61
|
+
monkeypatch.setattr(mod, "desktop_notify", FailingNotify())
|
|
62
|
+
import logging
|
|
63
|
+
|
|
64
|
+
with caplog.at_level(logging.INFO):
|
|
65
|
+
monkeypatch.setenv("COLLAB_TEST_MODE", "0")
|
|
66
|
+
mod._notify("Fail Title", "Fail Message")
|
|
67
|
+
assert "Fail Title" in caplog.text or "Fail Message" in caplog.text
|