Fly E2E Test
cmpnd-ai/dspy-cli
Deploy and test dspy-cli on Fly.io using local changes via temp git branch.
Conventions for writing and reviewing unit and integration tests for Git::Commands:: classes.
$ npx skills add ruby-git/ruby-git --skill command-test-conventions -a claude-codeProject install by default; add -g for ~/.claude/skills/.
$ gh skill install ruby-git/ruby-git command-test-conventions --agent claude-codeProject scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).
$ git clone --depth 1 https://github.com/ruby-git/ruby-git.git skills-src && mkdir -p .claude/skills && cp -r skills-src/.github/skills/command-test-conventions .claude/skills/command-test-conventions && rm -rf skills-srcUse ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.
Claude Code skills documentation · loads skills from .claude/skills/
Install the "command-test-conventions" agent skill from https://github.com/ruby-git/ruby-git/tree/main/.github/skills/command-test-conventions into .claude/skills/command-test-conventions/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "command-test-conventions", then confirm the skill loads.Claude Code copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$skill-installer install https://github.com/ruby-git/ruby-git/tree/main/.github/skills/command-test-conventionsType this inside Codex. $skill-installer <name> installs a curated skill from openai/skills. The installer writes to $CODEX_HOME/skills (default ~/.codex/skills). Restart Codex if the skill does not show up.
$ npx skills add ruby-git/ruby-git --skill command-test-conventions -a codexProject install goes to .agents/skills/; add -g for ~/.codex/skills/.
$ gh skill install ruby-git/ruby-git command-test-conventions --agent codexProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/ruby-git/ruby-git.git skills-src && mkdir -p .agents/skills && cp -r skills-src/.github/skills/command-test-conventions .agents/skills/command-test-conventions && rm -rf skills-srcUse ~/.agents/skills/ instead of .agents/skills for a personal install.
Codex skills documentation · loads skills from .agents/skills/
Install the "command-test-conventions" agent skill from https://github.com/ruby-git/ruby-git/tree/main/.github/skills/command-test-conventions into .agents/skills/command-test-conventions/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "command-test-conventions", then confirm the skill loads.Codex copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add ruby-git/ruby-git --skill command-test-conventions -a cursorProject install goes to .agents/skills/; add -g for ~/.cursor/skills/.
$ gh skill install ruby-git/ruby-git command-test-conventions --agent cursorProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/ruby-git/ruby-git.git skills-src && mkdir -p .cursor/skills && cp -r skills-src/.github/skills/command-test-conventions .cursor/skills/command-test-conventions && rm -rf skills-srcUse ~/.cursor/skills/ instead of .cursor/skills for a personal install.
Cursor skills documentation · loads skills from .cursor/skills/, .agents/skills/, .claude/skills/, .codex/skills/
Install the "command-test-conventions" agent skill from https://github.com/ruby-git/ruby-git/tree/main/.github/skills/command-test-conventions into .cursor/skills/command-test-conventions/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "command-test-conventions", then confirm the skill loads.Cursor copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gemini skills install https://github.com/ruby-git/ruby-git.git --path .github/skills/command-test-conventions--scope user (default) or --scope workspace; --path is the subfolder of the repo that holds the skill; --consent skips the security confirmation prompt.
$ npx skills add ruby-git/ruby-git --skill command-test-conventions -a gemini-cliProject install goes to .agents/skills/; add -g for ~/.gemini/skills/.
$ gh skill install ruby-git/ruby-git command-test-conventions --agent gemini-cliProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/ruby-git/ruby-git.git skills-src && mkdir -p .gemini/skills && cp -r skills-src/.github/skills/command-test-conventions .gemini/skills/command-test-conventions && rm -rf skills-srcUse ~/.gemini/skills/ instead of .gemini/skills for a personal install, then run /skills reload.
Gemini CLI skills documentation · loads skills from .gemini/skills/, .agents/skills/
Install the "command-test-conventions" agent skill from https://github.com/ruby-git/ruby-git/tree/main/.github/skills/command-test-conventions into .gemini/skills/command-test-conventions/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "command-test-conventions", then confirm the skill loads.Gemini CLI copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gh skill install ruby-git/ruby-git command-test-conventionsInstalls for Copilot at project scope by default; add --scope user for a personal install. Preview a skill first with gh skill preview. Needs GitHub CLI 2.90.0 or later (public preview).
$ npx skills add ruby-git/ruby-git --skill command-test-conventions -a github-copilotProject install goes to .agents/skills/; add -g for ~/.copilot/skills/.
$ git clone --depth 1 https://github.com/ruby-git/ruby-git.git skills-src && mkdir -p .github/skills && cp -r skills-src/.github/skills/command-test-conventions .github/skills/command-test-conventions && rm -rf skills-srcUse ~/.copilot/skills/ instead of .github/skills for a personal install. Commit .github/skills so cloud agent and code review can use it.
GitHub Copilot skills documentation · loads skills from .github/skills/, .claude/skills/, .agents/skills/
Install the "command-test-conventions" agent skill from https://github.com/ruby-git/ruby-git/tree/main/.github/skills/command-test-conventions into .github/skills/command-test-conventions/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "command-test-conventions", then confirm the skill loads.GitHub Copilot copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add ruby-git/ruby-git --skill command-test-conventions -a opencodeOpenCode documents no install command of its own. Project install goes to .agents/skills/; add -g for ~/.config/opencode/skills/.
$ gh skill install ruby-git/ruby-git command-test-conventions --agent opencodeProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/ruby-git/ruby-git.git skills-src && mkdir -p .opencode/skills && cp -r skills-src/.github/skills/command-test-conventions .opencode/skills/command-test-conventions && rm -rf skills-srcUse ~/.config/opencode/skills/ instead of .opencode/skills for a personal install.
OpenCode skills documentation · loads skills from .opencode/skills/, .claude/skills/, .agents/skills/
Install the "command-test-conventions" agent skill from https://github.com/ruby-git/ruby-git/tree/main/.github/skills/command-test-conventions into .opencode/skills/command-test-conventions/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "command-test-conventions", then confirm the skill loads.OpenCode copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
command-test-conventionsConventions for writing and reviewing unit and integration tests for Git::Commands:: classes.
Command Test Conventions is an agent skill from ruby-git/ruby-git. Conventions for writing and reviewing unit and integration tests for Git::Commands:: classes. Use when scaffolding new command tests or auditing existing ones.
Its SKILL.md is about 7.8k tokens, which your agent loads only when the skill is triggered. It is a single SKILL.md file with no bundled scripts.
It sits in Testing & QA, covering Integration testing, Unit testing and Project scaffolding. It works with Git. The repository describes itself as: Ruby/Git is a Ruby library that can be used to create, read and manipulate Git repositories by wrapping system calls to the git binary. The licence is MIT.
4 steps, taken from the first numbered list in SKILL.md.
Read from SKILL.md and the folder at commit f3bf20f. It shows what the files ask for, not the result of running them.
Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.
From allowed-tools in the SKILL.md frontmatter.
Shell commands in SKILL.md call:
gitFrom the folder's file list and the shell code blocks in SKILL.md.
Hosts in commands or code, which the agent is likely to contact:
git-scm.comFrom URLs in SKILL.md, links to its own repository left out.
Names no API keys, tokens, secrets or passwords.
From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.
Command Test Conventions loads about 7.8k tokens when it runs. Until then it costs about 46 tokens; SKILL.md has 3,072 words of instructions outside code blocks.
Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.
The automated check found no risky patterns in SKILL.md.
Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.
The full file from ruby-git/ruby-git at commit f3bf20f, republished under its MIT licence (© ruby-git). 3,072 words, ~7,800 tokens.
.claude/skills/command-test-conventions/SKILL.md (or your agent's skills folder).Conventions for writing and reviewing unit and integration tests for
Git::Commands::* classes.
The invocation needs the unit and/or integration spec file(s) to review. Including the corresponding command source file provides useful context for verifying argument coverage.
Prerequisite: Read the entire RSpec Unit Testing Standards skill (line 1 through EOF) before beginning. It defines the baseline Rules 1–28 that this skill extends. Without it, MUST-level structural, naming, stubbing, and coverage checks will not be applied.
Before deciding that test coverage is missing for an option, alias, or flag
form, determine the repository's minimum supported Git version from project
metadata. In this repository, git.gemspec declares git 2.43.0 or greater.
Coverage expectations for CLI forms must be based on the minimum supported Git
version, not only on the locally installed Git. Use version-matched upstream
documentation first, version-matched upstream source when needed, and local
git <command> -h output only as a supplemental check.
Do not require tests for newer-version-only forms that are not supported by the minimum supported Git version. Symmetrically, if the local Git omits or abbreviates a form that is supported in the minimum version, tests should still cover the minimum-version behavior.
Unit tests verify CLI argument building and command-layer behavior for each command.
.and_return value in an expected_result variable and assert expect(result).to eq(expected_result) to verify that #call passes through what
execution_context.command_capturing returns. This assertion belongs only in the
base invocation test — do not repeat it in every test.:force and :f)max_times: flag options: test with true (emits once) and the maximum integer
(emits N times), plus each alias with truetrue vs a string value like
'lines,cumulative')end_of_options-based operands, both alone and
combined with preceding operandstimeout:)allow_exit_status with a non-default
range: test that exit codes within the declared range return a result without
raising, and that exit codes outside the range raise FailedError. For example, if
the command declares allow_exit_status 0..1, test that exit codes 0 and 1
succeed, and that exit codes 2 and 128 raise FailedError. Commands that only
succeed at exit code 0 (the default) do not need a unit-level exit code test — the
integration error-handling test covers that path.ArgumentError) for per-argument validation failures: unknown
options, required: violations, type: mismatches, etc. Command classes generally
do not declare cross-argument constraint methods (conflicts, requires,
requires_one_of, requires_exactly_one_of, forbid_values, allowed_values,
etc.) — git validates its own option semantics. Every constraint a command does
declare gets an ArgumentError test. What makes a constraint permitted is defined in
Project Context — Validation Boundaries.Use the expect_command_capturing helper from spec_helper.rb (or
expect_command_streaming for streaming commands) which automatically includes
raise_on_failure: false:
expect_command_capturing('clone', '--', url, dir).and_return(command_result)When testing execution options, include forwarded keywords:
expect_command_capturing('clone', '--', url, dir, timeout: 30).and_return(command_result)These helpers expand to expect(execution_context).to receive(:command_capturing)...
— expect rather than allow because the call itself (the correct arguments
reaching git) is the behavior under test. See Rule 19 in the RSpec Unit Testing
Standards.
Commands that use Base#with_stdin pass an IO pipe read end as in: to
execution_context.command_capturing. Unit tests must capture that IO object and
assert its content. Use a block form on the expect to intercept keyword arguments:
# Helper defined in the spec file. The mode flag is an ordinary option on the class,
# so it is passed in by the caller rather than baked into the helper:
def expect_batch_command(*expected_cli_args, stdin_content: nil, **extra_opts)
expect(execution_context).to receive(:command_capturing) do |*args, **kwargs|
expect(args).to eq(['cat-file', *expected_cli_args])
expect(kwargs).to include(raise_on_failure: false, **extra_opts)
expect(kwargs[:in].read).to eq(stdin_content) if stdin_content
command_result
end
end
# Usage:
it 'passes the object via stdin and runs --batch-check' do
expect_batch_command('--batch-check', stdin_content: "HEAD\n")
command.call('HEAD', batch_check: true)
end
it 'writes each object on its own line to stdin' do
expect_batch_command('--batch-check', stdin_content: "HEAD\nv1.0\nabc123\n")
command.call('HEAD', 'v1.0', 'abc123', batch_check: true)
end
it 'includes --batch-all-objects and writes nothing to stdin' do
expect_batch_command('--batch-check', '--batch-all-objects', stdin_content: '')
command.call(batch_all_objects: true, batch_check: true)
end
# argv-invisible argument exception: :object is skip_cli: true, so it reaches git
# over stdin and never in argv. Git has no token to detect these incompatibilities
# with, so Ruby must enforce them.
# conflicts: can't pass objects AND bypass stdin; requires_one_of: must choose one.
it 'raises when mutually exclusive DSL inputs are combined' do
expect { command.call('HEAD', batch_all_objects: true, batch_check: true) }
.to raise_error(ArgumentError, /cannot specify :object and :batch_all_objects/)
endkwargs[:in].read works because Base#with_stdin writes to stdin on a background
thread and yields the read end immediately; the read call blocks until the writer
thread closes the pipe and EOF is reached, so the full content is returned. Test
stdin_content: '' explicitly for the no-input case (e.g. --batch-all-objects) to
confirm nothing is written.
Unit tests should exercise each code path through the command, not each possible input value. Avoid these patterns:
option: false for any flag_option. Passing false to a flag_option
(negatable or non-negatable) produces no output — identical to the base invocation
with no options. The "no arguments" test already covers this path. To exercise the
negative form of a negatable flag, use the no_ companion key: e.g.:
no_single_branch: true emits --no-single-branch, which is a distinct code path
worth testing.expect(result).to eq(expected_result) once as a contract check. Do not repeat
this assertion in other tests — one check per file is sufficient.max_times: flags. When a flag declares
max_times: N, test only true and the max integer N. Do not test intermediate
values (e.g. force: 1 when max_times: 2) — the DSL handles all valid integers
uniformly and intermediate values exercise the same code path.stash@{0},
stash@{2}, and 1 — they all flow through the same positional argument.#call with different mocked stdout values exercises
identical code. One test is sufficient unless the command parses or branches on the
output.The Arguments DSL has its own comprehensive spec (arguments_spec.rb) that tests
flag handling, value options, positionals, end_of_options, edge cases, and error
conditions. Command specs should test that the command uses the DSL correctly
(i.e., the right arguments reach execution_context.command_capturing), not re-test
the DSL's own behavior.
Two specific DSL re-test patterns that commonly appear but should be avoided:
end_of_options protection tests (dash-prefixed operands). When a command
declares end_of_options, the existing operand tests already verify that '--'
appears before operands in the expected argv sequence. Do not add a separate
test that passes a dash-prefixed operand (e.g. '-feature') to prove the
separator prevents misinterpretation: a dash-prefixed string exercises the
identical code path as any other string, and the DSL spec (arguments_spec.rb)
already covers end_of_options protection. The command spec only needs to show
'--' at the right position; the DSL spec demonstrates why that matters.required: operand rejection tests. When a command declares
operand :name, required: true, do not test that calling with no arguments
raises ArgumentError — the DSL spec covers required-operand validation. The
command spec should test what happens when the operand IS provided, not that
the DSL reports missing-argument errors correctly.Policy vs. interface testing: Command classes are neutral, faithful
representations of the git CLI. Their unit tests verify CLI argument building (the
neutral interface), not policy enforcement. Tests should not hardcode policy
assumptions — for example, a command spec should not always pass no_edit: true or
expect --no-edit unless the test is specifically exercising that option.
Anti-pattern: every
itblock in a command spec passesno_edit: true,no_progress: true, orno_color: true— this tests the facade's policy, not the command's interface.Correct pattern: test each option independently (
it 'passes --no-edit when no_edit is true'); test the default (no option passed) separately. Policy enforcement (which options the facade passes and why) is tested at the facade layer (for example,spec/unit/git/repository/remote_operations_spec.rb).
Where to test policy enforcement: Policy tests belong in the facade layer,
not in command specs. When a Git::Repository::* facade method sets policy defaults like
no_edit: true or no_progress: true, the corresponding facade unit spec
(e.g. spec/unit/git/repository/remote_operations_spec.rb) should verify those defaults reach the command:
# spec/unit/git/repository/remote_operations_spec.rb — facade policy-default test
describe '#pull' do
let(:pull_command) { instance_double(Git::Commands::Pull) }
let(:pull_result) { command_result('') }
it 'adds no_edit: true and no_progress: true for non-interactive execution' do
expect(Git::Commands::Pull).to receive(:new).with(execution_context).and_return(pull_command)
expect(pull_command)
.to receive(:call).with('origin', 'main', no_edit: true, no_progress: true).and_return(pull_result)
described_instance.pull('origin', 'main')
end
endThis separation ensures:
See "Command-layer neutrality" in CONTRIBUTING.md.
#initialize — omit from command specsDo not write a describe '#initialize' block in command specs. This is a
deliberate exception to Rule 2's SHOULD guidance for concrete subclasses. The full
reasoning chain:
Rule 2's have_attributes form requires public attributes. Base#initialize
stores @execution_context as a private instance variable with no attr_reader,
so there is nothing to pass to have_attributes. The form that Rule 2 uses cannot
be applied.
The only fallback is not_to raise_error, which is a Rule 24 violation.
Asserting that described_class.new(execution_context) does not raise merely
confirms the code runs — it is not an observable behavioral assertion.
Both Rule 2 purposes are already satisfied by other means:
let(:command) { described_class.new(execution_context) }
declaration at the top of every spec documents the constructor signature
as clearly as a dedicated block would.let(:command) is evaluated before every example.
If a subclass accidentally introduced a def initialize with a different
signature, every test in the file would immediately raise ArgumentError —
providing the same protection a dedicated block would.Base#initialize is covered by base_spec.rb. Command subclasses that do
not override #initialize gain nothing from repeating it.
Required fix if found: Remove any describe '#initialize' block that contains
only expect { described_class.new(execution_context) }.not_to raise_error — it is
a Rule 24 violation and provides no coverage value.
Unit tests are organized under describe '#call' with three sections:
context blocks, one per option/operand
variation. These are always present and come first.context 'exit code handling' — only for commands with allow_exit_status
ranges beyond 0..0. Uses mocked exit codes via command_result helper to test
that exit codes within the allowed range return a result and exit codes outside
the range raise FailedError.context 'input validation' — only for commands with validation rules. Covers
unsupported options and required arguments that raise ArgumentError.
Cross-argument constraints are usually absent, so there is usually nothing to
test here. Test whatever constraints a command does declare (e.g.
conflicts :object, :batch_all_objects and requires_one_of :object, :batch_all_objects in cat-file --batch, or conflicts :output, :out in
archive) — what makes a constraint permitted is defined in
Project Context — Validation Boundaries.The exit code and input validation blocks are optional — include them only when the
command has those behaviors. They always appear at the end of #call, in that order.
Required fix if found: The section names 'exit code handling' and 'input validation' are exact string literals — do not paraphrase. A context named
'with an unsupported option' or 'when the option is invalid' instead of
'input validation' MUST be renamed. These names are load-bearing identifiers: they
signal to reviewers at a glance which structural section they are looking at and what
it may or may not contain.
Unit test descriptions should be concise and action-oriented. Use descriptions like "includes the --cached flag", "passes both commits as operands", "combines commit with pathspecs".
Always use the emitted long-flag form in descriptions, never a short alias. The
DSL canonicalises aliases to the long form (e.g. :q → --quiet, :f → --force).
Writing 'adds -q flag' in an it description is misleading because the actual token
asserted in the expectation is '--quiet'. Use 'adds --quiet flag' instead.
Exception to RSpec Unit Testing Standards Rules 11–12 (subject and let ordering): Command unit tests intentionally omit
subjectwithindescribe '#call'. Because each test exercises a different argument combination, there is no single fixed call expressible as a sharedsubject. Uselet(:command)at theRSpec.describelevel and callcommand.call(...)directly inside eachitblock, overridingletinputs percontextblock as needed.
Example with all three sections:
RSpec.describe Git::Commands::Branch::Delete do
# Duck-type collaborator: command specs depend on the #command_capturing interface,
# not a single concrete ExecutionContext class.
let(:execution_context) { double('ExecutionContext') }
let(:command) { described_class.new(execution_context) }
describe '#call' do
# Argument building — flat contexts
context 'with single branch name' do
it 'passes the branch name' do
expected_result = command_result('Deleted branch feature.')
expect_command_capturing('branch', '-d', 'feature').and_return(expected_result)
result = command.call('feature')
expect(result).to eq(expected_result)
end
end
context 'with :force option' do
# ...
end
# Exit code handling — only when command declares allow_exit_status
context 'exit code handling' do
it 'returns result for exit code 0' do
# ... mock exit code 0, assert result returned ...
end
it 'returns result for exit code 1 (partial failure)' do
# ... mock exit code 1, assert result returned ...
end
it 'raises FailedError for exit code > 1' do
# ... mock exit code 128, assert FailedError raised ...
end
end
# Input validation — only when command validates input
context 'input validation' do
it 'raises ArgumentError for unsupported options' do
expect { command.call('branch', invalid: true) }
.to raise_error(ArgumentError, /Unsupported options/)
end
end
end
endExample with argument building only (no custom exit codes, no validation):
RSpec.describe Git::Commands::Stash::Pop do
# Duck-type collaborator: command specs depend on the #command_capturing interface,
# not a single concrete ExecutionContext class.
let(:execution_context) { double('ExecutionContext') }
let(:command) { described_class.new(execution_context) }
describe '#call' do
context 'with no arguments' do
# ...
end
context 'with stash reference' do
# ...
end
context 'with :index option' do
# ...
end
end
endIntegration tests are minimal smoke tests that confirm the command executes successfully against a real git repository. They should NOT test git's output format, parsing behavior, or specific content of stdout — those concerns belong in parser specs and facade/end-to-end specs.
Each integration spec file tests exactly one command class. Do not create multi-command workflow specs that chain commands together — that is the concern of facade or end-to-end tests.
Integration tests should only cover:
Git::CommandLine::Result with
expected output (e.g., non-empty for commands that produce output)git diff:
identical refs produce exit code 0 with empty output; differing refs produce exit
code ≤1 with non-empty output. This confirms that real git returns the exit codes
the command's allow_exit_status range expects.FailedError.
Every command must have at least one error handling test. Even commands with
non-default allow_exit_status ranges can be forced to fail (e.g., by removing
.git to trigger exit code 128).Do not write integration tests that assert on git's output format (e.g., matching
specific line patterns, status letters, or header syntax). The command's job is to
pass the correct arguments to git and return the result — verifying git's formatting
behavior is testing git, not the command. If a particular flag needs to be tested
(e.g., -M for rename detection), verify the flag appears in the arguments via a
unit test.
Branch workflow: Implement any new or updated tests on a feature branch. Never commit or push directly to
main— open a pull request when changes are ready to merge.
Integration tests must be organized into two context blocks under #call:
context 'when the command succeeds' — smoke tests, option variations, and exit
code variantscontext 'when the command fails' — error handling tests (FailedError)This grouping provides a consistent structure across all command specs and makes it immediately clear which tests cover the happy path vs. error conditions.
Simple command example (default exit code handling):
RSpec.describe Git::Commands::Add, :integration do
include_context 'in an empty repository'
subject(:command) { described_class.new(execution_context) }
describe '#call' do
context 'when the command succeeds' do
it 'returns a Git::CommandLine::Result' do
# ... valid invocation ...
end
end
context 'when the command fails' do
it 'raises FailedError with a nonexistent path' do
# git's error message phrasing varies by version — anchor on the stable input value
expect { command.call('nonexistent.txt') }
.to raise_error(Git::FailedError, /nonexistent\.txt/)
end
end
end
endCustom exit code example (command declares allow_exit_status):
RSpec.describe Git::Commands::Diff::Numstat, :integration do
include_context 'in a diff test repository'
subject(:command) { described_class.new(execution_context) }
describe '#call' do
context 'when the command succeeds' do
it 'returns exit code 0 with no differences' do
result = command.call('initial', 'initial')
expect(result.status.exitstatus).to eq(0)
expect(result.stdout).to be_empty
end
it 'succeeds with differences found' do
result = command.call('initial', 'after_modify')
expect(result.status.exitstatus).to eq(1)
expect(result.stdout).not_to be_empty
end
end
context 'when the command fails' do
it 'raises FailedError for invalid revision' do
# git's error message phrasing varies by version — anchor on the stable input value
expect { command.call('nonexistent-ref') }
.to raise_error(Git::FailedError, /nonexistent-ref/)
end
end
end
endWhen an integration test exercises an option that was introduced after the minimum
supported Git version (2.43.0), guard the example with
skip: unless_git(minimum_version, feature_description) to prevent failures on
installations that do not yet have the required Git version. The unless_git helper
is defined in spec/spec_helper.rb:
false when the installed Git meets the minimum version (tests run normally).Apply the guard to individual it blocks when only some tests in a context require a
newer version. Apply it to a context or describe block when all tests in that
group require the same minimum version.
# ✅ Different options introduced in different git versions — guard each `it` individually
it 'returns a Git::CommandLine::Result with the :retry option',
skip: unless_git('2.46.0', 'git am --retry') do
# ...
end
it 'returns a Git::CommandLine::Result with the :batch_updates option',
skip: unless_git('2.47.0', 'git update-ref --batch-updates') do
# ...
end
# ✅ All tests in the group require the same version — guard the context/describe block
RSpec.describe Git::Commands::Am::Retry, :integration,
skip: unless_git('2.46.0', 'git am --retry') do
# ...
endDetermine the correct minimum version by checking version-matched upstream git
documentation (e.g., https://git-scm.com/docs/git-worktree/2.33.0) rather than
relying only on the locally installed git binary.
Always specify initial_branch: 'main' when calling Git.init in test setup.
The in an empty repository shared context already does this for the primary repo,
but tests that create additional repositories in a before block (e.g., a bare
remote, a second clone target) must pass initial_branch: 'main' explicitly to
Git.init. Without it, the repo's HEAD points to whatever init.defaultBranch
is set to on the CI runner or developer's machine, making the test non-deterministic:
# ❌ Fragile — HEAD points to the system default branch name
Git.init(bare_dir, bare: true)
# ✅ Correct — HEAD always points to 'main'
Git.init(bare_dir, bare: true, initial_branch: 'main')No shell-outs in tests. Never use backticks, system(), or %x[] in tests. For
git commands (including setup steps), use execution_context.command_capturing — it
is portable across platforms, handles paths with spaces, and uses the same mechanism
the command classes themselves use. For example:
execution_context.command_capturing('rev-parse', 'HEAD').stdout.strip. For non-git
operations (file creation, directory manipulation, etc.), use Ruby's standard library
(FileUtils, File, Dir) instead of shelling out.
Write cross-platform tests. Avoid Unix-specific paths like /dev/null,
/dev/zero, or hardcoded /tmp. Use Ruby's standard library for temporary files and
directories (Dir.mktmpdir, Tempfile), and use File.join for path construction.
When creating failure scenarios, use portable approaches (e.g., create a regular file
and try to use it where a directory is expected) rather than platform-specific
tricks.
Do not use other Commands classes in tests. Each spec tests exactly one command
class. Use execution_context.command_capturing, repo, or standard library methods
for setup instead of instantiating other Commands classes. This maintains test
isolation and prevents bugs in one command from breaking another command's tests.
Require only the command under test. See Rule 5 in RSpec Unit Testing Standards (MUST). For command specs specifically: do not require other command classes even if they are not instantiated — unused requires create false coupling between specs.
Version-dependent tests. When a test's behavior varies by git version, use skip
inside the it block — not the skip: metadata on it. The metadata form evaluates
at the describe level where helpers like repo are not available, causing a load
error. For example:
it 'succeeds when no merge is in progress' do
skip 'requires git 2.46.0 or later' unless Git.git_version >= Git::Version.new(2, 46, 0)
expect { command.call }.not_to raise_error
endTest descriptions must match assertions. See Rule
9
in RSpec Unit Testing Standards (MUST). This applies equally to command specs: a test
described as "includes the --force flag" must assert that the flag appears in the
arguments, not merely that #call returns a result.
Regex patterns in test assertions should not use Ruby's /m modifier unless
intentionally matching across newlines. Git output is line-based, so patterns should
match within single lines.
Report only anomalies — skip items that comply. For each issue found, provide:
describe '#call' > context 'with :force option' > it '...')Group findings under two headings:
Required fixes — MUST-level violations from either skill
Suggested improvements — SHOULD-level deviations, ordered by impact
If no issues are found, say so in one sentence and stop.
© ruby-git, MIT. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file
Just SKILL.md in .github/skills/command-test-conventions of ruby-git/ruby-git.
Open the folder on GitHubat commit f3bf20f
Command Test Conventions next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.
| Skill | Stars | Used in | Tokens | Auto-check | Licence | Repo updated |
|---|---|---|---|---|---|---|
| Command Test Conventions this skillruby-git/ruby-git | 1.8k | — | ~7.8k | Automated safety check: Pass | MIT | |
| Fly E2E Testcmpnd-ai/dspy-cli | 137 | — | ~2.3k | Automated safety check: Notes | MIT | |
| Testing Guidefullstackhero/dotnet-starter-kit | 6.8k | — | ~935 | Automated safety check: Pass | MIT | |
| Workflow Regression Testspurefunctor/purescript-iris | 117 | — | ~1.8k | Automated safety check: Pass | Custom licence | |
| Scaffolding Oracle To Postgres Migration Test Projectgithub/awesome-copilot | 40k | — | ~687 | Automated safety check: Pass | MIT | |
| Appbuilder Testingadobe/skills | 195 | — | ~2.8k | Automated safety check: Pass | Apache-2.0 |
cmpnd-ai/dspy-cli
Deploy and test dspy-cli on Fly.io using local changes via temp git branch.
fullstackhero/dotnet-starter-kit
Write tests for an FSH feature — xUnit + Shouldly + NSubstitute + AutoFixture, with naming and AAA conventions.
purefunctor/purescript-iris
Workflow for producing auditable Git or jj history for a known compiler bug fix.
github/awesome-copilot
Scaffolds an xUnit integration test project targeting Oracle in .NET solutions.
adobe/skills
Generate and run tests for Adobe App Builder actions and UI components.
boshi-xixixi/TraeSkill
Scaffolds an xUnit integration test project for validating Oracle-to-PostgreSQL database migration behavior in .NET solutions.
ruby-git/ruby-git
Addresses unresolved pull request review threads and suppressed (low-confidence) Copilot review comments on the current branch, folds each fix into the…
ruby-git/ruby-git
Assesses what an API change would break before it is made, finds every usage, documents the impact and plans a deprecation or migration path.
ruby-git/ruby-git
Diagnoses and fixes failing GitHub Actions runs by identifying the failure, fetching only the relevant logs, finding the root cause and reproducing it locally.
ruby-git/ruby-git
Scaffolds and reviews `Git::Commands::*` classes in the ruby-git library, with unit tests, integration tests and YARD docs, using the Base command architecture.
ruby-git/ruby-git
Workflow for updating gem dependencies and fixing CVEs in the ruby-git project: assess with bundle outdated and audit, edit the gemspec, test, then commit with conventional messages.
ruby-git/ruby-git
Migrates a direct command call in Ruby Git's Git::Lib to a Git::Commands class, as part of a Strangler Fig redesign, with a plan, legacy tests and a pull request.
Works with
Categories
Conventions for writing and reviewing unit and integration tests for Git::Commands:: classes. Command Test Conventions is an agent skill from ruby-git/ruby-git. Conventions for writing and reviewing unit and integration tests for Git::Commands:: classes.
Command Test Conventions fits situations like: scaffolding new command tests; auditing existing ones.
Run `npx skills add ruby-git/ruby-git --skill command-test-conventions -a claude-code`. Or copy the skill folder (.github/skills/command-test-conventions in ruby-git/ruby-git) into .claude/skills/command-test-conventions in your project. Claude Code loads it when a task matches its description.
Run `npx skills add ruby-git/ruby-git --skill command-test-conventions -a codex`. Or copy the skill folder (.github/skills/command-test-conventions in ruby-git/ruby-git) into .agents/skills/command-test-conventions in your project. Codex loads it when a task matches its description.
Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add ruby-git/ruby-git --skill command-test-conventions -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/command-test-conventions, .gemini/skills/command-test-conventions, .github/skills/command-test-conventions and .opencode/skills/command-test-conventions in your project.
Going by SKILL.md and its folder, Command Test Conventions needs the command-line tools its instructions call (git).
SKILL.md names 1 domain. In commands or code: git-scm.com; the agent is likely to contact it when it follows the instructions. This is read from the text; nothing was executed.
Our automated static check of SKILL.md found no risky patterns, such as piping downloads into a shell, reading credential files or hidden Unicode. It is not a guarantee. Review the folder before installing.
Command Test Conventions is published under the MIT licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.
About 7.8k tokens (SKILL.md is roughly 31k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full.
Skills that share tags, products or a category with Command Test Conventions: Fly E2E Test (cmpnd-ai/dspy-cli, 137 stars), Testing Guide (fullstackhero/dotnet-starter-kit, 6.8k stars), Workflow Regression Tests (purefunctor/purescript-iris, 117 stars) and Scaffolding Oracle To Postgres Migration Test Project (github/awesome-copilot, 40k stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.
ruby-git (a GitHub organization) maintains it in ruby-git/ruby-git, which has 1,799 GitHub stars. The repository holds 30 skills in this directory. The repository was last updated on October 2, 2026.
Source: ruby-git/ruby-git on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.