8Integration tests for ``oct git init`` (Phase 4C — G-C1/G-C3).
12- Verify init bootstraps a git repo on a bare directory.
13- Verify .gitignore merge logic (append-only, no duplicates).
14- Verify .gitattributes creation and merge.
15- Verify --dry-run, --no-hooks, --github flags.
16- Verify idempotent re-runs.
17- Verify audit trail is written.
24 L3 — assertion details
29- All tests use tmp_path so they are automatically cleaned up.
36from pathlib
import Path
47 return shutil.which(
"git")
is not None
50def _git(repo: Path, *args: str, check: bool =
True):
51 return subprocess.run(
60def _run_init(root: Path, *extra_args: str) -> subprocess.CompletedProcess:
61 """Run ``oct git init`` in a subprocess with *root* as cwd."""
62 return subprocess.run(
63 [sys.executable,
"-m",
"oct.cli",
"git",
"init", *extra_args],
71 """Create a directory with minimal Option C project structure.
73 Includes docs/, tests/, oc_diagnostics/ so find_project_root works.
75 root = tmp_path / name
77 (root /
"docs").mkdir()
78 (root /
"tests").mkdir()
79 (root /
"oc_diagnostics").mkdir()
80 (root /
"logs").mkdir()
81 (root /
"src").mkdir()
82 (root /
"pyproject.toml").write_text(
83 f
'[project]\nname = "{name}"\n', encoding=
"utf-8",
89 """Create an Option C project inside a real git repo with one commit."""
91 _git(root,
"init",
"--quiet",
"-b",
"main")
92 _git(root,
"config",
"user.email",
"test@example.com")
93 _git(root,
"config",
"user.name",
"Test")
94 _git(root,
"config",
"commit.gpgsign",
"false")
95 _git(root,
"add",
"-A")
96 _git(root,
"commit",
"-m",
"initial",
"--quiet")
106 """Init on a directory that is not yet a git repo."""
108 @pytest.mark.skipif(not _git_available(), reason=
"git not on PATH")
112 assert result.returncode == 0, result.stderr
113 assert (root /
".git").is_dir()
115 @pytest.mark.skipif(not _git_available(), reason=
"git not on PATH")
119 assert result.returncode == 0, result.stderr
120 gi = root /
".gitignore"
122 content = gi.read_text(encoding=
"utf-8")
123 assert "__pycache__/" in content
124 assert ".env" in content
126 @pytest.mark.skipif(not _git_available(), reason=
"git not on PATH")
130 assert result.returncode == 0, result.stderr
131 ga = root /
".gitattributes"
133 content = ga.read_text(encoding=
"utf-8")
134 assert "*.py text eol=lf" in content
135 assert "*.pem binary" in content
136 assert "*.key binary" in content
138 @pytest.mark.skipif(not _git_available(), reason=
"git not on PATH")
142 content = (root /
".gitignore").read_text(encoding=
"utf-8")
143 assert "# --- Option C defaults ---" in content
144 assert "# --- end Option C ---" in content
153 """Running init twice should not duplicate entries."""
155 @pytest.mark.skipif(not _git_available(), reason=
"git not on PATH")
159 first_gi = (root /
".gitignore").read_text(encoding=
"utf-8")
163 assert result.returncode == 0
164 second_gi = (root /
".gitignore").read_text(encoding=
"utf-8")
165 assert first_gi == second_gi
167 @pytest.mark.skipif(not _git_available(), reason=
"git not on PATH")
172 assert "already up-to-date" in result.stdout
181 """Merge preserves existing entries and only appends missing ones."""
183 @pytest.mark.skipif(not _git_available(), reason=
"git not on PATH")
186 existing =
"# My custom ignores\nnode_modules/\n*.log\n"
187 (root /
".gitignore").write_text(existing, encoding=
"utf-8")
190 content = (root /
".gitignore").read_text(encoding=
"utf-8")
191 assert "node_modules/" in content
192 assert "*.log" in content
193 assert "__pycache__/" in content
195 @pytest.mark.skipif(not _git_available(), reason=
"git not on PATH")
199 (root /
".gitignore").write_text(
200 "__pycache__/\n.venv/\n", encoding=
"utf-8",
203 content = (root /
".gitignore").read_text(encoding=
"utf-8")
205 assert content.count(
"__pycache__/") == 1
214 """Merge creates or extends .gitattributes."""
216 @pytest.mark.skipif(not _git_available(), reason=
"git not on PATH")
220 ga = root /
".gitattributes"
222 content = ga.read_text(encoding=
"utf-8")
223 assert "*.py text eol=lf" in content
225 @pytest.mark.skipif(not _git_available(), reason=
"git not on PATH")
228 (root /
".gitattributes").write_text(
229 "*.py text eol=lf\n", encoding=
"utf-8",
232 content = (root /
".gitattributes").read_text(encoding=
"utf-8")
233 assert "*.pem binary" in content
235 assert content.count(
"*.py text eol=lf") == 1
245 @pytest.mark.skipif(not _git_available(), reason=
"git not on PATH")
249 assert result.returncode == 0
250 assert "[DRY RUN]" in result.stdout
251 assert not (root /
".gitignore").is_file()
252 assert not (root /
".gitattributes").is_file()
254 @pytest.mark.skipif(not _git_available(), reason=
"git not on PATH")
258 assert result.returncode == 0
259 assert "[DRY RUN]" in result.stdout
269 @pytest.mark.skipif(not _git_available(), reason=
"git not on PATH")
273 assert result.returncode == 0
274 assert not (root /
".pre-commit-config.yaml").is_file()
275 assert "Skipped hook installation" in result.stdout
277 @pytest.mark.skipif(not _git_available(), reason=
"git not on PATH")
281 assert result.returncode == 0
282 assert (root /
".pre-commit-config.yaml").is_file()
292 @pytest.mark.skipif(not _git_available(), reason=
"git not on PATH")
296 assert result.returncode == 0
297 wf = root /
".github" /
"workflows" /
"option-c.yml"
299 content = wf.read_text(encoding=
"utf-8")
300 assert "oct lint" in content
301 assert "oct format" in content
303 @pytest.mark.skipif(not _git_available(), reason=
"git not on PATH")
307 assert not (root /
".github").exists()
309 @pytest.mark.skipif(not _git_available(), reason=
"git not on PATH")
313 first = (root /
".github" /
"workflows" /
"option-c.yml").read_text(
317 assert "already exists" in result.stdout
318 second = (root /
".github" /
"workflows" /
"option-c.yml").read_text(
321 assert first == second
331 @pytest.mark.skipif(not _git_available(), reason=
"git not on PATH")
335 assert result.returncode == 0
337 audit_files = list((root /
"logs").glob(
"oct_git_audit*.jsonl"))
339 content = audit_files[0].read_text(encoding=
"utf-8")
340 assert "oct git init" in content
test_audit_record_written(self, tmp_path)
test_dry_run_on_existing_repo(self, tmp_path)
test_no_files_created(self, tmp_path)
test_creates_workflow(self, tmp_path)
test_no_workflow_without_flag(self, tmp_path)
test_workflow_idempotent(self, tmp_path)
test_merges_existing(self, tmp_path)
test_creates_from_scratch(self, tmp_path)
test_preserves_existing_entries(self, tmp_path)
test_skips_already_present_entries(self, tmp_path)
test_no_duplicates_on_rerun(self, tmp_path)
test_reports_up_to_date(self, tmp_path)
test_gitignore_has_marker_comments(self, tmp_path)
test_creates_gitattributes(self, tmp_path)
test_creates_gitignore(self, tmp_path)
test_creates_git_repo(self, tmp_path)
test_skips_hooks(self, tmp_path)
test_hooks_installed_by_default(self, tmp_path)
Path _make_git_project(Path tmp_path, str name="existing")
Path _make_option_c_dir(Path tmp_path, str name="fresh")
subprocess.CompletedProcess _run_init(Path root, *str extra_args)
_git(Path repo, *str args, bool check=True)