git 4.3.2 → 5.0.0.beta.1

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 (280) hide show
  1. checksums.yaml +4 -4
  2. data/.github/copilot-instructions.md +67 -2705
  3. data/.github/pull_request_template.md +3 -1
  4. data/.github/skills/breaking-change-analysis/SKILL.md +102 -0
  5. data/.github/skills/ci-cd-troubleshooting/SKILL.md +264 -0
  6. data/.github/skills/command-implementation/REFERENCE.md +993 -0
  7. data/.github/skills/command-implementation/SKILL.md +229 -0
  8. data/.github/skills/command-test-conventions/SKILL.md +660 -0
  9. data/.github/skills/command-yard-documentation/SKILL.md +426 -0
  10. data/.github/skills/dependency-management/SKILL.md +72 -0
  11. data/.github/skills/development-workflow/SKILL.md +506 -0
  12. data/.github/skills/extract-command-from-lib/SKILL.md +487 -0
  13. data/.github/skills/extract-facade-from-base-lib/SKILL.md +586 -0
  14. data/.github/skills/facade-implementation/REFERENCE.md +840 -0
  15. data/.github/skills/facade-implementation/SKILL.md +260 -0
  16. data/.github/skills/facade-test-conventions/SKILL.md +380 -0
  17. data/.github/skills/facade-yard-documentation/SKILL.md +429 -0
  18. data/.github/skills/make-skill-template/SKILL.md +176 -0
  19. data/.github/skills/pr-readiness-review/SKILL.md +185 -0
  20. data/.github/skills/project-context/SKILL.md +313 -0
  21. data/.github/skills/pull-request-review/SKILL.md +168 -0
  22. data/.github/skills/refactor-command-to-commandlineresult/SKILL.md +131 -0
  23. data/.github/skills/release-management/SKILL.md +125 -0
  24. data/.github/skills/review-arguments-dsl/CHECKLIST.md +788 -0
  25. data/.github/skills/review-arguments-dsl/SKILL.md +214 -0
  26. data/.github/skills/review-backward-compatibility/SKILL.md +275 -0
  27. data/.github/skills/review-cross-command-consistency/SKILL.md +139 -0
  28. data/.github/skills/reviewing-skills/SKILL.md +189 -0
  29. data/.github/skills/rspec-unit-testing-standards/SKILL.md +639 -0
  30. data/.github/skills/tdd-refactor-step/SKILL.md +236 -0
  31. data/.github/skills/test-debugging/SKILL.md +160 -0
  32. data/.github/skills/yard-documentation/SKILL.md +793 -0
  33. data/.github/workflows/continuous_integration.yml +3 -2
  34. data/.github/workflows/enforce_conventional_commits.yml +1 -1
  35. data/.github/workflows/experimental_continuous_integration.yml +2 -2
  36. data/.github/workflows/release.yml +3 -4
  37. data/.gitignore +8 -0
  38. data/.husky/pre-commit +13 -0
  39. data/.release-please-manifest.json +1 -1
  40. data/.rspec +3 -0
  41. data/.rubocop.yml +7 -3
  42. data/.rubocop_todo.yml +23 -5
  43. data/.yardopts +1 -0
  44. data/CHANGELOG.md +0 -40
  45. data/CONTRIBUTING.md +694 -53
  46. data/README.md +17 -5
  47. data/Rakefile +61 -9
  48. data/commitlint.test +4 -0
  49. data/git.gemspec +14 -8
  50. data/lib/git/args_builder.rb +0 -8
  51. data/lib/git/base.rb +486 -410
  52. data/lib/git/branch.rb +380 -43
  53. data/lib/git/branch_delete_failure.rb +31 -0
  54. data/lib/git/branch_delete_result.rb +63 -0
  55. data/lib/git/branch_info.rb +178 -0
  56. data/lib/git/branches.rb +130 -24
  57. data/lib/git/command_line/base.rb +245 -0
  58. data/lib/git/command_line/capturing.rb +249 -0
  59. data/lib/git/command_line/result.rb +96 -0
  60. data/lib/git/command_line/streaming.rb +194 -0
  61. data/lib/git/command_line.rb +43 -322
  62. data/lib/git/command_line_result.rb +4 -88
  63. data/lib/git/commands/add.rb +131 -0
  64. data/lib/git/commands/am/abort.rb +43 -0
  65. data/lib/git/commands/am/apply.rb +252 -0
  66. data/lib/git/commands/am/continue.rb +43 -0
  67. data/lib/git/commands/am/quit.rb +43 -0
  68. data/lib/git/commands/am/retry.rb +47 -0
  69. data/lib/git/commands/am/show_current_patch.rb +64 -0
  70. data/lib/git/commands/am/skip.rb +42 -0
  71. data/lib/git/commands/am.rb +33 -0
  72. data/lib/git/commands/apply.rb +237 -0
  73. data/lib/git/commands/archive/list_formats.rb +46 -0
  74. data/lib/git/commands/archive.rb +140 -0
  75. data/lib/git/commands/arguments.rb +3510 -0
  76. data/lib/git/commands/base.rb +403 -0
  77. data/lib/git/commands/branch/copy.rb +94 -0
  78. data/lib/git/commands/branch/create.rb +173 -0
  79. data/lib/git/commands/branch/delete.rb +80 -0
  80. data/lib/git/commands/branch/list.rb +162 -0
  81. data/lib/git/commands/branch/move.rb +94 -0
  82. data/lib/git/commands/branch/set_upstream.rb +86 -0
  83. data/lib/git/commands/branch/show_current.rb +49 -0
  84. data/lib/git/commands/branch/unset_upstream.rb +57 -0
  85. data/lib/git/commands/branch.rb +34 -0
  86. data/lib/git/commands/cat_file/batch.rb +364 -0
  87. data/lib/git/commands/cat_file/filtered.rb +105 -0
  88. data/lib/git/commands/cat_file/raw.rb +210 -0
  89. data/lib/git/commands/cat_file.rb +49 -0
  90. data/lib/git/commands/checkout/branch.rb +151 -0
  91. data/lib/git/commands/checkout/files.rb +115 -0
  92. data/lib/git/commands/checkout.rb +38 -0
  93. data/lib/git/commands/checkout_index.rb +105 -0
  94. data/lib/git/commands/clean.rb +100 -0
  95. data/lib/git/commands/clone.rb +240 -0
  96. data/lib/git/commands/commit.rb +272 -0
  97. data/lib/git/commands/commit_tree.rb +100 -0
  98. data/lib/git/commands/config_option_syntax/add.rb +83 -0
  99. data/lib/git/commands/config_option_syntax/get.rb +117 -0
  100. data/lib/git/commands/config_option_syntax/get_all.rb +115 -0
  101. data/lib/git/commands/config_option_syntax/get_color.rb +91 -0
  102. data/lib/git/commands/config_option_syntax/get_color_bool.rb +93 -0
  103. data/lib/git/commands/config_option_syntax/get_regexp.rb +115 -0
  104. data/lib/git/commands/config_option_syntax/get_urlmatch.rb +102 -0
  105. data/lib/git/commands/config_option_syntax/list.rb +107 -0
  106. data/lib/git/commands/config_option_syntax/remove_section.rb +74 -0
  107. data/lib/git/commands/config_option_syntax/rename_section.rb +78 -0
  108. data/lib/git/commands/config_option_syntax/replace_all.rb +104 -0
  109. data/lib/git/commands/config_option_syntax/set.rb +114 -0
  110. data/lib/git/commands/config_option_syntax/unset.rb +89 -0
  111. data/lib/git/commands/config_option_syntax/unset_all.rb +89 -0
  112. data/lib/git/commands/config_option_syntax.rb +56 -0
  113. data/lib/git/commands/describe.rb +155 -0
  114. data/lib/git/commands/diff.rb +656 -0
  115. data/lib/git/commands/diff_files.rb +518 -0
  116. data/lib/git/commands/diff_index.rb +496 -0
  117. data/lib/git/commands/fetch.rb +352 -0
  118. data/lib/git/commands/fsck.rb +136 -0
  119. data/lib/git/commands/gc.rb +132 -0
  120. data/lib/git/commands/grep.rb +338 -0
  121. data/lib/git/commands/init.rb +99 -0
  122. data/lib/git/commands/log.rb +632 -0
  123. data/lib/git/commands/ls_files.rb +191 -0
  124. data/lib/git/commands/ls_remote.rb +155 -0
  125. data/lib/git/commands/ls_tree.rb +131 -0
  126. data/lib/git/commands/maintenance/register.rb +75 -0
  127. data/lib/git/commands/maintenance/run.rb +104 -0
  128. data/lib/git/commands/maintenance/start.rb +66 -0
  129. data/lib/git/commands/maintenance/stop.rb +55 -0
  130. data/lib/git/commands/maintenance/unregister.rb +79 -0
  131. data/lib/git/commands/maintenance.rb +31 -0
  132. data/lib/git/commands/merge/abort.rb +44 -0
  133. data/lib/git/commands/merge/continue.rb +44 -0
  134. data/lib/git/commands/merge/quit.rb +46 -0
  135. data/lib/git/commands/merge/start.rb +245 -0
  136. data/lib/git/commands/merge.rb +28 -0
  137. data/lib/git/commands/merge_base.rb +86 -0
  138. data/lib/git/commands/mv.rb +77 -0
  139. data/lib/git/commands/name_rev.rb +114 -0
  140. data/lib/git/commands/pull.rb +377 -0
  141. data/lib/git/commands/push.rb +246 -0
  142. data/lib/git/commands/read_tree.rb +149 -0
  143. data/lib/git/commands/remote/add.rb +91 -0
  144. data/lib/git/commands/remote/get_url.rb +66 -0
  145. data/lib/git/commands/remote/list.rb +54 -0
  146. data/lib/git/commands/remote/prune.rb +61 -0
  147. data/lib/git/commands/remote/remove.rb +52 -0
  148. data/lib/git/commands/remote/rename.rb +69 -0
  149. data/lib/git/commands/remote/set_branches.rb +63 -0
  150. data/lib/git/commands/remote/set_head.rb +82 -0
  151. data/lib/git/commands/remote/set_url.rb +71 -0
  152. data/lib/git/commands/remote/set_url_add.rb +61 -0
  153. data/lib/git/commands/remote/set_url_delete.rb +64 -0
  154. data/lib/git/commands/remote/show.rb +71 -0
  155. data/lib/git/commands/remote/update.rb +72 -0
  156. data/lib/git/commands/remote.rb +42 -0
  157. data/lib/git/commands/repack.rb +277 -0
  158. data/lib/git/commands/reset.rb +147 -0
  159. data/lib/git/commands/rev_parse.rb +297 -0
  160. data/lib/git/commands/revert/abort.rb +45 -0
  161. data/lib/git/commands/revert/continue.rb +57 -0
  162. data/lib/git/commands/revert/quit.rb +47 -0
  163. data/lib/git/commands/revert/skip.rb +44 -0
  164. data/lib/git/commands/revert/start.rb +153 -0
  165. data/lib/git/commands/revert.rb +29 -0
  166. data/lib/git/commands/rm.rb +114 -0
  167. data/lib/git/commands/show.rb +632 -0
  168. data/lib/git/commands/show_ref/exclude_existing.rb +120 -0
  169. data/lib/git/commands/show_ref/exists.rb +78 -0
  170. data/lib/git/commands/show_ref/list.rb +145 -0
  171. data/lib/git/commands/show_ref/verify.rb +120 -0
  172. data/lib/git/commands/show_ref.rb +42 -0
  173. data/lib/git/commands/stash/apply.rb +75 -0
  174. data/lib/git/commands/stash/branch.rb +65 -0
  175. data/lib/git/commands/stash/clear.rb +41 -0
  176. data/lib/git/commands/stash/create.rb +58 -0
  177. data/lib/git/commands/stash/drop.rb +67 -0
  178. data/lib/git/commands/stash/list.rb +39 -0
  179. data/lib/git/commands/stash/pop.rb +78 -0
  180. data/lib/git/commands/stash/push.rb +103 -0
  181. data/lib/git/commands/stash/show.rb +149 -0
  182. data/lib/git/commands/stash/store.rb +63 -0
  183. data/lib/git/commands/stash.rb +38 -0
  184. data/lib/git/commands/status.rb +169 -0
  185. data/lib/git/commands/symbolic_ref/delete.rb +68 -0
  186. data/lib/git/commands/symbolic_ref/read.rb +95 -0
  187. data/lib/git/commands/symbolic_ref/update.rb +76 -0
  188. data/lib/git/commands/symbolic_ref.rb +38 -0
  189. data/lib/git/commands/tag/create.rb +139 -0
  190. data/lib/git/commands/tag/delete.rb +55 -0
  191. data/lib/git/commands/tag/list.rb +143 -0
  192. data/lib/git/commands/tag/verify.rb +71 -0
  193. data/lib/git/commands/tag.rb +26 -0
  194. data/lib/git/commands/update_ref/batch.rb +140 -0
  195. data/lib/git/commands/update_ref/delete.rb +92 -0
  196. data/lib/git/commands/update_ref/update.rb +106 -0
  197. data/lib/git/commands/update_ref.rb +42 -0
  198. data/lib/git/commands/version.rb +52 -0
  199. data/lib/git/commands/worktree/add.rb +140 -0
  200. data/lib/git/commands/worktree/list.rb +64 -0
  201. data/lib/git/commands/worktree/lock.rb +58 -0
  202. data/lib/git/commands/worktree/management_base.rb +51 -0
  203. data/lib/git/commands/worktree/move.rb +66 -0
  204. data/lib/git/commands/worktree/prune.rb +67 -0
  205. data/lib/git/commands/worktree/remove.rb +63 -0
  206. data/lib/git/commands/worktree/repair.rb +76 -0
  207. data/lib/git/commands/worktree/unlock.rb +47 -0
  208. data/lib/git/commands/worktree.rb +43 -0
  209. data/lib/git/commands/write_tree.rb +68 -0
  210. data/lib/git/commands.rb +89 -0
  211. data/lib/git/detached_head_info.rb +54 -0
  212. data/lib/git/diff.rb +297 -7
  213. data/lib/git/diff_file_numstat_info.rb +29 -0
  214. data/lib/git/diff_file_patch_info.rb +134 -0
  215. data/lib/git/diff_file_raw_info.rb +127 -0
  216. data/lib/git/diff_info.rb +169 -0
  217. data/lib/git/diff_path_status.rb +78 -19
  218. data/lib/git/diff_result.rb +32 -0
  219. data/lib/git/diff_stats.rb +59 -14
  220. data/lib/git/dirstat_info.rb +86 -0
  221. data/lib/git/errors.rb +65 -2
  222. data/lib/git/execution_context/global.rb +56 -0
  223. data/lib/git/execution_context/repository.rb +147 -0
  224. data/lib/git/execution_context.rb +482 -0
  225. data/lib/git/file_ref.rb +74 -0
  226. data/lib/git/fsck_object.rb +9 -9
  227. data/lib/git/fsck_result.rb +1 -1
  228. data/lib/git/lib.rb +1606 -1028
  229. data/lib/git/log.rb +15 -2
  230. data/lib/git/object.rb +92 -22
  231. data/lib/git/parsers/branch.rb +224 -0
  232. data/lib/git/parsers/cat_file.rb +111 -0
  233. data/lib/git/parsers/diff.rb +585 -0
  234. data/lib/git/parsers/fsck.rb +133 -0
  235. data/lib/git/parsers/grep.rb +42 -0
  236. data/lib/git/parsers/ls_tree.rb +58 -0
  237. data/lib/git/parsers/stash.rb +208 -0
  238. data/lib/git/parsers/tag.rb +257 -0
  239. data/lib/git/remote.rb +133 -9
  240. data/lib/git/repository/branching.rb +572 -0
  241. data/lib/git/repository/committing.rb +191 -0
  242. data/lib/git/repository/configuring.rb +156 -0
  243. data/lib/git/repository/diffing.rb +775 -0
  244. data/lib/git/repository/inspecting.rb +153 -0
  245. data/lib/git/repository/logging.rb +247 -0
  246. data/lib/git/repository/merging.rb +295 -0
  247. data/lib/git/repository/object_operations.rb +1101 -0
  248. data/lib/git/repository/path_resolver.rb +207 -0
  249. data/lib/git/repository/remote_operations.rb +753 -0
  250. data/lib/git/repository/shared_private.rb +51 -0
  251. data/lib/git/repository/staging.rb +390 -0
  252. data/lib/git/repository/stashing.rb +107 -0
  253. data/lib/git/repository/status_operations.rb +180 -0
  254. data/lib/git/repository/worktree_operations.rb +159 -0
  255. data/lib/git/repository.rb +264 -1
  256. data/lib/git/stash.rb +85 -4
  257. data/lib/git/stash_info.rb +104 -0
  258. data/lib/git/stashes.rb +130 -13
  259. data/lib/git/status.rb +224 -18
  260. data/lib/git/tag_delete_failure.rb +31 -0
  261. data/lib/git/tag_delete_result.rb +63 -0
  262. data/lib/git/tag_info.rb +105 -0
  263. data/lib/git/version.rb +109 -2
  264. data/lib/git/version_constraint.rb +81 -0
  265. data/lib/git/worktree.rb +120 -5
  266. data/lib/git/worktrees.rb +107 -7
  267. data/lib/git.rb +114 -18
  268. data/redesign/1_architecture_existing.md +54 -18
  269. data/redesign/2_architecture_redesign.md +365 -46
  270. data/redesign/3_architecture_implementation.md +1451 -54
  271. data/tasks/gem_tasks.rake +4 -0
  272. data/tasks/npm_tasks.rake +7 -0
  273. data/tasks/rspec.rake +48 -0
  274. data/tasks/test.rake +13 -1
  275. data/tasks/yard.rake +34 -7
  276. metadata +349 -20
  277. data/lib/git/index.rb +0 -6
  278. data/lib/git/path.rb +0 -38
  279. data/lib/git/working_directory.rb +0 -6
  280. /data/{release-please-config.json → .release-please-config.json} +0 -0
