8Integration tests for ``oct git check`` (Phase 4B / G-B3).
9Validates all six exit codes, the profile matrix, ``--fix`` round-trip,
10``--staged-only`` / ``--all`` scope, protected-branch override,
11audit trail, and AI-Trace emission.
15- One test per exit code (0 through 5).
16- "Secrets override" test: lint + secret → exit code 5, not 1.
17- Profile matrix: ``proto``/``compact``/``strict`` × violation type.
18- ``--fix`` auto-fix then re-verify.
19- ``--fix`` cannot bypass secrets.
20- Protected-branch override forces ``strict`` despite ``--profile proto``.
21- Audit row written to ``logs/git-audit-*.jsonl`` after every run.
28 L3 — assertion details
33- Every test uses ``temp_git_project`` (real git repo on branch ``main``
35- Tests that need a non-protected branch create one explicitly.
41from pathlib
import Path
45from tests.tests_git.conftest
import _git, commit_file
54 root: Path, *extra_args: str,
55) -> subprocess.CompletedProcess:
56 """Run ``oct git check`` in a subprocess and return the result."""
57 return subprocess.run(
58 [sys.executable,
"-m",
"oct.cli",
"git",
"check", *extra_args],
66 """Run ``oct git check --json`` and return the parsed output."""
67 result =
_run_check(root,
"--json", *extra_args)
68 return json.loads(result.stdout)
72 """Create and switch to a non-protected branch."""
73 _git(root,
"checkout",
"-b", branch)
77 """Read the last JSONL record from the newest audit file."""
78 logs_dir = root /
"logs"
79 files = sorted(logs_dir.glob(
"git-audit-*.jsonl"))
82 lines = files[-1].read_text(encoding=
"utf-8").strip().splitlines()
85 return json.loads(lines[-1])
94 """One test per exit code defined in blueprint §5.3."""
97 """Clean file on a non-protected branch → exit 0."""
99 make_compliant_py(
"src/clean.py")
100 _git(temp_git_project,
"add",
"src/clean.py")
103 assert data[
"exit_code"] == 0
104 assert data[
"lint_violations"] == 0
105 assert data[
"format_violations"] == 0
106 assert data[
"secrets_findings"] == []
109 self, temp_git_project, make_noncompliant_py
111 """Lint violation (missing header) → exit 1 (lint) or 3 (lint+format).
113 A missing-header file typically fails both lint and format checks
114 since the formatter also validates the header path. Exit code is
115 1 (lint only) or 3 (lint + format) depending on the file.
118 make_noncompliant_py(
"src/bad.py",
"missing_header")
119 _git(temp_git_project,
"add",
"src/bad.py")
122 assert data[
"exit_code"]
in (1, 3)
123 assert data[
"lint_violations"] > 0
126 """Format violation (wrong path in header) → exit includes format.
128 Note: a format violation also implies a lint violation for the
129 header check, so the actual exit code will be 3 (lint + format).
130 This test seeds a format-error file and asserts exit code >= 2.
133 make_noncompliant_py(
"src/fmterr.py",
"format_error")
134 _git(temp_git_project,
"add",
"src/fmterr.py")
137 assert data[
"exit_code"]
in (2, 3)
138 assert data[
"format_violations"] > 0
141 self, temp_git_project, make_noncompliant_py
143 """Both lint and format violations → exit 3."""
145 make_noncompliant_py(
"src/both.py",
"format_error")
146 _git(temp_git_project,
"add",
"src/both.py")
150 assert data[
"lint_violations"] > 0
151 assert data[
"format_violations"] > 0
152 assert data[
"exit_code"] == 3
155 """Hardcoded secret → exit 5 (always blocking)."""
157 make_noncompliant_py(
"src/secret.py",
"hardcoded_secret")
158 _git(temp_git_project,
"add",
"src/secret.py")
161 assert data[
"exit_code"] == 5
162 assert len(data[
"secrets_findings"]) > 0
165 """A ``.env`` file in staging → exit 5 (filename pattern)."""
167 (temp_git_project /
".env").write_text(
168 "SECRET_KEY=abc123\n", encoding=
"utf-8",
170 _git(temp_git_project,
"add",
".env")
173 assert data[
"exit_code"] == 5
174 findings = data[
"secrets_findings"]
175 assert any(
"secret file pattern" in f[
"reason"]
for f
in findings)
185 self, temp_git_project, make_noncompliant_py
187 """Lint violation + secret → exit 5, not 1."""
189 make_noncompliant_py(
"src/bad_lint.py",
"missing_header")
190 make_noncompliant_py(
"src/has_secret.py",
"hardcoded_secret")
191 _git(temp_git_project,
"add",
"-A")
194 assert data[
"exit_code"] == 5
203 """Verify enforcement per profile.
205 On a non-protected branch, the CLI --profile flag controls
210 self, temp_git_project, make_noncompliant_py
212 """Proto: lint is warn-only → exit 0 despite violations."""
214 make_noncompliant_py(
"src/bad.py",
"missing_header")
215 _git(temp_git_project,
"add",
"src/bad.py")
218 temp_git_project,
"--staged-only",
"--profile",
"proto",
222 assert data[
"exit_code"] == 0
223 assert data[
"profile"] ==
"proto"
226 self, temp_git_project, make_noncompliant_py
228 """Proto: secrets are ALWAYS blocking → exit 5."""
230 make_noncompliant_py(
"src/secret.py",
"hardcoded_secret")
231 _git(temp_git_project,
"add",
"src/secret.py")
234 temp_git_project,
"--staged-only",
"--profile",
"proto",
236 assert data[
"exit_code"] == 5
239 self, temp_git_project, make_noncompliant_py
241 """Compact: lint is blocking → exit 1."""
243 make_noncompliant_py(
"src/bad.py",
"missing_header")
244 _git(temp_git_project,
"add",
"src/bad.py")
247 temp_git_project,
"--staged-only",
"--profile",
"compact",
249 assert data[
"exit_code"]
in (1, 3)
250 assert data[
"lint_violations"] > 0
253 self, temp_git_project, make_noncompliant_py
255 """Strict: all checks blocking."""
257 make_noncompliant_py(
"src/bad.py",
"missing_header")
258 _git(temp_git_project,
"add",
"src/bad.py")
261 temp_git_project,
"--staged-only",
"--profile",
"strict",
263 assert data[
"exit_code"]
in (1, 3)
264 assert data[
"lint_violations"] > 0
274 """--staged-only checks only the index, not unstaged changes."""
277 path = make_compliant_py(
"src/staged.py")
278 _git(temp_git_project,
"add",
"src/staged.py")
281 (temp_git_project /
"src" /
"dirty.py").write_text(
282 "bad\n", encoding=
"utf-8",
287 assert data[
"files_checked"] == 1
288 assert data[
"exit_code"] == 0
291 """--all checks every Python file in the project."""
293 make_compliant_py(
"src/a.py")
294 make_compliant_py(
"src/b.py")
296 temp_git_project,
"src/a.py",
297 (temp_git_project /
"src" /
"a.py").read_text(encoding=
"utf-8"),
301 temp_git_project,
"src/b.py",
302 (temp_git_project /
"src" /
"b.py").read_text(encoding=
"utf-8"),
307 assert data[
"files_checked"] >= 2
317 self, temp_git_project, make_noncompliant_py
319 """--fix auto-fixes a header violation → re-verify exit 0."""
321 make_noncompliant_py(
"src/fixme.py",
"missing_header")
322 _git(temp_git_project,
"add",
"src/fixme.py")
325 temp_git_project,
"--staged-only",
"--fix",
330 assert "exit_code" in data
333 self, temp_git_project, make_noncompliant_py
335 """--fix with a secret → still exit 5 (secrets never auto-fixed)."""
337 make_noncompliant_py(
"src/secret.py",
"hardcoded_secret")
338 _git(temp_git_project,
"add",
"src/secret.py")
341 temp_git_project,
"--staged-only",
"--fix",
343 assert data[
"exit_code"] == 5
344 assert len(data[
"secrets_findings"]) > 0
354 """On main, --profile proto is overridden to strict."""
356 make_noncompliant_py(
"src/bad.py",
"missing_header")
357 _git(temp_git_project,
"add",
"src/bad.py")
360 temp_git_project,
"--staged-only",
"--profile",
"proto",
363 assert data[
"profile"] ==
"strict"
365 assert data[
"exit_code"]
in (1, 3)
368 self, temp_git_project, make_noncompliant_py
370 """On a feature branch, --profile proto is respected."""
372 make_noncompliant_py(
"src/bad.py",
"missing_header")
373 _git(temp_git_project,
"add",
"src/bad.py")
376 temp_git_project,
"--staged-only",
"--profile",
"proto",
378 assert data[
"profile"] ==
"proto"
380 assert data[
"exit_code"] == 0
390 """Every oct git check run writes an audit JSONL row."""
392 make_compliant_py(
"src/a.py")
393 _git(temp_git_project,
"add",
"src/a.py")
398 assert record
is not None
399 assert record[
"command"] ==
"oct git check"
400 assert "exit_code" in record
401 assert "timestamp" in record
402 assert "branch" in record
405 self, temp_git_project, make_noncompliant_py
407 """Audit row is written even when checks fail."""
409 make_noncompliant_py(
"src/bad.py",
"missing_header")
410 _git(temp_git_project,
"add",
"src/bad.py")
415 assert record
is not None
416 assert record[
"checks_passed"]
is False
417 assert record[
"exit_code"] != 0
427 self, temp_git_project, make_compliant_py
429 """JSON output contains all required fields."""
431 make_compliant_py(
"src/a.py")
432 _git(temp_git_project,
"add",
"src/a.py")
436 "scope",
"profile",
"files_checked",
"lint_violations",
437 "format_violations",
"secrets_findings",
"test_failures",
438 "exit_code",
"duration_ms",
440 assert required.issubset(data.keys())
443 self, temp_git_project, make_noncompliant_py
445 """Each secrets finding has path, line, and reason."""
447 make_noncompliant_py(
"src/secret.py",
"hardcoded_secret")
448 _git(temp_git_project,
"add",
"src/secret.py")
451 for finding
in data[
"secrets_findings"]:
452 assert "path" in finding
453 assert "line" in finding
454 assert "reason" in finding
test_audit_row_on_failure(self, temp_git_project, make_noncompliant_py)
test_audit_row_written(self, temp_git_project, make_compliant_py)
test_exit_3_lint_and_format(self, temp_git_project, make_noncompliant_py)
test_exit_2_format_only(self, temp_git_project, make_noncompliant_py)
test_exit_5_secrets(self, temp_git_project, make_noncompliant_py)
test_exit_5_filename_pattern(self, temp_git_project)
test_exit_1_or_3_lint_violation(self, temp_git_project, make_noncompliant_py)
test_exit_0_all_pass(self, temp_git_project, make_compliant_py)
test_fix_cannot_bypass_secrets(self, temp_git_project, make_noncompliant_py)
test_fix_resolves_header_violation(self, temp_git_project, make_noncompliant_py)
test_json_has_required_fields(self, temp_git_project, make_compliant_py)
test_json_secrets_findings_schema(self, temp_git_project, make_noncompliant_py)
test_strict_everything_blocks(self, temp_git_project, make_noncompliant_py)
test_proto_lint_does_not_block(self, temp_git_project, make_noncompliant_py)
test_proto_secrets_still_blocks(self, temp_git_project, make_noncompliant_py)
test_compact_lint_blocks(self, temp_git_project, make_noncompliant_py)
test_main_forces_strict(self, temp_git_project, make_noncompliant_py)
test_feature_branch_respects_cli_profile(self, temp_git_project, make_noncompliant_py)
test_all_checks_everything(self, temp_git_project, make_compliant_py)
test_staged_only(self, temp_git_project, make_compliant_py)
test_secrets_overrides_lint(self, temp_git_project, make_noncompliant_py)
dict _run_check_json(Path root, *str extra_args)
subprocess.CompletedProcess _run_check(Path root, *str extra_args)
_switch_to_feature_branch(Path root, str branch="feature/test")
dict|None _latest_audit_record(Path root)