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,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,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()
|