@@ -0,0 +1,639 @@
1
+ ---
2
+ name: rspec-unit-testing-standards
3
+ description: "Defines RSpec unit testing rules for this project covering structure, naming, setup patterns, stubbing, doubles, coverage, and test reliability. Use when writing, reviewing, or auditing RSpec specs under spec/unit/."
4
+ ---
5
+
6
+ # RSpec Unit Testing Standards
7
+
8
+ These rules govern the structure, organization, and quality of all RSpec unit tests
9
+ in this project. Apply them when writing new tests, reviewing existing ones, or
10
+ auditing test quality.
11
+
12
+ ## Priority Levels
13
+
14
+ Use RFC-style priority words to reduce ambiguity for AI behavior:
15
+
16
+ - **MUST**: mandatory; do not violate without a documented exception
17
+ - **SHOULD**: preferred default; may be overridden when a clearer test requires it
18
+
19
+ ## Contents
20
+
21
+ - [How to use this skill](#how-to-use-this-skill)
22
+ - [Related skills](#related-skills)
23
+ - [Structure](#structure)
24
+ - [Rule 1 (MUST): One top-level `RSpec.describe` block per class](#rule-1-must-one-top-level-rspecdescribe-block-per-class)
25
+ - [Rule 2 (MUST): One `describe` block per public method](#rule-2-must-one-describe-block-per-public-method)
26
+ - [Rule 3 (SHOULD): Add `# frozen_string_literal: true` at the top of every spec file](#rule-3-should-add--frozen_string_literal-true-at-the-top-of-every-spec-file)
27
+ - [Rule 4 (MUST): Spec file location must mirror source file location](#rule-4-must-spec-file-location-must-mirror-source-file-location)
28
+ - [Rule 5 (MUST): `require 'spec_helper'` and only the file(s) under test](#rule-5-must-require-spec_helper-and-only-the-files-under-test)
29
+ - [Rule 6 (MUST): Test only through the public interface](#rule-6-must-test-only-through-the-public-interface)
30
+ - [Naming and Organization](#naming-and-organization)
31
+ - [Rule 7 (SHOULD): Use `described_class`](#rule-7-should-use-described_class)
32
+ - [Rule 8 (MUST): `context` blocks describe conditions](#rule-8-must-context-blocks-describe-conditions)
33
+ - [Rule 9 (MUST): `it` blocks assert one concept, and the description must match the assertion](#rule-9-must-it-blocks-assert-one-concept-and-the-description-must-match-the-assertion)
34
+ - [Rule 10 (SHOULD): Use the standard nesting pattern](#rule-10-should-use-the-standard-nesting-pattern)
35
+ - [Setup and Subject](#setup-and-subject)
36
+ - [Rule 11 (SHOULD): Use a named `subject` at the top of each `describe #method` block](#rule-11-should-use-a-named-subject-at-the-top-of-each-describe-method-block)
37
+ - [Rule 12 (SHOULD): Immediately follow `subject` with `let` defaults](#rule-12-should-immediately-follow-subject-with-let-defaults)
38
+ - [Rule 13 (SHOULD): Define `let(:described_instance)` at the top level when multiple `describe` blocks share the same instance](#rule-13-should-define-letdescribed_instance-at-the-top-level-when-multiple-describe-blocks-share-the-same-instance)
39
+ - [Rule 14 (SHOULD): Prefer `subject` to represent the method call result](#rule-14-should-prefer-subject-to-represent-the-method-call-result)
40
+ - [Rule 15 (SHOULD): Do not use `subject` when testing side effects](#rule-15-should-do-not-use-subject-when-testing-side-effects)
41
+ - [Rule 16 (MUST): Use `let`/`let!` for inputs and shared setup; use `before` only for side effects](#rule-16-must-use-letlet-for-inputs-and-shared-setup-use-before-only-for-side-effects)
42
+ - [Rule 17 (SHOULD): Keep test setup local; extract only for substantial cross-file reuse](#rule-17-should-keep-test-setup-local-extract-only-for-substantial-cross-file-reuse)
43
+ - [Doubles and Stubbing](#doubles-and-stubbing)
44
+ - [Rule 18 (MUST): Stub calls to non-trivial external objects](#rule-18-must-stub-calls-to-non-trivial-external-objects)
45
+ - [Rule 19 (MUST): Use `allow` for incidental stubs; use `expect` for behavioral assertions](#rule-19-must-use-allow-for-incidental-stubs-use-expect-for-behavioral-assertions)
46
+ - [Rule 20 (MUST): Use verifying doubles](#rule-20-must-use-verifying-doubles)
47
+ - [Coverage](#coverage)
48
+ - [Rule 21 (MUST): Achieve 100% branch-level coverage](#rule-21-must-achieve-100-branch-level-coverage)
49
+ - [Rule 22 (MUST): Error assertions must specify both the error class and a message pattern](#rule-22-must-error-assertions-must-specify-both-the-error-class-and-a-message-pattern)
50
+ - [Rule 23 (MUST): Test edge cases within the relevant `context` block](#rule-23-must-test-edge-cases-within-the-relevant-context-block)
51
+ - [Rule 24 (MUST): Assert observable behavior, not implementation details](#rule-24-must-assert-observable-behavior-not-implementation-details)
52
+ - [Anti-pattern: structural identity and constant-existence tests](#anti-pattern-structural-identity-and-constant-existence-tests)
53
+ - [Test Reliability](#test-reliability)
54
+ - [Rule 25 (MUST): Keep unit tests deterministic](#rule-25-must-keep-unit-tests-deterministic)
55
+ - [Rule 26 (MUST): Isolate and restore global/process state](#rule-26-must-isolate-and-restore-globalprocess-state)
56
+ - [Rule 27 (MUST): Tests must be order-independent](#rule-27-must-tests-must-be-order-independent)
57
+ - [Rule 28 (MUST): Avoid `allow_any_instance_of` and `receive_message_chain`](#rule-28-must-avoid-allow_any_instance_of-and-receive_message_chain)
58
+ - [Verification](#verification)
59
+ - [Output](#output)
60
+
61
+ ## How to use this skill
62
+
63
+ These rules apply to all RSpec unit specs under `spec/unit/`. Extend this baseline
64
+ with domain-specific rules from related skills as needed.
65
+
66
+ Adoption and enforcement notes:
67
+
68
+ - Apply these rules as hard requirements for new and modified unit specs.
69
+ - Legacy specs may violate some rules; treat those as incremental cleanup work.
70
+ - Branch and line coverage are both reported by SimpleCov in this repository.
71
+ - `coverage_threshold: 100` is configured, but `fail_on_low_coverage` is currently
72
+ `false`; enforce Rule 21 during review by checking the coverage report until
73
+ strict failure is enabled.
74
+
75
+ ## Related skills
76
+
77
+ - [Command Test Conventions](../command-test-conventions/SKILL.md) — additional conventions
78
+ for `Git::Commands::*` unit and integration specs, built on top of these rules
79
+ - [Development Workflow](../development-workflow/SKILL.md) — TDD process that governs
80
+ when and how tests are written
81
+ - [PR Readiness Review](../pr-readiness-review/SKILL.md) — final quality gate that
82
+ verifies test compliance before opening a pull request
83
+ - [Pull Request Review](../pull-request-review/SKILL.md) — PR review process that
84
+ checks test quality against these standards
85
+
86
+ ## Structure
87
+
88
+ ### Rule 1 (MUST): One top-level `RSpec.describe` block per class
89
+
90
+ Use the class constant directly:
91
+
92
+ ```ruby
93
+ RSpec.describe Git::CommandLine::Capturing do
94
+ ```
95
+
96
+ Never use a string in place of the constant, even for backward-compat aliases:
97
+
98
+ ```ruby
99
+ # Bad — string describe; described_class is unavailable, coverage tooling may not
100
+ # map the spec to the source file, and typos go undetected at load time.
101
+ RSpec.describe 'Git::CommandLineResult' do
102
+ ```
103
+
104
+ If the constant is a backward-compat alias (e.g. `Git::CommandLineResult =
105
+ Git::CommandLine::Result`), use the alias constant itself as the describe argument —
106
+ the alias is a real Ruby constant and loads without issue. The test content should
107
+ verify that the alias points to the correct target using object identity (`be`),
108
+ which would not be caught implicitly by a `NameError` on the canonical constant:
109
+
110
+ ```ruby
111
+ RSpec.describe Git::CommandLineResult do
112
+ it 'is a backward-compatible alias for Git::CommandLine::Result' do
113
+ expect(described_class).to be(Git::CommandLine::Result)
114
+ end
115
+ end
116
+ ```
117
+
118
+ Do not test `#initialize` or other behavior here — that is already covered by the spec for the canonical class.
119
+
120
+ ### Rule 2 (MUST): One `describe` block per public method
121
+
122
+ Use `#method_name` for instance methods and `.method_name` for class methods.
123
+ Include `#initialize`:
124
+
125
+ ```ruby
126
+ describe '#call' do ...
127
+ describe '.build' do ...
128
+ describe '#initialize' do ...
129
+ ```
130
+
131
+ **Inherited `#initialize` in concrete subclasses (SHOULD):** If a class is
132
+ directly instantiated by callers but does not override `#initialize`, its spec
133
+ SHOULD still include a `describe '#initialize'` block using the minimal
134
+ `have_attributes` form (see Rule 13). This serves two purposes:
135
+
136
+ 1. The spec is self-contained documentation of the constructor signature — a reader
137
+ does not need to consult the ancestor's spec to know what arguments the class
138
+ accepts or what attributes it exposes.
139
+ 2. It guards against an accidental `def initialize` override in the subclass that
140
+ silently drops or misroutes an argument, which the ancestor's spec would not
141
+ catch.
142
+
143
+ Omit the inherited `#initialize` block only for abstract or internal classes that
144
+ callers never instantiate directly — those are sufficiently covered by the
145
+ ancestor's spec alone.
146
+
147
+ ### Rule 3 (SHOULD): Add `# frozen_string_literal: true` at the top of every spec file
148
+
149
+ Matches project-wide convention and catches accidental string mutation.
150
+
151
+ ### Rule 4 (MUST): Spec file location must mirror source file location
152
+
153
+ `lib/git/foo/bar.rb` maps to `spec/unit/git/foo/bar_spec.rb`. Deviating from this
154
+ makes specs hard to find and breaks coverage mapping.
155
+
156
+ ### Rule 5 (MUST): `require 'spec_helper'` and only the file(s) under test
157
+
158
+ Every unit spec MUST start with `require 'spec_helper'`, then require only the
159
+ Ruby file(s) it directly tests. Avoid requiring unrelated libraries or classes —
160
+ doing so creates false coupling where a rename or move breaks specs that don't even
161
+ test that class.
162
+
163
+ ### Rule 6 (MUST): Test only through the public interface
164
+
165
+ Never call private methods directly in tests. If private logic is hard to reach
166
+ through the public interface, stop and propose one of these remedies to the user:
167
+
168
+ - **Extract a class** — move the logic to a new class with its own public interface.
169
+ - **Make the method public** — promote it if it is genuinely part of the contract.
170
+ - **Redesign the public API** — split the public method into smaller public steps.
171
+
172
+ Never use `send`, `instance_variable_get`, or `__send__` to reach private state.
173
+
174
+ ## Naming and Organization
175
+
176
+ ### Rule 7 (SHOULD): Use `described_class`
177
+
178
+ Use `described_class` instead of repeating the class name inside the describe block:
179
+
180
+ ```ruby
181
+ subject(:result) { described_class.new(args).call }
182
+ ```
183
+
184
+ ### Rule 8 (MUST): `context` blocks describe conditions
185
+
186
+ Use prefixes "when", "with", or "without". Nest them under the relevant `describe`
187
+ block for different option combinations, input states, or environmental conditions:
188
+
189
+ ```ruby
190
+ context 'when the command fails' do ...
191
+ context 'with the :force option' do ...
192
+ context 'without a timeout' do ...
193
+ ```
194
+
195
+ ### Rule 9 (MUST): `it` blocks assert one concept, and the description must match the assertion
196
+
197
+ Each example tests a single logical behavior. A test described as "raises
198
+ ArgumentError when options conflict" must verify both the error class and a message
199
+ pattern — not just that any error was raised. Multiple `expect` calls are acceptable
200
+ if they all verify the same behavior — that is, a single application code change
201
+ would cause them all to fail together. For example, asserting the type, status, and
202
+ contents of a single return value is one concept ("the result carries the right
203
+ data"), not five separate behaviors:
204
+
205
+ ```ruby
206
+ # Good — all assertions verify one concept: the returned result is correct
207
+ it 'returns a result with the failure details' do
208
+ result = described_instance.run('status', raise_on_failure: false)
209
+ expect(result).to be_a(Git::CommandLineResult)
210
+ expect(result.status.success?).to be false
211
+ expect(result.status.exitstatus).to eq(1)
212
+ expect(result.stdout).to eq("modified: foo.rb\n")
213
+ expect(result.stderr).to eq('fatal: not a git repository')
214
+ end
215
+ ```
216
+
217
+ ### Rule 10 (SHOULD): Use the standard nesting pattern
218
+
219
+ ```
220
+ describe #method
221
+ context "when X"
222
+ context "with Y option"
223
+ it "does Z"
224
+ ```
225
+
226
+ > **Exception:** Simple methods with a single execution path and no meaningful input
227
+ > variations do not need `context` blocks. A `describe #method` block containing
228
+ > `it` directly is fine when there are no conditions worth naming.
229
+
230
+ ## Setup and Subject
231
+
232
+ ### Rule 11 (SHOULD): Use a named `subject` at the top of each `describe #method` block
233
+
234
+ Name the subject after what is returned. For simple cases, inline construction is
235
+ fine:
236
+
237
+ ```ruby
238
+ subject(:result) { described_class.new(command, options).call }
239
+ ```
240
+
241
+ When construction is complex or shared across multiple `describe` blocks, separate
242
+ construction into a `let` and reference it from `subject` (see Rule 14). Naming
243
+ the subject allows both `is_expected.to` one-liners and `expect(result).to` in
244
+ more descriptive examples. Do not override `subject` in nested `context` blocks
245
+ — vary behavior by overriding `let` inputs instead.
246
+
247
+ ### Rule 12 (SHOULD): Immediately follow `subject` with `let` defaults
248
+
249
+ `subject` must be the first declaration in a `describe` block. Do not place `let`
250
+ declarations above `subject` — doing so buries the call under test and inverts the
251
+ natural reading order (what is being tested → what inputs it receives).
252
+
253
+ Define `let` defaults for all inputs and arguments immediately after `subject`.
254
+ Nested `context` blocks override only the `let` values relevant to that scenario —
255
+ leave everything else at its default:
256
+
257
+ ```ruby
258
+ # Good — subject first, then let defaults
259
+ describe '#call' do
260
+ subject(:result) { described_class.new(command, options).call }
261
+
262
+ let(:command) { ['git', 'status'] }
263
+ let(:options) { {} }
264
+
265
+ context 'when options include timeout' do
266
+ let(:options) { { timeout: 10 } }
267
+ it { is_expected.to ... }
268
+ end
269
+ end
270
+
271
+ # Bad — let declarations above subject obscure what is under test
272
+ describe '#call' do
273
+ let(:command) { ['git', 'status'] }
274
+ let(:options) { {} }
275
+ subject(:result) { described_class.new(command, options).call }
276
+ end
277
+ ```
278
+
279
+ ### Rule 13 (SHOULD): Define `let(:described_instance)` at the top level when multiple `describe` blocks share the same instance
280
+
281
+ When two or more `describe #method` blocks instantiate the class identically, define
282
+ a single `let(:described_instance)` at the top of the `RSpec.describe` block instead
283
+ of repeating construction in every `subject`. Each `describe` block then defines
284
+ `subject` as the method call result, referencing `described_instance`:
285
+
286
+ ```ruby
287
+ RSpec.describe Git::CommandLine do
288
+ let(:command) { ['git', 'status'] }
289
+ let(:options) { {} }
290
+ let(:described_instance) { described_class.new(command, options) }
291
+
292
+ describe '#call' do
293
+ subject(:result) { described_instance.call }
294
+ ...
295
+ end
296
+
297
+ describe '#to_s' do
298
+ subject(:result) { described_instance.to_s }
299
+ ...
300
+ end
301
+ end
302
+ ```
303
+
304
+ Guidelines:
305
+
306
+ - **Use `let`, not `subject`** — avoids making it the implicit assertion target.
307
+ - **Reference only `let`-defined arguments** — no inline literals; nested contexts
308
+ must be able to override individual inputs.
309
+ - **Omit when construction is trivial** or varies between methods.
310
+ - For `#initialize`, alias it: `subject(:instance) { described_instance }`.
311
+ - **When `#initialize` only stores arguments via `attr_reader`, use a single
312
+ `have_attributes` example** — separate `it` blocks for each attribute add noise
313
+ without isolation benefit when there is no conditional logic to branch on:
314
+
315
+ ```ruby
316
+ describe '#initialize' do
317
+ subject(:instance) { described_instance }
318
+
319
+ it 'stores all constructor arguments' do
320
+ expect(instance).to have_attributes(
321
+ env: env,
322
+ binary_path: binary_path,
323
+ global_opts: global_opts,
324
+ logger: logger
325
+ )
326
+ end
327
+ end
328
+ ```
329
+
330
+ Use separate `it` blocks only when `#initialize` performs validation or
331
+ conditional logic — each branch then deserves its own example.
332
+
333
+ ### Rule 14 (SHOULD): Prefer `subject` to represent the method call result
334
+
335
+ Prefer `subject` as the return value of the public method under test. For simple
336
+ cases, inline construction is fine (see Rule 11). When construction is complex or
337
+ reused across multiple `describe` blocks, separate construction into a `let` so
338
+ that nested `context` blocks can override individual inputs without duplicating
339
+ the whole call:
340
+
341
+ ```ruby
342
+ # Good — complex construction is separated so inputs can be overridden
343
+ let(:instance) { described_class.new(complex, args, here) }
344
+ subject(:result) { instance.call }
345
+
346
+ # Avoid when construction is complex — nested contexts cannot vary individual args
347
+ subject(:result) { described_class.new(complex, args, here).call }
348
+ ```
349
+
350
+ > **Exception:** `#initialize` tests — the constructed object *is* the return value,
351
+ > so `subject` should be the instance (see Rule 13 aliasing guideline).
352
+
353
+ ### Rule 15 (SHOULD): Do not use `subject` when testing side effects
354
+
355
+ Use `change` and `raise_error` matchers instead of manual before/after assertions:
356
+
357
+ ```ruby
358
+ # Good
359
+ expect { action }.to change(collection, :size).by(1)
360
+ expect { action }.to change(obj, :attr).from('old').to('new')
361
+
362
+ # Bad
363
+ before_count = collection.size
364
+ action
365
+ expect(collection.size).to eq(before_count + 1)
366
+ ```
367
+
368
+ ### Rule 16 (MUST): Use `let`/`let!` for inputs and shared setup; use `before` only for side effects
369
+
370
+ Use `let` for values that are referenced directly in examples. Use `let!` when a
371
+ value must exist before the example runs but is not referenced directly (e.g., a
372
+ precondition that other code depends on). Use `before` only for imperative side
373
+ effects (e.g., creating files, setting env vars). Avoid instance variables set in
374
+ `before` blocks.
375
+
376
+ ### Rule 17 (SHOULD): Keep test setup local; extract only for substantial cross-file reuse
377
+
378
+ `shared_context` is appropriate when identical multi-step environment setup must be
379
+ shared across multiple spec files and duplicating it inline would be substantial.
380
+ Do not use `shared_context` within a single spec file — use the `let` hierarchy
381
+ instead. A unit test that needs a `shared_context` to run is almost certainly an
382
+ integration test; move it to `spec/integration/`.
383
+
384
+ The same restraint applies to shared helper methods. Do not extract a helper to a
385
+ support file solely because two spec files contain methods with similar structure.
386
+ If the helpers construct different doubles, mock different classes, or carry
387
+ different default attributes, the similarity is incidental — not meaningful
388
+ duplication. Each spec file should be self-contained and readable without jumping
389
+ to external helpers. Extract only when the helpers are truly identical and used
390
+ across three or more spec files.
391
+
392
+ Defining shared contexts:
393
+
394
+ - Define in `spec/support/contexts/`, named after the context string.
395
+ - Use only `let`, `let!`, and `before`/`after` — no `subject`, doubles, or
396
+ `described_instance`.
397
+ - Reference only `let`-defined values; never inline literals.
398
+
399
+ Consuming: always use explicit `include_context 'name'` — never metadata-based
400
+ auto-inclusion.
401
+
402
+ ## Doubles and Stubbing
403
+
404
+ ### Rule 18 (MUST): Stub calls to non-trivial external objects
405
+
406
+ Stub anything whose real involvement would make the test cross a unit boundary
407
+ (e.g., `ProcessExecuter`, file system, network). Do not stub simple value types
408
+ like `String`, `Integer`, or `Array`. The guiding question: would running the real
409
+ thing make this test not a unit test?
410
+
411
+ > **Exception:** Simple value objects with no IO (e.g., `Git::CommandLineResult`)
412
+ > can be used directly if doing so keeps the test a unit test.
413
+
414
+ ### Rule 19 (MUST): Use `allow` for incidental stubs; use `expect` for behavioral assertions
415
+
416
+ Reserve `expect(...).to receive(...)` for cases where the call itself is the
417
+ behavior under test. Use `allow` for everything else — overusing `expect` stubs
418
+ creates over-specified tests that break on irrelevant refactors:
419
+
420
+ ```ruby
421
+ # Good — incidental stub; test verifies the return value
422
+ allow(executer).to receive(:run).and_return(result)
423
+ expect(subject).to eq(expected)
424
+
425
+ # Good — the call itself is what's being verified
426
+ expect(executer).to receive(:run).with('git', 'status')
427
+ subject
428
+
429
+ # Good — the call is behavioral and arguments require destructuring;
430
+ # use a block with nested expects when .with() cannot cleanly express
431
+ # the assertion (e.g., complex kwargs mixed with positional args)
432
+ expect(executer).to receive(:run) do |*args, **opts|
433
+ expect(opts[:timeout_after]).to eq(5)
434
+ mock_result
435
+ end
436
+ subject
437
+ ```
438
+
439
+ ### Rule 20 (MUST): Use verifying doubles
440
+
441
+ Use `instance_double` / `class_double` rather than plain `double`:
442
+
443
+ ```ruby
444
+ # Good — ProcessExecuter.run is a class method; use class_double
445
+ let(:process_executer) { class_double(ProcessExecuter) }
446
+
447
+ # Bad
448
+ let(:process_executer) { double('ProcessExecuter') }
449
+ ```
450
+
451
+ > **Exceptions:**
452
+ >
453
+ > - Plain `double` is acceptable when the class being stubbed cannot be
454
+ > loaded in the test environment (e.g., a C extension or optional dependency
455
+ > not available in CI).
456
+ > - Plain `double` is acceptable for duck-type collaborators where there is no
457
+ > single concrete class to verify against (e.g., `execution_context` in
458
+ > command specs implements a duck-type interface, not one specific class).
459
+ > - Plain `double` is acceptable when the class delegates methods via
460
+ > `SimpleDelegator`, `Delegator`, or `method_missing`. `instance_double`
461
+ > only verifies methods that `method_defined?` returns `true` for, so
462
+ > delegated methods (e.g., `signaled?`, `exitstatus` forwarded from
463
+ > `Process::Status`) would be incorrectly rejected.
464
+ >
465
+ > In all cases, document the reason with an inline comment so the use of
466
+ > `double` is not mistaken for carelessness:
467
+ >
468
+ > ```ruby
469
+ > # Duck-type collaborator: command specs depend on the #command_capturing
470
+ > # interface, not a single concrete ExecutionContext class.
471
+ > let(:execution_context) { double('ExecutionContext') }
472
+ >
473
+ > # Plain double: ProcessExecuter result classes delegate to Process::Status
474
+ > # via SimpleDelegator/method_missing, so instance_double cannot verify the
475
+ > # delegated interface (signaled?, exitstatus, etc.).
476
+ > double('ProcessExecuter::ResultWithCapture', success?: true, signaled?: false)
477
+ > ```
478
+
479
+ ## Coverage
480
+
481
+ ### Rule 21 (MUST): Achieve 100% branch-level coverage
482
+
483
+ Every conditional path through the public interface must be exercised by at least
484
+ one example. If a branch cannot be reached through the public interface, that is a
485
+ design smell.
486
+
487
+ > **Exception:** Defensive guards that require breaking OS-level invariants to reach
488
+ > (e.g., `raise "unreachable"` that would require triggering out-of-memory) may be
489
+ > excluded. Mark them explicitly with `# :nocov:` and a brief comment explaining why
490
+ > — never leave branches silently uncovered.
491
+
492
+ ### Rule 22 (MUST): Error assertions must specify both the error class and a message pattern
493
+
494
+ `raise_error(ErrorClass)` alone is underspecified — any instance of that class
495
+ satisfies it regardless of cause. This applies equally when using the block form
496
+ to verify properties of the raised error object: the block does not substitute for
497
+ a message check, and RSpec allows both together.
498
+
499
+ ```ruby
500
+ # Good — class + message pattern only
501
+ expect { action }.to raise_error(ArgumentError, /cannot combine :force and :dry_run/)
502
+
503
+ # Good — class + message pattern + block to verify error properties
504
+ expect { action }.to raise_error(Git::FailedError, /git.*status/) do |error|
505
+ expect(error.result.status.exitstatus).to eq(1)
506
+ end
507
+
508
+ # Bad — passes for any ArgumentError, even unrelated ones
509
+ expect { action }.to raise_error(ArgumentError)
510
+
511
+ # Bad — block form is not an exception to the message requirement;
512
+ # still passes for any Git::FailedError regardless of cause
513
+ expect { action }.to raise_error(Git::FailedError) do |error|
514
+ expect(error.result.status.exitstatus).to eq(1)
515
+ end
516
+ ```
517
+
518
+ > **Version-variance exception:** When the error message is produced by an external
519
+ > library or by git itself and is likely to vary across git versions, use the loosest
520
+ > regexp that still distinguishes this error from an unrelated one. Anchor the pattern
521
+ > on something stable — the invalid input value, the subcommand name, or a keyword
522
+ > that appears in all known git versions. Never omit the message check entirely:
523
+ >
524
+ > ```ruby
525
+ > # Good — stable anchor, tolerates phrasing differences across git versions
526
+ > expect { command.call('nonexistent.txt') }
527
+ > .to raise_error(Git::FailedError, /nonexistent\.txt/)
528
+ >
529
+ > # Good — subcommand keyword is stable across versions
530
+ > expect { command.call('bad-ref') }
531
+ > .to raise_error(Git::FailedError, /bad-ref/)
532
+ >
533
+ > # Bad — passes for any Git::FailedError regardless of cause
534
+ > expect { command.call('nonexistent.txt') }.to raise_error(Git::FailedError)
535
+ > ```
536
+
537
+ ### Rule 23 (MUST): Test edge cases within the relevant `context` block
538
+
539
+ `nil`, empty collections, and boundary values belong alongside the normal cases for
540
+ the same method and condition — not in a separate "edge cases" block at the bottom.
541
+
542
+ ### Rule 24 (MUST): Assert observable behavior, not implementation details
543
+
544
+ Every `expect` must verify a meaningful outcome — a return value, a raised error,
545
+ a state change, or a message sent to a collaborator. Do not write assertions that
546
+ merely confirm the code runs without error or that mirror the implementation:
547
+
548
+ ```ruby
549
+ # Good — asserts the meaningful return value
550
+ expect(subject).to eq('v2.43.0')
551
+
552
+ # Bad — passes for any non-nil return, verifies nothing useful
553
+ expect(subject).not_to be_nil
554
+ ```
555
+
556
+ **Decision test — independent failure mode:** Before writing or approving an
557
+ assertion, ask: "What application code change would cause *only this test* to fail?"
558
+ If the answer is "nothing — every other test would also fail first," the assertion
559
+ is redundant and should be removed.
560
+
561
+ #### Anti-pattern: structural identity and constant-existence tests
562
+
563
+ Do not write tests that only verify a constant exists, that a `require` loaded
564
+ successfully, or that a namespace is a `Module` vs a `Class`:
565
+
566
+ ```ruby
567
+ # Bad — proves nothing about behavior; any real test that uses the class
568
+ # would fail with NameError first if the constant were missing
569
+ it 'exposes Capturing' do
570
+ expect(Git::CommandLine::Capturing).to be_a(Class)
571
+ end
572
+
573
+ # Bad — structural choice, not observable behavior from a caller's perspective
574
+ it 'is a module (not a class)' do
575
+ expect(described_class).to be_a(Module)
576
+ end
577
+ ```
578
+
579
+ Constant presence is proven implicitly by every test that instantiates or calls
580
+ the class. These tests add no coverage of behavior and should not be written or
581
+ approved in review.
582
+
583
+ ## Test Reliability
584
+
585
+ ### Rule 25 (MUST): Keep unit tests deterministic
586
+
587
+ Do not depend on real time, randomness, sleep-based timing, or external process
588
+ timing. Stub or freeze `Time.now`, `Process.clock_gettime`, `SecureRandom`, and
589
+ `rand` so results are repeatable. Never use `sleep` in unit tests.
590
+
591
+ ### Rule 26 (MUST): Isolate and restore global/process state
592
+
593
+ Unit tests must not leak state across examples. If a test modifies process or global
594
+ state (for example `ENV`, current working directory, locale, or global config),
595
+ restore that state before the example ends.
596
+
597
+ ### Rule 27 (MUST): Tests must be order-independent
598
+
599
+ Every unit test must pass when run alone and when run in randomized order. Do not
600
+ rely on side effects from other examples, files, or execution order.
601
+
602
+ ### Rule 28 (MUST): Avoid `allow_any_instance_of` and `receive_message_chain`
603
+
604
+ Do not use `allow_any_instance_of` or `receive_message_chain` in unit tests. They
605
+ hide object boundaries and create brittle tests.
606
+
607
+ > **Exception:** Allowed only when there is no practical seam and refactoring is out
608
+ > of scope for the current change. If used, add an inline comment explaining why and
609
+ > prefer introducing a seam in a follow-up change.
610
+
611
+ ## Verification
612
+
613
+ After writing or modifying tests, verify compliance before finishing:
614
+
615
+ 1. **Run the specs:** `bundle exec rspec spec/unit/path/to_spec.rb`
616
+ 2. **Check branch coverage** meets 100% (Rule 21) — open `coverage/index.html` and
617
+ confirm no uncovered branches in the class under test.
618
+ 3. **Re-check MUST rules.** Scan the spec against every MUST rule. Fix violations.
619
+ 4. **Run in random order** (Rule 27): `bundle exec rspec spec/unit/path/to_spec.rb --order rand`
620
+
621
+ Repeat until all checks pass.
622
+
623
+ ## Output
624
+
625
+ **When writing new tests**, produce the spec file and run through the Verification
626
+ checklist above. No additional structured output is required.
627
+
628
+ **When reviewing or auditing** existing tests, produce the following:
629
+
630
+ 1. A per-rule compliance table:
631
+
632
+ | Rule | Status | Issue |
633
+ | ---- | ------ | ----- |
634
+
635
+ Use **Pass**, **Fail**, or **N/A** for each rule.
636
+
637
+ 2. A summary of required fixes (MUST-level violations).
638
+
639
+ 3. A list of suggested improvements (SHOULD-level deviations), ordered by impact.