Install
$ agentstack add skill-thoughtbot-clean-rspec-output-clean-rspec-output ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
Clean Up Unexpected RSpec Output
A procedural workflow for identifying and eliminating unexpected output from a Rails RSpec suite, one issue at a time, verifying each fix in isolation and committing as you go.
When to use this
The user has an RSpec suite that produces unexpected output during test runs. This is typically a mix of:
- Ruby warnings (e.g.
warning: already initialized constant,warning: method redefined) - Gem deprecation notices
- Stray
puts,p,pp, orRails.loggercalls left in application or test code - ActiveRecord or ActionView deprecation warnings
- Output from third-party libraries that should be silenced in the test environment
The goal is a suite that runs with no unexpected output so that when something new appears, it's immediately visible.
Before you start
Ask the user which git branch this work should happen on, and explain why: the skill iterates through each unexpected output one at a time and creates a separate commit per fix, so that individual fixes can be reviewed or cherry-picked independently. Do not assume a branch name. If the branch does not exist, create it from the current branch. If it does exist, check it out. You can suggest a branch name eg. fix-unexpected-test-ouput
If the user has told you to ignore specific output patterns (e.g. a deprecation they plan to fix separately), note them and skip them when you encounter them. Do not invent patterns to ignore on your own.
Tool usage
Prefer native file and search tools over Bash for reading and searching log files:
- Use Read with
offsetandlimitto view slices of a file, notsed,head, ortail. - Use Grep to search within files, not Bash
grep.
Read and Grep do this natively without permission prompts, so the workflow stays uninterrupted and is faster as a result. Bash is still the right tool for running the suite, git operations, and anything that genuinely needs a shell. Reading and searching captured output is not one of those cases.
Fix scope: what counts as a fix
Fixes may change how a test runs. Fixes must not change what it covers.
A fix preserves every one of these:
- The assertions (
expect(...)calls, their matchers, and the values passed to them) - The branches the test exercises (loops, conditionals, locale/environment/role switches, multiple data variations)
- The setup data that drives those branches
If a change removes an assertion, collapses multiple variations into one, or narrows the scenarios the test walks through, it is not a fix. It is a change to the test, and it belongs in a separate, intentional commit, not in output cleanup.
When the unexpected output comes from code that the test genuinely needs to exercise, the fix belongs in the application code, in a helper, or in the test configuration. Not in the test itself.
Changes to tests that ARE legitimate
- Updating a deprecated RSpec API (e.g. replacing
expect_any_instance_ofwith an equivalent modern idiom), as long as the assertion semantics are preserved. - Updating setup that uses a deprecated framework API (e.g.
update_attributestoupdate), assertions untouched. - Replacing an inline
puts/ppthat was never load-bearing with nothing at all.
Anti-pattern: simplifying a test until the noise stops
A subtler variant of this anti-pattern is keeping the branch structure but substituting the values passed to assertions. If a branch produces noise because the test data is incomplete (e.g. a factory missing a required attribute), and the fix makes the assertion check a different value instead — one that happens not to produce noise — the branch still runs but no longer verifies what it was written to verify. This is the same loss of coverage as deleting the branch, just harder to see in a diff.
When test data is the root cause, the fix belongs in the test data.
If a test exercises multiple locales, roles, or data variations, and one of them produces unexpected output, do not delete the branches. The branches are the coverage.
Wrong:
# Before:
I18n.with_locale(:en) do
expect(page).to have_content(product_name)
end
I18n.with_locale(:pt) do
expect(page).to have_content(product_name)
end
# After (wrong, "simpler"):
expect(page).to have_content(product_name)
The "simpler" version no longer verifies Portuguese locale rendering at all. The English/Portuguese split was the point of the test. If I18n.with_locale(:pt) produced noise, silence whatever emitted it, do not stop testing Portuguese.
Right: trace the noise to its source (application code, a view helper, a gem), fix it there, and leave the assertions intact.
The workflow
1. Capture the full output
Before running the suite, create a task list using TaskCreate to track the full workflow. Create these tasks in order:
- Run plain RSpec suite
- Run documentation RSpec suite
- Fix all unexpected output (subtasks to be added after scanning)
- Final suite verification
Do not mark any task complete until you have actually finished it. The task list is the source of truth for what remains — do not consider the workflow done until all four tasks are marked complete.
Run the full suite first without any special formatting. The default progress format is compact enough that unexpected output stands out clearly against the dots and test summary:
bundle exec rspec 2>&1 | tee tmp/rspec-output.log
Wait for this command to finish completely before running the next one. Do not run these two commands in parallel. Both commands use the same shared test database, and concurrent runs will corrupt state and produce unreliable output.
This is the file you scan for unexpected output. Documentation format is too noisy to scan for strays - the test descriptions themselves become visual clutter.
Then run the suite a second time with documentation format:
bundle exec rspec --format documentation 2>&1 | tee tmp/rspec-output-doc.log
Wait for this run to finish before doing anything else. Do not start fixing issues while it is running. You need the documentation log to locate the source test for each issue, and without it you cannot complete the verification loop in step 3. Running fixes in parallel with the documentation capture means working without the data the fix depends on.
The documentation format prints the describe/context/it hierarchy inline, which lets you locate the exact test immediately before or after the stray output. The plain log (tmp/rspec-output.log) is the definitive source for triage in step 2 — it is the only log you should scan for unexpected output. Do not scan the documentation log for additional unexpected output; use it only to locate the specific test that produced output you have already identified from the plain log.
If the user has indicated the suite takes a long time, run both in the background and poll for completion rather than blocking the terminal, but do not start step 2 until both logs are on disk. Tell the user you are running in the background and will report back.
2. Scan for unexpected output
Read the plain log (tmp/rspec-output.log) and identify each distinct unexpected output. Group by likely root cause where possible, but plan to fix them one at a time.
Skip any patterns the user has flagged as "ignore for now".
Once you have the complete list of issues, update task 3 to add one subtask per issue, using the stray output text as the description (e.g. "Fix: warning: already initialized constant Foo"). The subtask list is now the definitive picture of remaining work — every issue must have a subtask, and no issue should be fixed without one.
3. For each issue, follow the verification loop
This loop is the core of the skill. Do not skip steps and do not reorder them. In particular, do not start planning or applying a fix until you have completed step 2 and seen the unexpected output appear in a single scoped command. Thinking ahead about the fix while still locating the source test is the most common way this workflow goes wrong.
- Locate the source test. Use the documentation-format log (
tmp/rspec-output-doc.log). In this format, each test description is its own line, so stray output appears as lines that don't match the indented description pattern, typically sandwiched between two test descriptions.
Use these tools in this order:
- Grep (not Bash
grep) to find the exact line number of the stray output. Use anchored patterns to avoid false matches against test descriptions: - Exact single-line match: pattern
^true$for a line that is literallytrue - Starts-with match: pattern
^Branch2 ID:for a line beginning with a known prefix - For deprecation or warning text, match on the distinctive prefix:
^\[DEPRECATION\]or^warning: - Read (not Bash
sed/head/tail) withoffsetandlimitto view the surrounding lines. A window of roughly 10 lines before and 5 after is usually enough to see the nearest test description.
The test description immediately preceding the stray output is almost always the one that produced it. If the output appears before any test description (e.g. during boot), it came from spec_helper.rb, rails_helper.rb, or an eagerly loaded initializer.
If the output comes from a setup hook or application code, the nearest test is still useful as a reproduction case.
- Reproduce in isolation. This is a hard gate. Run just that one test:
``bash bundle exec rspec spec/path/to/spec.rb:LINE `` You must see the unexpected output in this single-test run before you are allowed to propose, plan, or apply any fix. No exceptions. The point of the fix is to make this output stop appearing in this exact command, so without the baseline you cannot verify anything.
If the output does not appear, the output may depend on test ordering or another test's state. Widen the scope (run the full file, then the directory) until you can reproduce it reliably with a single command. That command, whatever its scope, becomes the reproduction case and the verification case in step 4.
If you cannot reproduce the output with any scoped command short of the full suite, stop and tell the user. Do not attempt a fix against the full suite as the only reproduction, the feedback loop is too slow to verify cleanly.
Only once you have a reliable, scoped reproduction may you proceed to step 3.
- Categorize and fix.
- Warnings (Ruby warnings, deprecation notices from gems or Rails): resolve the underlying cause. For
already initialized constant, find the duplicate definition. For deprecations, update to the non-deprecated API. Suppressing warnings is a last resort and should be called out explicitly to the user. - Stray puts / p / pp / Rails.logger calls: apply the most idiomatic fix without asking. In almost all cases this means deleting the stray call - it was left over from debugging. Note that
pporpof a boolean, integer, or short string often prints as a single-word line (true,false, a number), which is why the anchored regex patterns in step 1 matter - unanchored searches fortruewill match dozens of test descriptions. Exceptions: if the call looks intentional (inside a rake task, a generator, a CLI), leave it and flag it to the user. If a test genuinely needs to assert on logger output, refactor to useexpect(Rails.logger).to receive(...)instead of letting output leak. - Output from third-party code: silence at the test configuration level (e.g. in
spec/spec_helper.rborspec/rails_helper.rb), scoped as narrowly as possible. Prefer fixing the caller over globally silencing.
- Verify the fix. Re-run the same isolated test:
``bash bundle exec rspec spec/path/to/spec.rb:LINE `` Confirm the output is gone. If it is not, the fix is wrong or incomplete - iterate before moving on.
- Simplify the fix. Run the
/simplifyskill, scoped to the fix itself (the helper you added, the config change you made, the deprecated call you updated). This is a required step, not optional. Do not let/simplifytouch the surrounding test code, assertions, or branches that were not part of the fix. If/simplifyproposes collapsing or removing assertions, reject those suggestions, they violate the fix scope rule above.
- Check the diff. Before committing, review the diff and ask: does this change only how the test runs, or has it changed what it covers? If any assertion has been removed, any branch collapsed, or any data variation dropped, back out and rethink the fix, the noise source is elsewhere.
- Commit. Use the commit message format below unless the user has already told you to use a different format. Confirm the user is on the branch they specified at the start, then commit. One fix per commit, do not batch. After committing, mark that issue's subtask in task 3 complete.
- Move on. Return to step 3 with the next issue.
4. Final verification
When you believe all issues are resolved, re-run the full suite without the documentation format and confirm the output is clean (aside from any user-specified ignore patterns). If new unexpected output appears that was masked by earlier issues, loop back through step 3 for each.
Stop when the full suite runs without unexpected output. Mark the final verification task (task 4) complete.
Once complete, direct the user to review each commit to ensure that the changes are correct.
Commit messages
Use this format unless the user has already specified a preferred format:
- Title line
Before,followed by current state context- Explanation of the problem (the
Beforeand problem may be one paragraph if they are closely tied, or two paragraphs if separating them reads more clearly) Now,followed by what changed and the impact
Avoid overexplaining individual changes. Prefer to explain the overall problem being solved rather than re-explaining what can be seen in the diff.
Example:
Suppress Puma cluster-mode warnings in test environment
When Capybara starts a Puma server for JS feature specs, Puma loads
config/puma.rb and runs in single mode (no workers). The
before_worker_boot and after_worker_boot hooks defined there are
cluster-mode-only, so Puma emits warnings about them never executing.
Now, those hooks are skipped when RACK_ENV=test. They remain active
in all other environments where Puma runs in cluster mode as intended.
What this skill does not do
- It does not address test failures, only unexpected output from passing tests. If tests are failing, resolve those first using standard debugging.
- It does not rewrite the suite for speed or structure.
- It does not add new tests, only fixes noise in existing ones.
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: thoughtbot
- Source: thoughtbot/clean-rspec-output
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.