claude-dev-env 2.36.0 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/.agents/skills/_shared/advisor/scripts/README.md +1 -1
  2. package/.agents/skills/syncing-submodules/SKILL.md +101 -0
  3. package/.agents/skills/syncing-submodules/reference/run-tasks.md +9 -0
  4. package/.agents/skills/syncing-submodules/scripts/submodule_sync.py +383 -0
  5. package/.agents/skills/syncing-submodules/scripts/submodule_sync_constants/__init__.py +1 -0
  6. package/.agents/skills/syncing-submodules/scripts/submodule_sync_constants/config/__init__.py +1 -0
  7. package/.agents/skills/syncing-submodules/scripts/submodule_sync_constants/config/constants.py +66 -0
  8. package/.agents/skills/syncing-submodules/scripts/sync_parent_pointer.py +88 -0
  9. package/.agents/skills/syncing-submodules/scripts/test_submodule_sync.py +303 -0
  10. package/.agents/skills/syncing-submodules/scripts/test_sync_parent_pointer.py +83 -0
  11. package/.agents/skills/team-advisor/SKILL.md +11 -11
  12. package/_shared/advisor/advisor-protocol.md +51 -51
  13. package/_shared/advisor/reference/advisor-block.md +9 -9
  14. package/_shared/advisor/reference/astra-rung.md +39 -0
  15. package/_shared/advisor/reference/identity.md +4 -4
  16. package/_shared/advisor/reference/lifecycle.md +6 -6
  17. package/_shared/advisor/reference/spawn-walk-log.md +9 -9
  18. package/_shared/advisor/reference/third-party-bind.md +11 -11
  19. package/_shared/advisor/reference/warm-up.md +13 -13
  20. package/_shared/advisor/scripts/codex_astra_advisor.py +308 -0
  21. package/_shared/advisor/scripts/codex_astra_preflight.py +115 -0
  22. package/_shared/advisor/scripts/codex_astra_reply.py +159 -0
  23. package/_shared/advisor/scripts/config/advisor_scripts_constants/advisor_route_constants.py +3 -3
  24. package/_shared/advisor/scripts/config/advisor_scripts_constants/astra_advisor_constants.py +38 -0
  25. package/_shared/advisor/scripts/model_tier_run_validator.py +17 -18
  26. package/_shared/advisor/scripts/tests/test_codex_astra_advisor.py +271 -0
  27. package/_shared/advisor/scripts/tests/test_codex_astra_preflight.py +24 -0
  28. package/_shared/advisor/scripts/tests/test_codex_astra_reply.py +42 -0
  29. package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +55 -30
  30. package/_shared/advisor/scripts/tests/test_tier_model_ids.py +14 -4
  31. package/_shared/pr-loop/scripts/pr_loop_shared_constants/terminology_sweep_constants.py +14 -0
  32. package/_shared/pr-loop/scripts/terminology_sweep.py +138 -50
  33. package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +13 -1
  34. package/bin/ever-shipped-skills.mjs +1 -0
  35. package/bin/ever-shipped-skills.test.mjs +30 -0
  36. package/bin/git_hooks_installer.mjs +0 -1
  37. package/bin/git_hooks_installer.test.mjs +16 -1
  38. package/bin/install.mjs +5 -2
  39. package/bin/install.prune.test.mjs +36 -24
  40. package/bin/install.test.mjs +7 -0
  41. package/docs/references/team-advisor-skill.md +3 -3
  42. package/hooks/validators/README.md +35 -1
  43. package/package.json +1 -1
  44. package/scripts/check.ps1 +18 -6
  45. package/scripts/policy_lint/adapter_detectors.py +42 -2
  46. package/scripts/policy_lint/adapters.py +24 -0
  47. package/scripts/policy_lint/config/constants.py +11 -0
  48. package/scripts/policy_lint/engine.py +1 -1
  49. package/scripts/policy_lint/model.py +1 -0
  50. package/scripts/policy_lint/registry.py +10 -1
  51. package/scripts/repository_checks/__init__.py +1 -0
  52. package/scripts/repository_checks/claude_md.py +108 -0
  53. package/scripts/repository_checks/config/__init__.py +1 -0
  54. package/scripts/repository_checks/config/constants.py +150 -0
  55. package/scripts/repository_checks/env_var_documentation.py +243 -0
  56. package/scripts/repository_checks/hook_modules.py +34 -0
  57. package/scripts/repository_checks/models.py +37 -0
  58. package/scripts/repository_checks/package_inventory.py +118 -0
  59. package/scripts/repository_checks/pytest_testpaths.py +84 -0
  60. package/scripts/repository_checks/reporting.py +39 -0
  61. package/scripts/repository_checks/runner.py +93 -0
  62. package/scripts/repository_checks/tracked_secrets.py +147 -0
  63. package/scripts/repository_checks/tracked_tree.py +28 -0
  64. package/scripts/repository_policy.py +120 -0
  65. package/scripts/tests/repository_policy_test_support.py +89 -0
  66. package/scripts/tests/test_cde_lint.py +25 -1
  67. package/scripts/tests/test_claude_md.py +68 -0
  68. package/scripts/tests/test_env_var_documentation.py +133 -0
  69. package/scripts/tests/test_hook_modules.py +17 -0
  70. package/scripts/tests/test_models.py +26 -0
  71. package/scripts/tests/test_package_inventory.py +96 -0
  72. package/scripts/tests/test_policy_lint_rules_registry.py +117 -0
  73. package/scripts/tests/test_pytest_testpaths.py +66 -0
  74. package/scripts/tests/test_reporting.py +54 -0
  75. package/scripts/tests/test_repository_policy.py +71 -0
  76. package/scripts/tests/test_runner.py +59 -0
  77. package/scripts/tests/test_tracked_secrets.py +175 -0
  78. package/scripts/tests/test_tracked_tree.py +28 -0
  79. package/_shared/advisor/reference/sol-rung.md +0 -39
  80. package/_shared/advisor/scripts/codex_sol_advisor.py +0 -581
  81. package/_shared/advisor/scripts/config/advisor_scripts_constants/sol_advisor_constants.py +0 -38
  82. package/_shared/advisor/scripts/tests/test_codex_sol_advisor.py +0 -740
  83. package/hooks/git-hooks/post_commit.py +0 -241
  84. package/hooks/git-hooks/test_post_commit.py +0 -298
