Option C Tools
Loading...
Searching...
No Matches
test_oct_install.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# tests/tests_install/test_oct_install.py
4
5"""
6Purpose
7-------
8Validate the B1.4 ``oct install --editable`` safety wrapper. The
9heart of the command is the preflight matrix; the actual ``pip
10install -e`` subprocess is mocked because we don't want test runs to
11mutate the real venv's site-packages.
12
13Responsibilities
14----------------
15- Cover each preflight refusal: not-an-Option-C-project, inside a
16 worktree, wrong venv, existing shadow install.
17- Cover each override flag (--allow-worktree, --use-active-venv,
18 --reinstall, --allow-outside-project).
19- Cover the success path (preflights green, pip mocked to exit 0,
20 post-install diagnostics printed).
21- Cover the JSON output schema for both refusal and success.
22
23Diagnostics
24-----------
25Domain: GIT-TESTS
26Levels:
27 L2 -- test lifecycle
28 L3 -- assertion details
29
30Contracts
31---------
32- Tests use ``CliRunner`` from Click rather than spawning a
33 subprocess. This lets us monkey-patch the subprocess call cleanly.
34- Tests must not modify the developer's actual venv. Every
35 ``subprocess.run`` is patched to a stub that returns
36 ``returncode=0`` without performing any work.
37"""
38
39import json
40from pathlib import Path
41from unittest.mock import patch, MagicMock
42
43import pytest
44from click.testing import CliRunner
45
46from oct.install.oct_install import oct_install_cmd
47
48
49# =====================================================================
50# Helpers
51# =====================================================================
52
53
54def _make_oc_project(root: Path, name: str = "myproj") -> Path:
55 """Create the minimum-marker Option C project layout."""
56 root.mkdir(parents=True, exist_ok=True)
57 (root / "pyproject.toml").write_text(
58 f'[project]\nname = "{name}"\n', encoding="utf-8",
59 )
60 (root / ".octrc.json").write_text(
61 '{"linter": {"profile": "compact"}}',
62 encoding="utf-8",
63 )
64 return root.resolve()
65
66
67def _make_venv(root: Path) -> Path:
68 """Create a venv-shaped directory (just the markers we look at)."""
69 venv = root / ".venv"
70 venv.mkdir(parents=True, exist_ok=True)
71 bin_dir = venv / ("Scripts" if (venv / "Scripts").exists()
72 or _is_windows() else "bin")
73 bin_dir.mkdir(exist_ok=True)
74 return venv.resolve()
75
76
77def _is_windows() -> bool:
78 import sys as _sys
79 return _sys.platform.startswith("win")
80
81
82# =====================================================================
83# Preflight: not an Option C project
84# =====================================================================
85
86
88
89 def test_target_missing_pyproject_refuses(self, tmp_path: Path) -> None:
90 bad = tmp_path / "not_oc"
91 bad.mkdir()
92 (bad / ".octrc.json").write_text("{}", encoding="utf-8")
93 # No pyproject.toml -> not an Option C project.
94 runner = CliRunner()
95 with runner.isolated_filesystem(temp_dir=tmp_path):
96 result = runner.invoke(
97 oct_install_cmd, ["--editable", str(bad)],
98 )
99 assert result.exit_code == 1
100 assert "not an Option C project" in result.output
101
102 def test_target_missing_octrc_refuses(self, tmp_path: Path) -> None:
103 bad = tmp_path / "no_octrc"
104 bad.mkdir()
105 (bad / "pyproject.toml").write_text(
106 '[project]\nname = "x"\n', encoding="utf-8",
107 )
108 runner = CliRunner()
109 with runner.isolated_filesystem(temp_dir=tmp_path):
110 result = runner.invoke(
111 oct_install_cmd, ["--editable", str(bad)],
112 )
113 assert result.exit_code == 1
114 assert "not an Option C project" in result.output
115
116
117# =====================================================================
118# Preflight: inside a Claude Code worktree
119# =====================================================================
120
121
123
124 def test_worktree_target_refuses(self, tmp_path: Path) -> None:
125 # Build a Claude Code worktree-style path.
126 wt_root = tmp_path / ".claude" / "worktrees" / "abc123"
127 target = _make_oc_project(wt_root)
128 runner = CliRunner()
129 with runner.isolated_filesystem(temp_dir=tmp_path):
130 result = runner.invoke(
131 oct_install_cmd, ["--editable", str(target)],
132 )
133 assert result.exit_code == 1
134 assert "Claude Code worktree" in result.output
135
136 def test_allow_worktree_overrides(self, tmp_path: Path) -> None:
137 wt_root = tmp_path / ".claude" / "worktrees" / "abc123"
138 target = _make_oc_project(wt_root)
139 runner = CliRunner()
140 with patch("oct.install.oct_install.subprocess.run") as mock_run, \
141 patch(
142 "oct.install.oct_install._active_venv_root",
143 return_value=None,
144 ), \
145 patch(
146 "oct.install.oct_install._pip_show_location",
147 return_value=None,
148 ):
149 mock_run.return_value = MagicMock(
150 returncode=0, stdout="", stderr="",
151 )
152 result = runner.invoke(oct_install_cmd, [
153 "--editable", str(target),
154 "--allow-worktree", "--use-active-venv",
155 "--allow-outside-project",
156 ])
157 assert result.exit_code == 0, result.output
158
159
160# =====================================================================
161# Preflight: venv match
162# =====================================================================
163
164
166
167 def test_no_active_venv_refuses(self, tmp_path: Path) -> None:
168 target = _make_oc_project(tmp_path / "proj")
169 _make_venv(target)
170 runner = CliRunner()
171 with patch(
172 "oct.install.oct_install._active_venv_root",
173 return_value=None,
174 ):
175 result = runner.invoke(
176 oct_install_cmd, ["--editable", str(target)],
177 )
178 assert result.exit_code == 1
179 assert "no active venv" in result.output
180
181 def test_wrong_venv_refuses(self, tmp_path: Path) -> None:
182 target = _make_oc_project(tmp_path / "proj")
183 _make_venv(target)
184 wrong_venv = tmp_path / "elsewhere" / ".venv"
185 wrong_venv.mkdir(parents=True)
186 runner = CliRunner()
187 with patch(
188 "oct.install.oct_install._active_venv_root",
189 return_value=wrong_venv.resolve(),
190 ):
191 result = runner.invoke(
192 oct_install_cmd, ["--editable", str(target)],
193 )
194 assert result.exit_code == 1
195 assert "active venv is at" in result.output
196
197 def test_use_active_venv_skips_check(self, tmp_path: Path) -> None:
198 target = _make_oc_project(tmp_path / "proj")
199 _make_venv(target)
200 wrong_venv = tmp_path / "elsewhere" / ".venv"
201 wrong_venv.mkdir(parents=True)
202 runner = CliRunner()
203 with patch(
204 "oct.install.oct_install._active_venv_root",
205 return_value=wrong_venv.resolve(),
206 ), patch(
207 "oct.install.oct_install._pip_show_location",
208 return_value=None,
209 ), patch(
210 "oct.install.oct_install.subprocess.run",
211 ) as mock_run:
212 mock_run.return_value = MagicMock(
213 returncode=0, stdout="", stderr="",
214 )
215 result = runner.invoke(oct_install_cmd, [
216 "--editable", str(target),
217 "--use-active-venv", "--allow-outside-project",
218 ])
219 assert result.exit_code == 0, result.output
220
221
222# =====================================================================
223# Preflight: existing-shadow detection
224# =====================================================================
225
226
228
229 def test_shadow_in_other_worktree_refuses(self, tmp_path: Path) -> None:
230 target = _make_oc_project(tmp_path / "proj")
231 _make_venv(target)
232 other_worktree = tmp_path / "other_proj"
233 other_worktree.mkdir()
234 runner = CliRunner()
235 with patch(
236 "oct.install.oct_install._active_venv_root",
237 return_value=(target / ".venv").resolve(),
238 ), patch(
239 "oct.install.oct_install._pip_show_location",
240 return_value=other_worktree.resolve(),
241 ):
242 result = runner.invoke(oct_install_cmd, [
243 "--editable", str(target),
244 "--allow-outside-project",
245 ])
246 assert result.exit_code == 1
247 assert "already installed" in result.output
248 assert "silently shadow" in result.output
249
250 def test_shadow_with_reinstall_proceeds(self, tmp_path: Path) -> None:
251 target = _make_oc_project(tmp_path / "proj")
252 _make_venv(target)
253 other_worktree = tmp_path / "other_proj"
254 other_worktree.mkdir()
255 runner = CliRunner()
256 with patch(
257 "oct.install.oct_install._active_venv_root",
258 return_value=(target / ".venv").resolve(),
259 ), patch(
260 "oct.install.oct_install._pip_show_location",
261 return_value=other_worktree.resolve(),
262 ), patch(
263 "oct.install.oct_install.subprocess.run",
264 ) as mock_run:
265 mock_run.return_value = MagicMock(
266 returncode=0, stdout="", stderr="",
267 )
268 result = runner.invoke(oct_install_cmd, [
269 "--editable", str(target),
270 "--reinstall", "--allow-outside-project",
271 ])
272 assert result.exit_code == 0, result.output
273
274
275# =====================================================================
276# Success path + JSON
277# =====================================================================
278
279
281
283 self, tmp_path: Path,
284 ) -> None:
285 target = _make_oc_project(tmp_path / "proj")
286 _make_venv(target)
287 runner = CliRunner()
288 with patch(
289 "oct.install.oct_install._active_venv_root",
290 return_value=(target / ".venv").resolve(),
291 ), patch(
292 "oct.install.oct_install._pip_show_location",
293 ) as mock_show, patch(
294 "oct.install.oct_install._which_oct",
295 return_value=Path("/fake/bin/oct"),
296 ), patch(
297 "oct.install.oct_install.subprocess.run",
298 ) as mock_run:
299 mock_run.return_value = MagicMock(
300 returncode=0, stdout="", stderr="",
301 )
302 # Pre-install: not yet installed; post-install: located at target.
303 mock_show.side_effect = [None, target]
304 result = runner.invoke(oct_install_cmd, [
305 "--editable", str(target),
306 "--allow-outside-project",
307 ])
308 assert result.exit_code == 0, result.output
309 assert "myproj installed" in result.output
310
311 def test_json_success_schema(self, tmp_path: Path) -> None:
312 target = _make_oc_project(tmp_path / "proj")
313 _make_venv(target)
314 runner = CliRunner()
315 with patch(
316 "oct.install.oct_install._active_venv_root",
317 return_value=(target / ".venv").resolve(),
318 ), patch(
319 "oct.install.oct_install._pip_show_location",
320 ) as mock_show, patch(
321 "oct.install.oct_install._which_oct",
322 return_value=Path("/fake/bin/oct"),
323 ), patch(
324 "oct.install.oct_install.subprocess.run",
325 ) as mock_run:
326 mock_run.return_value = MagicMock(
327 returncode=0, stdout="", stderr="",
328 )
329 mock_show.side_effect = [None, target]
330 result = runner.invoke(oct_install_cmd, [
331 "--editable", str(target),
332 "--allow-outside-project", "--json",
333 ])
334 assert result.exit_code == 0, result.output
335 data = json.loads(result.output)
336 assert data["operation"] == "install"
337 assert data["package"] == "myproj"
338 assert data["exit_code"] == 0
339 assert "oct_binary" in data
340 assert "installed_location" in data
341
342 def test_json_refusal_schema(self, tmp_path: Path) -> None:
343 bad = tmp_path / "not_oc"
344 bad.mkdir()
345 runner = CliRunner()
346 result = runner.invoke(oct_install_cmd, [
347 "--editable", str(bad), "--json",
348 ])
349 assert result.exit_code == 1
350 data = json.loads(result.output)
351 assert data["exit_code"] == 1
352 assert data["errors"]
353 assert any("not an Option C project" in e for e in data["errors"])
354
355
356# =====================================================================
357# --editable required
358# =====================================================================
359
360
362
363 def test_missing_editable_refuses(self, tmp_path: Path) -> None:
364 target = _make_oc_project(tmp_path / "proj")
365 runner = CliRunner()
366 result = runner.invoke(oct_install_cmd, [str(target)])
367 assert result.exit_code == 1
368 assert "--editable" in result.output
None test_shadow_in_other_worktree_refuses(self, Path tmp_path)
None test_clean_install_succeeds_and_prints_diagnostics(self, Path tmp_path)
Path _make_oc_project(Path root, str name="myproj")