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,278 @@
1
+ """Targeted tests for watch command branches in src/main.py."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from contextlib import nullcontext
7
+
8
+
9
+ def test_run_cli_watch_pid_file_sets_namespace(monkeypatch):
10
+ import src.lock_client as lc
11
+ import src.main as main_mod
12
+
13
+ class _Client:
14
+ def __init__(self, local_only=False):
15
+ self.local_only = local_only
16
+
17
+ def watch(self, **kwargs):
18
+ called["kwargs"] = kwargs
19
+
20
+ called = {"kwargs": None}
21
+
22
+ monkeypatch.setattr(
23
+ main_mod,
24
+ "setup_collab_logging",
25
+ lambda **_k: None,
26
+ raising=False,
27
+ )
28
+ monkeypatch.setattr(main_mod, "_quiet_console_loggers", lambda: None, raising=False)
29
+ monkeypatch.setattr(main_mod, "LockClient", _Client, raising=False)
30
+
31
+ # _run_cli lazily imports these symbols from lock_client each invocation.
32
+ monkeypatch.setattr(lc, "LockClient", _Client)
33
+ monkeypatch.setattr(lc, "_quiet_console_loggers", lambda: None)
34
+ monkeypatch.setattr(lc, "_COLLAB_ROOT", ".collab")
35
+
36
+ import src.logging_config as logging_config
37
+
38
+ monkeypatch.setattr(logging_config, "setup_collab_logging", lambda **_k: None)
39
+
40
+ monkeypatch.setattr(
41
+ sys,
42
+ "argv",
43
+ [
44
+ "collab",
45
+ "watch",
46
+ "--pid-file",
47
+ "tmp.daemon.pid",
48
+ "--interval",
49
+ "2",
50
+ "--timeout",
51
+ "1",
52
+ ],
53
+ )
54
+
55
+ main_mod._run_cli()
56
+ assert lc.PID_FILE == "tmp.daemon.pid"
57
+ assert called["kwargs"] is not None
58
+ assert called["kwargs"]["interval"] == 2
59
+
60
+
61
+ def test_run_cli_active_auto_starts_and_reconciles(monkeypatch, capsys):
62
+ import src.lock_client as lc
63
+ import src.main as main_mod
64
+
65
+ called = {"status": 0, "start": 0, "reconcile": 0}
66
+
67
+ class _Client:
68
+ def __init__(self, local_only=False):
69
+ self.local_only = local_only
70
+
71
+ def daemon_status(self):
72
+ called["status"] += 1
73
+ return False
74
+
75
+ def daemon_start(self):
76
+ called["start"] += 1
77
+
78
+ def _reconcile(self):
79
+ called["reconcile"] += 1
80
+
81
+ def active(self):
82
+ return []
83
+
84
+ monkeypatch.setattr(
85
+ main_mod,
86
+ "setup_collab_logging",
87
+ lambda **_k: None,
88
+ raising=False,
89
+ )
90
+ monkeypatch.setattr(
91
+ main_mod,
92
+ "_quiet_console_loggers",
93
+ lambda: nullcontext(),
94
+ raising=False,
95
+ )
96
+ monkeypatch.setattr(main_mod, "LockClient", _Client, raising=False)
97
+
98
+ monkeypatch.setattr(lc, "LockClient", _Client)
99
+ monkeypatch.setattr(lc, "_quiet_console_loggers", lambda: nullcontext())
100
+ monkeypatch.setattr(lc, "_COLLAB_ROOT", ".collab")
101
+
102
+ import src.logging_config as logging_config
103
+
104
+ monkeypatch.setattr(logging_config, "setup_collab_logging", lambda **_k: None)
105
+
106
+ monkeypatch.delenv("PYTEST_CURRENT_TEST", raising=False)
107
+ monkeypatch.setenv("COLLAB_AUTO_START_WATCHER", "1")
108
+ monkeypatch.setattr(sys, "argv", ["collab", "active"])
109
+
110
+ main_mod._run_cli()
111
+ out = capsys.readouterr().out
112
+
113
+ assert called["status"] == 1
114
+ assert called["start"] == 1
115
+ assert called["reconcile"] == 1
116
+ assert "No active locks." in out
117
+
118
+
119
+ def test_run_cli_active_auto_start_can_be_disabled(monkeypatch, capsys):
120
+ import src.lock_client as lc
121
+ import src.main as main_mod
122
+
123
+ called = {"status": 0, "start": 0, "reconcile": 0}
124
+
125
+ class _Client:
126
+ def __init__(self, local_only=False):
127
+ self.local_only = local_only
128
+
129
+ def daemon_status(self):
130
+ called["status"] += 1
131
+ return False
132
+
133
+ def daemon_start(self):
134
+ called["start"] += 1
135
+
136
+ def _reconcile(self):
137
+ called["reconcile"] += 1
138
+
139
+ def active(self):
140
+ return []
141
+
142
+ monkeypatch.setattr(
143
+ main_mod,
144
+ "setup_collab_logging",
145
+ lambda **_k: None,
146
+ raising=False,
147
+ )
148
+ monkeypatch.setattr(
149
+ main_mod,
150
+ "_quiet_console_loggers",
151
+ lambda: nullcontext(),
152
+ raising=False,
153
+ )
154
+ monkeypatch.setattr(main_mod, "LockClient", _Client, raising=False)
155
+
156
+ monkeypatch.setattr(lc, "LockClient", _Client)
157
+ monkeypatch.setattr(lc, "_quiet_console_loggers", lambda: nullcontext())
158
+ monkeypatch.setattr(lc, "_COLLAB_ROOT", ".collab")
159
+
160
+ import src.logging_config as logging_config
161
+
162
+ monkeypatch.setattr(logging_config, "setup_collab_logging", lambda **_k: None)
163
+
164
+ monkeypatch.delenv("PYTEST_CURRENT_TEST", raising=False)
165
+ monkeypatch.setenv("COLLAB_AUTO_START_WATCHER", "0")
166
+ monkeypatch.setattr(sys, "argv", ["collab", "active"])
167
+
168
+ main_mod._run_cli()
169
+ out = capsys.readouterr().out
170
+
171
+ assert called["status"] == 0
172
+ assert called["start"] == 0
173
+ assert called["reconcile"] == 0
174
+ assert "No active locks." in out
175
+
176
+
177
+ def test_is_truthy_env_uses_default_when_missing(monkeypatch):
178
+ import src.main as main_mod
179
+
180
+ monkeypatch.delenv("COLLAB_TEST_BOOL", raising=False)
181
+ assert main_mod._is_truthy_env("COLLAB_TEST_BOOL", default=True) is True
182
+ assert main_mod._is_truthy_env("COLLAB_TEST_BOOL", default=False) is False
183
+
184
+
185
+ def test_ensure_watcher_running_non_target_command(monkeypatch):
186
+ import src.main as main_mod
187
+
188
+ class _Client:
189
+ pass
190
+
191
+ monkeypatch.delenv("PYTEST_CURRENT_TEST", raising=False)
192
+ monkeypatch.setenv("COLLAB_AUTO_START_WATCHER", "1")
193
+ assert main_mod._ensure_watcher_running(_Client(), "release") is False
194
+
195
+
196
+ def test_ensure_watcher_running_skips_when_daemon_alive(monkeypatch):
197
+ import src.main as main_mod
198
+
199
+ class _Client:
200
+ def daemon_status(self):
201
+ return True
202
+
203
+ def daemon_start(self):
204
+ raise AssertionError("daemon_start should not be called")
205
+
206
+ monkeypatch.delenv("PYTEST_CURRENT_TEST", raising=False)
207
+ monkeypatch.setenv("COLLAB_AUTO_START_WATCHER", "1")
208
+ assert main_mod._ensure_watcher_running(_Client(), "active") is False
209
+
210
+
211
+ def test_ensure_watcher_running_start_failure(monkeypatch):
212
+ import src.main as main_mod
213
+
214
+ class _Client:
215
+ def daemon_status(self):
216
+ raise RuntimeError("status failed")
217
+
218
+ def daemon_start(self):
219
+ raise RuntimeError("start failed")
220
+
221
+ monkeypatch.delenv("PYTEST_CURRENT_TEST", raising=False)
222
+ monkeypatch.setenv("COLLAB_AUTO_START_WATCHER", "1")
223
+ assert main_mod._ensure_watcher_running(_Client(), "status") is False
224
+
225
+
226
+ def test_run_cli_active_reconcile_exception_is_non_fatal(monkeypatch, capsys):
227
+ import src.lock_client as lc
228
+ import src.main as main_mod
229
+
230
+ called = {"start": 0}
231
+
232
+ class _Client:
233
+ def __init__(self, local_only=False):
234
+ self.local_only = local_only
235
+
236
+ def daemon_status(self):
237
+ return False
238
+
239
+ def daemon_start(self):
240
+ called["start"] += 1
241
+
242
+ def _reconcile(self):
243
+ raise RuntimeError("reconcile boom")
244
+
245
+ def active(self):
246
+ return []
247
+
248
+ monkeypatch.setattr(
249
+ main_mod,
250
+ "setup_collab_logging",
251
+ lambda **_k: None,
252
+ raising=False,
253
+ )
254
+ monkeypatch.setattr(
255
+ main_mod,
256
+ "_quiet_console_loggers",
257
+ lambda: nullcontext(),
258
+ raising=False,
259
+ )
260
+ monkeypatch.setattr(main_mod, "LockClient", _Client, raising=False)
261
+
262
+ monkeypatch.setattr(lc, "LockClient", _Client)
263
+ monkeypatch.setattr(lc, "_quiet_console_loggers", lambda: nullcontext())
264
+ monkeypatch.setattr(lc, "_COLLAB_ROOT", ".collab")
265
+
266
+ import src.logging_config as logging_config
267
+
268
+ monkeypatch.setattr(logging_config, "setup_collab_logging", lambda **_k: None)
269
+
270
+ monkeypatch.delenv("PYTEST_CURRENT_TEST", raising=False)
271
+ monkeypatch.setenv("COLLAB_AUTO_START_WATCHER", "1")
272
+ monkeypatch.setattr(sys, "argv", ["collab", "active"])
273
+
274
+ main_mod._run_cli()
275
+ out = capsys.readouterr().out
276
+
277
+ assert called["start"] == 1
278
+ assert "No active locks." in out
tests/conftest.py ADDED
@@ -0,0 +1,167 @@
1
+ import atexit
2
+ import importlib.util
3
+ import os
4
+ import re
5
+ import shutil
6
+ import signal
7
+ import subprocess
8
+ import sys
9
+ import tempfile
10
+ from pathlib import Path
11
+
12
+ import pytest
13
+
14
+ # ============================================================================
15
+ # EARLY ISOLATION (MUST HAPPEN BEFORE TEST COLLECTION)
16
+ # ============================================================================
17
+ # Pytest imports test modules during the test collection phase, which happens
18
+ # *before* session-scoped fixtures execute. Since lock_client and watcher
19
+ # modules eagerly evaluate os.getenv("COLLAB_PID_FILE") at module load time,
20
+ # we MUST set test isolation variables at the top level of this file.
21
+
22
+ _session_temp_dir = tempfile.mkdtemp(prefix="collab_test_")
23
+
24
+ os.environ["COLLAB_STATE_DIR"] = _session_temp_dir
25
+ os.environ["COLLAB_PID_FILE"] = os.path.join(_session_temp_dir, "daemon.pid")
26
+ os.environ["COLLAB_TEST_MODE"] = "1"
27
+
28
+ # We forcibly mock these for ALL tests to prevent accidental production leakage.
29
+ # Individual tests can still use monkeypatch if they need specific dummy values.
30
+ os.environ["SUPABASE_URL"] = "http://localhost:54321"
31
+ os.environ["SUPABASE_ANON_KEY"] = "test-anon-key-session"
32
+
33
+
34
+ def _cleanup_session_temp():
35
+ try:
36
+ shutil.rmtree(_session_temp_dir)
37
+ except Exception:
38
+ pass
39
+
40
+
41
+ def _is_test_watcher_cmdline(cmdline: str) -> bool:
42
+ """Return True for watcher processes started by test runs only."""
43
+ text = (cmdline or "").lower().replace('"', "")
44
+ if "lock_client.py watch" not in text:
45
+ return False
46
+ if "--daemon" not in text:
47
+ return False
48
+ if "--pid-file" not in text:
49
+ return False
50
+ # Strictly match temp test namespaces; never touch production .collab/.daemon.pid.
51
+ return (
52
+ "pytest-of-" in text
53
+ or "collab_test_" in text
54
+ or "mockcmms_pytest_collab_" in text
55
+ )
56
+
57
+
58
+ def _terminate_orphan_test_watchers() -> None:
59
+ """Best-effort kill for orphaned test watcher daemons.
60
+
61
+ Keeps scope narrow to test-mode watcher cmdlines so production daemons are never
62
+ affected.
63
+ """
64
+ try:
65
+ if os.name == "nt":
66
+ proc = subprocess.run(
67
+ [
68
+ "powershell",
69
+ "-NoProfile",
70
+ "-Command",
71
+ (
72
+ "Get-CimInstance Win32_Process | "
73
+ "Where-Object { $_.Name -eq 'pythonw.exe' "
74
+ "-or $_.Name -eq 'python.exe' } | "
75
+ "Select-Object ProcessId,CommandLine | "
76
+ "ConvertTo-Json -Compress"
77
+ ),
78
+ ],
79
+ capture_output=True,
80
+ text=True,
81
+ check=False,
82
+ )
83
+ raw = (proc.stdout or "").strip()
84
+ if not raw:
85
+ return
86
+ import json
87
+
88
+ rows = json.loads(raw)
89
+ if isinstance(rows, dict):
90
+ rows = [rows]
91
+ for row in rows:
92
+ pid = int(row.get("ProcessId", 0) or 0)
93
+ cmdline = row.get("CommandLine") or ""
94
+ if pid > 0 and _is_test_watcher_cmdline(cmdline):
95
+ try:
96
+ os.kill(pid, signal.SIGTERM)
97
+ except Exception:
98
+ pass
99
+ else:
100
+ proc = subprocess.run(
101
+ ["ps", "-eo", "pid,args"],
102
+ capture_output=True,
103
+ text=True,
104
+ check=False,
105
+ )
106
+ for line in (proc.stdout or "").splitlines():
107
+ m = re.match(r"^\s*(\d+)\s+(.*)$", line)
108
+ if not m:
109
+ continue
110
+ pid = int(m.group(1))
111
+ cmdline = m.group(2)
112
+ if _is_test_watcher_cmdline(cmdline):
113
+ try:
114
+ os.kill(pid, signal.SIGTERM)
115
+ except Exception:
116
+ pass
117
+ except Exception:
118
+ pass
119
+
120
+
121
+ # Ensure test mode is explicitly kept, do not clear it, so late-firing
122
+ # atexit hooks attached to test processes still skip network calls.
123
+ atexit.register(_cleanup_session_temp)
124
+ atexit.register(_terminate_orphan_test_watchers)
125
+
126
+
127
+ def pytest_sessionfinish(session, exitstatus):
128
+ """Cleanup any orphaned test watcher daemons at end of a pytest session."""
129
+ _terminate_orphan_test_watchers()
130
+
131
+
132
+ def _load_logging_config_module():
133
+ collab_root = Path(__file__).resolve().parents[2]
134
+ logging_config_path = collab_root / "logging_config.py"
135
+ spec = importlib.util.spec_from_file_location(
136
+ "collab.logging_config_test_cleanup", str(logging_config_path)
137
+ )
138
+ assert spec and spec.loader
139
+ module = importlib.util.module_from_spec(spec)
140
+ spec.loader.exec_module(module) # type: ignore[arg-type]
141
+ return module
142
+
143
+
144
+ @pytest.fixture(autouse=True)
145
+ def _close_collab_logging_after_each_test():
146
+ yield
147
+ try:
148
+ _load_logging_config_module().close_collab_logging()
149
+ except Exception:
150
+ pass
151
+
152
+
153
+ # ============================================================================
154
+ # MOCKS & FIXTURES
155
+ # ============================================================================
156
+
157
+
158
+ class FakeNotification:
159
+ def notify(self, **kwargs):
160
+ pass
161
+
162
+
163
+ class FakePlyer:
164
+ notification = FakeNotification()
165
+
166
+
167
+ sys.modules["plyer"] = FakePlyer() # type: ignore[assignment]
File without changes
File without changes
File without changes
@@ -0,0 +1,76 @@
1
+ """CI-only packaging smoke test.
2
+
3
+ Builds an sdist+wheel and verifies the produced wheel can be installed into an ephemeral
4
+ venv and that the installed package exposes the `collab` import surface and a non-empty
5
+ `__version__` string.
6
+
7
+ This test is intentionally marked `packaging` and should only run in CI.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import os
13
+ import subprocess
14
+ import sys
15
+ from pathlib import Path
16
+
17
+ import pytest
18
+
19
+
20
+ def _find_repo_root(start: Path) -> Path:
21
+ cur = start.resolve()
22
+ for p in [cur] + list(cur.parents):
23
+ if (p / "pyproject.toml").exists() or (p / "setup.py").exists():
24
+ return p
25
+ raise RuntimeError("Could not find project root (pyproject.toml or setup.py)")
26
+
27
+
28
+ @pytest.mark.packaging
29
+ def test_smoke_install_build_and_import(tmp_path: Path) -> None:
30
+ repo_root = _find_repo_root(Path(__file__))
31
+ dist_dir = tmp_path / "dist"
32
+ dist_dir.mkdir(parents=True, exist_ok=True)
33
+
34
+ # Build sdist+wheel
35
+ subprocess.check_call(
36
+ [
37
+ sys.executable,
38
+ "-m",
39
+ "build",
40
+ "--sdist",
41
+ "--wheel",
42
+ "--outdir",
43
+ str(dist_dir),
44
+ ],
45
+ cwd=str(repo_root),
46
+ )
47
+
48
+ # Create ephemeral venv
49
+ venv_dir = tmp_path / "venv"
50
+ subprocess.check_call([sys.executable, "-m", "venv", str(venv_dir)])
51
+ if os.name == "nt":
52
+ venv_dir / "Scripts" / "pip.exe"
53
+ py = venv_dir / "Scripts" / "python.exe"
54
+ else:
55
+ venv_dir / "bin" / "pip"
56
+ py = venv_dir / "bin" / "python"
57
+
58
+ # Install wheel (use `python -m pip` which is more reliable across platforms)
59
+ wheels = list(dist_dir.glob("*.whl"))
60
+ assert wheels, "no wheel produced"
61
+ subprocess.check_call(
62
+ [str(py), "-m", "pip", "install", "--upgrade", "pip"]
63
+ ) # ensure modern pip
64
+ subprocess.check_call([str(py), "-m", "pip", "install", str(wheels[0])])
65
+
66
+ # Validate import surface
67
+ subprocess.check_call(
68
+ [
69
+ str(py),
70
+ "-c",
71
+ "import importlib;print(importlib.import_module('collab').__version__)",
72
+ ]
73
+ )
74
+
75
+ # Validate CLI surface via module execution (portable across platforms)
76
+ subprocess.check_call([str(py), "-m", "collab", "--help"])