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,43 @@
1
+ """Unit tests for package imports and basic module availability.
2
+
3
+ Tests verify that all critical modules can be imported and instantiated without errors.
4
+ This is a basic sanity check for package structure.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+
10
+ def test_lock_client_import() -> None:
11
+ """Verify LockClient module can be imported."""
12
+ from src.lock_client import LockClient # noqa: F401
13
+
14
+
15
+ def test_main_module_import() -> None:
16
+ """Verify main module can be imported."""
17
+ from src import main # noqa: F401
18
+
19
+
20
+ def test_live_locks_watcher_import() -> None:
21
+ """Verify live_locks_watcher module can be imported."""
22
+ from src import live_locks_watcher # noqa: F401
23
+
24
+
25
+ def test_logging_config_import() -> None:
26
+ """Verify logging_config module can be imported."""
27
+ from src import logging_config # noqa: F401
28
+
29
+
30
+ def test_lock_client_instantiation() -> None:
31
+ """Verify LockClient can be instantiated in test mode."""
32
+ from src.lock_client import LockClient
33
+
34
+ # Should not crash; connection errors are acceptable
35
+ try:
36
+ client = LockClient(local_only=True)
37
+ assert client is not None
38
+ except Exception as exc:
39
+ # Connection errors expected in CI; import/instantiation is verified
40
+ assert any(
41
+ word in str(exc).lower()
42
+ for word in ["connect", "supabase", "timeout", "network"]
43
+ )
File without changes
@@ -0,0 +1,220 @@
1
+ """Integration tests for CLI command contract parity — Phase 1 exit criteria.
2
+
3
+ These tests verify that the collab package's CLI command surface behaves
4
+ consistently across all invocation patterns and matches expected output formats.
5
+
6
+ Tests validate:
7
+ - Command availability and exit codes
8
+ - CLI help and documentation
9
+ - Backward compatibility invocation patterns
10
+ - Safe command behavior in test environments
11
+
12
+ No behavior deltas are acceptable for Phase 1 exit criteria.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import os
18
+ import subprocess
19
+ import sys
20
+
21
+ import pytest
22
+
23
+
24
+ def run_collab_cli(*args: str, expect_success: bool = True) -> tuple[int, str, str]:
25
+ """Execute collab CLI via python -m collab.__main__ and capture results.
26
+
27
+ IMPORTANT: Runs in a fully isolated environment so destructive commands
28
+ (release-all, cleanup, etc.) never touch the real Supabase database.
29
+
30
+ Args:
31
+ *args: Command arguments (e.g., "active", "status", "file.py")
32
+ expect_success: If True, assert exit code is 0
33
+
34
+ Returns:
35
+ Tuple of (exit_code, stdout, stderr)
36
+ """
37
+ cmd = [sys.executable, "-m", "collab.__main__"] + list(args)
38
+
39
+ # Build an isolated env that prevents the subprocess from hitting
40
+ # real Supabase even if it re-loads .env (python-dotenv's load_dotenv
41
+ # does NOT override pre-existing env vars by default).
42
+ isolated_env = os.environ.copy()
43
+ isolated_env.update(
44
+ {
45
+ "COLLAB_TEST_MODE": "1",
46
+ "SUPABASE_URL": "http://localhost:54321",
47
+ "SUPABASE_ANON_KEY": "test-anon-key-integration",
48
+ "COLLAB_SILENT_DAEMON": "1",
49
+ "COLLAB_AUTO_START_WATCHER": "0",
50
+ }
51
+ )
52
+
53
+ result = subprocess.run(
54
+ cmd,
55
+ capture_output=True,
56
+ text=True,
57
+ encoding="utf-8",
58
+ errors="replace",
59
+ cwd=".",
60
+ env=isolated_env,
61
+ )
62
+
63
+ if expect_success and result.returncode != 0:
64
+ pytest.fail(
65
+ f"Command failed unexpectedly: {' '.join(cmd)}\n"
66
+ f"Exit code: {result.returncode}\n"
67
+ f"stdout: {result.stdout}\n"
68
+ f"stderr: {result.stderr}"
69
+ )
70
+
71
+ return result.returncode, result.stdout, result.stderr
72
+
73
+
74
+ class TestCLICommandAvailability:
75
+ """Verify all golden commands are available and respond correctly."""
76
+
77
+ def test_help_command_succeeds(self) -> None:
78
+ """Verify --help produces usage information."""
79
+ exit_code, stdout, _ = run_collab_cli("--help")
80
+ assert exit_code == 0
81
+ assert "usage" in stdout.lower() or "collab" in stdout.lower()
82
+
83
+ def test_active_command_available(self) -> None:
84
+ """Verify 'active' command is available and returns valid output."""
85
+ exit_code, stdout, _ = run_collab_cli("active")
86
+ assert exit_code == 0
87
+ # Output should indicate lock status (even if empty)
88
+ assert len(stdout) > 0
89
+
90
+ def test_status_command_accepts_file_argument(self) -> None:
91
+ """Verify 'status' command accepts file path argument."""
92
+ exit_code, stdout, _ = run_collab_cli("status", "example.py")
93
+ assert exit_code == 0
94
+ assert len(stdout) > 0
95
+
96
+ def test_history_command_available(self) -> None:
97
+ """Verify 'history' command returns valid output."""
98
+ exit_code, stdout, _ = run_collab_cli("history")
99
+ assert exit_code == 0
100
+ assert len(stdout) > 0
101
+
102
+ def test_daemon_status_command_available(self) -> None:
103
+ """Verify 'daemon-status' command is available."""
104
+ exit_code, _, _ = run_collab_cli(
105
+ "daemon-status",
106
+ expect_success=False,
107
+ )
108
+ # Exit code can be 0 (running) or 1 (not running); both are valid
109
+ assert exit_code in (0, 1)
110
+
111
+ def test_cleanup_command_available(self) -> None:
112
+ """Verify 'cleanup' command is available and safe."""
113
+ exit_code, _, _ = run_collab_cli(
114
+ "cleanup",
115
+ expect_success=False,
116
+ )
117
+ # Cleanup should not crash even if no processes exist
118
+ assert exit_code in (0, 1)
119
+
120
+ def test_help_documents_golden_commands(self) -> None:
121
+ """Verify help output documents all primary commands."""
122
+ _, help_output, _ = run_collab_cli("--help")
123
+
124
+ expected_commands = [
125
+ "acquire",
126
+ "release",
127
+ "active",
128
+ "status",
129
+ "daemon-start",
130
+ "daemon-stop",
131
+ "daemon-status",
132
+ "history",
133
+ "reconcile",
134
+ ]
135
+
136
+ for cmd in expected_commands:
137
+ assert cmd in help_output, f"Command '{cmd}' not found in help output"
138
+
139
+
140
+ class TestBackwardCompatibilityInvocation:
141
+ """Verify all documented invocation patterns work identically."""
142
+
143
+ def test_python_m_collab_main_entrypoint(self) -> None:
144
+ """Verify 'python -m collab.__main__' invocation works."""
145
+ exit_code, stdout, _ = run_collab_cli("--help")
146
+ assert exit_code == 0
147
+ assert len(stdout) > 0
148
+
149
+ def test_run_py_entrypoint(self) -> None:
150
+ """Verify 'python run.py' backward compatibility entrypoint."""
151
+ result = subprocess.run(
152
+ [sys.executable, "run.py", "--help"],
153
+ capture_output=True,
154
+ text=True,
155
+ cwd=".",
156
+ )
157
+ assert result.returncode == 0
158
+ assert len(result.stdout) > 0
159
+
160
+ def test_collab_console_script_exists(self) -> None:
161
+ """Verify 'collab' console script is registered in package."""
162
+ # Check pyproject.toml registration
163
+ with open("pyproject.toml") as f:
164
+ content = f.read()
165
+ assert 'collab = "src.lock_client:main"' in content
166
+
167
+
168
+ class TestCLICommandVariants:
169
+ """Verify specific command patterns work as expected."""
170
+
171
+ @pytest.mark.parametrize(
172
+ "command,expected_exit_success",
173
+ [
174
+ (["active"], True),
175
+ (["status", "dummy_file.py"], True),
176
+ (["history", "--limit", "5"], True),
177
+ (["release-all"], True),
178
+ (["cleanup"], True),
179
+ ],
180
+ )
181
+ def test_command_execution_patterns(
182
+ self, command: list[str], expected_exit_success: bool
183
+ ) -> None:
184
+ """Verify command execution patterns return expected exit codes."""
185
+ exit_code, _, _ = run_collab_cli(
186
+ *command,
187
+ expect_success=expected_exit_success,
188
+ )
189
+
190
+ if expected_exit_success:
191
+ assert exit_code == 0
192
+ else:
193
+ # Should exit cleanly even if it fails
194
+ assert exit_code in (0, 1, 2)
195
+
196
+ def test_invalid_command_fails_gracefully(self) -> None:
197
+ """Verify invalid commands fail with meaningful exit codes."""
198
+ exit_code, _, stderr = run_collab_cli(
199
+ "nonexistent-command",
200
+ expect_success=False,
201
+ )
202
+ # Should fail (non-zero exit) with error output
203
+ assert exit_code != 0
204
+ assert len(stderr) > 0 or len(stderr) == 0 # Either way is OK
205
+
206
+
207
+ class TestCLIDashboardAssetIntegrity:
208
+ """Verify dashboard assets are properly bundled in package."""
209
+
210
+ def test_dashboard_index_html_exists(self) -> None:
211
+ """Verify dashboard HTML asset is present."""
212
+ dashboard_path = os.path.join("src", "dashboard", "index.html")
213
+ assert os.path.exists(
214
+ dashboard_path
215
+ ), f"Dashboard not found at {dashboard_path}"
216
+
217
+ def test_dashboard_command_available(self) -> None:
218
+ """Verify dashboard command is registered."""
219
+ _, help_output, _ = run_collab_cli("--help")
220
+ assert "dashboard" in help_output.lower()
File without changes
File without changes
File without changes
@@ -0,0 +1,5 @@
1
+ """Package marker for live_locks_watcher unit tests.
2
+
3
+ This allows tests in this directory to use relative imports like `from ._helpers import
4
+ load_watcher_module`.
5
+ """
@@ -0,0 +1,123 @@
1
+ """Helpers for live_locks_watcher tests.
2
+
3
+ Provide a stable loader that imports the watcher module from `src` and ensures optional
4
+ imports are mocked so tests don't fail at import-time when running in CI without
5
+ optional deps.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import importlib.util
11
+ import os
12
+ import shutil
13
+ import sys
14
+ import tempfile
15
+ from pathlib import Path
16
+ from unittest import mock
17
+
18
+ # Mock optional external modules that the watcher may import at module load
19
+ # time to avoid ImportError / SystemExit during tests.
20
+ sys.modules.setdefault("supabase", mock.MagicMock())
21
+ sys.modules.setdefault("plyer", mock.MagicMock())
22
+
23
+
24
+ def _find_repo_root() -> Path:
25
+ here = Path(__file__).resolve()
26
+ for parent in here.parents:
27
+ if (parent / "pyproject.toml").exists() and (parent / "src").exists():
28
+ return parent
29
+ raise FileNotFoundError("Could not locate repository root")
30
+
31
+
32
+ def load_watcher_module():
33
+ proj_root = _find_repo_root()
34
+ module_path = proj_root / "src" / "live_locks_watcher.py"
35
+ mod_name = "src.live_locks_watcher"
36
+ # If already loaded, return the cached module but reset volatile
37
+ # module-level state so tests execute with predictable defaults.
38
+ # Many tests expect the same module *instance* (a `watcher` variable
39
+ # created at import-time), while also needing fresh global state
40
+ # between test invocations. Reset known mutable globals here.
41
+ if mod_name in sys.modules:
42
+ mod = sys.modules[mod_name]
43
+ for _attr, _val in (
44
+ ("_shutdown_done", False),
45
+ ("_local_owned_locks", set()),
46
+ ("_active_conflicts", set()),
47
+ ("_warned_remote_locks", set()),
48
+ ("_known_remote_locks", set()),
49
+ ("SESSION_TOKEN", ""),
50
+ ("_dashboard_url", None),
51
+ ("_dashboard_server", None),
52
+ ("_dashboard_thread", None),
53
+ ):
54
+ try:
55
+ setattr(mod, _attr, _val)
56
+ except Exception:
57
+ pass
58
+ # Use a test-local PID file to avoid colliding with any real watcher
59
+ # running on the developer machine or CI environment.
60
+ try:
61
+ tmp_pid_name = f"pytest_collab_{os.getpid()}.daemon.pid"
62
+ tmp_pid = os.path.join(tempfile.gettempdir(), tmp_pid_name)
63
+ setattr(mod, "PID_FILE", tmp_pid)
64
+ except Exception:
65
+ pass
66
+ # Use a test-local isolated .collab root to avoid touching repo files.
67
+ try:
68
+ tmp_collab_dir = os.path.join(
69
+ tempfile.gettempdir(), f"pytest_collab_{os.getpid()}_collab"
70
+ )
71
+ os.makedirs(tmp_collab_dir, exist_ok=True)
72
+ # Mirror the real dashboard template into the test collab root
73
+ try:
74
+ repo_dashboard = Path(proj_root) / "src" / "dashboard"
75
+ if repo_dashboard.exists():
76
+ shutil.copytree(
77
+ str(repo_dashboard),
78
+ os.path.join(tmp_collab_dir, "dashboard"),
79
+ dirs_exist_ok=True,
80
+ )
81
+ except Exception:
82
+ pass
83
+ setattr(mod, "_COLLAB_ROOT", tmp_collab_dir)
84
+ except Exception:
85
+ pass
86
+ return mod
87
+
88
+ spec = importlib.util.spec_from_file_location(mod_name, str(module_path))
89
+ assert spec and spec.loader
90
+ mod = importlib.util.module_from_spec(spec)
91
+ # Insert into sys.modules early so recursive imports / monkeypatching
92
+ # that reference the module name get the same instance.
93
+ sys.modules[mod_name] = mod
94
+ spec.loader.exec_module(mod) # type: ignore[attr-defined]
95
+ # Also set a test-local PID file by default to avoid false positives when
96
+ # detecting existing watcher processes on the developer machine.
97
+ try:
98
+ tmp_pid_name = f"pytest_collab_{os.getpid()}.daemon.pid"
99
+ tmp_pid = os.path.join(tempfile.gettempdir(), tmp_pid_name)
100
+ setattr(mod, "PID_FILE", tmp_pid)
101
+ except Exception:
102
+ pass
103
+ # Use a test-local isolated .collab root so tests don't modify repo files.
104
+ try:
105
+ tmp_collab_dir = os.path.join(
106
+ tempfile.gettempdir(), f"pytest_collab_{os.getpid()}_collab"
107
+ )
108
+ os.makedirs(tmp_collab_dir, exist_ok=True)
109
+ # Mirror the real dashboard template into the test collab root
110
+ try:
111
+ repo_dashboard = Path(proj_root) / "src" / "dashboard"
112
+ if repo_dashboard.exists():
113
+ shutil.copytree(
114
+ str(repo_dashboard),
115
+ os.path.join(tmp_collab_dir, "dashboard"),
116
+ dirs_exist_ok=True,
117
+ )
118
+ except Exception:
119
+ pass
120
+ setattr(mod, "_COLLAB_ROOT", tmp_collab_dir)
121
+ except Exception:
122
+ pass
123
+ return mod
@@ -0,0 +1,18 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+
5
+ import pytest
6
+
7
+ from ._helpers import load_watcher_module
8
+
9
+
10
+ @pytest.fixture(autouse=True)
11
+ def isolate_collab(monkeypatch, tmp_path):
12
+ """Autouse fixture: ensure the watcher module uses a test-local `.collab` root and
13
+ PID file so tests cannot accidentally modify repository files."""
14
+ mod = load_watcher_module()
15
+ monkeypatch.setattr(mod, "_COLLAB_ROOT", str(tmp_path))
16
+ pid = tmp_path / f"pytest_collab_{os.getpid()}.daemon.pid"
17
+ monkeypatch.setattr(mod, "PID_FILE", str(pid))
18
+ yield
@@ -0,0 +1,188 @@
1
+ """Dashboard and server-start tests for live_locks_watcher."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from ._helpers import load_watcher_module
6
+
7
+
8
+ def test_start_dashboard_server_returns_url(monkeypatch):
9
+ mod = load_watcher_module()
10
+ import http.server
11
+
12
+ monkeypatch.setattr(mod, "SUPABASE_URL", "https://test.supabase.co")
13
+ monkeypatch.setattr(mod, "SUPABASE_ANON_KEY", "test_key")
14
+ monkeypatch.setattr(mod, "SUPABASE_SERVICE_ROLE_KEY", None)
15
+ monkeypatch.setattr(mod, "DEVELOPER_ID", "alice")
16
+
17
+ class FakeServer:
18
+ def __init__(self, addr, handler):
19
+ self.server_address = (addr[0], 9999)
20
+
21
+ def serve_forever(self):
22
+ pass
23
+
24
+ def shutdown(self):
25
+ pass
26
+
27
+ monkeypatch.setattr(http.server, "ThreadingHTTPServer", FakeServer)
28
+
29
+ url = mod._start_dashboard_server()
30
+ assert url is not None
31
+ assert url.startswith("http://127.0.0.1:9999/")
32
+ assert url.endswith(".html")
33
+
34
+
35
+ def test_start_dashboard_server_missing_html(monkeypatch):
36
+ mod = load_watcher_module()
37
+ monkeypatch.setattr(mod, "_RESOURCE_ROOT", "/nonexistent/path")
38
+ result = mod._start_dashboard_server()
39
+ assert result is None
40
+
41
+
42
+ def test_start_dashboard_server_read_error(monkeypatch, tmp_path):
43
+ mod = load_watcher_module()
44
+ dashboard_dir = tmp_path / "dashboard"
45
+ dashboard_dir.mkdir()
46
+ html_file = dashboard_dir / "index.html"
47
+ html_file.write_text("test")
48
+ monkeypatch.setattr(mod, "_RESOURCE_ROOT", str(tmp_path))
49
+
50
+ original_open = open
51
+
52
+ def failing_open(path, *args, **kwargs):
53
+ if "index.html" in str(path):
54
+ raise PermissionError("denied")
55
+ return original_open(path, *args, **kwargs)
56
+
57
+ monkeypatch.setattr("builtins.open", failing_open)
58
+ result = mod._start_dashboard_server()
59
+ assert result is None
60
+
61
+
62
+ def test_start_dashboard_server_http_server_error(monkeypatch, tmp_path):
63
+ mod = load_watcher_module()
64
+ import http.server
65
+
66
+ dashboard_dir = tmp_path / "dashboard"
67
+ dashboard_dir.mkdir()
68
+ (dashboard_dir / "index.html").write_text("<html></html>")
69
+ monkeypatch.setattr(mod, "_RESOURCE_ROOT", str(tmp_path))
70
+ monkeypatch.setattr(mod, "SUPABASE_URL", "https://test.supabase.co")
71
+ monkeypatch.setattr(mod, "SUPABASE_ANON_KEY", "key")
72
+ monkeypatch.setattr(mod, "DEVELOPER_ID", "alice")
73
+
74
+ def raising_server(*args, **kwargs):
75
+ raise OSError("Address already in use")
76
+
77
+ monkeypatch.setattr(http.server, "ThreadingHTTPServer", raising_server)
78
+
79
+ result = mod._start_dashboard_server()
80
+ assert result is None
81
+
82
+
83
+ def test_start_dashboard_server_migrated(tmp_path, monkeypatch):
84
+ mod = load_watcher_module()
85
+ # Prepare dashboard directory
86
+ d = tmp_path / "dashboard"
87
+ d.mkdir(parents=True, exist_ok=True)
88
+ (d / "index.html").write_text("<html>ok</html>")
89
+ monkeypatch.setattr(mod, "_RESOURCE_ROOT", str(tmp_path))
90
+
91
+ # Monkeypatch the ThreadingHTTPServer to avoid binding sockets
92
+ import http.server as _http
93
+ import threading as _threading
94
+
95
+ class FakeServer:
96
+ def __init__(self, addr, handler):
97
+ self.server_address = ("127.0.0.1", 11111)
98
+
99
+ def serve_forever(self):
100
+ return None
101
+
102
+ def shutdown(self):
103
+ return None
104
+
105
+ class FakeThread:
106
+ def __init__(self, target, daemon=True):
107
+ self._target = target
108
+
109
+ def start(self):
110
+ return None
111
+
112
+ monkeypatch.setattr(_http, "ThreadingHTTPServer", FakeServer)
113
+ monkeypatch.setattr(_threading, "Thread", FakeThread)
114
+
115
+ url = mod._start_dashboard_server()
116
+ assert url is not None
117
+
118
+
119
+ def test_start_dashboard_server_tmpfile_error(monkeypatch, tmp_path):
120
+ mod = load_watcher_module()
121
+ import tempfile
122
+
123
+ dashboard_dir = tmp_path / "dashboard"
124
+ dashboard_dir.mkdir()
125
+ (dashboard_dir / "index.html").write_text("<html></html>")
126
+ monkeypatch.setattr(mod, "_RESOURCE_ROOT", str(tmp_path))
127
+ monkeypatch.setattr(mod, "SUPABASE_URL", "https://test.supabase.co")
128
+ monkeypatch.setattr(mod, "SUPABASE_ANON_KEY", "key")
129
+ monkeypatch.setattr(mod, "DEVELOPER_ID", "alice")
130
+
131
+ def failing_tmpfile(**kwargs):
132
+ raise OSError("disk full")
133
+
134
+ monkeypatch.setattr(tempfile, "NamedTemporaryFile", failing_tmpfile)
135
+
136
+ result = mod._start_dashboard_server()
137
+ assert result is None
138
+
139
+
140
+ def test_start_dashboard_server_unlink_error(monkeypatch, tmp_path):
141
+ mod = load_watcher_module()
142
+ import http.server
143
+
144
+ dashboard_dir = tmp_path / "dashboard"
145
+ dashboard_dir.mkdir()
146
+ (dashboard_dir / "index.html").write_text("<html></html>")
147
+ monkeypatch.setattr(mod, "_RESOURCE_ROOT", str(tmp_path))
148
+ monkeypatch.setattr(mod, "SUPABASE_URL", "https://test.supabase.co")
149
+ monkeypatch.setattr(mod, "SUPABASE_ANON_KEY", "key")
150
+ monkeypatch.setattr(mod, "DEVELOPER_ID", "alice")
151
+
152
+ def raising_server(*args, **kwargs):
153
+ raise OSError("port in use")
154
+
155
+ monkeypatch.setattr(http.server, "ThreadingHTTPServer", raising_server)
156
+
157
+ original_unlink = __import__("os").unlink
158
+
159
+ def failing_unlink(path):
160
+ if str(path).endswith(".html"):
161
+ raise OSError("cannot unlink")
162
+ return original_unlink(path)
163
+
164
+ monkeypatch.setattr(__import__("os"), "unlink", failing_unlink)
165
+
166
+ result = mod._start_dashboard_server()
167
+ assert result is None
168
+
169
+
170
+ # ---- Auto-migrated from migrated_remaining ----
171
+
172
+
173
+ def test_start_dashboard_server_missing_and_success_moved(tmp_path, monkeypatch):
174
+ mod = load_watcher_module()
175
+ tmp_root = tmp_path / "collab_root"
176
+ (tmp_root / "dashboard").mkdir(parents=True)
177
+ # missing file
178
+ monkeypatch.setattr(mod, "_RESOURCE_ROOT", str(tmp_root))
179
+ url = mod._start_dashboard_server()
180
+ assert url is None
181
+
182
+ # create file and succeed
183
+ (tmp_root / "dashboard" / "index.html").write_text("<html>ok</html>")
184
+ url2 = mod._start_dashboard_server()
185
+ assert url2 and url2.startswith("http://127.0.0.1:")
186
+
187
+
188
+ watcher = load_watcher_module()
@@ -0,0 +1,56 @@
1
+ """Developer identity tests for live_locks_watcher."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import subprocess
6
+
7
+ from ._helpers import load_watcher_module
8
+
9
+
10
+ def test_get_developer_id_from_env(monkeypatch):
11
+ mod = load_watcher_module()
12
+ # Force git to fail and ensure environment fallback is used
13
+ monkeypatch.setenv("USERNAME", "test_developer")
14
+
15
+ def mock_check_output(cmd, *a, **k):
16
+ raise subprocess.CalledProcessError(1, cmd)
17
+
18
+ monkeypatch.setattr(subprocess, "check_output", mock_check_output)
19
+
20
+ result = mod._get_developer_id()
21
+ assert result == "test_developer"
22
+
23
+
24
+ def test_get_developer_id_from_git(monkeypatch):
25
+ mod = load_watcher_module()
26
+ monkeypatch.delenv("DEVELOPER_ID", raising=False)
27
+
28
+ def mock_check_output(cmd, *a, **k):
29
+ if "user.name" in cmd:
30
+ return b"git_user\n"
31
+ raise subprocess.CalledProcessError(1, cmd)
32
+
33
+ monkeypatch.setattr(subprocess, "check_output", mock_check_output)
34
+
35
+ result = mod._get_developer_id()
36
+ assert result == "git_user"
37
+
38
+
39
+ # ---- Auto-migrated from migrated_remaining ----
40
+
41
+
42
+ def test_is_ephemeral_dev_empty():
43
+ mod = load_watcher_module()
44
+ # Cover the branch where dev_id is falsy and returns False
45
+ assert mod._is_ephemeral_dev("") is False
46
+
47
+
48
+ def test_is_ephemeral_dev_matches_prefix():
49
+ """_is_ephemeral_dev returns True when dev_id starts with an ephemeral prefix."""
50
+ mod = load_watcher_module()
51
+ assert mod._is_ephemeral_dev("test_dev_42") is True
52
+ assert mod._is_ephemeral_dev("ci-runner") is True
53
+ assert mod._is_ephemeral_dev("regular_user") is False
54
+
55
+
56
+ watcher = load_watcher_module()