8Integration tests for ``oct git ignore`` (Phase 4G).
12- Verify pattern addition into the OCT-managed block.
13- Verify pattern removal from the OCT-managed block.
14- Verify ``--list`` partitions OCT-managed and user patterns.
15- Verify pattern validation rejects unsafe inputs.
16- Verify the same fenced block is shared with ``oct git init``.
17- Verify JSON output schema.
25 L3 -- assertion details
30- All tests use subprocess invocation against ``oct.cli``.
31- All tests use tmp_path fixtures so they are automatically cleaned up.
38from pathlib
import Path
44 return shutil.which(
"git")
is not None
48 root: Path, *extra_args: str,
49) -> subprocess.CompletedProcess:
50 """Run ``oct git ignore`` in a subprocess."""
51 return subprocess.run(
52 [sys.executable,
"-m",
"oct.cli",
"git",
"ignore", *extra_args],
66 @pytest.mark.skipif(not _git_available(), reason=
"git not on PATH")
69 assert result.returncode == 0, result.stderr
70 text = (temp_git_project /
".gitignore").read_text(encoding=
"utf-8")
71 assert "*.tmp" in text
72 assert "# --- Option C defaults ---" in text
73 assert "# --- end Option C ---" in text
75 @pytest.mark.skipif(not _git_available(), reason=
"git not on PATH")
79 assert result.returncode == 0, result.stderr
81 text = (temp_git_project /
".gitignore").read_text(encoding=
"utf-8")
82 assert text.count(
"*.tmp") == 1
84 @pytest.mark.skipif(not _git_available(), reason=
"git not on PATH")
87 temp_git_project,
"*.tmp",
"*.log",
"scratch/",
89 assert result.returncode == 0, result.stderr
90 text = (temp_git_project /
".gitignore").read_text(encoding=
"utf-8")
91 for pat
in (
"*.tmp",
"*.log",
"scratch/"):
102 @pytest.mark.skipif(not _git_available(), reason=
"git not on PATH")
105 result =
_run_ignore(temp_git_project,
"--remove",
"*.tmp")
106 assert result.returncode == 0, result.stderr
107 text = (temp_git_project /
".gitignore").read_text(encoding=
"utf-8")
108 assert "*.tmp" not in text
110 @pytest.mark.skipif(not _git_available(), reason=
"git not on PATH")
112 self, temp_git_project: Path,
117 gitignore = temp_git_project /
".gitignore"
118 existing = gitignore.read_text(encoding=
"utf-8")
119 if not existing.endswith(
"\n"):
121 gitignore.write_text(
122 existing +
"user_managed.txt\n", encoding=
"utf-8",
125 temp_git_project,
"--remove",
"user_managed.txt",
127 assert result.returncode == 1
128 assert "outside the oct-managed block" in result.stderr.lower()
130 @pytest.mark.skipif(not _git_available(), reason=
"git not on PATH")
132 self, temp_git_project: Path,
136 temp_git_project,
"--remove",
"does_not_exist.xyz",
138 assert result.returncode == 1
139 assert "not found" in result.stderr.lower()
149 @pytest.mark.skipif(not _git_available(), reason=
"git not on PATH")
151 self, temp_git_project: Path,
154 gitignore = temp_git_project /
".gitignore"
155 existing = gitignore.read_text(encoding=
"utf-8")
if gitignore.is_file()
else ""
156 gitignore.write_text(
157 existing +
"\nuser_managed.txt\n", encoding=
"utf-8",
162 result =
_run_ignore(temp_git_project,
"--list",
"--json")
163 assert result.returncode == 0, result.stderr
164 data = json.loads(result.stdout)
165 assert "*.tmp" in data[
"oct_managed"]
166 assert "user_managed.txt" in data[
"other"]
167 assert "user_managed.txt" not in data[
"oct_managed"]
177 @pytest.mark.skipif(not _git_available(), reason=
"git not on PATH")
179 result =
_run_ignore(temp_git_project,
"!important.txt")
180 assert result.returncode == 1
181 assert "negation" in result.stderr.lower()
183 @pytest.mark.skipif(not _git_available(), reason=
"git not on PATH")
185 self, temp_git_project: Path,
187 result =
_run_ignore(temp_git_project,
"/absolute")
188 assert result.returncode == 1
189 assert "absolute" in result.stderr.lower()
191 @pytest.mark.skipif(not _git_available(), reason=
"git not on PATH")
193 result =
_run_ignore(temp_git_project,
"bad pattern with spaces")
194 assert result.returncode == 1
204 @pytest.mark.skipif(not _git_available(), reason=
"git not on PATH")
206 self, temp_git_project: Path,
210 [sys.executable,
"-m",
"oct.cli",
"git",
"init"],
211 cwd=temp_git_project, capture_output=
True, text=
True,
214 result =
_run_ignore(temp_git_project,
"*.scratch")
215 assert result.returncode == 0, result.stderr
217 text = (temp_git_project /
".gitignore").read_text(encoding=
"utf-8")
219 assert text.count(
"# --- Option C defaults ---") == 1
220 assert text.count(
"# --- end Option C ---") == 1
222 assert "*.scratch" in text
232 @pytest.mark.skipif(not _git_available(), reason=
"git not on PATH")
234 result =
_run_ignore(temp_git_project,
"--json",
"*.tmp")
235 assert result.returncode == 0, result.stderr
236 data = json.loads(result.stdout)
237 assert data[
"operation"] ==
"add"
238 assert data[
"patterns_added"] == [
"*.tmp"]
239 assert data[
"exit_code"] == 0
249 @pytest.mark.skipif(not _git_available(), reason=
"git not on PATH")
252 assert result.returncode == 0, result.stderr
254 audit_dir = temp_git_project /
"logs"
255 audit_files = list(audit_dir.glob(
"git-audit-*.jsonl"))
256 assert audit_files,
"no audit file written"
259 for af
in audit_files:
260 for line
in af.read_text(encoding=
"utf-8").splitlines():
262 rec = json.loads(line)
263 except json.JSONDecodeError:
265 if rec.get(
"command") ==
"oct git ignore":
270 assert found,
"no 'oct git ignore' audit record found"
None test_appends_to_oct_block(self, Path temp_git_project)
None test_duplicate_is_noop(self, Path temp_git_project)
None test_multiple_patterns(self, Path temp_git_project)
None test_audit_record_written(self, Path temp_git_project)
None test_oct_init_and_oct_ignore_share_block(self, Path temp_git_project)
None test_add_json_schema(self, Path temp_git_project)
None test_list_partitions_correctly(self, Path temp_git_project)
None test_remove_nonexistent_pattern(self, Path temp_git_project)
None test_remove_outside_block_refused(self, Path temp_git_project)
None test_remove_from_oct_block(self, Path temp_git_project)
None test_absolute_pattern_rejected(self, Path temp_git_project)
None test_negation_rejected(self, Path temp_git_project)
None test_bad_chars_rejected(self, Path temp_git_project)
subprocess.CompletedProcess _run_ignore(Path root, *str extra_args)