Option C Tools
Loading...
Searching...
No Matches
test_workspace_status.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# tests/tests_git/test_workspace_status.py
4
5"""
6Purpose
7-------
8Integration test for L1 / F1 leanest first cut: ``oct git status``
9invoked from a workspace root (a directory containing
10``.option_c_workspace.json``) succeeds without the
11``--root-dir . --allow-outside-project`` escape hatch that pre-F1
12users needed.
13
14Responsibilities
15----------------
16- Verify that placing a workspace manifest at a parent directory
17 makes ``oct git status`` work from that parent.
18- Verify backwards compat: ``oct git status`` from inside a
19 subproject still resolves to the subproject root (not the
20 workspace root).
21
22Diagnostics
23-----------
24Domain: GIT-TESTS
25Levels:
26 L2 -- test lifecycle
27 L3 -- assertion details
28"""
29
30import json
31import shutil
32import subprocess
33import sys
34from pathlib import Path
35
36import pytest
37
38
39def _git_available() -> bool:
40 return shutil.which("git") is not None
41
42
43def _git(repo: Path, *args: str, check: bool = True):
44 return subprocess.run(
45 ["git", *args], cwd=repo, check=check,
46 capture_output=True, text=True,
47 )
48
49
51 cwd: Path, *extra_args: str,
52) -> subprocess.CompletedProcess:
53 return subprocess.run(
54 [sys.executable, "-m", "oct.cli", "git", "status", *extra_args],
55 cwd=cwd, capture_output=True, text=True,
56 )
57
58
59@pytest.fixture
60def workspace_with_subproject(tmp_path: Path) -> tuple[Path, Path]:
61 """Create a workspace with one Option C subproject inside.
62
63 Layout::
64
65 <ws_root>/
66 ├── .option_c_workspace.json
67 ├── .git/
68 └── subproj/
69 ├── .option_c/
70 ├── .octrc.json
71 ├── pyproject.toml
72 ├── docs/ARCHITECTURE.md
73 ├── oc_diagnostics/
74 ├── tests/
75 ├── logs/
76 ├── debug_config.json
77 └── src/
78 """
79 if not _git_available():
80 pytest.skip("git binary not available on PATH")
81
82 ws_root = tmp_path / "myws"
83 ws_root.mkdir()
84 _git(ws_root, "init", "--quiet", "-b", "main")
85 _git(ws_root, "config", "user.email", "test@example.com")
86 _git(ws_root, "config", "user.name", "Test")
87 _git(ws_root, "config", "commit.gpgsign", "false")
88
89 manifest = {
90 "version": "1.0",
91 "name": "Test Workspace",
92 "subprojects": [{"path": "subproj", "name": "subproj"}],
93 }
94 (ws_root / ".option_c_workspace.json").write_text(
95 json.dumps(manifest, indent=2), encoding="utf-8",
96 )
97
98 sub = ws_root / "subproj"
99 sub.mkdir()
100 (sub / ".option_c").mkdir()
101 (sub / "src").mkdir()
102 (sub / "tests").mkdir()
103 (sub / "docs").mkdir()
104 (sub / "logs").mkdir()
105 (sub / "oc_diagnostics").mkdir()
106 (sub / ".octrc.json").write_text(
107 json.dumps({"linter": {"profile": "compact"}}),
108 encoding="utf-8",
109 )
110 (sub / "pyproject.toml").write_text(
111 '[project]\nname = "subproj"\n', encoding="utf-8",
112 )
113 (sub / "debug_config.json").write_text(
114 json.dumps({"domains": {"GIT": {"level": 1, "color": "cyan"}}}),
115 encoding="utf-8",
116 )
117 (sub / "docs" / "ARCHITECTURE.md").write_text(
118 "# Architecture\n", encoding="utf-8",
119 )
120
121 _git(ws_root, "add", "-A")
122 _git(ws_root, "commit", "-m", "initial", "--quiet")
123
124 return ws_root.resolve(), sub.resolve()
125
126
127# =====================================================================
128# L1 win: status from workspace root
129# =====================================================================
130
131
133
135 self, workspace_with_subproject,
136 ) -> None:
137 ws_root, _sub = workspace_with_subproject
138 result = _run_status(ws_root)
139 assert result.returncode == 0, result.stderr
140 # Should show the branch line (no "Could not locate project root").
141 assert "On branch main" in result.stdout
142 assert "Could not locate project root" not in result.stderr
143
144
145# =====================================================================
146# Backwards compat: status from inside subproject still works
147# =====================================================================
148
149
150# =====================================================================
151# F1.2 fan-out: workspace_view JSON + per-subproject grouping
152# =====================================================================
153
154
155@pytest.fixture
157 """Two-subproject workspace fixture for F1.2 fan-out tests."""
158 if not _git_available():
159 pytest.skip("git binary not available on PATH")
160
161 ws_root = tmp_path / "ws2"
162 ws_root.mkdir()
163 _git(ws_root, "init", "--quiet", "-b", "main")
164 _git(ws_root, "config", "user.email", "test@example.com")
165 _git(ws_root, "config", "user.name", "Test")
166 _git(ws_root, "config", "commit.gpgsign", "false")
167
168 manifest = {
169 "version": "1.0",
170 "name": "Two-Sub Workspace",
171 "workspace_files": ["CLAUDE.md", "README.md"],
172 "subprojects": [
173 {"path": "alpha", "name": "alpha", "profile": "compact"},
174 {"path": "beta", "name": "beta", "profile": "compact"},
175 ],
176 }
177 (ws_root / ".option_c_workspace.json").write_text(
178 json.dumps(manifest, indent=2), encoding="utf-8",
179 )
180
181 for sp_name in ("alpha", "beta"):
182 sub = ws_root / sp_name
183 sub.mkdir()
184 (sub / ".option_c").mkdir()
185 (sub / "src").mkdir()
186 (sub / "tests").mkdir()
187 (sub / "docs").mkdir()
188 (sub / "logs").mkdir()
189 (sub / "oc_diagnostics").mkdir()
190 (sub / ".octrc.json").write_text(
191 json.dumps({"linter": {"profile": "compact"}}),
192 encoding="utf-8",
193 )
194 (sub / "pyproject.toml").write_text(
195 f'[project]\nname = "{sp_name}"\n', encoding="utf-8",
196 )
197 (sub / "debug_config.json").write_text(
198 json.dumps({"domains": {"GIT": {"level": 1, "color": "cyan"}}}),
199 encoding="utf-8",
200 )
201 (sub / "docs" / "ARCHITECTURE.md").write_text(
202 "# Architecture\n", encoding="utf-8",
203 )
204
205 # Workspace-level files.
206 (ws_root / "CLAUDE.md").write_text("# CLAUDE\n", encoding="utf-8")
207 (ws_root / "README.md").write_text("# README\n", encoding="utf-8")
208
209 _git(ws_root, "add", "-A")
210 _git(ws_root, "commit", "-m", "initial", "--quiet")
211
212 return (
213 ws_root.resolve(),
214 (ws_root / "alpha").resolve(),
215 (ws_root / "beta").resolve(),
216 )
217
218
220
222 self, workspace_with_two_subprojects,
223 ) -> None:
224 ws_root, _alpha, _beta = workspace_with_two_subprojects
225 result = _run_status(ws_root, "--json")
226 assert result.returncode == 0, result.stderr
227 data = json.loads(result.stdout)
228 assert "workspace_view" in data
229 assert data["workspace_view"] is not None
230 assert set(data["workspace_view"].keys()) == {
231 "alpha", "beta", "workspace",
232 }
233
235 self, workspace_with_two_subprojects,
236 ) -> None:
237 ws_root, alpha, beta = workspace_with_two_subprojects
238 # Stage one file in each subproject.
239 (alpha / "src" / "a.py").write_text("x = 1\n", encoding="utf-8")
240 (beta / "src" / "b.py").write_text("x = 2\n", encoding="utf-8")
241 _git(ws_root, "add", "alpha/src/a.py", "beta/src/b.py")
242
243 result = _run_status(ws_root, "--json")
244 assert result.returncode == 0, result.stderr
245 data = json.loads(result.stdout)
246 wv = data["workspace_view"]
247 # Each subproject sees its own staged file.
248 alpha_paths = [e["path"] for e in wv["alpha"]["staged"]]
249 beta_paths = [e["path"] for e in wv["beta"]["staged"]]
250 assert "src/a.py" in alpha_paths
251 assert "src/b.py" in beta_paths
252 # No cross-leak.
253 assert "src/b.py" not in alpha_paths
254 assert "src/a.py" not in beta_paths
255
257 self, workspace_with_two_subprojects,
258 ) -> None:
259 ws_root, _alpha, _beta = workspace_with_two_subprojects
260 # Modify a workspace-level file (CLAUDE.md is in workspace_files).
261 (ws_root / "CLAUDE.md").write_text(
262 "# CLAUDE — modified\n", encoding="utf-8",
263 )
264 result = _run_status(ws_root, "--json")
265 data = json.loads(result.stdout)
266 wv = data["workspace_view"]
267 # CLAUDE.md should appear in the workspace bucket.
268 ws_modified = [e["path"] for e in wv["workspace"]["modified"]]
269 assert "CLAUDE.md" in ws_modified
270 # And NOT in any subproject bucket.
271 for sp_name in ("alpha", "beta"):
272 sp_modified = [e["path"] for e in wv[sp_name]["modified"]]
273 assert "CLAUDE.md" not in sp_modified
274
276 self, workspace_with_two_subprojects,
277 ) -> None:
278 ws_root, alpha, _beta = workspace_with_two_subprojects
279 (alpha / "src" / "x.py").write_text("x = 1\n", encoding="utf-8")
280 _git(ws_root, "add", "alpha/src/x.py")
281 result = _run_status(ws_root, "--json")
282 data = json.loads(result.stdout)
283 # Top-level legacy keys still present and populated.
284 assert "staged" in data
285 assert "modified" in data
286 assert "untracked" in data
287 # The flat staged list still includes the file (workspace-relative).
288 flat_paths = [e["path"] for e in data["staged"]]
289 assert any("x.py" in p for p in flat_paths)
290
291
293
295 self, workspace_with_subproject,
296 ) -> None:
297 _ws_root, sub = workspace_with_subproject
298 # Add an unstaged change inside the subproject so we have
299 # something to look at.
300 (sub / "src" / "new.py").write_text(
301 "x = 1\n", encoding="utf-8",
302 )
303 result = _run_status(sub, "--json")
304 assert result.returncode == 0, result.stderr
305 data = json.loads(result.stdout)
306 # Untracked path should be subproject-relative ("src/new.py"),
307 # NOT workspace-relative ("subproj/src/new.py").
308 untracked_paths = [e["path"] for e in data["untracked"]]
309 assert any("src/new.py" in p for p in untracked_paths), (
310 f"expected subproject-relative path, got: {untracked_paths}"
311 )
312 assert all("subproj/" not in p for p in untracked_paths), (
313 f"backwards-compat broken: workspace prefix appeared in "
314 f"subproject-mode output: {untracked_paths}"
315 )
None test_legacy_keys_preserved(self, workspace_with_two_subprojects)
None test_workspace_files_route_to_workspace_bucket(self, workspace_with_two_subprojects)
None test_status_groups_by_subproject(self, workspace_with_two_subprojects)
None test_workspace_view_present_in_json(self, workspace_with_two_subprojects)
None test_status_from_workspace_root_works(self, workspace_with_subproject)
None test_status_from_subproject_resolves_to_subproject(self, workspace_with_subproject)
tuple[Path, Path] workspace_with_subproject(Path tmp_path)
subprocess.CompletedProcess _run_status(Path cwd, *str extra_args)
_git(Path repo, *str args, bool check=True)