8Integration tests for ``oct git commit`` (Phase 4D — G-D3, G-D4, G-D6, G-D7).
12- Verify exit codes 0-4 for commit scenarios.
13- Verify the non-bypassable secrets invariant (G-D4).
14- Verify --force, --no-verify, --json flags.
15- Verify protected-branch guards (G-D7).
16- Verify large-file advisory warnings (G-D6).
24 L3 — assertion details
29- All tests use subprocess invocation (same pattern as test_git_check.py).
30- All tests use tmp_path fixtures so they are automatically cleaned up.
37from pathlib
import Path
41from tests.tests_git.conftest
import _git, commit_file
50 return shutil.which(
"git")
is not None
53def _run_commit(root: Path, *extra_args: str) -> subprocess.CompletedProcess:
54 """Run ``oct git commit`` in a subprocess."""
55 return subprocess.run(
56 [sys.executable,
"-m",
"oct.cli",
"git",
"commit", *extra_args],
64 """Run ``oct git commit --json`` and parse JSON output."""
66 return json.loads(result.stdout)
70 """Read the last JSONL line from the newest audit file."""
72 (root /
"logs").glob(
"oct_git_audit*.jsonl"),
73 key=
lambda p: p.stat().st_mtime,
77 lines = audit_files[-1].read_text(encoding=
"utf-8").strip().splitlines()
80 return json.loads(lines[-1])
90 @pytest.mark.skipif(not _git_available(), reason=
"git not on PATH")
92 """Staged compliant file + valid message → exit 0, commit made."""
93 _git(temp_git_project,
"checkout",
"-b",
"feature/clean")
94 path = make_compliant_py(
"src/good.py")
95 _git(temp_git_project,
"add",
"src/good.py")
97 temp_git_project,
"-m",
"feat: add good module",
99 assert result.returncode == 0
101 log = _git(temp_git_project,
"log",
"--oneline",
"-1")
102 assert "add good module" in log.stdout
104 @pytest.mark.skipif(not _git_available(), reason=
"git not on PATH")
106 """Staged file with lint violations → exit 1."""
107 _git(temp_git_project,
"checkout",
"-b",
"feature/lint")
108 path = make_noncompliant_py(
"src/bad.py",
"missing_header")
109 _git(temp_git_project,
"add",
"src/bad.py")
111 temp_git_project,
"-m",
"feat: add bad module",
113 assert result.returncode == 1
115 @pytest.mark.skipif(not _git_available(), reason=
"git not on PATH")
117 """Staged file with hardcoded secret → exit 2."""
118 _git(temp_git_project,
"checkout",
"-b",
"feature/secrets")
119 path = make_noncompliant_py(
"src/secret.py",
"hardcoded_secret")
120 _git(temp_git_project,
"add",
"src/secret.py")
122 temp_git_project,
"-m",
"feat: add secret module",
124 assert result.returncode == 2
126 @pytest.mark.skipif(not _git_available(), reason=
"git not on PATH")
128 """Invalid commit message → exit 3."""
129 _git(temp_git_project,
"checkout",
"-b",
"feature/msg")
130 path = make_compliant_py(
"src/good.py")
131 _git(temp_git_project,
"add",
"src/good.py")
133 temp_git_project,
"-m",
"bad message no conventional format",
135 assert result.returncode == 3
137 @pytest.mark.skipif(not _git_available(), reason=
"git not on PATH")
139 """Nothing staged → exit 4."""
140 _git(temp_git_project,
"checkout",
"-b",
"feature/empty")
142 temp_git_project,
"-m",
"feat: nothing here",
144 assert result.returncode == 4
154 @pytest.mark.skipif(not _git_available(), reason=
"git not on PATH")
156 _git(temp_git_project,
"checkout",
"-b",
"feature/sec1")
157 path = make_noncompliant_py(
"src/secret.py",
"hardcoded_secret")
158 _git(temp_git_project,
"add",
"src/secret.py")
160 temp_git_project,
"-m",
"feat: bad",
"--force",
162 assert result.returncode == 2
164 @pytest.mark.skipif(not _git_available(), reason=
"git not on PATH")
166 _git(temp_git_project,
"checkout",
"-b",
"feature/sec2")
167 path = make_noncompliant_py(
"src/secret.py",
"hardcoded_secret")
168 _git(temp_git_project,
"add",
"src/secret.py")
170 temp_git_project,
"-m",
"feat: bad",
"--no-verify",
172 assert result.returncode == 2
174 @pytest.mark.skipif(not _git_available(), reason=
"git not on PATH")
176 self, temp_git_project, make_noncompliant_py,
178 _git(temp_git_project,
"checkout",
"-b",
"feature/sec3")
179 path = make_noncompliant_py(
"src/secret.py",
"hardcoded_secret")
180 _git(temp_git_project,
"add",
"src/secret.py")
182 temp_git_project,
"-m",
"feat: bad",
"--force",
"--no-verify",
184 assert result.returncode == 2
194 @pytest.mark.skipif(not _git_available(), reason=
"git not on PATH")
196 """--force with lint violations → commit succeeds."""
197 _git(temp_git_project,
"checkout",
"-b",
"feature/force1")
198 path = make_noncompliant_py(
"src/bad.py",
"missing_header")
199 _git(temp_git_project,
"add",
"src/bad.py")
201 temp_git_project,
"-m",
"feat: forced",
"--force",
203 assert result.returncode == 0
204 log = _git(temp_git_project,
"log",
"--oneline",
"-1")
205 assert "forced" in log.stdout
207 @pytest.mark.skipif(not _git_available(), reason=
"git not on PATH")
209 """--force with invalid message → commit succeeds."""
210 _git(temp_git_project,
"checkout",
"-b",
"feature/force2")
211 path = make_compliant_py(
"src/good.py")
212 _git(temp_git_project,
"add",
"src/good.py")
214 temp_git_project,
"-m",
"bad message",
"--force",
216 assert result.returncode == 0
226 @pytest.mark.skipif(not _git_available(), reason=
"git not on PATH")
228 _git(temp_git_project,
"checkout",
"-b",
"feature/noverify")
229 path = make_compliant_py(
"src/good.py")
230 _git(temp_git_project,
"add",
"src/good.py")
232 temp_git_project,
"-m",
"feat: test no verify",
"--no-verify",
234 assert result.returncode == 0
235 assert "Skipping git hooks" in result.stderr
247 """Overwrite octrc to disable auto-branching for block tests."""
249 octrc_path = root /
".octrc.json"
250 octrc = _json.loads(octrc_path.read_text(encoding=
"utf-8"))
251 octrc.setdefault(
"git", {})[
"auto_branch_on_protected"] =
False
252 octrc_path.write_text(_json.dumps(octrc, indent=2), encoding=
"utf-8")
254 @pytest.mark.skipif(not _git_available(), reason=
"git not on PATH")
256 """On main with strict profile and auto-branch disabled → blocked."""
258 path = make_compliant_py(
"src/good.py")
259 _git(temp_git_project,
"add",
"src/good.py")
261 temp_git_project,
"-m",
"feat: on main",
262 "--profile",
"strict",
265 assert result.returncode == 1
266 assert "protected branch" in result.stderr.lower()
268 @pytest.mark.skipif(not _git_available(), reason=
"git not on PATH")
270 """On a feature branch → no protected-branch block."""
271 _git(temp_git_project,
"checkout",
"-b",
"feature/test")
272 path = make_compliant_py(
"src/good.py")
273 _git(temp_git_project,
"add",
"src/good.py")
275 temp_git_project,
"-m",
"feat: on feature",
277 assert result.returncode == 0
279 @pytest.mark.skipif(not _git_available(), reason=
"git not on PATH")
281 """On main with compact profile and auto-branch disabled → blocked.
283 Note: resolve_effective_profile forces strict on protected branches,
284 so the profile override is ignored. The test verifies that the
285 commit is blocked because the effective profile is always strict
289 path = make_compliant_py(
"src/good.py")
290 _git(temp_git_project,
"add",
"src/good.py")
292 temp_git_project,
"-m",
"feat: on main",
293 "--profile",
"compact",
296 assert result.returncode == 1
306 @pytest.mark.skipif(not _git_available(), reason=
"git not on PATH")
308 """Staged file > threshold → advisory warning printed."""
309 _git(temp_git_project,
"checkout",
"-b",
"feature/bigfile")
310 big = temp_git_project /
"src" /
"bigfile.bin"
312 big.write_bytes(b
"\x00" * (11 * 1024 * 1024))
313 _git(temp_git_project,
"add",
"src/bigfile.bin")
315 temp_git_project,
"-m",
"feat: big file",
"--force",
318 assert "Warning:" in result.stderr
and (
"MB" in result.stderr
or "LFS" in result.stderr)
328 @pytest.mark.skipif(not _git_available(), reason=
"git not on PATH")
330 _git(temp_git_project,
"checkout",
"-b",
"feature/msg")
331 path = make_compliant_py(
"src/good.py")
332 _git(temp_git_project,
"add",
"src/good.py")
334 temp_git_project,
"-m",
"feat(auth): add validation",
336 assert result.returncode == 0
338 @pytest.mark.skipif(not _git_available(), reason=
"git not on PATH")
340 _git(temp_git_project,
"checkout",
"-b",
"feature/badmsg")
341 path = make_compliant_py(
"src/good.py")
342 _git(temp_git_project,
"add",
"src/good.py")
343 _git(temp_git_project,
"add",
"tests/test_good.py")
345 temp_git_project,
"-m",
"Updated the thing.",
347 assert result.returncode == 3
348 assert "validation failed" in result.stderr.lower()
or "Conventional Commits" in result.stderr
358 @pytest.mark.skipif(not _git_available(), reason=
"git not on PATH")
360 _git(temp_git_project,
"checkout",
"-b",
"feature/audit")
361 path = make_compliant_py(
"src/good.py")
362 _git(temp_git_project,
"add",
"src/good.py")
364 temp_git_project,
"-m",
"feat: audited commit",
366 assert result.returncode == 0
369 assert record[
"command"] ==
"oct git commit"
370 assert record[
"exit_code"] == 0
372 @pytest.mark.skipif(not _git_available(), reason=
"git not on PATH")
375 temp_git_project,
"-m",
"feat: nothing staged",
377 assert result.returncode == 4
380 assert record[
"command"] ==
"oct git commit"
381 assert record[
"exit_code"] == 4
391 @pytest.mark.skipif(not _git_available(), reason=
"git not on PATH")
393 _git(temp_git_project,
"checkout",
"-b",
"feature/json")
394 path = make_compliant_py(
"src/good.py")
395 _git(temp_git_project,
"add",
"src/good.py")
397 temp_git_project,
"-m",
"feat: json test",
399 assert data[
"exit_code"] == 0
400 assert data[
"message"] ==
"feat: json test"
401 assert "branch" in data
403 @pytest.mark.skipif(not _git_available(), reason=
"git not on PATH")
406 temp_git_project,
"-m",
"feat: nothing staged",
408 assert data[
"exit_code"] == 4
409 assert len(data[
"errors"]) > 0
419 @pytest.mark.skipif(not _git_available(), reason=
"git not on PATH")
421 self, temp_git_project, make_compliant_py,
423 _git(temp_git_project,
"checkout",
"-b",
"feature/multi-m")
424 make_compliant_py(
"src/multi.py")
425 _git(temp_git_project,
"add",
"src/multi.py")
428 "-m",
"feat: multi-paragraph subject",
429 "-m",
"Body paragraph one explains the why.",
430 "-m",
"Body paragraph two adds detail.",
432 assert result.returncode == 0, result.stderr
435 proc = subprocess.run(
436 [
"git",
"log",
"-1",
"--pretty=format:%B"],
437 cwd=temp_git_project, check=
True,
438 capture_output=
True, text=
True,
441 assert "feat: multi-paragraph subject" in body
442 assert "Body paragraph one explains the why." in body
443 assert "Body paragraph two adds detail." in body
445 assert "subject\n\nBody paragraph one" in body
446 assert "one explains the why.\n\nBody paragraph two" in body
test_failure_audit(self, temp_git_project)
test_success_audit(self, temp_git_project, make_compliant_py)
test_valid_message_accepted(self, temp_git_project, make_compliant_py)
test_invalid_message_rejected(self, temp_git_project, make_compliant_py)
test_exit_0_clean_commit(self, temp_git_project, make_compliant_py)
test_exit_2_secrets(self, temp_git_project, make_noncompliant_py)
test_exit_4_nothing_staged(self, temp_git_project)
test_exit_1_lint_failure(self, temp_git_project, make_noncompliant_py)
test_exit_3_invalid_message(self, temp_git_project, make_compliant_py)
test_force_bypasses_message_validation(self, temp_git_project, make_compliant_py)
test_force_bypasses_lint(self, temp_git_project, make_noncompliant_py)
test_json_on_failure(self, temp_git_project)
test_json_on_success(self, temp_git_project, make_compliant_py)
test_large_file_warning(self, temp_git_project)
test_no_verify_prints_warning(self, temp_git_project, make_compliant_py)
test_main_strict_blocks(self, temp_git_project, make_compliant_py)
None _disable_auto_branch(Path root)
test_feature_branch_no_block(self, temp_git_project, make_compliant_py)
test_main_compact_warns_but_proceeds(self, temp_git_project, make_compliant_py)
test_repeated_m_joins_with_blank_line(self, temp_git_project, make_compliant_py)
test_force_and_no_verify_does_not_bypass_secrets(self, temp_git_project, make_noncompliant_py)
test_force_does_not_bypass_secrets(self, temp_git_project, make_noncompliant_py)
test_no_verify_does_not_bypass_secrets(self, temp_git_project, make_noncompliant_py)
subprocess.CompletedProcess _run_commit(Path root, *str extra_args)
dict|None _latest_audit_record(Path root)
dict _run_commit_json(Path root, *str extra_args)