8Integration tests for ``oct git add`` (Phase 4G).
12- Verify whole-file staging stages the requested paths.
13- Verify ``--update`` and ``--all`` behave like ``git add -u`` / ``-A``.
14- Verify ``--dry-run`` does not modify the index.
15- Verify ``--patch`` constructs the correct argv (passthrough is mocked).
16- Verify mutual exclusivity rules.
17- Verify JSON output schema.
18- Verify the post-stage quality gate advisory.
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
42from tests.tests_git.conftest
import _git
46 return shutil.which(
"git")
is not None
49def _run_add(root: Path, *extra_args: str) -> subprocess.CompletedProcess:
50 """Run ``oct git add`` in a subprocess."""
51 return subprocess.run(
52 [sys.executable,
"-m",
"oct.cli",
"git",
"add", *extra_args],
60 """Return file names currently staged (index)."""
61 result = subprocess.run(
62 [
"git",
"diff",
"--cached",
"--name-only"],
63 cwd=repo, check=
True, capture_output=
True, text=
True,
66 line.strip()
for line
in result.stdout.splitlines()
if line.strip()
77 @pytest.mark.skipif(not _git_available(), reason=
"git not on PATH")
79 (temp_git_project /
"src" /
"a.py").write_text(
80 "x = 1\n", encoding=
"utf-8",
82 result =
_run_add(temp_git_project,
"src/a.py")
83 assert result.returncode == 0, result.stderr
89 @pytest.mark.skipif(not _git_available(), reason=
"git not on PATH")
91 for name
in (
"a.py",
"b.py",
"c.py"):
92 (temp_git_project /
"src" / name).write_text(
93 "x = 1\n", encoding=
"utf-8",
96 temp_git_project,
"src/a.py",
"src/b.py",
"src/c.py",
98 assert result.returncode == 0, result.stderr
100 for name
in (
"src/a.py",
"src/b.py",
"src/c.py"):
106 @pytest.mark.skipif(not _git_available(), reason=
"git not on PATH")
108 self, temp_git_project: Path,
111 (temp_git_project /
"src" /
"tracked.py").write_text(
112 "x = 1\n", encoding=
"utf-8",
114 _git(temp_git_project,
"add",
"src/tracked.py")
115 _git(temp_git_project,
"commit",
"-m",
"add tracked",
"--quiet")
118 (temp_git_project /
"src" /
"tracked.py").write_text(
119 "x = 2\n", encoding=
"utf-8",
122 (temp_git_project /
"src" /
"untracked.py").write_text(
123 "x = 3\n", encoding=
"utf-8",
126 result =
_run_add(temp_git_project,
"--update")
127 assert result.returncode == 0, result.stderr
129 assert "src/tracked.py" in names
130 assert "src/untracked.py" not in names
135 @pytest.mark.skipif(not _git_available(), reason=
"git not on PATH")
137 (temp_git_project /
"src" /
"new.py").write_text(
138 "x = 1\n", encoding=
"utf-8",
140 result =
_run_add(temp_git_project,
"--all")
141 assert result.returncode == 0, result.stderr
152 @pytest.mark.skipif(not _git_available(), reason=
"git not on PATH")
154 (temp_git_project /
"src" /
"a.py").write_text(
155 "x = 1\n", encoding=
"utf-8",
157 result =
_run_add(temp_git_project,
"--dry-run",
"src/a.py")
158 assert result.returncode == 0, result.stderr
159 assert "Would stage" in result.stdout
171 @pytest.mark.skipif(not _git_available(), reason=
"git not on PATH")
173 self, temp_git_project: Path, tmp_path: Path,
176 outside = tmp_path /
"outside.py"
177 outside.write_text(
"x = 1\n", encoding=
"utf-8")
178 result =
_run_add(temp_git_project, str(outside))
180 assert result.returncode == 1
181 assert "outside project scope" in result.stderr.lower()
191 @pytest.mark.skipif(not _git_available(), reason=
"git not on PATH")
193 (temp_git_project /
"src" /
"a.py").write_text(
194 "x = 1\n", encoding=
"utf-8",
196 result =
_run_add(temp_git_project,
"--json",
"src/a.py")
197 assert result.returncode == 0, result.stderr
198 data = json.loads(result.stdout)
199 for key
in (
"operation",
"paths_staged",
"paths_skipped",
200 "post_check",
"exit_code",
"errors"):
201 assert key
in data, f
"missing key: {key}"
202 assert data[
"operation"] ==
"add"
203 assert data[
"exit_code"] == 0
213 @pytest.mark.skipif(not _git_available(), reason=
"git not on PATH")
215 self, temp_git_project: Path,
217 result =
_run_add(temp_git_project,
"--patch",
"--all")
218 assert result.returncode == 1
219 assert "cannot be combined" in result.stderr.lower()
221 @pytest.mark.skipif(not _git_available(), reason=
"git not on PATH")
223 self, temp_git_project: Path,
225 result =
_run_add(temp_git_project,
"--update",
"--all")
226 assert result.returncode == 1
227 assert "mutually exclusive" in result.stderr.lower()
237 @pytest.mark.skipif(not _git_available(), reason=
"git not on PATH")
239 self, temp_git_project: Path,
242 assert result.returncode == 1
243 assert "no paths to stage" in result.stderr.lower()
253 @pytest.mark.skipif(not _git_available(), reason=
"git not on PATH")
255 self, temp_git_project: Path,
260 ignored_dir = temp_git_project /
"src" /
"build_artefacts"
261 ignored_dir.mkdir(parents=
True, exist_ok=
True)
262 f = ignored_dir /
"tracked.txt"
263 f.write_text(
"x\n", encoding=
"utf-8")
264 _git(temp_git_project,
"add",
"src/build_artefacts/tracked.txt")
266 temp_git_project,
"commit",
"-m",
"track artefact",
"--quiet",
270 gitignore = temp_git_project /
".gitignore"
272 gitignore.read_text(encoding=
"utf-8")
273 if gitignore.is_file()
else ""
275 gitignore.write_text(
276 existing +
"\nsrc/build_artefacts/\n",
279 f.write_text(
"modified\n", encoding=
"utf-8")
282 temp_git_project,
"src/build_artefacts/tracked.txt",
285 assert result.returncode == 0, (
286 f
"L2: expected exit 0 on staged-with-warning; "
287 f
"got {result.returncode}\nstderr: {result.stderr}"
302 @pytest.mark.skipif(not _git_available(), reason=
"git not on PATH")
304 (temp_git_project /
"src" /
"a.py").write_text(
305 "x = 1\n", encoding=
"utf-8",
307 result =
_run_add(temp_git_project,
"src/a.py")
308 assert result.returncode == 0, result.stderr
311 audit_dir = temp_git_project /
"logs"
312 assert audit_dir.is_dir()
313 audit_files = list(audit_dir.glob(
"git-audit-*.jsonl"))
314 assert audit_files,
"no audit file written"
317 for af
in audit_files:
318 for line
in af.read_text(encoding=
"utf-8").splitlines():
320 rec = json.loads(line)
321 except json.JSONDecodeError:
323 if rec.get(
"command") ==
"oct git add":
328 assert found,
"no 'oct git add' audit record found"
None test_all_stages_untracked(self, Path temp_git_project)
None test_audit_record_written(self, Path temp_git_project)
None test_dry_run_does_not_stage(self, Path temp_git_project)
None test_tracked_file_in_ignored_dir_exits_zero(self, Path temp_git_project)
None test_json_schema(self, Path temp_git_project)
None test_stages_three_files(self, Path temp_git_project)
None test_update_with_all_rejected(self, Path temp_git_project)
None test_patch_with_all_rejected(self, Path temp_git_project)
None test_no_paths_no_flags_errors(self, Path temp_git_project)
None test_outside_path_skipped_with_warning(self, Path temp_git_project, Path tmp_path)
None test_stages_one_file(self, Path temp_git_project)
None test_update_only_stages_tracked(self, Path temp_git_project)
subprocess.CompletedProcess _run_add(Path root, *str extra_args)
list[str] _staged_names(Path repo)