SIGN IN SIGN UP
rtk-ai / rtk UNCLAIMED

CLI proxy that reduces LLM token consumption by 60-90% on common dev commands. Single Rust binary, zero dependencies

0 0 0 Rust
feat(ruby): add Ruby on Rails support (rspec, rubocop, rake, bundle) (#724) * feat(ruby): add Ruby on Rails support (rspec, rubocop, rake, bundle) Unifies 5 competing PRs (#198, #292, #379, #534, #643) into a single coherent implementation. New commands: - rtk rspec: JSON parsing with text fallback (60%+ savings) - rtk rubocop: JSON parsing, group by cop/severity (60%+ savings) - rtk rake test: Minitest state machine parser (85-90% savings) - rtk bundle install: TOML filter, strip Using lines (90%+ savings) Shared infrastructure: ruby_exec(), fallback_tail(), exit_code_from_output(), count_tokens() in utils.rs. Discover/rewrite rules for rspec, rubocop, rake, rails, bundle including bundle exec and bin/ variants. E2E smoke tests (scripts/test-ruby.sh) covering all 4 commands. 56 new unit tests + 4 inline TOML tests. All 1035 tests passing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Navid EMAD <navid.emad@yespark.fr> * fix(ruby): use TEST= env var for rake single-file test in smoke tests Rails' `rake test` ignores positional file args; use `TEST=path` syntax. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Navid EMAD <navid.emad@yespark.fr> * docs(ruby): add Ruby module architecture and update attribution Integrate ARCHITECTURE.md Ruby Module Architecture section and CLAUDE.md module table/fork-features from PR #643. Update PR description attribution. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Navid EMAD <navid.emad@yespark.fr> * chore: remove PULL_REQUEST_DESCRIPTION.md from repo PR description lives on GitHub, no need to track in the codebase. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Navid EMAD <navid.emad@yespark.fr> --------- Signed-off-by: Navid EMAD <navid.emad@yespark.fr> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 15:04:59 +01:00
#!/usr/bin/env bash
#
# RTK Smoke Tests — Ruby (RSpec, RuboCop, Minitest, Bundle)
# Creates a minimal Rails app, exercises all Ruby RTK filters, then cleans up.
# Usage: bash scripts/test-ruby.sh
#
# Prerequisites: rtk (installed), ruby, bundler, rails gem
# Duration: ~60-120s (rails new + bundle install dominate)
#
set -euo pipefail
PASS=0
FAIL=0
SKIP=0
FAILURES=()
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m'
# ── Helpers ──────────────────────────────────────────
assert_ok() {
local name="$1"; shift
local output
if output=$("$@" 2>&1); then
PASS=$((PASS + 1))
printf " ${GREEN}PASS${NC} %s\n" "$name"
else
FAIL=$((FAIL + 1))
FAILURES+=("$name")
printf " ${RED}FAIL${NC} %s\n" "$name"
printf " cmd: %s\n" "$*"
printf " out: %s\n" "$(echo "$output" | head -3)"
fi
}
assert_contains() {
local name="$1"; local needle="$2"; shift 2
local output
if output=$("$@" 2>&1) && echo "$output" | grep -q "$needle"; then
PASS=$((PASS + 1))
printf " ${GREEN}PASS${NC} %s\n" "$name"
else
FAIL=$((FAIL + 1))
FAILURES+=("$name")
printf " ${RED}FAIL${NC} %s\n" "$name"
printf " expected: '%s'\n" "$needle"
printf " got: %s\n" "$(echo "$output" | head -3)"
fi
}
# Allow non-zero exit but check output
assert_output() {
local name="$1"; local needle="$2"; shift 2
local output
output=$("$@" 2>&1) || true
if echo "$output" | grep -qi "$needle"; then
PASS=$((PASS + 1))
printf " ${GREEN}PASS${NC} %s\n" "$name"
else
FAIL=$((FAIL + 1))
FAILURES+=("$name")
printf " ${RED}FAIL${NC} %s\n" "$name"
printf " expected: '%s'\n" "$needle"
printf " got: %s\n" "$(echo "$output" | head -3)"
fi
}
skip_test() {
local name="$1"; local reason="$2"
SKIP=$((SKIP + 1))
printf " ${YELLOW}SKIP${NC} %s (%s)\n" "$name" "$reason"
}
# Assert command exits with non-zero and output matches needle
assert_exit_nonzero() {
local name="$1"; local needle="$2"; shift 2
local output
local rc=0
output=$("$@" 2>&1) || rc=$?
if [[ $rc -ne 0 ]] && echo "$output" | grep -qi "$needle"; then
PASS=$((PASS + 1))
printf " ${GREEN}PASS${NC} %s (exit=%d)\n" "$name" "$rc"
else
FAIL=$((FAIL + 1))
FAILURES+=("$name")
printf " ${RED}FAIL${NC} %s (exit=%d)\n" "$name" "$rc"
if [[ $rc -eq 0 ]]; then
printf " expected non-zero exit, got 0\n"
else
printf " expected: '%s'\n" "$needle"
fi
printf " out: %s\n" "$(echo "$output" | head -3)"
fi
}
section() {
printf "\n${BOLD}${CYAN}── %s ──${NC}\n" "$1"
}
# ── Prerequisite checks ─────────────────────────────
RTK=$(command -v rtk || echo "")
if [[ -z "$RTK" ]]; then
echo "rtk not found in PATH. Run: cargo install --path ."
exit 1
fi
if ! command -v ruby >/dev/null 2>&1; then
echo "ruby not found in PATH. Install Ruby first."
exit 1
fi
if ! command -v bundle >/dev/null 2>&1; then
echo "bundler not found in PATH. Run: gem install bundler"
exit 1
fi
if ! command -v rails >/dev/null 2>&1; then
echo "rails not found in PATH. Run: gem install rails"
exit 1
fi
# ── Preamble ─────────────────────────────────────────
printf "${BOLD}RTK Smoke Tests — Ruby (RSpec, RuboCop, Minitest, Bundle)${NC}\n"
printf "Binary: %s (%s)\n" "$RTK" "$(rtk --version)"
printf "Ruby: %s\n" "$(ruby --version)"
printf "Rails: %s\n" "$(rails --version)"
printf "Bundler: %s\n" "$(bundle --version)"
printf "Date: %s\n\n" "$(date '+%Y-%m-%d %H:%M')"
# ── Temp dir + cleanup trap ──────────────────────────
TMPDIR=$(mktemp -d /tmp/rtk-ruby-smoke-XXXXXX)
trap 'rm -rf "$TMPDIR"' EXIT
printf "${BOLD}Setting up temporary Rails app in %s ...${NC}\n" "$TMPDIR"
# ── Setup phase (not counted in assertions) ──────────
cd "$TMPDIR"
# 1. Create minimal Rails app
printf " → rails new (--minimal --skip-git --skip-docker) ...\n"
rails new rtk_smoke_app --minimal --skip-git --skip-docker --quiet 2>&1 | tail -1 || true
cd rtk_smoke_app
# 2. Add rspec-rails and rubocop to Gemfile
cat >> Gemfile <<'GEMFILE'
group :development, :test do
gem 'rspec-rails'
gem 'rubocop', require: false
end
GEMFILE
# 3. Bundle install
printf " → bundle install ...\n"
bundle install --quiet 2>&1 | tail -1 || true
# 4. Generate scaffold (creates model + minitest files)
printf " → rails generate scaffold Post ...\n"
rails generate scaffold Post title:string body:text published:boolean --quiet 2>&1 | tail -1 || true
# 5. Install RSpec + create manual spec file
printf " → rails generate rspec:install ...\n"
rails generate rspec:install --quiet 2>&1 | tail -1 || true
mkdir -p spec/models
cat > spec/models/post_spec.rb <<'SPEC'
require 'rails_helper'
RSpec.describe Post, type: :model do
it "is valid with valid attributes" do
post = Post.new(title: "Test", body: "Body", published: false)
expect(post).to be_valid
end
end
SPEC
# 6. Create + migrate database
printf " → rails db:create && db:migrate ...\n"
rails db:create --quiet 2>&1 | tail -1 || true
rails db:migrate --quiet 2>&1 | tail -1 || true
# 7. Create a file with intentional RuboCop offenses
printf " → creating rubocop_bait.rb with intentional offenses ...\n"
cat > app/models/rubocop_bait.rb <<'BAIT'
class RubocopBait < ApplicationRecord
def messy_method()
x = 1
y = 2
if x == 1
puts "hello world"
end
return nil
end
end
BAIT
# 8. Create a failing RSpec spec
printf " → creating failing rspec spec ...\n"
cat > spec/models/post_fail_spec.rb <<'FAILSPEC'
require 'rails_helper'
RSpec.describe Post, type: :model do
it "intentionally fails validation check" do
post = Post.new(title: "Hello", body: "World", published: false)
expect(post.title).to eq("Wrong Title On Purpose")
end
end
FAILSPEC
# 9. Create an RSpec spec with pending example
printf " → creating rspec spec with pending example ...\n"
cat > spec/models/post_pending_spec.rb <<'PENDSPEC'
require 'rails_helper'
RSpec.describe Post, type: :model do
it "is valid with title" do
post = Post.new(title: "OK", body: "Body", published: false)
expect(post).to be_valid
end
it "will support markdown later" do
pending "Not yet implemented"
expect(Post.new.render_markdown).to eq("<p>hello</p>")
end
end
PENDSPEC
# 10. Create a failing minitest test
printf " → creating failing minitest test ...\n"
cat > test/models/post_fail_test.rb <<'FAILTEST'
require "test_helper"
class PostFailTest < ActiveSupport::TestCase
test "intentionally fails" do
assert_equal "wrong", Post.new(title: "right").title
end
end
FAILTEST
# 11. Create a passing minitest test
printf " → creating passing minitest test ...\n"
cat > test/models/post_pass_test.rb <<'PASSTEST'
require "test_helper"
class PostPassTest < ActiveSupport::TestCase
test "post is valid" do
post = Post.new(title: "OK", body: "Body", published: false)
assert post.valid?
end
end
PASSTEST
printf "\n${BOLD}Setup complete. Running tests...${NC}\n"
# ══════════════════════════════════════════════════════
# Test sections
# ══════════════════════════════════════════════════════
# ── 1. RSpec ─────────────────────────────────────────
section "RSpec"
assert_output "rtk rspec (with failure)" \
"failed" \
rtk rspec
assert_output "rtk rspec spec/models/post_spec.rb (pass)" \
"RSpec.*passed" \
rtk rspec spec/models/post_spec.rb
assert_output "rtk rspec spec/models/post_fail_spec.rb (fail)" \
"failed\|❌" \
rtk rspec spec/models/post_fail_spec.rb
# ── 2. RuboCop ───────────────────────────────────────
section "RuboCop"
assert_output "rtk rubocop (with offenses)" \
"offense" \
rtk rubocop
assert_output "rtk rubocop app/ (with offenses)" \
"rubocop_bait\|offense" \
rtk rubocop app/
# ── 3. Minitest (rake test) ──────────────────────────
section "Minitest (rake test)"
assert_output "rtk rake test (with failure)" \
"failure\|error\|FAIL" \
rtk rake test
assert_output "rtk rake test single passing file" \
"ok rake test\|0 failures" \
rtk rake test TEST=test/models/post_pass_test.rb
assert_exit_nonzero "rtk rake test single failing file" \
"failure\|FAIL" \
rtk rake test test/models/post_fail_test.rb
# ── 4. Bundle install ────────────────────────────────
section "Bundle install"
assert_output "rtk bundle install (idempotent)" \
"bundle\|ok\|complete\|install" \
rtk bundle install
# ── 5. Exit code preservation ────────────────────────
section "Exit code preservation"
assert_exit_nonzero "rtk rspec exits non-zero on failure" \
"failed\|failure" \
rtk rspec spec/models/post_fail_spec.rb
assert_exit_nonzero "rtk rubocop exits non-zero on offenses" \
"offense" \
rtk rubocop app/models/rubocop_bait.rb
assert_exit_nonzero "rtk rake test exits non-zero on failure" \
"failure\|FAIL" \
rtk rake test test/models/post_fail_test.rb
# ── 6. bundle exec variants ─────────────────────────
section "bundle exec variants"
assert_output "bundle exec rspec spec/models/post_spec.rb" \
"passed\|example" \
rtk bundle exec rspec spec/models/post_spec.rb
assert_output "bundle exec rubocop app/" \
"offense" \
rtk bundle exec rubocop app/
# ── 7. RuboCop autocorrect ───────────────────────────
section "RuboCop autocorrect"
# Copy bait file so autocorrect has something to fix
cp app/models/rubocop_bait.rb app/models/rubocop_bait_ac.rb
sed -i.bak 's/RubocopBait/RubocopBaitAc/' app/models/rubocop_bait_ac.rb
assert_output "rtk rubocop -A (autocorrect)" \
"autocorrected\|rubocop\|ok\|offense\|inspected" \
rtk rubocop -A app/models/rubocop_bait_ac.rb
# Clean up autocorrect test file
rm -f app/models/rubocop_bait_ac.rb app/models/rubocop_bait_ac.rb.bak
# ── 8. RSpec pending ─────────────────────────────────
section "RSpec pending"
assert_output "rtk rspec with pending example" \
"pending" \
rtk rspec spec/models/post_pending_spec.rb
# ── 9. RSpec text fallback ───────────────────────────
section "RSpec text fallback"
assert_output "rtk rspec --format documentation (text path)" \
"valid\|example\|post" \
rtk rspec --format documentation spec/models/post_spec.rb
# ── 10. RSpec empty suite ────────────────────────────
section "RSpec empty suite"
assert_output "rtk rspec nonexistent tag" \
"0 examples\|No examples" \
rtk rspec --tag nonexistent spec/models/post_spec.rb
# ── 11. Token savings ────────────────────────────────
section "Token savings"
# rspec (passing spec)
raw_len=$( (bundle exec rspec spec/models/post_spec.rb 2>&1 || true) | wc -c | tr -d ' ')
rtk_len=$( (rtk rspec spec/models/post_spec.rb 2>&1 || true) | wc -c | tr -d ' ')
if [[ "$rtk_len" -lt "$raw_len" ]]; then
PASS=$((PASS + 1))
printf " ${GREEN}PASS${NC} rspec: rtk (%s bytes) < raw (%s bytes)\n" "$rtk_len" "$raw_len"
else
FAIL=$((FAIL + 1))
FAILURES+=("token savings: rspec")
printf " ${RED}FAIL${NC} rspec: rtk (%s bytes) >= raw (%s bytes)\n" "$rtk_len" "$raw_len"
fi
# rubocop (exits non-zero on offenses, so || true)
raw_len=$( (bundle exec rubocop app/ 2>&1 || true) | wc -c | tr -d ' ')
rtk_len=$( (rtk rubocop app/ 2>&1 || true) | wc -c | tr -d ' ')
if [[ "$rtk_len" -lt "$raw_len" ]]; then
PASS=$((PASS + 1))
printf " ${GREEN}PASS${NC} rubocop: rtk (%s bytes) < raw (%s bytes)\n" "$rtk_len" "$raw_len"
else
FAIL=$((FAIL + 1))
FAILURES+=("token savings: rubocop")
printf " ${RED}FAIL${NC} rubocop: rtk (%s bytes) >= raw (%s bytes)\n" "$rtk_len" "$raw_len"
fi
# rake test (passing file)
raw_len=$( (bundle exec rake test TEST=test/models/post_pass_test.rb 2>&1 || true) | wc -c | tr -d ' ')
rtk_len=$( (rtk rake test test/models/post_pass_test.rb 2>&1 || true) | wc -c | tr -d ' ')
if [[ "$rtk_len" -lt "$raw_len" ]]; then
PASS=$((PASS + 1))
printf " ${GREEN}PASS${NC} rake test: rtk (%s bytes) < raw (%s bytes)\n" "$rtk_len" "$raw_len"
else
FAIL=$((FAIL + 1))
FAILURES+=("token savings: rake test")
printf " ${RED}FAIL${NC} rake test: rtk (%s bytes) >= raw (%s bytes)\n" "$rtk_len" "$raw_len"
fi
# bundle install (idempotent)
raw_len=$( (bundle install 2>&1 || true) | wc -c | tr -d ' ')
rtk_len=$( (rtk bundle install 2>&1 || true) | wc -c | tr -d ' ')
if [[ "$rtk_len" -lt "$raw_len" ]]; then
PASS=$((PASS + 1))
printf " ${GREEN}PASS${NC} bundle install: rtk (%s bytes) < raw (%s bytes)\n" "$rtk_len" "$raw_len"
else
FAIL=$((FAIL + 1))
FAILURES+=("token savings: bundle install")
printf " ${RED}FAIL${NC} bundle install: rtk (%s bytes) >= raw (%s bytes)\n" "$rtk_len" "$raw_len"
fi
# ── 12. Verbose flag ─────────────────────────────────
section "Verbose flag (-v)"
assert_output "rtk -v rspec (verbose)" \
"RSpec\|passed\|Running\|example" \
rtk -v rspec spec/models/post_spec.rb
# ══════════════════════════════════════════════════════
# Report
# ══════════════════════════════════════════════════════
printf "\n${BOLD}══════════════════════════════════════${NC}\n"
printf "${BOLD}Results: ${GREEN}%d passed${NC}, ${RED}%d failed${NC}, ${YELLOW}%d skipped${NC}\n" "$PASS" "$FAIL" "$SKIP"
if [[ ${#FAILURES[@]} -gt 0 ]]; then
printf "\n${RED}Failures:${NC}\n"
for f in "${FAILURES[@]}"; do
printf " - %s\n" "$f"
done
fi
printf "${BOLD}══════════════════════════════════════${NC}\n"
exit "$FAIL"