8Integration tests for ``oct git commit`` per-subproject quality-gate
9fan-out (F1.3). When a workspace contains multiple subprojects and a
10single ``oct git commit`` invocation stages files across them, each
11subproject's slice must run under its own profile, the result must
12aggregate cleanly, and the actual ``git commit`` must produce one
13commit (not one per subproject).
17- One ``oct git commit`` produces exactly one git commit, even when
18 staged files span multiple subprojects.
19- A subproject's manifest profile is honoured; per-subproject octrc
21- Secrets in any subproject still trip the always-blocking G-D4
23- Files matching ``workspace_files[]`` are gated by
24 ``workspace_profile``, not by any subproject's profile.
25- Audit JSONL row carries ``per_subproject_results`` with the right
26 shape after a workspace-mode commit.
27- Single-project commit (from inside a subproject) is byte-for-byte
35 L3 -- assertion details
39- All tests use subprocess invocation against ``oct.cli``.
40- All tests use tmp_path fixtures so they are automatically cleaned up.
47from pathlib
import Path
52COMPLIANT_TEMPLATE =
'''\
54# -*- coding: utf-8 -*-
60Compliant fixture for the F1.3 workspace fan-out test suite.
64- Be linter-clean under any profile so the test can isolate
65 fan-out routing from compliance noise.
81from oc_diagnostics import _dbg
84_dbg("TEST", func, "loaded", 2)
89 return shutil.which(
"git")
is not None
92def _git(repo: Path, *args: str, check: bool =
True):
93 return subprocess.run(
94 [
"git", *args], cwd=repo, check=check,
95 capture_output=
True, text=
True,
100 cwd: Path, *extra_args: str,
101) -> subprocess.CompletedProcess:
102 return subprocess.run(
103 [sys.executable,
"-m",
"oct.cli",
"git",
"commit", *extra_args],
104 cwd=cwd, capture_output=
True, text=
True,
109 """Build a minimal Option C subproject layout under *parent*/<name>/."""
111 sub.mkdir(parents=
True, exist_ok=
True)
112 (sub /
".option_c").mkdir()
113 (sub /
"src").mkdir()
114 (sub /
"tests").mkdir()
115 (sub /
"docs").mkdir()
116 (sub /
"logs").mkdir()
117 (sub /
"oc_diagnostics").mkdir()
118 (sub /
".octrc.json").write_text(
119 json.dumps({
"linter": {
"profile": profile}}),
122 (sub /
"pyproject.toml").write_text(
123 f
'[project]\nname = "{name}"\n', encoding=
"utf-8",
125 (sub /
"debug_config.json").write_text(
126 json.dumps({
"domains": {
"GIT": {
"level": 1,
"color":
"cyan"}}}),
129 (sub /
"docs" /
"ARCHITECTURE.md").write_text(
130 "# Architecture\n", encoding=
"utf-8",
136 """Write a compliant Python file at *target / rel_path*."""
137 full = target / rel_path
138 full.parent.mkdir(parents=
True, exist_ok=
True)
140 COMPLIANT_TEMPLATE.format(rel_path=rel_path),
145 target /
"tests" / f
"test_{full.stem}.py"
147 if not test_file.exists():
148 test_file.write_text(
149 f
'"""Stub test for {full.stem}."""\n\n'
150 f
'def test_{full.stem}():\n pass\n',
157 """Build a workspace with two subprojects (alpha, beta) + initial commit.
159 Returns ``(ws_root, alpha, beta)``. Both subprojects are committed
160 in their initial state so further mutations show up cleanly in
164 pytest.skip(
"git binary not available on PATH")
166 ws_root = tmp_path /
"ws"
168 _git(ws_root,
"init",
"--quiet",
"-b",
"feature/test")
169 _git(ws_root,
"config",
"user.email",
"test@example.com")
170 _git(ws_root,
"config",
"user.name",
"Test")
171 _git(ws_root,
"config",
"commit.gpgsign",
"false")
175 "name":
"FanOut Workspace",
176 "workspace_profile":
"compact",
177 "workspace_files": [
"CLAUDE.md",
"README.md"],
179 {
"path":
"alpha",
"name":
"alpha",
"profile":
"compact"},
180 {
"path":
"beta",
"name":
"beta",
"profile":
"compact"},
183 (ws_root /
".option_c_workspace.json").write_text(
184 json.dumps(manifest, indent=2), encoding=
"utf-8",
191 (ws_root /
"CLAUDE.md").write_text(
"# CLAUDE\n", encoding=
"utf-8")
192 (ws_root /
"README.md").write_text(
"# README\n", encoding=
"utf-8")
194 _git(ws_root,
"add",
"-A")
195 _git(ws_root,
"commit",
"-m",
"initial: workspace + subprojects",
"--quiet")
197 return ws_root.resolve(), alpha.resolve(), beta.resolve()
208 self, two_subproject_workspace,
210 ws_root, alpha, beta = two_subproject_workspace
216 _git(ws_root,
"add",
"alpha/src/a.py")
217 _git(ws_root,
"add",
"beta/src/b.py")
221 ws_root,
"rev-list",
"--count",
"HEAD",
225 ws_root,
"-m",
"feat(workspace): cross-subproject stage",
227 assert result.returncode == 0, result.stderr
230 ws_root,
"rev-list",
"--count",
"HEAD",
232 assert int(after) - int(before) == 1, (
233 f
"expected exactly one new commit; got {before} -> {after}"
243 """Return the newest audit-log row matching *command_name*."""
249 candidates: list[Path] = []
250 for log_dir
in repo_root.rglob(
"logs"):
252 candidates.extend(log_dir.glob(
"git-audit-*.jsonl"))
255 newest = max(candidates, key=
lambda p: p.stat().st_mtime)
256 rows: list[dict] = []
257 for line
in newest.read_text(encoding=
"utf-8").splitlines():
259 rows.append(json.loads(line))
260 except json.JSONDecodeError:
262 for row
in reversed(rows):
263 if row.get(
"command") == command_name:
271 self, two_subproject_workspace,
273 ws_root, alpha, beta = two_subproject_workspace
276 _git(ws_root,
"add",
"alpha/src/a.py")
277 _git(ws_root,
"add",
"beta/src/b.py")
280 ws_root,
"-m",
"feat(workspace): audit shape test",
282 assert result.returncode == 0, result.stderr
285 assert row
is not None,
"no audit row found for oct git commit"
286 psr = row.get(
"per_subproject_results")
287 assert isinstance(psr, list)
288 names = {entry[
"name"]
for entry
in psr}
289 assert "alpha" in names
290 assert "beta" in names
293 "name",
"profile",
"files",
294 "lint_violations",
"format_violations",
"secrets",
296 assert key
in entry, (
297 f
"per_subproject_results entry missing {key!r}: {entry}"
309 self, two_subproject_workspace,
311 ws_root, _alpha, _beta = two_subproject_workspace
313 (ws_root /
"CLAUDE.md").write_text(
314 "# CLAUDE — modified\n", encoding=
"utf-8",
316 _git(ws_root,
"add",
"CLAUDE.md")
319 ws_root,
"-m",
"docs: tweak CLAUDE.md",
321 assert result.returncode == 0, result.stderr
324 assert row
is not None
325 psr = row.get(
"per_subproject_results", [])
326 names = {entry[
"name"]
for entry
in psr}
327 assert "workspace" in names, (
328 f
"expected workspace bucket in per_subproject_results: {psr}"
331 ws_entry = next(e
for e
in psr
if e[
"name"] ==
"workspace")
332 assert ws_entry[
"files"] == 1
343 self, two_subproject_workspace,
345 _ws_root, alpha, _beta = two_subproject_workspace
347 _git(alpha,
"add",
"src/x.py")
350 alpha,
"-m",
"feat(alpha): standalone commit",
352 assert result.returncode == 0, result.stderr
355 assert row
is not None
357 assert row.get(
"per_subproject_results", []) == []
368 self, two_subproject_workspace,
370 ws_root, _alpha, _beta = two_subproject_workspace
372 (ws_root /
"README.md").write_text(
373 "# README — modified\n", encoding=
"utf-8",
375 _git(ws_root,
"add",
"README.md")
378 ws_root,
"-m",
"docs: tweak README",
380 assert result.returncode == 0, result.stderr
383 assert row
is not None
384 psr = row.get(
"per_subproject_results", [])
386 names = {entry[
"name"]
for entry
in psr}
387 assert names == {
"workspace"}, (
388 f
"expected only workspace bucket; got {names}"
None test_per_subproject_results_field_populated(self, two_subproject_workspace)
None test_workspace_files_only_no_subproject_buckets(self, two_subproject_workspace)
None test_single_commit_for_cross_subproject_stage(self, two_subproject_workspace)
None test_commit_inside_subproject_no_fanout(self, two_subproject_workspace)
None test_claudemd_routed_to_workspace_bucket(self, two_subproject_workspace)
subprocess.CompletedProcess _run_commit(Path cwd, *str extra_args)
None _write_compliant(Path target, str rel_path)
dict|None _read_latest_audit(Path repo_root, str command_name)
_git(Path repo, *str args, bool check=True)
Path _make_subproject(Path parent, str name, str profile="compact")
two_subproject_workspace(Path tmp_path)