8Integration tests for ``oct git hooks`` subgroup (Phase 4D — G-D8..G-D11).
12- Verify ``oct git hooks install`` creates .pre-commit-config.yaml.
13- Verify ``oct git hooks status`` reports correct hook states.
14- Verify ``oct git hooks remove`` removes OCT hooks.
15- Verify deprecation shim for ``oct install-hooks``.
22 L3 — assertion details
27- All tests use subprocess invocation and tmp_path fixtures.
34from pathlib
import Path
45 return shutil.which(
"git")
is not None
48def _git(repo: Path, *args: str, check: bool =
True):
49 return subprocess.run(
58def _run_hooks(root: Path, *extra_args: str) -> subprocess.CompletedProcess:
59 """Run ``oct git hooks <subcmd>`` in a subprocess."""
60 return subprocess.run(
61 [sys.executable,
"-m",
"oct.cli",
"git",
"hooks", *extra_args],
69 """Run the deprecated ``oct install-hooks``."""
70 return subprocess.run(
71 [sys.executable,
"-m",
"oct.cli",
"install-hooks", *extra_args],
79 """Create a git repo with Option C project structure."""
80 root = tmp_path / name
82 (root /
"docs").mkdir()
83 (root /
"tests").mkdir()
84 (root /
"oc_diagnostics").mkdir()
85 (root /
"logs").mkdir()
86 (root /
"src").mkdir()
87 (root /
"pyproject.toml").write_text(
88 f
'[project]\nname = "{name}"\n', encoding=
"utf-8",
90 _git(root,
"init",
"--quiet",
"-b",
"main")
91 _git(root,
"config",
"user.email",
"test@example.com")
92 _git(root,
"config",
"user.name",
"Test")
93 _git(root,
"config",
"commit.gpgsign",
"false")
94 _git(root,
"add",
"-A")
95 _git(root,
"commit",
"-m",
"initial",
"--quiet")
106 @pytest.mark.skipif(not _git_available(), reason=
"git not on PATH")
110 assert result.returncode == 0
111 config = root /
".pre-commit-config.yaml"
112 assert config.is_file()
113 content = config.read_text(encoding=
"utf-8")
114 assert "oct-quality-gate" in content
115 assert "oct-secrets-check" in content
116 assert "oct-commit-msg" in content
117 assert "oct-pre-push" in content
119 @pytest.mark.skipif(not _git_available(), reason=
"git not on PATH")
124 assert result.returncode == 1
125 assert "already exists" in result.stderr
127 @pytest.mark.skipif(not _git_available(), reason=
"git not on PATH")
132 config = root /
".pre-commit-config.yaml"
133 config.write_text(
"# custom content\n", encoding=
"utf-8")
134 result =
_run_hooks(root,
"install",
"--force")
135 assert result.returncode == 0
136 content = config.read_text(encoding=
"utf-8")
137 assert "oct-quality-gate" in content
139 @pytest.mark.skipif(not _git_available(), reason=
"git not on PATH")
141 """Config file should have proper YAML structure."""
144 content = (root /
".pre-commit-config.yaml").read_text(encoding=
"utf-8")
145 assert "repos:" in content
146 assert "- repo: local" in content
147 assert "hooks:" in content
157 @pytest.mark.skipif(not _git_available(), reason=
"git not on PATH")
162 assert result.returncode == 0
163 for hook_id
in (
"oct-quality-gate",
"oct-secrets-check",
164 "oct-commit-msg",
"oct-pre-push"):
165 assert f
"[INSTALLED] {hook_id}" in result.stdout
167 @pytest.mark.skipif(not _git_available(), reason=
"git not on PATH")
171 assert result.returncode == 0
172 for hook_id
in (
"oct-quality-gate",
"oct-secrets-check",
173 "oct-commit-msg",
"oct-pre-push"):
174 assert f
"[MISSING] {hook_id}" in result.stdout
176 @pytest.mark.skipif(not _git_available(), reason=
"git not on PATH")
180 config = root /
".pre-commit-config.yaml"
182 "repos:\n - repo: local\n hooks:\n"
183 " - id: oct-quality-gate\n"
186 " language: system\n",
190 assert "[INSTALLED] oct-quality-gate" in result.stdout
191 assert "[MISSING] oct-secrets-check" in result.stdout
201 @pytest.mark.skipif(not _git_available(), reason=
"git not on PATH")
203 """When only OCT hooks exist, remove deletes the file."""
207 assert result.returncode == 0
208 assert not (root /
".pre-commit-config.yaml").is_file()
210 @pytest.mark.skipif(not _git_available(), reason=
"git not on PATH")
212 """Remove when no config file exists → graceful."""
215 assert result.returncode == 0
217 @pytest.mark.skipif(not _git_available(), reason=
"git not on PATH")
219 """Non-OCT hooks should survive removal."""
221 config = root /
".pre-commit-config.yaml"
223 "repos:\n - repo: local\n hooks:\n"
224 " - id: custom-hook\n"
225 " name: My Custom Hook\n"
226 " entry: echo custom\n"
227 " language: system\n"
228 " - id: oct-quality-gate\n"
229 " name: Option C Quality Gate\n"
230 " entry: oct git check --staged-only\n"
231 " language: system\n",
235 assert result.returncode == 0
237 assert config.is_file()
238 content = config.read_text(encoding=
"utf-8")
239 assert "custom-hook" in content
240 assert "oct-quality-gate" not in content
250 @pytest.mark.skipif(not _git_available(), reason=
"git not on PATH")
254 assert result.returncode == 0
255 assert "deprecated" in result.stderr.lower()
257 assert (root /
".pre-commit-config.yaml").is_file()
259 @pytest.mark.skipif(not _git_available(), reason=
"git not on PATH")
264 assert result.returncode == 0
265 assert "deprecated" in result.stderr.lower()
275 @pytest.mark.skipif(not _git_available(), reason=
"git not on PATH")
279 assert result.returncode == 0
280 audit_files = list((root /
"logs").glob(
"oct_git_audit*.jsonl"))
282 content = audit_files[-1].read_text(encoding=
"utf-8")
283 assert "oct git hooks install" in content
285 @pytest.mark.skipif(not _git_available(), reason=
"git not on PATH")
290 assert result.returncode == 0
291 audit_files = list((root /
"logs").glob(
"oct_git_audit*.jsonl"))
293 content = audit_files[-1].read_text(encoding=
"utf-8")
294 assert "oct git hooks remove" in content
test_remove_audit(self, tmp_path)
test_install_audit(self, tmp_path)
test_deprecated_with_force(self, tmp_path)
test_deprecated_command_works(self, tmp_path)
test_creates_config(self, tmp_path)
test_force_overwrites(self, tmp_path)
test_refuses_overwrite_without_force(self, tmp_path)
test_yaml_content_valid(self, tmp_path)
test_remove_no_file(self, tmp_path)
test_remove_preserves_non_oct_hooks(self, tmp_path)
test_remove_deletes_file(self, tmp_path)
test_partial_hooks(self, tmp_path)
test_all_installed(self, tmp_path)
test_none_installed(self, tmp_path)
subprocess.CompletedProcess _run_hooks(Path root, *str extra_args)
_git(Path repo, *str args, bool check=True)
Path _make_option_c_git_project(Path tmp_path, str name="hookproj")
subprocess.CompletedProcess _run_install_hooks(Path root, *str extra_args)