@@ -1,241 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Git post-commit hook: Auto-update parent repos when committing in a submodule.
4
-
5
- When you commit in a submodule, this hook:
6
- 1. Detects if current repo is a submodule of a parent
7
- 2. Stages the submodule update in the parent
8
- 3. Creates a commit in the parent pointing to the new submodule commit
9
-
10
- This prevents the "lost work" issue where submodule commits aren't tracked by parent.
11
- """
12
-
13
- from __future__ import annotations
14
-
15
- import subprocess
16
- import sys
17
- from enum import StrEnum
18
- from pathlib import Path
19
-
20
- from git_hooks_constants import (
21
- GH_EXECUTABLE_NAME,
22
- GH_PR_VIEW_ARGUMENTS,
23
- GH_PR_VIEW_TIMEOUT_SECONDS,
24
- GIT_COMMAND_SUCCESS_EXIT_CODE,
25
- GIT_EXECUTABLE_NAME,
26
- )
27
- from pull_request_handoff import build_pull_request_reminder
28
-
29
-
30
- class ParentPointerStatus(StrEnum):
31
- """States returned by a parent pointer update."""
32
-
33
- UPDATED = "updated"
34
- UNCHANGED = "unchanged"
35
- FAILED = "failed"
36
-
37
-
38
- ParentPointerUpdate = tuple[ParentPointerStatus, str]
39
-
40
-
41
- def execute_git(
42
- *arguments: str,
43
- cwd: Path,
44
- ) -> subprocess.CompletedProcess[str]:
45
- """Run a Git command and return its completed process."""
46
- try:
47
- return subprocess.run(
48
- [GIT_EXECUTABLE_NAME, *arguments],
49
- cwd=cwd,
50
- check=False,
51
- capture_output=True,
52
- text=True,
53
- )
54
- except OSError as error:
55
- return subprocess.CompletedProcess(
56
- [GIT_EXECUTABLE_NAME, *arguments],
57
- 1,
58
- "",
59
- str(error),
60
- )
61
-
62
-
63
- def run_git(*arguments: str, cwd: Path) -> str:
64
- """Run a successful Git command and return its trimmed standard output."""
65
- command_result = execute_git(*arguments, cwd=cwd)
66
- if command_result.returncode != GIT_COMMAND_SUCCESS_EXIT_CODE:
67
- return ""
68
- return command_result.stdout.strip()
69
-
70
-
71
- def run_git_from_current_directory(*arguments: str) -> str:
72
- """Run a successful Git command from the hook's current directory."""
73
- return run_git(*arguments, cwd=Path.cwd())
74
-
75
-
76
- def find_parent_repo(repo_dir: Path) -> Path | None:
77
- """Return the Git superproject that owns the current repository."""
78
- parent_path_text = run_git(
79
- "rev-parse",
80
- "--show-superproject-working-tree",
81
- cwd=repo_dir,
82
- )
83
- if not parent_path_text:
84
- return None
85
-
86
- parent_path = Path(parent_path_text).resolve()
87
- if not parent_path.is_dir():
88
- return None
89
- return parent_path
90
-
91
-
92
- def find_submodule_path(parent_repo: Path, repo_dir: Path) -> Path | None:
93
- """Return the submodule path relative to its parent repository."""
94
- try:
95
- return repo_dir.resolve().relative_to(parent_repo.resolve())
96
- except ValueError:
97
- return None
98
-
99
-
100
- def build_literal_pathspec(submodule_path: Path) -> str:
101
- """Build a Git pathspec that treats every path character literally."""
102
- return f":(literal){submodule_path.as_posix()}"
103
-
104
-
105
- def get_git_failure_diagnostic(
106
- command_result: subprocess.CompletedProcess[str],
107
- ) -> str:
108
- """Return the most useful diagnostic from a failed Git command."""
109
- return command_result.stderr.strip() or command_result.stdout.strip()
110
-
111
-
112
- def build_parent_commit_message(
113
- repo_name: str,
114
- commit_hash: str,
115
- commit_message: str,
116
- ) -> str:
117
- """Build the parent commit message for a submodule pointer update."""
118
- return (
119
- f"chore: update {repo_name} submodule to {commit_hash}\n\n"
120
- f"Submodule commit: {commit_message}\n\n"
121
- "Co-Authored-By: Claude <noreply@anthropic.com>"
122
- )
123
-
124
-
125
- def update_parent_pointer(
126
- parent_repo: Path,
127
- repo_dir: Path,
128
- commit_hash: str,
129
- commit_message: str,
130
- ) -> ParentPointerUpdate:
131
- """Commit the current submodule pointer in its parent repository."""
132
- submodule_path = find_submodule_path(parent_repo, repo_dir)
133
- if submodule_path is None:
134
- return ParentPointerStatus.FAILED, "could not resolve the submodule path"
135
-
136
- submodule_reference = build_literal_pathspec(submodule_path)
137
- add_result = execute_git("add", "--", submodule_reference, cwd=parent_repo)
138
- if add_result.returncode != GIT_COMMAND_SUCCESS_EXIT_CODE:
139
- return ParentPointerStatus.FAILED, get_git_failure_diagnostic(add_result)
140
-
141
- staged_difference = execute_git(
142
- "diff",
143
- "--cached",
144
- "--quiet",
145
- "--",
146
- submodule_reference,
147
- cwd=parent_repo,
148
- )
149
- if staged_difference.returncode == GIT_COMMAND_SUCCESS_EXIT_CODE:
150
- return ParentPointerStatus.UNCHANGED, ""
151
- if staged_difference.returncode != 1:
152
- return ParentPointerStatus.FAILED, get_git_failure_diagnostic(staged_difference)
153
-
154
- parent_commit_message = build_parent_commit_message(
155
- repo_dir.name,
156
- commit_hash,
157
- commit_message,
158
- )
159
- commit_result = execute_git(
160
- "commit",
161
- "--only",
162
- "-m",
163
- parent_commit_message,
164
- "--",
165
- submodule_reference,
166
- cwd=parent_repo,
167
- )
168
- if commit_result.returncode != GIT_COMMAND_SUCCESS_EXIT_CODE:
169
- return ParentPointerStatus.FAILED, get_git_failure_diagnostic(commit_result)
170
- return ParentPointerStatus.UPDATED, ""
171
-
172
-
173
- def print_pull_request_reminder(repo_dir: Path) -> None:
174
- """Print a reminder when the committed repository has an open pull request."""
175
- try:
176
- result = subprocess.run(
177
- [GH_EXECUTABLE_NAME, *GH_PR_VIEW_ARGUMENTS],
178
- cwd=repo_dir,
179
- check=False,
180
- capture_output=True,
181
- text=True,
182
- timeout=GH_PR_VIEW_TIMEOUT_SECONDS,
183
- )
184
- except (OSError, subprocess.TimeoutExpired):
185
- return
186
- pull_request_url = result.stdout.strip()
187
- if result.returncode == 0 and pull_request_url:
188
- print(build_pull_request_reminder(pull_request_url))
189
-
190
-
191
- def main() -> int:
192
- """Update a parent repository after a submodule commit."""
193
- repo_path_text = run_git_from_current_directory("rev-parse", "--show-toplevel")
194
- if not repo_path_text:
195
- return 0
196
-
197
- repo_dir = Path(repo_path_text).resolve()
198
- parent_repo = find_parent_repo(repo_dir)
199
- if parent_repo is None:
200
- print_pull_request_reminder(repo_dir)
201
- return 0
202
-
203
- commit_msg = run_git("log", "-1", "--pretty=%s", cwd=repo_dir)
204
- commit_hash = run_git("rev-parse", "HEAD", cwd=repo_dir)
205
- short_commit_hash = run_git("rev-parse", "--short", "HEAD", cwd=repo_dir)
206
- if not commit_hash or not short_commit_hash:
207
- print_pull_request_reminder(repo_dir)
208
- return 0
209
-
210
- print()
211
- print("=== Submodule Parent Update ===")
212
- print(f"Submodule: {repo_dir.name} @ {short_commit_hash}")
213
- print(f"Parent: {parent_repo}")
214
-
215
- parent_pointer_status, parent_pointer_diagnostic = update_parent_pointer(
216
- parent_repo,
217
- repo_dir,
218
- commit_hash,
219
- commit_msg,
220
- )
221
- if parent_pointer_status is ParentPointerStatus.FAILED:
222
- print("Parent update failed.")
223
- if parent_pointer_diagnostic:
224
- print(f"Git diagnostic: {parent_pointer_diagnostic}")
225
- print_pull_request_reminder(repo_dir)
226
- return 0
227
- if parent_pointer_status is ParentPointerStatus.UNCHANGED:
228
- print("Parent already up to date.")
229
- print_pull_request_reminder(repo_dir)
230
- return 0
231
-
232
- print("Parent updated successfully.")
233
- print("================================")
234
- print()
235
- print_pull_request_reminder(repo_dir)
236
-
237
- return 0
238
-
239
-
240
- if __name__ == "__main__":
241
- sys.exit(main())
@@ -1,298 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import os
4
- import subprocess
5
- from pathlib import Path
6
-
7
- import post_commit
8
- import pytest
9
- from git_hooks_constants import GIT_EXECUTABLE_NAME
10
-
11
-
12
- def build_fixture_git_environment() -> dict[str, str]:
13
- """Copy the process environment without inherited Git state overrides."""
14
- return {
15
- each_name: each_value
16
- for each_name, each_value in os.environ.items()
17
- if not each_name.upper().startswith("GIT_")
18
- }
19
-
20
-
21
- def clear_inherited_git_environment(monkeypatch: pytest.MonkeyPatch) -> None:
22
- """Remove inherited Git state overrides from the current test process."""
23
- for each_name in list(os.environ):
24
- if each_name.upper().startswith("GIT_"):
25
- monkeypatch.delenv(each_name, raising=False)
26
-
27
-
28
- def test_fixture_git_environment_preserves_process_execution_path(
29
- monkeypatch: pytest.MonkeyPatch,
30
- ) -> None:
31
- """The fixture environment keeps PATH while removing Git overrides."""
32
- monkeypatch.setenv("PATH", "fixture-execution-path")
33
- monkeypatch.setenv("GIT_DIR", "unrelated-repository")
34
-
35
- fixture_environment = build_fixture_git_environment()
36
-
37
- assert fixture_environment["PATH"] == "fixture-execution-path"
38
- assert all(not each_name.upper().startswith("GIT_") for each_name in fixture_environment)
39
-
40
-
41
- def run_fixture_git(repository_path: Path, *arguments: str) -> str:
42
- """Run Git in a fixture repository and return standard output."""
43
- completed_process = subprocess.run(
44
- [GIT_EXECUTABLE_NAME, *arguments],
45
- cwd=repository_path,
46
- check=False,
47
- capture_output=True,
48
- text=True,
49
- env=build_fixture_git_environment(),
50
- )
51
- assert completed_process.returncode == 0, completed_process.stderr
52
- return completed_process.stdout.strip()
53
-
54
-
55
- def configure_fixture_identity(repository_path: Path) -> None:
56
- """Configure a stable identity for a fixture repository."""
57
- run_fixture_git(repository_path, "config", "user.name", "Fixture User")
58
- run_fixture_git(
59
- repository_path,
60
- "config",
61
- "user.email",
62
- "fixture@example.invalid",
63
- )
64
- run_fixture_git(repository_path, "config", "commit.gpgsign", "false")
65
- fixture_hooks_directory = repository_path / ".fixture-hooks"
66
- fixture_hooks_directory.mkdir(exist_ok=True)
67
- run_fixture_git(
68
- repository_path,
69
- "config",
70
- "core.hooksPath",
71
- str(fixture_hooks_directory),
72
- )
73
-
74
-
75
- def initialize_fixture_repository(repository_path: Path) -> None:
76
- """Create and configure a fixture Git repository."""
77
- repository_path.mkdir()
78
- run_fixture_git(repository_path, "init", "-b", "main")
79
- configure_fixture_identity(repository_path)
80
-
81
-
82
- def test_native_submodule_commit_records_parent_pointer(
83
- tmp_path: Path,
84
- monkeypatch: pytest.MonkeyPatch,
85
- ) -> None:
86
- """A real submodule commit creates the matching parent pointer commit."""
87
- unrelated_repository = tmp_path / "unrelated-repository"
88
- initialize_fixture_repository(unrelated_repository)
89
- monkeypatch.setenv("GIT_DIR", str(unrelated_repository / ".git"))
90
-
91
- source_repository = tmp_path / "source-repository"
92
- parent_repository = tmp_path / "parent-repository"
93
- initialize_fixture_repository(source_repository)
94
- (source_repository / "README.md").write_text("fixture source\n", encoding="utf-8")
95
- run_fixture_git(source_repository, "add", "README.md")
96
- run_fixture_git(source_repository, "commit", "-m", "Create fixture source")
97
-
98
- initialize_fixture_repository(parent_repository)
99
- (parent_repository / "README.md").write_text("fixture parent\n", encoding="utf-8")
100
- run_fixture_git(parent_repository, "add", "README.md")
101
- run_fixture_git(parent_repository, "commit", "-m", "Create fixture parent")
102
- run_fixture_git(
103
- parent_repository,
104
- "-c",
105
- "protocol.file.allow=always",
106
- "submodule",
107
- "add",
108
- str(source_repository),
109
- "nested/sub[module]",
110
- )
111
- run_fixture_git(parent_repository, "commit", "-am", "Add fixture submodule")
112
-
113
- submodule_repository = parent_repository / "nested" / "sub[module]"
114
- configure_fixture_identity(submodule_repository)
115
- (parent_repository / "staged-note.txt").write_text("keep staged\n", encoding="utf-8")
116
- run_fixture_git(parent_repository, "add", "staged-note.txt")
117
- (submodule_repository / "change.txt").write_text("fixture change\n", encoding="utf-8")
118
- run_fixture_git(submodule_repository, "add", "change.txt")
119
- run_fixture_git(submodule_repository, "commit", "-m", "Record fixture change")
120
- clear_inherited_git_environment(monkeypatch)
121
- monkeypatch.chdir(submodule_repository)
122
- assert post_commit.main() == 0
123
-
124
- submodule_commit_hash = run_fixture_git(submodule_repository, "rev-parse", "HEAD")
125
- parent_commit_subject = run_fixture_git(parent_repository, "log", "-1", "--pretty=%s")
126
- parent_commit_body = run_fixture_git(parent_repository, "log", "-1", "--pretty=%B")
127
- recorded_pointer_hash = run_fixture_git(
128
- parent_repository,
129
- "rev-parse",
130
- "HEAD:nested/sub[module]",
131
- )
132
-
133
- assert recorded_pointer_hash == submodule_commit_hash
134
- assert parent_commit_subject == (
135
- f"chore: update sub[module] submodule to {submodule_commit_hash}"
136
- )
137
- assert "Submodule commit: Record fixture change" in parent_commit_body
138
- assert run_fixture_git(parent_repository, "status", "--short") == "A staged-note.txt"
139
-
140
-
141
- def test_parent_update_returns_git_diagnostic_for_failed_add(
142
- tmp_path: Path,
143
- monkeypatch: pytest.MonkeyPatch,
144
- ) -> None:
145
- """A failed parent add returns the Git diagnostic and literal pathspec."""
146
- parent_repository = tmp_path / "parent-repository"
147
- submodule_repository = parent_repository / "sub[module]"
148
- submodule_repository.mkdir(parents=True)
149
- recorded_commands: list[tuple[str, ...]] = []
150
-
151
- def fail_git_add(*arguments: str, cwd: Path) -> subprocess.CompletedProcess[str]:
152
- recorded_commands.append(arguments)
153
- return subprocess.CompletedProcess(
154
- ["git", *arguments],
155
- 1,
156
- "",
157
- "fatal: fixture add failure",
158
- )
159
-
160
- monkeypatch.setattr(post_commit, "execute_git", fail_git_add)
161
-
162
- parent_update = post_commit.update_parent_pointer(
163
- parent_repository,
164
- submodule_repository,
165
- "a" * 40,
166
- "Fixture commit",
167
- )
168
-
169
- assert parent_update == (
170
- post_commit.ParentPointerStatus.FAILED,
171
- "fatal: fixture add failure",
172
- )
173
- assert recorded_commands == [("add", "--", ":(literal)sub[module]")]
174
-
175
-
176
- def test_main_prints_git_failure_diagnostic(
177
- tmp_path: Path,
178
- monkeypatch: pytest.MonkeyPatch,
179
- capsys: pytest.CaptureFixture[str],
180
- ) -> None:
181
- """The hook prints the Git diagnostic when the parent update fails."""
182
- submodule_repository = tmp_path / "submodule"
183
- parent_repository = tmp_path / "parent"
184
- submodule_repository.mkdir()
185
- parent_repository.mkdir()
186
- monkeypatch.setattr(
187
- post_commit,
188
- "run_git_from_current_directory",
189
- lambda *arguments: str(submodule_repository),
190
- )
191
- monkeypatch.setattr(post_commit, "find_parent_repo", lambda repo: parent_repository)
192
- monkeypatch.setattr(post_commit, "run_git", lambda *arguments, cwd: "a" * 40)
193
- monkeypatch.setattr(
194
- post_commit,
195
- "update_parent_pointer",
196
- lambda *arguments: (
197
- post_commit.ParentPointerStatus.FAILED,
198
- "fatal: fixture commit failure",
199
- ),
200
- )
201
-
202
- assert post_commit.main() == 0
203
- assert "Git diagnostic: fatal: fixture commit failure" in capsys.readouterr().out
204
-
205
-
206
- @pytest.mark.parametrize(
207
- "completed_process",
208
- [
209
- subprocess.CompletedProcess(["gh"], 1, "", "no pull request"),
210
- subprocess.CompletedProcess(["gh"], 1, "", "auth failed"),
211
- ],
212
- )
213
- def test_pull_request_reminder_is_silent_without_successful_url(
214
- tmp_path: Path,
215
- monkeypatch: pytest.MonkeyPatch,
216
- capsys: pytest.CaptureFixture[str],
217
- completed_process: subprocess.CompletedProcess[str],
218
- ) -> None:
219
- monkeypatch.setattr(post_commit.subprocess, "run", lambda *args, **kwargs: completed_process)
220
-
221
- post_commit.print_pull_request_reminder(tmp_path)
222
-
223
- assert capsys.readouterr().out == ""
224
-
225
-
226
- def test_pull_request_reminder_prints_url(
227
- tmp_path: Path,
228
- monkeypatch: pytest.MonkeyPatch,
229
- capsys: pytest.CaptureFixture[str],
230
- ) -> None:
231
- monkeypatch.setattr(
232
- post_commit.subprocess,
233
- "run",
234
- lambda *args, **kwargs: subprocess.CompletedProcess(
235
- ["gh"], 0, "https://github.com/example/repo/pull/1\n", ""
236
- ),
237
- )
238
-
239
- post_commit.print_pull_request_reminder(tmp_path)
240
-
241
- output = capsys.readouterr().out
242
- assert output.startswith("Review this pull request: https://github.com/example/repo/pull/1\n")
243
- assert "<PR link>" not in output
244
- assert "Do not use the branch name, commit message, labels, current title" in output
245
- assert (
246
- "Before writing, inventory every independently observable behavior in the full diff."
247
- in output
248
- and "Lead with the central behavior for readability, then give every other behavior its own short paragraph or bullet."
249
- in output
250
- and "For each behavior, state the trigger or observer, the Before -> After result" in output
251
- and "OVERRIDE:" not in output
252
- )
253
- assert "gh pr edit https://github.com/example/repo/pull/1" in output
254
- assert output.rstrip().endswith("Report the saved result.")
255
-
256
-
257
- @pytest.mark.parametrize("exception", [FileNotFoundError(), subprocess.TimeoutExpired("gh", 5)])
258
- def test_pull_request_reminder_ignores_missing_gh_and_timeout(
259
- tmp_path: Path,
260
- monkeypatch: pytest.MonkeyPatch,
261
- capsys: pytest.CaptureFixture[str],
262
- exception: Exception,
263
- ) -> None:
264
- def raise_exception(*args: object, **kwargs: object) -> None:
265
- raise exception
266
-
267
- monkeypatch.setattr(post_commit.subprocess, "run", raise_exception)
268
-
269
- post_commit.print_pull_request_reminder(tmp_path)
270
-
271
- assert capsys.readouterr().out == ""
272
-
273
-
274
- def test_main_runs_pull_request_reminder_after_parent_update(
275
- tmp_path: Path,
276
- monkeypatch: pytest.MonkeyPatch,
277
- ) -> None:
278
- events: list[str] = []
279
- repo = tmp_path / "repo"
280
- parent = tmp_path / "parent"
281
- repo.mkdir()
282
- parent.mkdir()
283
- monkeypatch.setattr(post_commit, "run_git_from_current_directory", lambda *args: str(repo))
284
- monkeypatch.setattr(post_commit, "find_parent_repo", lambda repo_dir: parent)
285
- monkeypatch.setattr(post_commit, "run_git", lambda *args, cwd: "a" * 40)
286
- monkeypatch.setattr(
287
- post_commit,
288
- "update_parent_pointer",
289
- lambda *args: events.append("parent") or (post_commit.ParentPointerStatus.UPDATED, ""),
290
- )
291
- monkeypatch.setattr(
292
- post_commit,
293
- "print_pull_request_reminder",
294
- lambda repo_dir: events.append("reminder"),
295
- )
296
-
297
- assert post_commit.main() == 0
298
- assert events == ["parent", "reminder"]