chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:01:57 +08:00
commit 9dda3e2451
399 changed files with 118131 additions and 0 deletions
+163
View File
@@ -0,0 +1,163 @@
from __future__ import annotations
import asyncio
import os
import subprocess
import tempfile
from pathlib import Path
from .config import TaskConfig
from .manifest import (
RunManifest,
SessionEntry,
TbEntry,
TbTaskEntry,
write_manifest,
)
from .session import run_all_sessions, setup_codebase, setup_rtk
from .terminal_bench import run_terminal_bench
from .vm import create_vm_pool, destroy_vm_pool
ROOT_DIR = Path(__file__).resolve().parent.parent
def _create_tarball(source_dir: Path) -> str:
fd, tarball = tempfile.mkstemp(suffix=".tar.gz")
os.close(fd)
try:
subprocess.run(
["tar", "czf", tarball, "-C", str(source_dir), "."],
check=True,
)
except Exception:
Path(tarball).unlink(missing_ok=True)
raise
return tarball
def _print_step(step: int, total: int, msg: str):
print(f"\n[{step}/{total}] {msg}")
def _session_to_entry(r) -> SessionEntry:
return SessionEntry(
vm_name=r.vm_name,
group=r.group,
stdout_json=f"{r.vm_name}-stdout.json",
otel_log=f"{r.vm_name}-otel.log",
rtk_db=f"{r.vm_name}-tracking.db" if r.rtk_db_path else None,
exit_code=r.exit_code,
error=r.error or None,
)
def _tb_to_entry(r) -> TbEntry:
return TbEntry(
vm_name=r.vm_name,
group=r.group,
total=r.total,
passed=r.passed,
failed=r.failed,
tasks=[TbTaskEntry(name=t.name, passed=t.passed, duration_s=t.duration_s) for t in r.tasks],
error=r.error,
)
async def run_benchmark(
task: TaskConfig,
vms: int,
api_key: str,
output_dir: Path,
cloud_init: Path | None = None,
terminal_bench: bool = False,
keep_vms: bool = False,
) -> RunManifest:
if cloud_init is None:
cloud_init = ROOT_DIR / "cloud-init-base.yaml"
output_dir.mkdir(parents=True, exist_ok=True)
total_steps = 5 if terminal_bench else 4
vm_names: list[str] = []
local_tarball: str | None = None
manifest = RunManifest(
task_name=task.name,
model=task.model,
vm_count=vms,
)
try:
_print_step(1, total_steps, f"Creating {vms * 2} VMs ({vms} RTK ON + {vms} RTK OFF)")
vm_names = await create_vm_pool(vms, cloud_init)
print(f" VMs ready: {', '.join(vm_names)}")
_print_step(2, total_steps, "Setting up codebases")
if not task.codebase.is_github:
local_tarball = _create_tarball(task.codebase.local_path())
await asyncio.gather(*(
setup_codebase(name, task.codebase, local_tarball)
for name in vm_names
))
print(" Codebases deployed")
_print_step(3, total_steps, "Configuring RTK on ON VMs")
setup_script = ROOT_DIR / "setup-rtk.sh"
on_vms = [n for n in vm_names if "-on-" in n]
off_vms = [n for n in vm_names if "-off-" in n]
await asyncio.gather(*(setup_rtk(vm, setup_script) for vm in on_vms))
print(f" RTK configured on {len(on_vms)} VMs")
_print_step(4, total_steps, f"Running Claude sessions (timeout: {task.timeout_minutes}min)")
results = await run_all_sessions(vm_names, task, api_key, output_dir)
on_ok = [r for r in results if r.group == "on" and not r.error]
off_ok = [r for r in results if r.group == "off" and not r.error]
errors = [r for r in results if r.error]
print(f" Completed: {len(on_ok)} ON, {len(off_ok)} OFF, {len(errors)} errors")
for r in errors:
print(f" {r.vm_name}: {r.error}")
manifest.sessions = [_session_to_entry(r) for r in results]
if terminal_bench:
_print_step(5, total_steps, "Running terminal-bench precision tests")
tb_on = await asyncio.gather(*(
run_terminal_bench(vm, "on", task.model, api_key)
for vm in on_vms
))
tb_off = await asyncio.gather(*(
run_terminal_bench(vm, "off", task.model, api_key)
for vm in off_vms
))
manifest.terminal_bench = [_tb_to_entry(r) for r in list(tb_on) + list(tb_off)]
ok_on = [r for r in tb_on if not r.error]
ok_off = [r for r in tb_off if not r.error]
if ok_on and ok_off:
on_total = sum(r.total for r in ok_on)
on_passed = sum(r.passed for r in ok_on)
off_total = sum(r.total for r in ok_off)
off_passed = sum(r.passed for r in ok_off)
on_rate = on_passed / on_total if on_total else 0
off_rate = off_passed / off_total if off_total else 0
print(f" terminal-bench: ON pass rate={on_rate:.0%}, OFF pass rate={off_rate:.0%}, delta={on_rate - off_rate:+.0%}")
tb_errors = [r for r in list(tb_on) + list(tb_off) if r.error]
for r in tb_errors:
print(f" {r.vm_name}: {r.error}")
write_manifest(manifest, output_dir)
print(f"\n Manifest written to {output_dir / 'manifest.json'}")
finally:
if local_tarball:
Path(local_tarball).unlink(missing_ok=True)
if not keep_vms and vm_names:
print("\nCleaning up VMs...")
await destroy_vm_pool(vm_names)
print(" VMs destroyed")
return manifest
+698
View File
@@ -0,0 +1,698 @@
#!/usr/bin/env bash
set -e
# Use local release build if available, otherwise fall back to installed rtk
if [ -f "./target/release/rtk" ]; then
RTK="$(cd "$(dirname ./target/release/rtk)" && pwd)/$(basename ./target/release/rtk)"
elif command -v rtk &> /dev/null; then
RTK="$(command -v rtk)"
else
echo "Error: rtk not found. Run 'cargo build --release' or install rtk."
exit 1
fi
BENCH_DIR="$(pwd)/scripts/benchmark"
RTK_ROOT="$(pwd)"
if [ -z "$CI" ]; then
rm -rf "$BENCH_DIR"
mkdir -p "$BENCH_DIR/unix" "$BENCH_DIR/rtk" "$BENCH_DIR/diff"
fi
safe_name() {
echo "$1" | tr ' /' '_-' | tr -cd 'a-zA-Z0-9_-'
}
count_tokens() {
local input="$1"
local len=${#input}
echo $(( (len + 3) / 4 ))
}
TOTAL_UNIX=0
TOTAL_RTK=0
TOTAL_TESTS=0
GOOD_TESTS=0
FAIL_TESTS=0
WARN_TESTS=0
NEGATIVE_TESTS=0
bench() {
local name="$1"
local unix_cmd="$2"
local rtk_cmd="$3"
unix_out=$(eval "$unix_cmd" 2>/dev/null || true)
rtk_out=$(eval "$rtk_cmd" 2>/dev/null || true)
unix_tokens=$(count_tokens "$unix_out")
rtk_tokens=$(count_tokens "$rtk_out")
TOTAL_TESTS=$((TOTAL_TESTS + 1))
local icon=""
local tag=""
if [ -z "$rtk_out" ] && [ -n "$unix_out" ]; then
icon="❌"
tag="FAIL"
FAIL_TESTS=$((FAIL_TESTS + 1))
TOTAL_UNIX=$((TOTAL_UNIX + unix_tokens))
TOTAL_RTK=$((TOTAL_RTK + unix_tokens))
elif [ "$rtk_tokens" -gt "$unix_tokens" ] && [ "$unix_tokens" -gt 0 ]; then
icon="🔴"
tag="NEG"
NEGATIVE_TESTS=$((NEGATIVE_TESTS + 1))
TOTAL_UNIX=$((TOTAL_UNIX + unix_tokens))
TOTAL_RTK=$((TOTAL_RTK + rtk_tokens))
elif [ "$unix_tokens" -gt 0 ] && [ "$rtk_tokens" -eq "$unix_tokens" ]; then
icon="⚠️"
tag="WARN"
WARN_TESTS=$((WARN_TESTS + 1))
TOTAL_UNIX=$((TOTAL_UNIX + unix_tokens))
TOTAL_RTK=$((TOTAL_RTK + rtk_tokens))
elif [ "$unix_tokens" -gt 0 ]; then
local savings=$(( (unix_tokens - rtk_tokens) * 100 / unix_tokens ))
if [ "$savings" -lt 60 ]; then
icon="⚠️"
tag="WARN"
WARN_TESTS=$((WARN_TESTS + 1))
else
icon="✅"
tag="GOOD"
GOOD_TESTS=$((GOOD_TESTS + 1))
fi
TOTAL_UNIX=$((TOTAL_UNIX + unix_tokens))
TOTAL_RTK=$((TOTAL_RTK + rtk_tokens))
else
icon="⏭️"
tag="SKIP"
WARN_TESTS=$((WARN_TESTS + 1))
fi
if [ "$tag" = "FAIL" ]; then
printf "%s %-24s │ %-40s │ %-40s │ %6d → %6s (--)\n" \
"$icon" "$name" "$unix_cmd" "$rtk_cmd" "$unix_tokens" "-"
else
if [ "$unix_tokens" -gt 0 ]; then
local pct=$(( (unix_tokens - rtk_tokens) * 100 / unix_tokens ))
else
local pct=0
fi
printf "%s %-24s │ %-40s │ %-40s │ %6d → %6d (%+d%%)\n" \
"$icon" "$name" "$unix_cmd" "$rtk_cmd" "$unix_tokens" "$rtk_tokens" "$pct"
fi
if [ -z "$CI" ]; then
local filename=$(safe_name "$name")
local prefix="GOOD"
[ "$tag" = "FAIL" ] && prefix="FAIL"
[ "$tag" = "NEG" ] && prefix="NEG"
[ "$tag" = "WARN" ] && prefix="WARN"
[ "$tag" = "SKIP" ] && prefix="SKIP"
local ts=$(date "+%d/%m/%Y %H:%M:%S")
printf "# %s\n> %s\n\n\`\`\`bash\n$ %s\n\`\`\`\n\n\`\`\`\n%s\n\`\`\`\n" \
"$name" "$ts" "$unix_cmd" "$unix_out" > "$BENCH_DIR/unix/${filename}.md"
printf "# %s\n> %s\n\n\`\`\`bash\n$ %s\n\`\`\`\n\n\`\`\`\n%s\n\`\`\`\n" \
"$name" "$ts" "$rtk_cmd" "$rtk_out" > "$BENCH_DIR/rtk/${filename}.md"
{
echo "# Diff: $name"
echo "> $ts"
echo ""
echo "| Metric | Unix | RTK |"
echo "|--------|------|-----|"
echo "| Tokens | $unix_tokens | $rtk_tokens |"
echo ""
echo "## Unix"
echo "\`\`\`"
echo "$unix_out"
echo "\`\`\`"
echo ""
echo "## RTK"
echo "\`\`\`"
echo "$rtk_out"
echo "\`\`\`"
} > "$BENCH_DIR/diff/${prefix}-${filename}.md"
fi
}
section() {
echo ""
echo "── $1 ──"
}
# ═══════════════════════════════════════════
echo "RTK Benchmark"
echo "═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════"
printf " %-24s │ %-40s │ %-40s │ %s\n" "TEST" "SHELL" "RTK" "TOKENS"
echo "───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────"
# ===================
# ls
# ===================
section "ls"
bench "ls" "ls -la" "$RTK ls"
bench "ls src/" "ls -la src/" "$RTK ls src/"
bench "ls -l src/" "ls -l src/" "$RTK ls -l src/"
bench "ls -la src/" "ls -la src/" "$RTK ls -la src/"
bench "ls -lh src/" "ls -lh src/" "$RTK ls -lh src/"
bench "ls src/ -l" "ls -l src/" "$RTK ls src/ -l"
bench "ls -a" "ls -la" "$RTK ls -a"
bench "ls multi" "ls -la src/ scripts/" "$RTK ls src/ scripts/"
# ===================
# tree
# ===================
if command -v tree &>/dev/null; then
section "tree"
bench "tree" "tree -L 2" "$RTK tree -L 2"
bench "tree src/" "tree src/ -L 2" "$RTK tree src/ -L 2"
else
echo ""
echo "⏭️ tree (not installed, skipped)"
fi
# ===================
# read
# ===================
section "read"
bench "read" "cat src/main.rs" "$RTK read src/main.rs"
bench "read -l minimal" "cat src/main.rs" "$RTK read src/main.rs -l minimal"
bench "read -l aggressive" "cat src/main.rs" "$RTK read src/main.rs -l aggressive"
bench "read -n" "cat -n src/main.rs" "$RTK read src/main.rs -n"
# ===================
# find
# ===================
section "find"
bench "find *" "find . -type f" "$RTK find '*'"
bench "find *.rs" "find . -name '*.rs' -type f" "$RTK find '*.rs'"
bench "find --max 10" "find . -not -path './target/*' -not -path './.git/*' -type f | head -10" "$RTK find '*' --max 10"
bench "find --max 100" "find . -not -path './target/*' -not -path './.git/*' -type f | head -100" "$RTK find '*' --max 100"
# ===================
# git
# ===================
section "git"
bench "git status" "git status" "$RTK git status"
bench "git log -n 10" "git log -10" "$RTK git log -n 10"
bench "git log -n 5" "git log -5" "$RTK git log -n 5"
bench "git diff" "git diff HEAD~1 2>/dev/null || echo ''" "$RTK git diff HEAD~1"
bench "git show" "git show HEAD --stat 2>/dev/null || true" "$RTK git show HEAD --stat"
# ===================
# grep
# ===================
section "grep"
bench "grep fn" "grep -rn 'fn ' src/ || true" "$RTK grep -rn 'fn ' src/"
bench "grep struct" "grep -rn 'struct ' src/ || true" "$RTK grep -rn 'struct ' src/"
bench "grep -l 40" "grep -rn 'fn ' src/ || true" "$RTK grep -rn 'fn ' src/ -l 40"
bench "grep -c" "grep -ron 'fn ' src/ || true" "$RTK grep -rc 'fn ' src/"
# ===================
# rg (native ripgrep, recursive by default, same output filter)
# ===================
section "rg"
bench "rg fn" "rg -n 'fn ' src/ || true" "$RTK rg 'fn ' src/"
bench "rg struct" "rg -n 'struct ' src/ || true" "$RTK rg 'struct ' src/"
bench "rg -l files" "rg -l 'fn ' src/ || true" "$RTK rg -l 'fn ' src/"
bench "rg -c count" "rg -c 'fn ' src/ || true" "$RTK rg -c 'fn ' src/"
# ===================
# json
# ===================
section "json"
cat > /tmp/rtk_bench.json << 'JSONEOF'
{
"name": "rtk",
"version": "0.2.1",
"config": {
"debug": false,
"max_depth": 10,
"filters": ["node_modules", "target", ".git"]
},
"dependencies": {
"serde": "1.0",
"clap": "4.0",
"anyhow": "1.0"
}
}
JSONEOF
bench "json" "cat /tmp/rtk_bench.json" "$RTK json /tmp/rtk_bench.json"
bench "json -d 2" "cat /tmp/rtk_bench.json" "$RTK json /tmp/rtk_bench.json -d 2"
rm -f /tmp/rtk_bench.json
# ===================
# deps
# ===================
section "deps"
bench "deps" "cat Cargo.toml" "$RTK deps"
# ===================
# env
# ===================
section "env"
bench "env" "env" "$RTK env"
bench "env -f PATH" "env | grep PATH" "$RTK env -f PATH"
# ===================
# err
# ===================
section "err"
if command -v cargo &>/dev/null; then
bench "err cargo build" "cargo build 2>&1 || true" "$RTK err cargo build 2>&1"
else
echo "⏭️ err cargo build (cargo not in PATH, skipped)"
fi
# ===================
# test
# ===================
section "test"
if command -v cargo &>/dev/null; then
bench "test cargo test" "cargo test 2>&1 || true" "$RTK test cargo test 2>&1"
else
echo "⏭️ test cargo test (cargo not in PATH, skipped)"
fi
# ===================
# log
# ===================
section "log"
LOG_FILE="/tmp/rtk_bench_sample.log"
cat > "$LOG_FILE" << 'LOGEOF'
2024-01-15 10:00:01 INFO Application started
2024-01-15 10:00:02 INFO Loading configuration
2024-01-15 10:00:03 ERROR Connection failed: timeout
2024-01-15 10:00:04 ERROR Connection failed: timeout
2024-01-15 10:00:05 ERROR Connection failed: timeout
2024-01-15 10:00:06 ERROR Connection failed: timeout
2024-01-15 10:00:07 ERROR Connection failed: timeout
2024-01-15 10:00:08 WARN Retrying connection
2024-01-15 10:00:09 INFO Connection established
2024-01-15 10:00:10 INFO Processing request
2024-01-15 10:00:11 INFO Processing request
2024-01-15 10:00:12 INFO Processing request
2024-01-15 10:00:13 INFO Request completed
LOGEOF
bench "log" "cat $LOG_FILE" "$RTK log $LOG_FILE"
rm -f "$LOG_FILE"
# ===================
# summary
# ===================
section "summary"
if command -v cargo &>/dev/null; then
bench "summary cargo --help" "cargo --help" "$RTK summary cargo --help"
else
echo "⏭️ summary cargo --help (cargo not in PATH, skipped)"
fi
if command -v rustc &>/dev/null; then
bench "summary rustc --help" "rustc --help 2>/dev/null || echo 'rustc not found'" "$RTK summary rustc --help"
else
echo "⏭️ summary rustc --help (rustc not in PATH, skipped)"
fi
# ===================
# cargo
# ===================
section "cargo"
if command -v cargo &>/dev/null; then
bench "cargo build" "cargo build 2>&1 || true" "$RTK cargo build 2>&1"
bench "cargo test" "cargo test 2>&1 || true" "$RTK cargo test 2>&1"
bench "cargo clippy" "cargo clippy 2>&1 || true" "$RTK cargo clippy 2>&1"
bench "cargo check" "cargo check 2>&1 || true" "$RTK cargo check 2>&1"
else
echo "⏭️ cargo build/test/clippy/check (cargo not in PATH, skipped)"
fi
# ===================
# smart
# ===================
section "smart"
bench "smart main.rs" "cat src/main.rs" "$RTK smart src/main.rs"
# ===================
# wc
# ===================
section "wc"
bench "wc" "wc Cargo.toml src/main.rs" "$RTK wc Cargo.toml src/main.rs"
# ===================
# curl
# ===================
section "curl"
if command -v curl &> /dev/null; then
bench "curl json" "curl -s https://mockhttp.org/json" "$RTK curl https://mockhttp.org/json"
bench "curl text" "curl -s https://mockhttp.org/robots.txt" "$RTK curl https://mockhttp.org/robots.txt"
fi
# ===================
# wget
# ===================
if command -v wget &> /dev/null; then
section "wget"
bench "wget" "wget -qO- https://mockhttp.org/json" "$RTK wget https://mockhttp.org/json"
rm -f json 2>/dev/null
fi
# ===================
# npm (standalone — does not require package.json)
# ===================
if command -v npm &> /dev/null; then
section "npm"
bench "npm list" "npm list -g --depth 0 2>&1 || true" "$RTK npm list -g --depth 0"
fi
# ===================
# Modern JavaScript Stack (skip si pas de package.json)
# ===================
if [ -f "package.json" ]; then
section "modern JS stack"
if command -v tsc &> /dev/null || [ -f "node_modules/.bin/tsc" ]; then
bench "tsc" "tsc --noEmit 2>&1 || true" "$RTK tsc --noEmit 2>&1"
fi
if command -v prettier &> /dev/null || [ -f "node_modules/.bin/prettier" ]; then
bench "prettier --check" "prettier --check . 2>&1 || true" "$RTK prettier --check ."
fi
if command -v eslint &> /dev/null || [ -f "node_modules/.bin/eslint" ]; then
bench "lint" "eslint . 2>&1 || true" "$RTK lint ."
fi
if [ -f "next.config.js" ] || [ -f "next.config.mjs" ] || [ -f "next.config.ts" ]; then
if command -v next &> /dev/null || [ -f "node_modules/.bin/next" ]; then
bench "next build" "next build 2>&1 || true" "$RTK next build"
fi
fi
if [ -f "playwright.config.ts" ] || [ -f "playwright.config.js" ]; then
if command -v playwright &> /dev/null || [ -f "node_modules/.bin/playwright" ]; then
bench "playwright test" "playwright test 2>&1 || true" "$RTK playwright test"
fi
fi
if [ -f "prisma/schema.prisma" ]; then
if command -v prisma &> /dev/null || [ -f "node_modules/.bin/prisma" ]; then
bench "prisma generate" "prisma generate 2>&1 || true" "$RTK prisma generate"
fi
fi
if command -v vitest &> /dev/null || [ -f "node_modules/.bin/vitest" ]; then
bench "vitest" "vitest run --reporter=json 2>&1 || true" "$RTK vitest"
fi
if command -v pnpm &> /dev/null; then
bench "pnpm list" "pnpm list --depth 0 2>&1 || true" "$RTK pnpm list --depth 0"
bench "pnpm outdated" "pnpm outdated 2>&1 || true" "$RTK pnpm outdated"
fi
fi
# ===================
# gh (skip si pas dispo ou pas dans un repo)
# ===================
if command -v gh &> /dev/null && git rev-parse --git-dir &> /dev/null && gh auth status &> /dev/null; then
section "gh"
bench "gh pr list" "gh pr list 2>&1 || true" "$RTK gh pr list"
bench "gh run list" "gh run list 2>&1 || true" "$RTK gh run list"
fi
# ===================
# glab
# ===================
if command -v glab &> /dev/null; then
section "glab"
bench "glab mr list" "glab mr list 2>&1 || true" "$RTK glab mr list"
bench "glab issue list" "glab issue list 2>&1 || true" "$RTK glab issue list"
fi
# ===================
# gt (Graphite)
# ===================
if command -v gt &> /dev/null; then
section "gt"
bench "gt log" "gt log 2>&1 || true" "$RTK gt log"
fi
# ===================
# docker
# ===================
if command -v docker &> /dev/null; then
section "docker"
bench "docker ps" "docker ps 2>/dev/null || true" "$RTK docker ps"
bench "docker images" "docker images 2>/dev/null || true" "$RTK docker images"
fi
# ===================
# kubectl
# ===================
if command -v kubectl &> /dev/null; then
section "kubectl"
bench "kubectl pods" "kubectl get pods 2>/dev/null || true" "$RTK kubectl pods"
bench "kubectl services" "kubectl get services 2>/dev/null || true" "$RTK kubectl services"
fi
# ===================
# Python (avec fixtures temporaires)
# ===================
if command -v python3 &> /dev/null && command -v ruff &> /dev/null && command -v pytest &> /dev/null; then
section "python"
PYTHON_FIXTURE=$(mktemp -d)
cd "$PYTHON_FIXTURE"
cat > pyproject.toml << 'PYEOF'
[project]
name = "rtk-bench"
version = "0.1.0"
[tool.ruff]
line-length = 88
PYEOF
cat > sample.py << 'PYEOF'
import os
import sys
import json
def process_data(x):
if x == None: # E711: comparison to None
return []
result = []
for i in range(len(x)): # C416: unnecessary list comprehension
result.append(x[i] * 2)
return result
def unused_function(): # F841: local variable assigned but never used
temp = 42
return None
PYEOF
cat > test_sample.py << 'PYEOF'
from sample import process_data
def test_process_data():
assert process_data([1, 2, 3]) == [2, 4, 6]
def test_process_data_none():
assert process_data(None) == []
PYEOF
bench "ruff check" "ruff check . 2>&1 || true" "$RTK ruff check ."
bench "pytest" "pytest -v 2>&1 || true" "$RTK pytest -v"
if command -v pip &>/dev/null; then
bench "pip list" "pip list 2>&1 || true" "$RTK pip list"
fi
if command -v mypy &>/dev/null; then
bench "mypy" "mypy sample.py 2>&1 || true" "$RTK mypy sample.py"
fi
cd "$RTK_ROOT"
rm -rf "$PYTHON_FIXTURE"
fi
# ===================
# Go (avec fixtures temporaires)
# ===================
if command -v go &> /dev/null && command -v golangci-lint &> /dev/null; then
section "go"
GO_FIXTURE=$(mktemp -d)
cd "$GO_FIXTURE"
cat > go.mod << 'GOEOF'
module bench
go 1.21
GOEOF
cat > main.go << 'GOEOF'
package main
import "fmt"
func Add(a, b int) int {
return a + b
}
func Multiply(a, b int) int {
return a * b
}
func main() {
fmt.Println(Add(2, 3))
fmt.Println(Multiply(4, 5))
}
GOEOF
cat > main_test.go << 'GOEOF'
package main
import "testing"
func TestAdd(t *testing.T) {
result := Add(2, 3)
if result != 5 {
t.Errorf("Add(2, 3) = %d; want 5", result)
}
}
func TestMultiply(t *testing.T) {
result := Multiply(4, 5)
if result != 20 {
t.Errorf("Multiply(4, 5) = %d; want 20", result)
}
}
GOEOF
bench "golangci-lint" "golangci-lint run 2>&1 || true" "$RTK golangci-lint run"
bench "go test" "go test -v 2>&1 || true" "$RTK go test -v"
bench "go build" "go build ./... 2>&1 || true" "$RTK go build ./..."
bench "go vet" "go vet ./... 2>&1 || true" "$RTK go vet ./..."
cd "$RTK_ROOT"
rm -rf "$GO_FIXTURE"
fi
# ===================
# Ruby
# ===================
if command -v ruby &> /dev/null; then
section "ruby"
if command -v rake &>/dev/null; then
bench "rake -T" "rake -T 2>&1 || true" "$RTK rake -T"
fi
if command -v rubocop &>/dev/null; then
bench "rubocop" "rubocop --format simple 2>&1 || true" "$RTK rubocop --format simple"
fi
if command -v rspec &>/dev/null; then
bench "rspec --dry-run" "rspec --dry-run 2>&1 || true" "$RTK rspec --dry-run"
fi
fi
# ===================
# dotnet
# ===================
if command -v dotnet &> /dev/null; then
section "dotnet"
bench "dotnet --info" "dotnet --info 2>&1 || true" "$RTK dotnet --info"
fi
# ===================
# aws
# ===================
if command -v aws &> /dev/null; then
section "aws"
bench "aws --version" "aws --version 2>&1 || true" "$RTK aws --version"
fi
# ===================
# psql
# ===================
if command -v psql &> /dev/null; then
section "psql"
bench "psql --version" "psql --version 2>&1 || true" "$RTK psql --version"
fi
# ===================
# rewrite (verify rewrite works with and without quotes)
# ===================
section "rewrite"
bench_rewrite() {
local name="$1"
local cmd="$2"
local expected="$3"
result=$(eval "$cmd" 2>&1 || true)
TOTAL_TESTS=$((TOTAL_TESTS + 1))
if [ "$result" = "$expected" ]; then
printf "✅ %-24s │ %-40s │ %s\n" "$name" "$cmd" "$result"
GOOD_TESTS=$((GOOD_TESTS + 1))
else
printf "❌ %-24s │ %-40s │ got: %s (expected: %s)\n" "$name" "$cmd" "$result" "$expected"
FAIL_TESTS=$((FAIL_TESTS + 1))
fi
}
bench_rewrite "rewrite quoted" "$RTK rewrite 'git status'" "rtk git status"
bench_rewrite "rewrite unquoted" "$RTK rewrite git status" "rtk git status"
bench_rewrite "rewrite ls -al" "$RTK rewrite ls -al" "rtk ls -al"
bench_rewrite "rewrite npm exec" "$RTK rewrite npm exec" "rtk npm exec"
bench_rewrite "rewrite cargo test" "$RTK rewrite cargo test" "rtk cargo test"
bench_rewrite "rewrite compound" "$RTK rewrite 'cargo test && git push'" "rtk cargo test && rtk git push"
# ===================
# Summary
# ===================
echo ""
echo "═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════"
if [ "$TOTAL_TESTS" -gt 0 ]; then
GOOD_PCT=$((GOOD_TESTS * 100 / TOTAL_TESTS))
if [ "$TOTAL_UNIX" -gt 0 ]; then
TOTAL_SAVED=$((TOTAL_UNIX - TOTAL_RTK))
TOTAL_SAVE_PCT=$((TOTAL_SAVED * 100 / TOTAL_UNIX))
else
TOTAL_SAVED=0
TOTAL_SAVE_PCT=0
fi
echo ""
echo "$GOOD_TESTS good ⚠️ $WARN_TESTS warn 🔴 $NEGATIVE_TESTS negative ❌ $FAIL_TESTS fail $GOOD_TESTS/$TOTAL_TESTS ($GOOD_PCT%)"
echo " Tokens: $TOTAL_UNIX$TOTAL_RTK (-$TOTAL_SAVE_PCT%)"
echo ""
if [ -z "$CI" ]; then
echo " Debug: $BENCH_DIR/{unix,rtk,diff}/"
fi
echo ""
EXIT_CODE=0
if [ "$NEGATIVE_TESTS" -gt 0 ]; then
echo " BENCHMARK FAILED: $NEGATIVE_TESTS filter(s) produced more tokens than raw output"
EXIT_CODE=1
fi
if [ "$FAIL_TESTS" -gt 0 ]; then
echo " BENCHMARK FAILED: $FAIL_TESTS filter(s) returned empty output"
EXIT_CODE=1
fi
if [ "$GOOD_PCT" -lt 60 ] && [ "$EXIT_CODE" -eq 0 ]; then
echo " WARNING: $GOOD_PCT% good (target 60%)"
fi
exit $EXIT_CODE
fi
+11
View File
@@ -0,0 +1,11 @@
#!/usr/bin/env bun
/**
* Delete the RTK test VM.
* Usage: bun run scripts/benchmark/cleanup.ts
*/
import { vmDelete } from "./lib/vm";
console.log("Deleting rtk-test VM...");
await vmDelete();
console.log("Done.");
+315
View File
@@ -0,0 +1,315 @@
#cloud-config
# RTK Integration Test VM — Ubuntu 24.04
# Installs all tools needed for comprehensive RTK testing (~200 commands)
# Usage: multipass launch --name rtk-test --cloud-init scripts/benchmark/cloud-init.yaml --cpus 2 --memory 4G --disk 20G 24.04
package_update: true
package_upgrade: false
packages:
# System tools
- curl
- wget
- jq
- git
- make
- cmake
- rsync
- sqlite3
- shellcheck
- yamllint
- postgresql-client
- docker.io
- containerd
- python3
- python3-pip
- python3-venv
- pipx
# Build essentials (for Rust compilation)
- build-essential
- pkg-config
- libssl-dev
- libsqlite3-dev
# Misc
- hyperfine
- unzip
- tree
runcmd:
# ── Rust toolchain ──
- su - ubuntu -c 'curl --proto "=https" --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y'
# ── Node.js 22 + package managers ──
- curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
- apt-get install -y nodejs
- npm install -g pnpm yarn
- npm install -g eslint prettier typescript
- npm install -g markdownlint-cli
# ── Go 1.22 ──
- curl -fsSL https://go.dev/dl/go1.22.5.linux-amd64.tar.gz | tar -C /usr/local -xz
- echo 'export PATH=$PATH:/usr/local/go/bin:/home/ubuntu/go/bin' >> /home/ubuntu/.bashrc
- su - ubuntu -c 'export PATH=$PATH:/usr/local/go/bin && go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest'
# ── Python tools ──
- pipx install ruff
- pipx install mypy
- pipx install poetry
- pip3 install --break-system-packages pytest uv pre-commit
# ── .NET 8 SDK ──
- |
wget https://dot.net/v1/dotnet-install.sh -O /tmp/dotnet-install.sh
chmod +x /tmp/dotnet-install.sh
/tmp/dotnet-install.sh --channel 8.0 --install-dir /usr/local/share/dotnet
ln -sf /usr/local/share/dotnet/dotnet /usr/local/bin/dotnet
echo 'export DOTNET_ROOT=/usr/local/share/dotnet' >> /home/ubuntu/.bashrc
# ── Terraform ──
- |
wget -qO- https://apt.releases.hashicorp.com/gpg | gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" > /etc/apt/sources.list.d/hashicorp.list
apt-get update && apt-get install -y terraform
# ── Helm ──
- curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
# ── Hadolint ──
- |
wget -qO /usr/local/bin/hadolint https://github.com/hadolint/hadolint/releases/latest/download/hadolint-Linux-x86_64
chmod +x /usr/local/bin/hadolint
# ── Docker setup ──
- usermod -aG docker ubuntu
- systemctl enable docker
- systemctl start docker
# ── kubectl (standalone binary) ──
- |
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl
rm kubectl
# ── ansible ──
- pip3 install --break-system-packages ansible-core
# ── Mock tools (too heavy to install) ──
- |
cat > /usr/local/bin/gcloud << 'MOCK'
#!/bin/bash
if [ "$1" = "version" ] || [ "$1" = "--version" ]; then
echo "Google Cloud SDK 400.0.0"
echo "bq 2.0.80"
echo "core 2023.01.01"
echo "gsutil 5.17"
else
echo "gcloud mock: $*"
fi
MOCK
chmod +x /usr/local/bin/gcloud
- |
cat > /usr/local/bin/shopify << 'MOCK'
#!/bin/bash
echo "Shopify CLI 3.0.0 (mock)"
if [ "$1" = "theme" ] && [ "$2" = "check" ]; then
echo "Running theme check..."
echo " 1 issue found"
echo " [warn] Missing alt text on image"
fi
MOCK
chmod +x /usr/local/bin/shopify
- |
cat > /usr/local/bin/pio << 'MOCK'
#!/bin/bash
if [ "$1" = "--version" ]; then echo "PlatformIO Core, version 6.1.0"
elif [ "$1" = "run" ]; then
echo "Processing esp32dev (platform: espressif32; board: esp32dev)"
echo "Linking .pio/build/esp32dev/firmware.elf"
echo "========================= [SUCCESS] ========================="
fi
MOCK
chmod +x /usr/local/bin/pio
- |
cat > /usr/local/bin/quarto << 'MOCK'
#!/bin/bash
if [ "$1" = "--version" ]; then echo "1.3.450"
elif [ "$1" = "render" ]; then echo "Rendering document..."; echo "Output created: document.html"
fi
MOCK
chmod +x /usr/local/bin/quarto
- |
cat > /usr/local/bin/sops << 'MOCK'
#!/bin/bash
if [ "$1" = "--version" ]; then echo "sops 3.7.3"; fi
MOCK
chmod +x /usr/local/bin/sops
- |
cat > /usr/local/bin/swift << 'MOCK'
#!/bin/bash
if [ "$1" = "--version" ]; then echo "Swift version 5.9.2 (swift-5.9.2-RELEASE)"
elif [ "$1" = "build" ]; then echo "Compiling Swift module..."; echo "Build complete! (0.42s)"
fi
MOCK
chmod +x /usr/local/bin/swift
# ── Fake test projects ──
# Node.js project with errors
- |
su - ubuntu -c '
mkdir -p /tmp/test-node/src && cd /tmp/test-node
npm init -y >/dev/null 2>&1
echo "{\"compilerOptions\":{\"strict\":true,\"noEmit\":true,\"target\":\"ES2020\",\"module\":\"ESNext\",\"moduleResolution\":\"node\"},\"include\":[\"src\"]}" > tsconfig.json
echo "const x: number = \"not a number\";\nconst unused = 42;\nfunction greet(name: string): string { return name }\ngreet(123);" > src/index.ts
echo "{\"rules\":{\"no-unused-vars\":\"error\",\"semi\":[\"error\",\"always\"]}}" > .eslintrc.json
echo "const x = 1;const y=2; const z =3" > src/ugly.ts
'
# Python project with errors
- |
su - ubuntu -c '
mkdir -p /tmp/test-python && cd /tmp/test-python
cat > main.py << "PYEOF"
import os
import sys
unused_import = 1
def add(a: int, b: int) -> str:
return a + b
x: int = "hello"
PYEOF
cat > test_main.py << "PYEOF"
def test_pass():
assert 1 + 1 == 2
def test_fail():
assert 1 + 1 == 3, "math is broken"
PYEOF
cat > pyproject.toml << "PYEOF"
[tool.ruff]
line-length = 80
select = ["E", "F", "W"]
[tool.mypy]
strict = true
[tool.pytest.ini_options]
testpaths = ["."]
PYEOF
'
# Go project with errors
- |
su - ubuntu -c '
export PATH=$PATH:/usr/local/go/bin
mkdir -p /tmp/test-go && cd /tmp/test-go
go mod init test-go 2>/dev/null
cat > main.go << "GOEOF"
package main
import "fmt"
func main() { fmt.Println("hello") }
func unused() { var x int; _ = x }
GOEOF
cat > main_test.go << "GOEOF"
package main
import "testing"
func TestPass(t *testing.T) { if 1+1 != 2 { t.Fatal("math") } }
func TestFail(t *testing.T) { t.Fatal("expected failure") }
GOEOF
'
# Rust project with errors
- |
su - ubuntu -c '
export PATH=$HOME/.cargo/bin:$PATH
mkdir -p /tmp/test-rust && cd /tmp/test-rust
cargo init --name test-rust 2>/dev/null
cat > src/main.rs << "RSEOF"
fn main() {
let x = vec![1, 2, 3];
let _y = x.iter().map(|i| i.clone()).collect::<Vec<_>>();
println!("hello");
}
#[cfg(test)]
mod tests {
#[test] fn test_pass() { assert_eq!(1 + 1, 2); }
#[test] fn test_fail() { assert_eq!(1 + 1, 3); }
}
RSEOF
'
# Dockerfiles for hadolint
- |
su - ubuntu -c '
cat > /tmp/Dockerfile.bad << "DEOF"
FROM ubuntu:latest
RUN apt-get update && apt-get install -y curl wget git
RUN cd /tmp && wget http://example.com/script.sh && bash script.sh
EXPOSE 80 443 8080
DEOF
'
# Shell/YAML/Markdown test files
- |
su - ubuntu -c '
printf "#!/bin/bash\necho \$foo\nls *.txt\ncd \$(pwd)\n[ -f file ] && rm file\n" > /tmp/test.sh
printf "foo: bar\nbaz: qux\nlist:\n - item1\n - item2\ntruthy: yes\n" > /tmp/test.yaml
printf "#Header without space\nSome text\n\n* List item\n+ Mixed markers\n" > /tmp/test.md
'
# Git repo for testing
- |
su - ubuntu -c '
mkdir -p /tmp/test-git && cd /tmp/test-git
git init && git config user.email "test@rtk.dev" && git config user.name "RTK Test"
for i in $(seq 1 20); do echo "line $i" >> file.txt && git add file.txt && git commit -m "feat: commit number $i"; done
echo "modified" >> file.txt && echo "new file" > new.txt
'
# Large log file for dedup testing
- |
su - ubuntu -c '
for i in $(seq 1 500); do
printf "[2026-03-25 10:00:00] INFO Starting service...\n[2026-03-25 10:00:01] WARN Connection timeout\n[2026-03-25 10:00:01] ERROR Failed to connect: refused\n"
done > /tmp/large.log
for i in $(seq 1 50); do echo "[2026-03-25 10:05:00] FATAL Out of memory"; done >> /tmp/large.log
'
# .env file
- |
su - ubuntu -c '
printf "DATABASE_URL=postgres://user:pass@localhost:5432/db\nAPI_KEY=sk-1234567890abcdef\nSECRET_TOKEN=ghp_xxxx\nNODE_ENV=production\nPORT=3000\n" > /tmp/.env
'
# Makefile
- |
su - ubuntu -c '
printf ".PHONY: all test\nall:\n\t@echo Building...\n\t@echo Build complete\ntest:\n\t@echo Running tests...\n\t@echo 2 tests passed\n" > /tmp/Makefile
'
# Terraform project
- |
su - ubuntu -c '
mkdir -p /tmp/test-terraform && cd /tmp/test-terraform
printf "terraform {\n required_version = \">= 1.0\"\n}\nresource \"null_resource\" \"test\" {\n triggers = { always = timestamp() }\n}\noutput \"test\" { value = \"hello\" }\n" > main.tf
'
# Helm chart
- su - ubuntu -c 'mkdir -p /tmp/test-helm && cd /tmp/test-helm && helm create test-chart 2>/dev/null || true'
# .NET project
- |
export DOTNET_ROOT=/usr/local/share/dotnet
su - ubuntu -c '
export DOTNET_ROOT=/usr/local/share/dotnet && export PATH=$PATH:$DOTNET_ROOT
mkdir -p /tmp/test-dotnet && cd /tmp/test-dotnet
dotnet new console -n TestApp --force 2>/dev/null || true
'
# Signal completion
- touch /home/ubuntu/.cloud-init-complete
- chown ubuntu:ubuntu /home/ubuntu/.cloud-init-complete
- echo "RTK cloud-init setup complete" | tee /var/log/rtk-setup.log
final_message: "RTK test VM ready in $UPTIME seconds"
+113
View File
@@ -0,0 +1,113 @@
/**
* Report generation for RTK integration test results.
*/
import type { TestResult } from "./test";
import { getCounts, getResults } from "./test";
interface BuildInfo {
buildTime: number;
binarySize: number;
version: string;
branch: string;
commit: string;
}
export function generateReport(buildInfo: BuildInfo): string {
const { total, passed, failed, skipped } = getCounts();
const results = getResults();
const passRate = total > 0 ? Math.round((passed * 100) / total) : 0;
const lines: string[] = [];
lines.push("======================================================");
lines.push(" RTK INTEGRATION TEST REPORT");
lines.push("======================================================");
lines.push("");
lines.push(`Date: ${new Date().toISOString()}`);
lines.push(`Branch: ${buildInfo.branch}`);
lines.push(`Commit: ${buildInfo.commit}`);
lines.push(`Version: ${buildInfo.version}`);
lines.push(`Binary: ${buildInfo.binarySize} bytes`);
lines.push(`Build: ${buildInfo.buildTime}s`);
lines.push("");
// Summary
lines.push("--- Summary ---");
lines.push(`Total: ${total}`);
lines.push(`Passed: ${passed} (${passRate}%)`);
lines.push(`Failed: ${failed}`);
lines.push(`Skipped: ${skipped}`);
lines.push("");
// Group results by phase (name prefix before ":")
const phases = new Map<string, TestResult[]>();
for (const r of results) {
const colonIdx = r.name.indexOf(":");
const phase = colonIdx > 0 ? r.name.slice(0, colonIdx) : "misc";
if (!phases.has(phase)) phases.set(phase, []);
phases.get(phase)!.push(r);
}
for (const [phase, phaseResults] of phases) {
const pPassed = phaseResults.filter((r) => r.status === "PASS").length;
const pTotal = phaseResults.length;
lines.push(`--- ${phase} (${pPassed}/${pTotal}) ---`);
for (const r of phaseResults) {
const shortName = r.name.includes(":") ? r.name.split(":")[1] : r.name;
lines.push(` ${r.status.padEnd(4)} | ${shortName} | ${r.detail}`);
}
lines.push("");
}
// Failures detail
const failures = results.filter((r) => r.status === "FAIL");
if (failures.length > 0) {
lines.push("--- Failures ---");
for (const f of failures) {
lines.push(` ${f.name}: ${f.detail}`);
}
lines.push("");
}
// Token savings summary
const savingsResults = results.filter((r) => r.savings !== undefined);
if (savingsResults.length > 0) {
const avgSavings = Math.round(
savingsResults.reduce((sum, r) => sum + (r.savings ?? 0), 0) /
savingsResults.length
);
const minSavings = Math.min(
...savingsResults.map((r) => r.savings ?? 100)
);
const maxSavings = Math.max(...savingsResults.map((r) => r.savings ?? 0));
lines.push("--- Token Savings ---");
lines.push(`Average: ${avgSavings}%`);
lines.push(`Min: ${minSavings}%`);
lines.push(`Max: ${maxSavings}%`);
lines.push("");
}
// Verdict
lines.push("======================================================");
if (failed === 0) {
lines.push(" Verdict: READY FOR RELEASE");
} else {
lines.push(` Verdict: NOT READY (${failed} failures)`);
}
lines.push("======================================================");
return lines.join("\n");
}
/** Save report to file */
export async function saveReport(
buildInfo: BuildInfo,
outPath: string
): Promise<string> {
const report = generateReport(buildInfo);
await Bun.write(outPath, report);
console.log(`\nReport saved to: ${outPath}`);
return report;
}
+167
View File
@@ -0,0 +1,167 @@
/**
* Test helpers for RTK integration testing.
*/
import { vmExec, RTK_BIN } from "./vm";
export type TestStatus = "PASS" | "FAIL" | "SKIP";
export interface TestResult {
name: string;
status: TestStatus;
detail: string;
exitCode?: number;
outputSize?: number;
savings?: number;
duration?: number;
}
const results: TestResult[] = [];
export function getResults(): TestResult[] {
return results;
}
export function getCounts() {
const total = results.length;
const passed = results.filter((r) => r.status === "PASS").length;
const failed = results.filter((r) => r.status === "FAIL").length;
const skipped = results.filter((r) => r.status === "SKIP").length;
return { total, passed, failed, skipped };
}
function record(result: TestResult) {
results.push(result);
const icon =
result.status === "PASS"
? "\x1b[32mPASS\x1b[0m"
: result.status === "FAIL"
? "\x1b[31mFAIL\x1b[0m"
: "\x1b[33mSKIP\x1b[0m";
console.log(` ${icon} | ${result.name} | ${result.detail}`);
}
/**
* Test a command exits with expected code and doesn't crash.
* expectedExit: number or "any" (just checks no signal death)
*/
export async function testCmd(
name: string,
cmd: string,
expectedExit: number | "any" = 0
): Promise<TestResult> {
const start = Date.now();
const { stdout, stderr, exitCode } = await vmExec(cmd);
const duration = Date.now() - start;
const outputSize = stdout.length + stderr.length;
let status: TestStatus;
let detail: string;
if (expectedExit === "any") {
// Just check it didn't die from signal (exit >= 128)
if (exitCode < 128) {
status = "PASS";
detail = `exit=${exitCode} | ${outputSize}b | ${duration}ms`;
} else {
status = "FAIL";
detail = `SIGNAL exit=${exitCode} | ${outputSize}b`;
}
} else if (exitCode === expectedExit) {
status = "PASS";
detail = `exit=${exitCode} | ${outputSize}b | ${duration}ms`;
} else {
status = "FAIL";
detail = `expected exit=${expectedExit}, got ${exitCode} | ${outputSize}b`;
}
const result: TestResult = {
name,
status,
detail,
exitCode,
outputSize,
duration,
};
record(result);
return result;
}
/**
* Test token savings: compare raw command output vs RTK filtered output.
*/
export async function testSavings(
name: string,
rawCmd: string,
rtkCmd: string,
targetPct: number
): Promise<TestResult> {
const raw = await vmExec(rawCmd);
const rtk = await vmExec(rtkCmd);
const rawSize = raw.stdout.length;
const rtkSize = rtk.stdout.length;
if (rawSize === 0) {
const result: TestResult = {
name,
status: "SKIP",
detail: "raw output empty",
};
record(result);
return result;
}
const savings = Math.round(100 - (rtkSize * 100) / rawSize);
let status: TestStatus;
let detail: string;
if (savings >= targetPct) {
status = "PASS";
detail = `raw=${rawSize}b filtered=${rtkSize}b savings=${savings}% (target: >=${targetPct}%)`;
} else {
status = "FAIL";
detail = `savings=${savings}% < target ${targetPct}% (raw=${rawSize}b filtered=${rtkSize}b)`;
}
const result: TestResult = { name, status, detail, savings };
record(result);
return result;
}
/**
* Test rewrite engine: input -> expected output.
*/
export async function testRewrite(
input: string,
expected: string
): Promise<TestResult> {
const escaped = input.replace(/'/g, "'\\''");
const { stdout } = await vmExec(`${RTK_BIN} rewrite '${escaped}'`);
const actual = stdout.trim();
let status: TestStatus;
let detail: string;
if (actual === expected) {
status = "PASS";
detail = `'${input}' -> '${actual}'`;
} else {
status = "FAIL";
detail = `'${input}' -> expected '${expected}', got '${actual}'`;
}
const result: TestResult = { name: `rewrite: ${input}`, status, detail };
record(result);
return result;
}
/**
* Skip a test with a reason.
*/
export function skipTest(name: string, reason: string): TestResult {
const result: TestResult = { name, status: "SKIP", detail: reason };
record(result);
return result;
}
+181
View File
@@ -0,0 +1,181 @@
/**
* Multipass VM management for RTK integration testing.
*/
import { $ } from "bun";
const VM_NAME = "rtk-test";
const CLOUD_INIT = "scripts/benchmark/cloud-init.yaml";
export interface VmInfo {
name: string;
state: string;
ipv4: string;
}
/** Check if VM exists and is running */
export async function vmExists(): Promise<boolean> {
const result = await $`multipass list --format json`.quiet();
const data = JSON.parse(result.stdout.toString());
return data.list?.some((vm: VmInfo) => vm.name === VM_NAME) ?? false;
}
/** Check if VM is running */
export async function vmRunning(): Promise<boolean> {
const result = await $`multipass list --format json`.quiet();
const data = JSON.parse(result.stdout.toString());
const vm = data.list?.find((v: VmInfo) => v.name === VM_NAME);
return vm?.state === "Running";
}
/** Create a new VM with cloud-init (20 min timeout for full provisioning) */
export async function vmCreate(): Promise<void> {
console.log(`[vm] Creating ${VM_NAME} with cloud-init (this takes ~10-15 min)...`);
// --timeout 1200 = 20 min for cloud-init to finish installing Rust, Go, Node, .NET, etc.
await $`multipass launch --name ${VM_NAME} --cpus 2 --memory 4G --disk 20G --timeout 1200 --cloud-init ${CLOUD_INIT} 24.04`;
}
/** Start existing VM */
export async function vmStart(): Promise<void> {
console.log(`[vm] Starting ${VM_NAME}...`);
await $`multipass start ${VM_NAME}`;
}
/** Execute a command in the VM, returns stdout (60s timeout per test by default) */
export async function vmExec(
cmd: string,
timeoutMs = 60_000
): Promise<{
stdout: string;
stderr: string;
exitCode: number;
}> {
const exec = $`multipass exec ${VM_NAME} -- bash -c ${cmd}`
.quiet()
.nothrow()
.then((r) => ({
stdout: r.stdout.toString(),
stderr: r.stderr.toString(),
exitCode: r.exitCode,
}));
const timeout = new Promise<{ stdout: string; stderr: string; exitCode: number }>((_, reject) =>
setTimeout(() => reject(new Error(`vmExec timed out after ${timeoutMs}ms: ${cmd}`)), timeoutMs)
);
return Promise.race([exec, timeout]);
}
/** Transfer a file to the VM */
export async function vmTransfer(
localPath: string,
remotePath: string
): Promise<void> {
await $`multipass transfer ${localPath} ${VM_NAME}:${remotePath}`;
}
/** Wait for cloud-init to complete (max 40 min — installs Rust, Go, Node, .NET, etc.) */
export async function vmWaitReady(maxWaitSec = 2400): Promise<boolean> {
console.log("[vm] Waiting for cloud-init...");
const start = Date.now();
while ((Date.now() - start) / 1000 < maxWaitSec) {
const { exitCode } = await vmExec(
"test -f /home/ubuntu/.cloud-init-complete"
);
if (exitCode === 0) {
const elapsed = Math.round((Date.now() - start) / 1000);
console.log(`[vm] Cloud-init complete after ${elapsed}s`);
return true;
}
await Bun.sleep(10_000);
}
console.error("[vm] Cloud-init timed out!");
return false;
}
/** Transfer RTK source and build in release mode */
export async function vmBuildRtk(projectRoot: string): Promise<{
buildTime: number;
binarySize: number;
version: string;
}> {
console.log("[vm] Transferring RTK source...");
// Create tarball excluding heavy dirs and macOS resource forks (._*)
await $`COPYFILE_DISABLE=1 tar czf /tmp/rtk-src.tar.gz --exclude target --exclude .git --exclude node_modules --exclude "index.html*" --exclude "._*" -C ${projectRoot} .`;
await vmTransfer("/tmp/rtk-src.tar.gz", "/tmp/rtk-src.tar.gz");
await vmExec(
"mkdir -p /home/ubuntu/rtk && cd /home/ubuntu/rtk && tar xzf /tmp/rtk-src.tar.gz"
);
console.log("[vm] Building RTK (release)...");
const start = Date.now();
const { stdout, exitCode } = await vmExec(
"export PATH=$HOME/.cargo/bin:$PATH && cd /home/ubuntu/rtk && cargo build --release 2>&1 | tail -5"
);
const buildTime = Math.round((Date.now() - start) / 1000);
if (exitCode !== 0) {
throw new Error(`Build failed:\n${stdout}`);
}
const { stdout: sizeStr } = await vmExec(
"stat -c%s /home/ubuntu/rtk/target/release/rtk"
);
const binarySize = parseInt(sizeStr.trim(), 10);
const { stdout: version } = await vmExec(
"/home/ubuntu/rtk/target/release/rtk --version"
);
console.log(
`[vm] Build OK in ${buildTime}s — ${binarySize} bytes — ${version.trim()}`
);
return { buildTime, binarySize, version: version.trim() };
}
/** Delete the VM */
export async function vmDelete(): Promise<void> {
console.log(`[vm] Deleting ${VM_NAME}...`);
await $`multipass delete ${VM_NAME} --purge`.nothrow();
}
/** Ensure VM is ready (create or reuse) */
export async function vmEnsureReady(): Promise<void> {
if (await vmExists()) {
if (!(await vmRunning())) {
await vmStart();
}
console.log(`[vm] Reusing existing VM ${VM_NAME}`);
// Check if cloud-init is still running
const { exitCode } = await vmExec(
"test -f /home/ubuntu/.cloud-init-complete"
);
if (exitCode !== 0) {
console.log("[vm] Cloud-init still running, waiting...");
const ready = await vmWaitReady();
if (!ready) {
throw new Error(
"Cloud-init timed out. Check: multipass exec rtk-test -- cat /var/log/cloud-init-output.log"
);
}
}
} else {
await vmCreate();
// multipass launch --timeout should wait, but double-check
const { exitCode } = await vmExec(
"test -f /home/ubuntu/.cloud-init-complete"
);
if (exitCode !== 0) {
const ready = await vmWaitReady();
if (!ready) {
throw new Error(
"Cloud-init timed out. Check: multipass exec rtk-test -- cat /var/log/cloud-init-output.log"
);
}
}
}
}
export const RTK_BIN = "/home/ubuntu/rtk/target/release/rtk";
+17
View File
@@ -0,0 +1,17 @@
#!/usr/bin/env bun
/**
* Fast rebuild: reuse existing VM, just transfer source and recompile.
* Usage: bun run scripts/benchmark/rebuild.ts
*/
import { vmEnsureReady, vmBuildRtk } from "./lib/vm";
const PROJECT_ROOT = new URL("../../", import.meta.url).pathname.replace(/\/$/, "");
await vmEnsureReady();
const info = await vmBuildRtk(PROJECT_ROOT);
console.log(`\nRebuild complete:`);
console.log(` Version: ${info.version}`);
console.log(` Binary: ${info.binarySize} bytes`);
console.log(` Time: ${info.buildTime}s`);
+425
View File
@@ -0,0 +1,425 @@
#!/usr/bin/env bun
/**
* RTK Full Integration Test Suite — Multipass VM
*
* Usage:
* bun run scripts/benchmark/run.ts # Full suite
* bun run scripts/benchmark/run.ts --quick # Skip slow phases (perf, concurrency)
* bun run scripts/benchmark/run.ts --phase 3 # Run specific phase only
*
* Prerequisites:
* brew install multipass
*/
import { $ } from "bun";
import { vmEnsureReady, vmBuildRtk, vmExec, RTK_BIN } from "./lib/vm";
import { testCmd, testSavings, testRewrite, skipTest, getCounts } from "./lib/test";
import { saveReport } from "./lib/report";
const args = process.argv.slice(2);
const quick = args.includes("--quick");
const phaseArg = args.includes("--phase")
? parseInt(args[args.indexOf("--phase") + 1], 10)
: null;
const phaseOnly = phaseArg !== null && !Number.isNaN(phaseArg) ? phaseArg : null;
if (args.includes("--phase") && phaseOnly === null) {
console.error("Error: --phase requires a number (e.g. --phase 3)");
process.exit(1);
}
const reportPath = args.includes("--report")
? args[args.indexOf("--report") + 1]
: `${new URL("../../", import.meta.url).pathname.replace(/\/$/, "")}/benchmark-report.txt`;
const PROJECT_ROOT = new URL("../../", import.meta.url).pathname.replace(/\/$/, "");
const RTK = RTK_BIN;
function shouldRun(phase: number): boolean {
return phaseOnly === null || phaseOnly === phase;
}
function heading(phase: number, title: string) {
console.log(`\n\x1b[34m[Phase ${phase}] ${title}\x1b[0m`);
}
// ══════════════════════════════════════════════════════════════
// Phase 0: VM Setup
// ══════════════════════════════════════════════════════════════
console.log("\x1b[34m[rtk-test] RTK Full Integration Test Suite\x1b[0m");
console.log(`Project: ${PROJECT_ROOT}`);
await vmEnsureReady();
// ══════════════════════════════════════════════════════════════
// Phase 1: Transfer & Build
// ══════════════════════════════════════════════════════════════
heading(1, "Transfer & Build");
const branch = (await $`git -C ${PROJECT_ROOT} branch --show-current`.text()).trim();
const commit = (await $`git -C ${PROJECT_ROOT} log --oneline -1`.text()).trim();
const buildInfo = await vmBuildRtk(PROJECT_ROOT);
// Binary size check
// ARM Linux release binaries are ~6.5MB (vs ~4MB x86 stripped).
// CLAUDE.md target is <5MB for stripped x86 release builds.
// VM builds are ARM + not fully stripped, so we use a relaxed 8MB limit here.
const sizeLimit = 8_388_608; // 8MB (relaxed for ARM Linux VM)
if (buildInfo.binarySize < sizeLimit) {
console.log(` \x1b[32mPASS\x1b[0m | binary size | ${buildInfo.binarySize} bytes < 8MB`);
} else {
console.log(` \x1b[31mFAIL\x1b[0m | binary size | ${buildInfo.binarySize} bytes >= 8MB`);
}
// ══════════════════════════════════════════════════════════════
// Phase 2: Cargo Quality (fmt, clippy, test)
// ══════════════════════════════════════════════════════════════
if (shouldRun(2)) {
heading(2, "Cargo Quality");
await testCmd(
"quality:cargo fmt",
"export PATH=$HOME/.cargo/bin:$PATH && cd /home/ubuntu/rtk && cargo fmt --all --check 2>&1"
);
await testCmd(
"quality:cargo clippy",
"export PATH=$HOME/.cargo/bin:$PATH && cd /home/ubuntu/rtk && cargo clippy --all-targets -- -D warnings 2>&1"
);
await testCmd(
"quality:cargo test",
"export PATH=$HOME/.cargo/bin:$PATH && cd /home/ubuntu/rtk && cargo test --all 2>&1"
);
}
// ══════════════════════════════════════════════════════════════
// Phase 3: Rust Built-in Commands
// ══════════════════════════════════════════════════════════════
if (shouldRun(3)) {
heading(3, "Rust Built-in Commands");
// Git
await testCmd("git:status", `cd /tmp/test-git && ${RTK} git status`);
await testCmd("git:log", `cd /tmp/test-git && ${RTK} git log -5`);
await testCmd("git:log --oneline", `cd /tmp/test-git && ${RTK} git log --oneline -10`);
await testCmd("git:diff", `cd /tmp/test-git && ${RTK} git diff`, "any");
await testCmd("git:branch", `cd /tmp/test-git && ${RTK} git branch`);
await testCmd("git:add --dry-run", `cd /tmp/test-git && ${RTK} git add --dry-run .`, "any");
// Files
await testCmd("files:ls", `${RTK} ls /home/ubuntu/rtk`);
await testCmd("files:ls src/", `${RTK} ls /home/ubuntu/rtk/src/`);
await testCmd("files:ls -R", `${RTK} ls -R /home/ubuntu/rtk/src/`);
await testCmd("files:read", `${RTK} read /home/ubuntu/rtk/src/main.rs`);
await testCmd("files:read aggressive", `${RTK} read /home/ubuntu/rtk/src/main.rs -l aggressive`);
await testCmd("files:smart", `${RTK} smart /home/ubuntu/rtk/src/main.rs`);
await testCmd("files:find *.rs", `${RTK} find '*.rs' /home/ubuntu/rtk/src/`);
await testCmd("files:wc", `${RTK} wc /home/ubuntu/rtk/src/main.rs`);
await testCmd("files:diff", `${RTK} diff /home/ubuntu/rtk/src/main.rs /home/ubuntu/rtk/src/utils.rs`);
// Search
await testCmd("search:grep", `${RTK} grep 'fn main' /home/ubuntu/rtk/src/`);
// Data
await testCmd("data:json", `${RTK} json /tmp/test-node/package.json`);
await testCmd("data:deps", `cd /home/ubuntu/rtk && ${RTK} deps`);
await testCmd("data:env", `${RTK} env`);
// Runners
await testCmd("runner:summary", `${RTK} summary 'echo hello world'`);
// BUG: rtk err swallows exit code — tracked in #846
await testCmd("runner:err", `${RTK} err false`, "any");
await testCmd("runner:test", `${RTK} test 'echo ok'`, "any");
// Logs
await testCmd("log:large", `${RTK} log /tmp/large.log`);
// Network
await testCmd("net:curl", `${RTK} curl https://mockhttp.org/get`, "any");
// GitHub
await testCmd("gh:pr list", `cd /home/ubuntu/rtk && ${RTK} gh pr list`, "any");
// Cargo (test project has intentional test failure → exit 101)
await testCmd("cargo:build", `export PATH=$HOME/.cargo/bin:$PATH && cd /tmp/test-rust && ${RTK} cargo build`);
await testCmd("cargo:test", `export PATH=$HOME/.cargo/bin:$PATH && cd /tmp/test-rust && ${RTK} cargo test`, 101);
await testCmd("cargo:clippy", `export PATH=$HOME/.cargo/bin:$PATH && cd /tmp/test-rust && ${RTK} cargo clippy`);
// Python (test project has intentional failures)
await testCmd("python:pytest", `cd /tmp/test-python && ${RTK} pytest`, 1);
await testCmd("python:ruff check", `cd /tmp/test-python && ${RTK} ruff check .`, 1);
await testCmd("python:mypy", `cd /tmp/test-python && ${RTK} mypy .`, 1);
await testCmd("python:pip list", `${RTK} pip list`);
// Go (test project has intentional test failure)
await testCmd("go:test", `export PATH=$PATH:/usr/local/go/bin && cd /tmp/test-go && ${RTK} go test ./...`, 1);
await testCmd("go:build", `export PATH=$PATH:/usr/local/go/bin && cd /tmp/test-go && ${RTK} go build .`, 1);
await testCmd("go:vet", `export PATH=$PATH:/usr/local/go/bin && cd /tmp/test-go && ${RTK} go vet ./...`, 1);
await testCmd("go:golangci-lint", `export PATH=$PATH:/usr/local/go/bin:$HOME/go/bin && cd /tmp/test-go && ${RTK} golangci-lint run`, 1);
// TypeScript
await testCmd("ts:tsc", `cd /tmp/test-node && ${RTK} tsc --noEmit`, "any");
// Linters
await testCmd("lint:eslint", `cd /tmp/test-node && ${RTK} lint 'eslint src/'`, "any");
await testCmd("lint:prettier", `cd /tmp/test-node && ${RTK} prettier --check src/`, "any");
// Docker
await testCmd("docker:ps", `${RTK} docker ps`, "any");
await testCmd("docker:images", `${RTK} docker images`, "any");
// Kubernetes
await testCmd("k8s:pods", `${RTK} kubectl pods`, "any");
// .NET
await testCmd("dotnet:build", `export DOTNET_ROOT=/usr/local/share/dotnet && export PATH=$PATH:$DOTNET_ROOT && cd /tmp/test-dotnet/TestApp 2>/dev/null && ${RTK} dotnet build || echo 'dotnet skip'`, "any");
// Meta
await testCmd("meta:gain", `${RTK} gain`);
await testCmd("meta:gain --history", `${RTK} gain --history`);
await testCmd("meta:proxy", `${RTK} proxy echo 'proxy test'`);
await testCmd("meta:verify", `${RTK} verify`, "any");
}
// ══════════════════════════════════════════════════════════════
// Phase 4: TOML Filter Commands
// ══════════════════════════════════════════════════════════════
if (shouldRun(4)) {
heading(4, "TOML Filter Commands");
// System
await testCmd("toml:df", `${RTK} df -h`);
await testCmd("toml:du", `${RTK} du -sh /tmp`, "any");
await testCmd("toml:ps", `${RTK} ps aux`);
await testCmd("toml:ping", `${RTK} ping -c 2 127.0.0.1`);
// Build tools
await testCmd("toml:make", `cd /tmp && ${RTK} make -f Makefile`, "any");
await testCmd("toml:rsync", `${RTK} rsync --version`);
// Linters
await testCmd("toml:shellcheck", `${RTK} shellcheck /tmp/test.sh`, "any");
await testCmd("toml:hadolint", `${RTK} hadolint /tmp/Dockerfile.bad`, "any");
await testCmd("toml:yamllint", `${RTK} yamllint /tmp/test.yaml`, "any");
await testCmd("toml:markdownlint", `${RTK} markdownlint /tmp/test.md`, "any");
// Cloud/Infra
await testCmd("toml:terraform", `${RTK} terraform --version`, "any");
await testCmd("toml:helm", `${RTK} helm version`, "any");
await testCmd("toml:ansible", `${RTK} ansible-playbook --version`, "any");
// Mocked tools
await testCmd("toml:gcloud", `${RTK} gcloud version`);
await testCmd("toml:shopify", `${RTK} shopify theme check`, "any");
await testCmd("toml:pio", `${RTK} pio run`, "any");
await testCmd("toml:quarto", `${RTK} quarto render`, "any");
await testCmd("toml:sops", `${RTK} sops --version`);
// Swift ecosystem
await testCmd("toml:swift build", `${RTK} swift build`, "any");
await testCmd("toml:swift test", `${RTK} swift test`, "any");
await testCmd("toml:swift run", `${RTK} swift run`, "any");
await testCmd("toml:swift package", `${RTK} swift package resolve`, "any");
await testCmd("toml:swiftlint", `${RTK} swiftlint`, "any");
await testCmd("toml:swiftformat", `${RTK} swiftformat`, "any");
await testCmd("toml:kubectl", `${RTK} kubectl version --client`, "any");
}
// ══════════════════════════════════════════════════════════════
// Phase 5: Hook Rewrite Engine
// ══════════════════════════════════════════════════════════════
if (shouldRun(5)) {
heading(5, "Hook Rewrite Engine");
// Basic rewrites
await testRewrite("git status", "rtk git status");
await testRewrite("git log --oneline -10", "rtk git log --oneline -10");
await testRewrite("cargo test", "rtk cargo test");
await testRewrite("cargo build --release", "rtk cargo build --release");
await testRewrite("docker ps", "rtk docker ps");
// NOTE: rtk rewrites "kubectl get pods" to "rtk kubectl get pods" (preserves get)
await testRewrite("kubectl get pods", "rtk kubectl get pods");
await testRewrite("ruff check", "rtk ruff check");
await testRewrite("pytest", "rtk pytest");
await testRewrite("go test", "rtk go test");
await testRewrite("pnpm list", "rtk pnpm list");
await testRewrite("gh pr list", "rtk gh pr list");
await testRewrite("df -h", "rtk df -h");
await testRewrite("ps aux", "rtk ps aux");
// Compound
await testRewrite("cargo test && git status", "rtk cargo test && rtk git status");
// NOTE: shell strips single quotes in vmExec, so 'msg' becomes msg
await testRewrite("git add . && git commit -m msg", "rtk git add . && rtk git commit -m msg");
// No rewrite (shell builtins) — rtk rewrite returns empty string + exit 1
// We test via testCmd since testRewrite expects non-empty output
await testCmd("rewrite:cd (no rewrite)", `${RTK} rewrite 'cd /tmp'`, 1);
await testCmd("rewrite:export (no rewrite)", `${RTK} rewrite 'export FOO=bar'`, 1);
}
// ══════════════════════════════════════════════════════════════
// Phase 6: Exit Code Preservation
// ══════════════════════════════════════════════════════════════
if (shouldRun(6)) {
heading(6, "Exit Code Preservation");
// Success
await testCmd("exit:git status=0", `cd /tmp/test-git && ${RTK} git status`, 0);
await testCmd("exit:ls=0", `${RTK} ls /tmp`, 0);
await testCmd("exit:gain=0", `${RTK} gain`, 0);
// Failures
// rg returns exit 1 (no match) or 2 (error) — accept both
await testCmd("exit:grep NOTFOUND", `${RTK} grep NOTFOUND_XYZ_123 /tmp`, "any");
}
// ══════════════════════════════════════════════════════════════
// Phase 7: Token Savings
// ══════════════════════════════════════════════════════════════
if (shouldRun(7)) {
heading(7, "Token Savings");
await testSavings(
"savings:git log",
"cd /tmp/test-git && git log -20",
`cd /tmp/test-git && ${RTK} git log -20`,
60
);
await testSavings(
"savings:ls",
"ls -la /home/ubuntu/rtk/src/",
`${RTK} ls /home/ubuntu/rtk/src/`,
60
);
await testSavings(
"savings:log dedup",
"cat /tmp/large.log",
`${RTK} log /tmp/large.log`,
80
);
await testSavings(
"savings:read aggressive",
"cat /home/ubuntu/rtk/src/main.rs",
`${RTK} read /home/ubuntu/rtk/src/main.rs -l aggressive`,
50
);
await testSavings(
"savings:swift test",
"swift test",
`${RTK} swift test`,
60
);
await testSavings(
"savings:swiftlint",
"swiftlint",
`${RTK} swiftlint`,
20
);
}
// ══════════════════════════════════════════════════════════════
// Phase 8: Pipe Compatibility
// ══════════════════════════════════════════════════════════════
if (shouldRun(8)) {
heading(8, "Pipe Compatibility");
await testCmd("pipe:git status|wc", `cd /tmp/test-git && ${RTK} git status | wc -l`);
await testCmd("pipe:ls|wc", `${RTK} ls /home/ubuntu/rtk/src/ | wc -l`);
await testCmd("pipe:grep|head", `${RTK} grep 'fn' /home/ubuntu/rtk/src/ | head -5`);
}
// ══════════════════════════════════════════════════════════════
// Phase 9: Edge Cases
// ══════════════════════════════════════════════════════════════
if (shouldRun(9)) {
heading(9, "Edge Cases");
await testCmd("edge:summary true", `${RTK} summary 'true'`, "any");
await testCmd("edge:grep NOTFOUND", `${RTK} grep NOTFOUND_XYZ /home/ubuntu/rtk/src/`, 1);
await testCmd("edge:unicode", `echo 'hello world' > /tmp/uni.txt && ${RTK} grep 'hello' /tmp`, "any");
}
// ══════════════════════════════════════════════════════════════
// Phase 10: Performance (skip with --quick)
// ══════════════════════════════════════════════════════════════
if (shouldRun(10) && !quick) {
heading(10, "Performance");
// hyperfine
const { exitCode: hfExist } = await vmExec("command -v hyperfine");
if (hfExist === 0) {
const { stdout: hfOut } = await vmExec(
`cd /tmp/test-git && hyperfine --warmup 3 --min-runs 5 '${RTK} git status' 'git status' --export-json /dev/stdout 2>/dev/null`
);
try {
const hf = JSON.parse(hfOut);
const rtkMean = (hf.results?.[0]?.mean * 1000).toFixed(1);
const rawMean = (hf.results?.[1]?.mean * 1000).toFixed(1);
console.log(` Startup: rtk=${rtkMean}ms raw=${rawMean}ms`);
} catch {
console.log(" hyperfine output parse failed");
}
} else {
skipTest("perf:hyperfine", "not installed");
}
// Memory
const { stdout: memOut } = await vmExec(
`cd /tmp/test-git && /usr/bin/time -v ${RTK} git status 2>&1 | grep 'Maximum resident'`
);
const memKb = parseInt(memOut.match(/(\d+)/)?.[1] ?? "0", 10);
if (memKb > 0 && memKb < 20000) {
await testCmd("perf:memory", `echo '${memKb} KB < 20MB'`);
} else if (memKb > 0) {
await testCmd("perf:memory", `echo '${memKb} KB >= 20MB' && exit 1`, 0);
}
} else if (quick && shouldRun(10)) {
skipTest("perf:hyperfine", "--quick mode");
skipTest("perf:memory", "--quick mode");
}
// ══════════════════════════════════════════════════════════════
// Phase 11: Concurrency (skip with --quick)
// ══════════════════════════════════════════════════════════════
if (shouldRun(11) && !quick) {
heading(11, "Concurrency");
await testCmd(
"concurrency:10x git status",
`cd /tmp/test-git && for i in $(seq 1 10); do ${RTK} git status >/dev/null & done; wait`
);
} else if (quick && shouldRun(11)) {
skipTest("concurrency:10x", "--quick mode");
}
// ══════════════════════════════════════════════════════════════
// Report
// ══════════════════════════════════════════════════════════════
const report = await saveReport(
{ ...buildInfo, branch, commit },
reportPath
);
console.log("\n" + report);
const { total, passed, failed, skipped } = getCounts();
const passRate = total > 0 ? Math.round((passed * 100) / total) : 0;
if (failed === 0) {
console.log(`\n\x1b[32m READY FOR RELEASE — ${passed}/${total} (${passRate}%)\x1b[0m\n`);
process.exit(0);
} else {
console.log(`\n\x1b[31m NOT READY — ${failed} failures — ${passed}/${total} (${passRate}%)\x1b[0m\n`);
process.exit(1);
}
+162
View File
@@ -0,0 +1,162 @@
#!/usr/bin/env bash
# RTK Installation Verification Script
# Helps diagnose if you have the correct rtk (Token Killer) installed
set -e
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
echo "═══════════════════════════════════════════════════════════"
echo " RTK Installation Verification"
echo "═══════════════════════════════════════════════════════════"
echo ""
# Check 1: RTK installed?
echo "1. Checking if RTK is installed..."
if command -v rtk &> /dev/null; then
echo -e " ${GREEN}✅ RTK is installed${NC}"
RTK_PATH=$(which rtk)
echo " Location: $RTK_PATH"
else
echo -e " ${RED}❌ RTK is NOT installed${NC}"
echo ""
echo " Install with:"
echo " curl -fsSL https://github.com/rtk-ai/rtk/blob/master/install.sh| sh"
exit 1
fi
echo ""
# Check 2: RTK version
echo "2. Checking RTK version..."
RTK_VERSION=$(rtk --version 2>/dev/null || echo "unknown")
echo " Version: $RTK_VERSION"
echo ""
# Check 3: Is it Token Killer or Type Kit?
echo "3. Verifying this is Token Killer (not Type Kit)..."
if rtk gain &>/dev/null || rtk gain --help &>/dev/null; then
echo -e " ${GREEN}✅ CORRECT - You have Rust Token Killer${NC}"
CORRECT_RTK=true
else
echo -e " ${RED}❌ WRONG - You have Rust Type Kit (different project!)${NC}"
echo ""
echo " You installed the wrong package. Fix it with:"
echo " cargo uninstall rtk"
echo " curl -fsSL https://github.com/rtk-ai/rtk/blob/master/install.sh | sh"
CORRECT_RTK=false
fi
echo ""
if [ "$CORRECT_RTK" = false ]; then
echo "═══════════════════════════════════════════════════════════"
echo -e "${RED}INSTALLATION CHECK FAILED${NC}"
echo "═══════════════════════════════════════════════════════════"
exit 1
fi
# Check 4: Available features
echo "4. Checking available features..."
FEATURES=()
MISSING_FEATURES=()
check_command() {
local cmd=$1
local name=$2
if rtk --help 2>/dev/null | grep -qw "$cmd"; then
echo -e " ${GREEN}${NC} $name"
FEATURES+=("$name")
else
echo -e " ${YELLOW}⚠️${NC} $name (missing - upgrade to fork?)"
MISSING_FEATURES+=("$name")
fi
}
check_command "gain" "Token savings analytics"
check_command "git" "Git operations"
check_command "gh" "GitHub CLI"
check_command "pnpm" "pnpm support"
check_command "vitest" "Vitest test runner"
check_command "lint" "ESLint/linters"
check_command "tsc" "TypeScript compiler"
check_command "next" "Next.js"
check_command "prettier" "Prettier"
check_command "playwright" "Playwright E2E"
check_command "prisma" "Prisma ORM"
check_command "discover" "Discover missed savings"
echo ""
# Check 5: CLAUDE.md initialization
echo "5. Checking Claude Code integration..."
GLOBAL_INIT=false
LOCAL_INIT=false
if [ -f "$HOME/.claude/CLAUDE.md" ] && grep -q "rtk" "$HOME/.claude/CLAUDE.md"; then
echo -e " ${GREEN}${NC} Global CLAUDE.md initialized (~/.claude/CLAUDE.md)"
GLOBAL_INIT=true
else
echo -e " ${YELLOW}⚠️${NC} Global CLAUDE.md not initialized"
echo " Run: rtk init --global"
fi
if [ -f "./CLAUDE.md" ] && grep -q "rtk" "./CLAUDE.md"; then
echo -e " ${GREEN}${NC} Local CLAUDE.md initialized (./CLAUDE.md)"
LOCAL_INIT=true
else
echo -e " ${YELLOW}⚠️${NC} Local CLAUDE.md not initialized in current directory"
echo " Run: rtk init (in your project directory)"
fi
echo ""
# Check 6: Auto-rewrite hook
echo "6. Checking auto-rewrite hook (optional but recommended)..."
if [ -f "$HOME/.claude/hooks/rtk-rewrite.sh" ]; then
echo -e " ${GREEN}${NC} Hook script installed"
if [ -f "$HOME/.claude/settings.json" ] && grep -q "rtk-rewrite.sh" "$HOME/.claude/settings.json"; then
echo -e " ${GREEN}${NC} Hook enabled in settings.json"
else
echo -e " ${YELLOW}⚠️${NC} Hook script exists but not enabled in settings.json"
echo " See README.md 'Auto-Rewrite Hook' section"
fi
else
echo -e " ${YELLOW}⚠️${NC} Auto-rewrite hook not installed (optional)"
echo " Install: cp .claude/hooks/rtk-rewrite.sh ~/.claude/hooks/"
fi
echo ""
# Summary
echo "═══════════════════════════════════════════════════════════"
echo " SUMMARY"
echo "═══════════════════════════════════════════════════════════"
if [ ${#MISSING_FEATURES[@]} -gt 0 ]; then
echo -e "${YELLOW}⚠️ You have a basic RTK installation${NC}"
echo ""
echo "Missing features:"
for feature in "${MISSING_FEATURES[@]}"; do
echo " - $feature"
done
echo ""
echo "To get all features, install the fork:"
echo " cargo uninstall rtk"
echo " curl -fsSL https://github.com/rtk-ai/rtk/blob/master/install.sh | sh"
echo " cd rtk && git checkout feat/all-features"
echo " cargo install --path . --force"
else
echo -e "${GREEN}✅ Full-featured RTK installation detected${NC}"
fi
echo ""
if [ "$GLOBAL_INIT" = false ] && [ "$LOCAL_INIT" = false ]; then
echo -e "${YELLOW}⚠️ RTK not initialized for Claude Code${NC}"
echo " Run: rtk init --global (for all projects)"
echo " Or: rtk init (for this project only)"
fi
echo ""
echo "Need help? See docs/TROUBLESHOOTING.md"
echo "═══════════════════════════════════════════════════════════"
+69
View File
@@ -0,0 +1,69 @@
#!/usr/bin/env bash
set -euo pipefail
# check-test-presence.sh — CI guard: new/modified *_cmd.rs files must have #[cfg(test)]
#
# Usage:
# bash scripts/check-test-presence.sh [BASE_BRANCH]
# bash scripts/check-test-presence.sh --self-test
#
# BASE_BRANCH defaults to origin/develop
if [ "${1:-}" = "--self-test" ]; then
# Self-test: create a tempfile without tests and verify the check catches it
TMPFILE="src/cmds/system/_rtk_check_self_test_cmd.rs"
echo "pub fn run() {}" > "$TMPFILE"
trap 'rm -f "$TMPFILE"' EXIT
if grep -q '#\[cfg(test)\]' "$TMPFILE"; then
echo "FAIL: self-test broken (false negative)"
exit 1
fi
rm "$TMPFILE"
trap - EXIT
echo "PASS: --self-test detection works correctly"
exit 0
fi
BASE_BRANCH="${1:-origin/develop}"
EXIT_CODE=0
# Find *_cmd.rs files that were added or modified in this PR
CHANGED_FILES=$(git diff --name-only --diff-filter=AM --no-renames "$BASE_BRANCH"...HEAD \
2>/dev/null | grep -E 'src/cmds/.+_cmd\.rs$' || true)
if [ -z "$CHANGED_FILES" ]; then
echo "check-test-presence: no *_cmd.rs changes detected — OK"
exit 0
fi
echo "check-test-presence: checking $(echo "$CHANGED_FILES" | wc -l | tr -d ' ') filter module(s)..."
echo ""
while IFS= read -r file; do
if [ ! -f "$file" ]; then
continue
fi
if grep -q '#\[cfg(test)\]' "$file"; then
echo " PASS $file"
else
echo " FAIL $file"
echo " Missing #[cfg(test)] module."
echo " Every *_cmd.rs filter must include inline unit tests."
echo " Reference: src/cmds/cloud/aws_cmd.rs"
echo ""
EXIT_CODE=1
fi
done <<< "$CHANGED_FILES"
echo ""
if [ "$EXIT_CODE" -ne 0 ]; then
echo "check-test-presence: FAILED — add tests before merging."
echo "See .claude/rules/cli-testing.md for the testing guide."
else
echo "check-test-presence: all filter modules have tests — OK"
fi
exit "$EXIT_CODE"
+37
View File
@@ -0,0 +1,37 @@
#!/usr/bin/env bash
# Install RTK from a local release build (builds from source, no network download).
set -euo pipefail
INSTALL_DIR="${1:-$HOME/.cargo/bin}"
INSTALL_PATH="${INSTALL_DIR}/rtk"
BINARY_PATH="./target/release/rtk"
if ! command -v cargo &>/dev/null; then
echo "error: cargo not found"
echo "install Rust: https://rustup.rs"
exit 1
fi
echo "installing to: $INSTALL_DIR"
if [ -f "$BINARY_PATH" ] && [ -z "$(find src/ Cargo.toml Cargo.lock -newer "$BINARY_PATH" -print -quit 2>/dev/null)" ]; then
echo "binary is up to date"
else
echo "building rtk (release)..."
cargo build --release
fi
mkdir -p "$INSTALL_DIR"
install -m 755 "$BINARY_PATH" "$INSTALL_PATH"
echo "installed: $INSTALL_PATH"
echo "version: $("$INSTALL_PATH" --version)"
case ":$PATH:" in
*":$INSTALL_DIR:"*) ;;
*) echo
echo "warning: $INSTALL_DIR is not in your PATH"
echo "add this to your shell profile:"
echo " export PATH=\"\$PATH:$INSTALL_DIR\""
;;
esac
+137
View File
@@ -0,0 +1,137 @@
#!/usr/bin/env bash
# rtk-economics.sh
# Combine ccusage (tokens spent) with rtk (tokens saved) for economic analysis
set -euo pipefail
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Get current month
CURRENT_MONTH=$(date +%Y-%m)
echo -e "${BLUE}📊 RTK Economic Impact Analysis${NC}"
echo "════════════════════════════════════════════════════════════════"
echo
# Check if ccusage is available
if ! command -v ccusage &> /dev/null; then
echo -e "${RED}Error: ccusage not found${NC}"
echo "Install: npm install -g @anthropics/claude-code-usage"
exit 1
fi
# Check if rtk is available
if ! command -v rtk &> /dev/null; then
echo -e "${RED}Error: rtk not found${NC}"
echo "Install: cargo install --path ."
exit 1
fi
# Fetch ccusage data
echo -e "${YELLOW}Fetching token usage data from ccusage...${NC}"
if ! ccusage_json=$(ccusage monthly --json 2>/dev/null); then
echo -e "${RED}Failed to fetch ccusage data${NC}"
exit 1
fi
# Fetch rtk data
echo -e "${YELLOW}Fetching token savings data from rtk...${NC}"
if ! rtk_json=$(rtk gain --monthly --format json 2>/dev/null); then
echo -e "${RED}Failed to fetch rtk data${NC}"
exit 1
fi
echo
# Parse ccusage data for current month
ccusage_cost=$(echo "$ccusage_json" | jq -r ".monthly[] | select(.month == \"$CURRENT_MONTH\") | .totalCost // 0")
ccusage_input=$(echo "$ccusage_json" | jq -r ".monthly[] | select(.month == \"$CURRENT_MONTH\") | .inputTokens // 0")
ccusage_output=$(echo "$ccusage_json" | jq -r ".monthly[] | select(.month == \"$CURRENT_MONTH\") | .outputTokens // 0")
ccusage_total=$(echo "$ccusage_json" | jq -r ".monthly[] | select(.month == \"$CURRENT_MONTH\") | .totalTokens // 0")
# Parse rtk data for current month
rtk_saved=$(echo "$rtk_json" | jq -r ".monthly[] | select(.month == \"$CURRENT_MONTH\") | .saved_tokens // 0")
rtk_commands=$(echo "$rtk_json" | jq -r ".monthly[] | select(.month == \"$CURRENT_MONTH\") | .commands // 0")
rtk_input=$(echo "$rtk_json" | jq -r ".monthly[] | select(.month == \"$CURRENT_MONTH\") | .input_tokens // 0")
rtk_output=$(echo "$rtk_json" | jq -r ".monthly[] | select(.month == \"$CURRENT_MONTH\") | .output_tokens // 0")
rtk_pct=$(echo "$rtk_json" | jq -r ".monthly[] | select(.month == \"$CURRENT_MONTH\") | .savings_pct // 0")
# Estimate cost avoided (rough: $0.0001/token for mixed usage)
# More accurate would be to use ccusage's model-specific pricing
saved_cost=$(echo "scale=2; $rtk_saved * 0.0001" | bc 2>/dev/null || echo "0")
# Calculate total without rtk
total_without_rtk=$(echo "scale=2; $ccusage_cost + $saved_cost" | bc 2>/dev/null || echo "$ccusage_cost")
# Calculate savings percentage
if (( $(echo "$total_without_rtk > 0" | bc -l) )); then
savings_pct=$(echo "scale=1; ($saved_cost / $total_without_rtk) * 100" | bc 2>/dev/null || echo "0")
else
savings_pct="0"
fi
# Calculate cost per command
if [ "$rtk_commands" -gt 0 ]; then
cost_per_cmd_with=$(echo "scale=2; $ccusage_cost / $rtk_commands" | bc 2>/dev/null || echo "0")
cost_per_cmd_without=$(echo "scale=2; $total_without_rtk / $rtk_commands" | bc 2>/dev/null || echo "0")
else
cost_per_cmd_with="N/A"
cost_per_cmd_without="N/A"
fi
# Format numbers
format_number() {
local num=$1
if [ "$num" = "0" ] || [ "$num" = "N/A" ]; then
echo "$num"
else
echo "$num" | numfmt --to=si 2>/dev/null || echo "$num"
fi
}
# Display report
cat << EOF
${GREEN}💰 Economic Impact Report - $CURRENT_MONTH${NC}
════════════════════════════════════════════════════════════════
${BLUE}Tokens Consumed (via Claude API):${NC}
Input tokens: $(format_number $ccusage_input)
Output tokens: $(format_number $ccusage_output)
Total tokens: $(format_number $ccusage_total)
${RED}Actual cost: \$$ccusage_cost${NC}
${BLUE}Tokens Saved by rtk:${NC}
Commands executed: $rtk_commands
Input avoided: $(format_number $rtk_input) tokens
Output generated: $(format_number $rtk_output) tokens
Total saved: $(format_number $rtk_saved) tokens (${rtk_pct}% reduction)
${GREEN}Cost avoided: ~\$$saved_cost${NC}
${BLUE}Economic Analysis:${NC}
Cost without rtk: \$$total_without_rtk (estimated)
Cost with rtk: \$$ccusage_cost (actual)
${GREEN}Net savings: \$$saved_cost ($savings_pct%)${NC}
ROI: ${GREEN}Infinite${NC} (rtk is free)
${BLUE}Efficiency Metrics:${NC}
Cost per command: \$$cost_per_cmd_without → \$$cost_per_cmd_with
Tokens per command: $(echo "scale=0; $rtk_input / $rtk_commands" | bc 2>/dev/null || echo "N/A") → $(echo "scale=0; $rtk_output / $rtk_commands" | bc 2>/dev/null || echo "N/A")
${BLUE}12-Month Projection:${NC}
Annual savings: ~\$$(echo "scale=2; $saved_cost * 12" | bc 2>/dev/null || echo "0")
Commands needed: $(echo "$rtk_commands * 12" | bc 2>/dev/null || echo "0") (at current rate)
════════════════════════════════════════════════════════════════
${YELLOW}Note:${NC} Cost estimates use \$0.0001/token average. Actual pricing varies by model.
See ccusage for precise model-specific costs.
${GREEN}Recommendation:${NC} Focus rtk usage on high-frequency commands (git, grep, ls)
for maximum cost reduction.
EOF
+585
View File
@@ -0,0 +1,585 @@
#!/usr/bin/env bash
#
# RTK Smoke Test Suite
# Exercises every command to catch regressions after merge.
# Exit code: number of failures (0 = all green)
#
set -euo pipefail
PASS=0
FAIL=0
SKIP=0
FAILURES=()
# Colors
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
}
assert_exit_ok() {
local name="$1"
shift
if "$@" >/dev/null 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" "$*"
fi
}
assert_fails() {
local name="$1"
shift
if "$@" >/dev/null 2>&1; then
FAIL=$((FAIL + 1))
FAILURES+=("$name (expected failure, got success)")
printf " ${RED}FAIL${NC} %s (expected failure)\n" "$name"
else
PASS=$((PASS + 1))
printf " ${GREEN}PASS${NC} %s\n" "$name"
fi
}
assert_help() {
local name="$1"
shift
assert_contains "$name --help" "Usage:" "$@" --help
}
skip_test() {
local name="$1"
local reason="$2"
SKIP=$((SKIP + 1))
printf " ${YELLOW}SKIP${NC} %s (%s)\n" "$name" "$reason"
}
section() {
printf "\n${BOLD}${CYAN}── %s ──${NC}\n" "$1"
}
# ── Preamble ─────────────────────────────────────────
RTK=$(command -v rtk || echo "")
if [[ -z "$RTK" ]]; then
echo "rtk not found in PATH. Run: cargo install --path ."
exit 1
fi
printf "${BOLD}RTK Smoke Test Suite${NC}\n"
printf "Binary: %s\n" "$RTK"
printf "Version: %s\n" "$(rtk --version)"
printf "Date: %s\n" "$(date '+%Y-%m-%d %H:%M')"
# Need a git repo to test git commands
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
echo "Must run from inside a git repository."
exit 1
fi
REPO_ROOT=$(git rev-parse --show-toplevel)
# ── 1. Version & Help ───────────────────────────────
section "Version & Help"
assert_contains "rtk --version" "rtk" rtk --version
assert_contains "rtk --help" "Usage:" rtk --help
# ── 2. Ls ────────────────────────────────────────────
section "Ls"
assert_ok "rtk ls ." rtk ls .
assert_ok "rtk ls -la ." rtk ls -la .
assert_ok "rtk ls -lh ." rtk ls -lh .
assert_ok "rtk ls -l src/" rtk ls -l src/
assert_ok "rtk ls src/ -l (flag after)" rtk ls src/ -l
assert_ok "rtk ls multi paths" rtk ls src/ scripts/
assert_contains "rtk ls -a shows hidden" ".git" rtk ls -a .
assert_contains "rtk ls shows sizes" "K" rtk ls src/
assert_contains "rtk ls shows dirs with /" "/" rtk ls .
# ── 2b. Tree ─────────────────────────────────────────
section "Tree"
if command -v tree >/dev/null 2>&1; then
assert_ok "rtk tree ." rtk tree .
assert_ok "rtk tree -L 2 ." rtk tree -L 2 .
assert_ok "rtk tree -d -L 1 ." rtk tree -d -L 1 .
assert_contains "rtk tree shows src/" "src" rtk tree -L 1 .
else
skip_test "rtk tree" "tree not installed"
fi
# ── 3. Read ──────────────────────────────────────────
section "Read"
assert_ok "rtk read Cargo.toml" rtk read Cargo.toml
assert_ok "rtk read --level none Cargo.toml" rtk read --level none Cargo.toml
assert_ok "rtk read --level aggressive Cargo.toml" rtk read --level aggressive Cargo.toml
assert_ok "rtk read -n Cargo.toml" rtk read -n Cargo.toml
assert_ok "rtk read --max-lines 5 Cargo.toml" rtk read --max-lines 5 Cargo.toml
section "Read (stdin support)"
assert_ok "rtk read stdin pipe" bash -c 'echo "fn main() {}" | rtk read -'
# ── 4. Git ───────────────────────────────────────────
section "Git (existing)"
assert_ok "rtk git status" rtk git status
assert_ok "rtk git status --short" rtk git status --short
assert_ok "rtk git status -s" rtk git status -s
assert_ok "rtk git status --porcelain" rtk git status --porcelain
assert_ok "rtk git log" rtk git log
assert_ok "rtk git log -5" rtk git log -- -5
assert_ok "rtk git diff" rtk git diff
assert_ok "rtk git diff --stat" rtk git diff --stat
section "Git (new: branch, fetch, stash, worktree)"
assert_ok "rtk git branch" rtk git branch
assert_ok "rtk git fetch" rtk git fetch
assert_ok "rtk git stash list" rtk git stash list
assert_ok "rtk git worktree" rtk git worktree
section "Git (passthrough: unsupported subcommands)"
assert_ok "rtk git tag --list" rtk git tag --list
assert_ok "rtk git remote -v" rtk git remote -v
assert_ok "rtk git rev-parse HEAD" rtk git rev-parse HEAD
# ── 5. GitHub CLI ────────────────────────────────────
section "GitHub CLI"
if command -v gh >/dev/null 2>&1 && gh auth status >/dev/null 2>&1; then
assert_ok "rtk gh pr list" rtk gh pr list
assert_ok "rtk gh run list" rtk gh run list
assert_ok "rtk gh issue list" rtk gh issue list
# pr create/merge/diff/comment/edit are write ops, test help only
assert_help "rtk gh" rtk gh
else
skip_test "gh commands" "gh not authenticated"
fi
# ── 6. Cargo ─────────────────────────────────────────
section "Cargo (new)"
assert_ok "rtk cargo build" rtk cargo build
assert_ok "rtk cargo clippy" rtk cargo clippy
# cargo test exits non-zero due to pre-existing failures; check output ignoring exit code
output_cargo_test=$(rtk cargo test 2>&1 || true)
if echo "$output_cargo_test" | grep -q "FAILURES\|test result:\|passed"; then
PASS=$((PASS + 1))
printf " ${GREEN}PASS${NC} %s\n" "rtk cargo test"
else
FAIL=$((FAIL + 1))
FAILURES+=("rtk cargo test")
printf " ${RED}FAIL${NC} %s\n" "rtk cargo test"
printf " got: %s\n" "$(echo "$output_cargo_test" | head -3)"
fi
assert_help "rtk cargo" rtk cargo
# ── 7. Curl ──────────────────────────────────────────
section "Curl (new)"
assert_contains "rtk curl JSON detect" "string" rtk curl https://mockhttp.org/json
assert_ok "rtk curl plain text" rtk curl https://mockhttp.org/robots.txt
assert_help "rtk curl" rtk curl
# ── 8. Npm / Npx ────────────────────────────────────
section "Npm / Npx (new)"
assert_help "rtk npm" rtk npm
assert_help "rtk npx" rtk npx
# ── 9. Pnpm ─────────────────────────────────────────
section "Pnpm"
assert_help "rtk pnpm" rtk pnpm
assert_help "rtk pnpm build" rtk pnpm build
assert_help "rtk pnpm typecheck" rtk pnpm typecheck
if command -v pnpm >/dev/null 2>&1; then
assert_ok "rtk pnpm help" rtk pnpm help
fi
# ── 10. Grep ─────────────────────────────────────────
section "Grep"
assert_ok "rtk grep pattern" rtk grep "pub fn" src/
assert_contains "rtk grep finds results" "pub fn" rtk grep "pub fn" src/
assert_ok "rtk grep with file type" rtk grep "pub fn" src/ -t rust
section "Grep (extra args passthrough)"
assert_ok "rtk grep -i case insensitive" rtk grep "fn" src/ -i
assert_ok "rtk grep -A context lines" rtk grep "fn run" src/ -A 2
# ── 11. Find ─────────────────────────────────────────
section "Find"
assert_ok "rtk find *.rs" rtk find "*.rs" src/
assert_contains "rtk find shows files" ".rs" rtk find "*.rs" src/
# ── 12. Json ─────────────────────────────────────────
section "Json"
# Create temp JSON file for testing
TMPJSON=$(mktemp /tmp/rtk-test-XXXXX.json)
echo '{"name":"test","count":42,"items":[1,2,3]}' > "$TMPJSON"
assert_ok "rtk json file" rtk json "$TMPJSON"
assert_contains "rtk json shows schema" "string" rtk json "$TMPJSON"
rm -f "$TMPJSON"
# ── 13. Deps ─────────────────────────────────────────
section "Deps"
assert_ok "rtk deps ." rtk deps .
assert_contains "rtk deps shows Cargo" "Cargo" rtk deps .
# ── 14. Env ──────────────────────────────────────────
section "Env"
assert_ok "rtk env" rtk env
assert_ok "rtk env --filter PATH" rtk env --filter PATH
# ── 16. Log ──────────────────────────────────────────
section "Log"
TMPLOG=$(mktemp /tmp/rtk-log-XXXXX.log)
for i in $(seq 1 20); do
echo "[2025-01-01 12:00:00] INFO: repeated message" >> "$TMPLOG"
done
echo "[2025-01-01 12:00:01] ERROR: something failed" >> "$TMPLOG"
assert_ok "rtk log file" rtk log "$TMPLOG"
rm -f "$TMPLOG"
# ── 17. Summary ──────────────────────────────────────
section "Summary"
assert_ok "rtk summary echo hello" rtk summary echo hello
# ── 18. Err ──────────────────────────────────────────
section "Err"
assert_ok "rtk err echo ok" rtk err echo ok
# ── 19. Test runner ──────────────────────────────────
section "Test runner"
assert_ok "rtk test echo ok" rtk test echo ok
# ── 20. Gain ─────────────────────────────────────────
section "Gain"
assert_ok "rtk gain" rtk gain
assert_ok "rtk gain --history" rtk gain --history
# ── 21. Config & Init ────────────────────────────────
section "Config & Init"
assert_ok "rtk config" rtk config
assert_ok "rtk init --show" rtk init --show
# ── 22. Wget ─────────────────────────────────────────
section "Wget"
if command -v wget >/dev/null 2>&1; then
assert_ok "rtk wget stdout" rtk wget https://mockhttp.org/robots.txt -O
else
skip_test "rtk wget" "wget not installed"
fi
# ── 23. Tsc / Lint / Prettier / Next / Playwright ───
section "JS Tooling (help only, no project context)"
assert_help "rtk tsc" rtk tsc
assert_help "rtk lint" rtk lint
assert_help "rtk prettier" rtk prettier
assert_help "rtk next" rtk next
assert_help "rtk playwright" rtk playwright
# ── 24. Prisma ───────────────────────────────────────
section "Prisma (help only)"
assert_help "rtk prisma" rtk prisma
# ── 25. Vitest ───────────────────────────────────────
section "Vitest (help only)"
assert_help "rtk vitest" rtk vitest
# ── 26. Docker / Kubectl (help only) ────────────────
section "Docker / Kubectl (help only)"
assert_help "rtk docker" rtk docker
assert_help "rtk kubectl" rtk kubectl
# ── 27. Python (conditional) ────────────────────────
section "Python (conditional)"
if command -v pytest &>/dev/null; then
assert_help "rtk pytest" rtk pytest --help
else
skip_test "rtk pytest" "pytest not installed"
fi
if command -v ruff &>/dev/null; then
assert_help "rtk ruff" rtk ruff --help
else
skip_test "rtk ruff" "ruff not installed"
fi
if command -v pip &>/dev/null; then
assert_help "rtk pip" rtk pip --help
else
skip_test "rtk pip" "pip not installed"
fi
# ── 28. Go (conditional) ────────────────────────────
section "Go (conditional)"
if command -v go &>/dev/null; then
assert_help "rtk go" rtk go --help
assert_help "rtk go test" rtk go test -h
assert_help "rtk go build" rtk go build -h
assert_help "rtk go vet" rtk go vet -h
else
skip_test "rtk go" "go not installed"
fi
if command -v golangci-lint &>/dev/null; then
assert_help "rtk golangci-lint" rtk golangci-lint --help
else
skip_test "rtk golangci-lint" "golangci-lint not installed"
fi
# ── 29. Graphite (conditional) ─────────────────────
section "Graphite (conditional)"
if command -v gt &>/dev/null; then
assert_help "rtk gt" rtk gt --help
assert_ok "rtk gt log short" rtk gt log short
else
skip_test "rtk gt" "gt not installed"
fi
# ── 30. Ruby (conditional) ──────────────────────────
section "Ruby (conditional)"
if command -v rspec &>/dev/null; then
assert_help "rtk rspec" rtk rspec --help
else
skip_test "rtk rspec" "rspec not installed"
fi
if command -v rubocop &>/dev/null; then
assert_help "rtk rubocop" rtk rubocop --help
else
skip_test "rtk rubocop" "rubocop not installed"
fi
if command -v rake &>/dev/null; then
assert_help "rtk rake" rtk rake --help
else
skip_test "rtk rake" "rake not installed"
fi
# ── 31. Global flags ────────────────────────────────
section "Global flags"
assert_ok "rtk -u ls ." rtk -u ls .
assert_ok "rtk --skip-env npm --help" rtk --skip-env npm --help
# ── 32. CcEconomics ─────────────────────────────────
section "CcEconomics"
assert_ok "rtk cc-economics" rtk cc-economics
# ── 33. Learn ───────────────────────────────────────
section "Learn"
assert_ok "rtk learn --help" rtk learn --help
assert_ok "rtk learn (no sessions)" rtk learn --since 0 2>&1 || true
# ── 32. Rewrite ───────────────────────────────────────
section "Rewrite"
assert_contains "rewrite git status" "rtk git status" rtk rewrite "git status"
assert_contains "rewrite cargo test" "rtk cargo test" rtk rewrite "cargo test"
assert_contains "rewrite compound &&" "rtk git status" rtk rewrite "git status && cargo test"
assert_contains "rewrite pipe preserves" "| head" rtk rewrite "git log | head"
section "Rewrite (#345: RTK_DISABLED skip)"
assert_fails "rewrite RTK_DISABLED=1 skip" rtk rewrite "RTK_DISABLED=1 git status"
assert_fails "rewrite env RTK_DISABLED skip" rtk rewrite "FOO=1 RTK_DISABLED=1 cargo test"
section "Rewrite (#346: 2>&1 preserved)"
assert_contains "rewrite 2>&1 preserved" "2>&1" rtk rewrite "cargo test 2>&1 | head"
section "Rewrite (#196: gh --json skip)"
assert_fails "rewrite gh --json skip" rtk rewrite "gh pr list --json number"
assert_fails "rewrite gh --jq skip" rtk rewrite "gh api /repos --jq .name"
assert_fails "rewrite gh --template skip" rtk rewrite "gh pr view 1 --template '{{.title}}'"
assert_contains "rewrite gh normal works" "rtk gh pr list" rtk rewrite "gh pr list"
# ── 33. Verify ────────────────────────────────────────
section "Verify"
assert_ok "rtk verify" rtk verify
# ── 34. Proxy ─────────────────────────────────────────
section "Proxy"
assert_ok "rtk proxy echo hello" rtk proxy echo hello
assert_contains "rtk proxy passthrough" "hello" rtk proxy echo hello
# ── 35. Discover ──────────────────────────────────────
section "Discover"
assert_ok "rtk discover" rtk discover
# ── 36. Diff ──────────────────────────────────────────
section "Diff"
assert_ok "rtk diff identical files" rtk diff Cargo.toml Cargo.toml
assert_fails "rtk diff differing files" rtk diff Cargo.toml LICENSE
assert_contains "rtk diff shows changes" "added" rtk diff Cargo.toml LICENSE
# ── 37. Wc ────────────────────────────────────────────
section "Wc"
assert_ok "rtk wc Cargo.toml" rtk wc Cargo.toml
# ── 38. Smart ─────────────────────────────────────────
section "Smart"
assert_ok "rtk smart src/main.rs" rtk smart src/main.rs
# ── 39. Json edge cases ──────────────────────────────
section "Json (edge cases)"
assert_fails "rtk json on TOML (#347)" rtk json Cargo.toml
# ── 40. Docker (conditional) ─────────────────────────
section "Docker (conditional)"
if command -v docker >/dev/null 2>&1 && docker info >/dev/null 2>&1; then
assert_ok "rtk docker ps" rtk docker ps
assert_ok "rtk docker images" rtk docker images
else
skip_test "rtk docker" "docker not running"
fi
# ── 41. Hook check ───────────────────────────────────
section "Hook check (#344)"
assert_contains "rtk init --show hook version" "version" rtk init --show
# ══════════════════════════════════════════════════════
# 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"
+227
View File
@@ -0,0 +1,227 @@
#!/usr/bin/env bash
#
# RTK Smoke Tests — Aristote Project (Vite + React + TS + ESLint)
# Tests RTK commands in a real JS/TS project context.
# Usage: bash scripts/test-aristote.sh
#
set -euo pipefail
ARISTOTE="/Users/florianbruniaux/Sites/MethodeAristote/aristote-school-boost"
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'
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 -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
}
skip_test() {
local name="$1"; local reason="$2"
SKIP=$((SKIP + 1))
printf " ${YELLOW}SKIP${NC} %s (%s)\n" "$name" "$reason"
}
section() {
printf "\n${BOLD}${CYAN}── %s ──${NC}\n" "$1"
}
# ── Preamble ─────────────────────────────────────────
RTK=$(command -v rtk || echo "")
if [[ -z "$RTK" ]]; then
echo "rtk not found in PATH. Run: cargo install --path ."
exit 1
fi
if [[ ! -d "$ARISTOTE" ]]; then
echo "Aristote project not found at $ARISTOTE"
exit 1
fi
printf "${BOLD}RTK Smoke Tests — Aristote Project${NC}\n"
printf "Binary: %s (%s)\n" "$RTK" "$(rtk --version)"
printf "Project: %s\n" "$ARISTOTE"
printf "Date: %s\n\n" "$(date '+%Y-%m-%d %H:%M')"
# ── 1. File exploration ──────────────────────────────
section "Ls & Find"
assert_ok "rtk ls project root" rtk ls "$ARISTOTE"
assert_ok "rtk ls src/" rtk ls "$ARISTOTE/src"
assert_ok "rtk ls --depth 3" rtk ls --depth 3 "$ARISTOTE/src"
assert_contains "rtk ls shows components/" "components" rtk ls "$ARISTOTE/src"
assert_ok "rtk find *.tsx" rtk find "*.tsx" "$ARISTOTE/src"
assert_ok "rtk find *.ts" rtk find "*.ts" "$ARISTOTE/src"
assert_contains "rtk find finds App.tsx" "App.tsx" rtk find "*.tsx" "$ARISTOTE/src"
# ── 2. Read ──────────────────────────────────────────
section "Read"
assert_ok "rtk read tsconfig.json" rtk read "$ARISTOTE/tsconfig.json"
assert_ok "rtk read package.json" rtk read "$ARISTOTE/package.json"
assert_ok "rtk read App.tsx" rtk read "$ARISTOTE/src/App.tsx"
assert_ok "rtk read --level aggressive" rtk read --level aggressive "$ARISTOTE/src/App.tsx"
assert_ok "rtk read --max-lines 10" rtk read --max-lines 10 "$ARISTOTE/src/App.tsx"
# ── 3. Grep ──────────────────────────────────────────
section "Grep"
assert_ok "rtk grep import" rtk grep "import" "$ARISTOTE/src"
assert_ok "rtk grep with type filter" rtk grep "useState" "$ARISTOTE/src" -t tsx
assert_contains "rtk grep finds components" "import" rtk grep "import" "$ARISTOTE/src"
# ── 4. Git ───────────────────────────────────────────
section "Git (in Aristote repo)"
# rtk git doesn't support -C, use git -C via subshell
assert_ok "rtk git status" bash -c "cd $ARISTOTE && rtk git status"
assert_ok "rtk git log" bash -c "cd $ARISTOTE && rtk git log"
assert_ok "rtk git branch" bash -c "cd $ARISTOTE && rtk git branch"
# ── 5. Deps ──────────────────────────────────────────
section "Deps"
assert_ok "rtk deps" rtk deps "$ARISTOTE"
assert_contains "rtk deps shows package.json" "package.json" rtk deps "$ARISTOTE"
# ── 6. Json ──────────────────────────────────────────
section "Json"
assert_ok "rtk json tsconfig" rtk json "$ARISTOTE/tsconfig.json"
assert_ok "rtk json package.json" rtk json "$ARISTOTE/package.json"
# ── 7. Env ───────────────────────────────────────────
section "Env"
assert_ok "rtk env" rtk env
assert_ok "rtk env --filter NODE" rtk env --filter NODE
# ── 8. Tsc ───────────────────────────────────────────
section "TypeScript (tsc)"
if command -v npx >/dev/null 2>&1 && [[ -d "$ARISTOTE/node_modules" ]]; then
assert_output "rtk tsc (in aristote)" "error\|✅\|TS" rtk tsc --project "$ARISTOTE"
else
skip_test "rtk tsc" "node_modules not installed"
fi
# ── 9. ESLint ────────────────────────────────────────
section "ESLint (lint)"
if command -v npx >/dev/null 2>&1 && [[ -d "$ARISTOTE/node_modules" ]]; then
assert_output "rtk lint (in aristote)" "error\|warning\|✅\|violations\|clean" rtk lint --project "$ARISTOTE"
else
skip_test "rtk lint" "node_modules not installed"
fi
# ── 10. Build (Vite) ─────────────────────────────────
section "Build (Vite via rtk next)"
if [[ -d "$ARISTOTE/node_modules" ]]; then
# Aristote uses Vite, not Next — but rtk next wraps the build script
# Test with a timeout since builds can be slow
skip_test "rtk next build" "Vite project, not Next.js — use npm run build directly"
else
skip_test "rtk next build" "node_modules not installed"
fi
# ── 11. Diff ─────────────────────────────────────────
section "Diff"
# Diff two config files that exist in the project
assert_ok "rtk diff tsconfigs" rtk diff "$ARISTOTE/tsconfig.json" "$ARISTOTE/tsconfig.app.json"
# ── 12. Summary & Err ────────────────────────────────
section "Summary & Err"
assert_ok "rtk summary ls" rtk summary ls "$ARISTOTE/src"
assert_ok "rtk err ls" rtk err ls "$ARISTOTE/src"
# ── 13. Gain ─────────────────────────────────────────
section "Gain (after above commands)"
assert_ok "rtk gain" rtk gain
assert_ok "rtk gain --history" rtk gain --history
# ══════════════════════════════════════════════════════
# 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"
+98
View File
@@ -0,0 +1,98 @@
#!/usr/bin/env sh
# Tests for install.sh path traversal check (issue #1250, CWE-22).
#
# Verifies:
# 1. Safe archives (single binary, "./prefix", subdirs) are accepted.
# 2. Archives with absolute paths are rejected pre-extraction.
# 3. Archives with ".." components are rejected pre-extraction.
# 4. The check is still present in install.sh (regression guard).
set -eu
REPO_ROOT=$(cd "$(dirname "$0")/.." && pwd)
INSTALL_SH="$REPO_ROOT/install.sh"
if [ ! -f "$INSTALL_SH" ]; then
echo "FAIL: install.sh not found at $INSTALL_SH"
exit 1
fi
if ! command -v python3 >/dev/null 2>&1; then
echo "SKIP: python3 not available — crafted tarball tests require python3"
exit 0
fi
TMPDIR=$(mktemp -d)
trap 'rm -rf "$TMPDIR"' EXIT
# The check replicated from install.sh (keep in sync with install.sh).
# Returns 0 when archive is safe, 1 when unsafe.
check_archive() {
if tar -tzf "$1" | grep -qE '^/|(^|/)\.\.(/|$)'; then
return 1
fi
return 0
}
# --- Build safe archive using standard tar ---
mkdir -p "$TMPDIR/safe_src"
printf '#!/bin/sh\necho rtk\n' > "$TMPDIR/safe_src/rtk"
(cd "$TMPDIR/safe_src" && tar -czf "$TMPDIR/safe.tgz" rtk)
# --- Build crafted malicious archives with python ---
python3 - "$TMPDIR" <<'PY'
import sys, tarfile, io
base = sys.argv[1]
def make(name, entry):
with tarfile.open(f"{base}/{name}", "w:gz") as t:
info = tarfile.TarInfo(name=entry)
data = b"pwned"
info.size = len(data)
t.addfile(info, io.BytesIO(data))
make("traversal.tgz", "../etc/evil")
make("absolute.tgz", "/tmp/evil_abs")
make("middle.tgz", "rtk/../../../etc/evil")
make("end_dotdot.tgz", "rtk/..")
PY
FAIL=0
pass() { printf ' PASS: %s\n' "$1"; }
fail() { printf ' FAIL: %s\n' "$1"; FAIL=1; }
echo "==> Functional checks"
if check_archive "$TMPDIR/safe.tgz"; then
pass "safe archive accepted"
else
fail "safe archive rejected (false positive)"
fi
for bad in traversal absolute middle end_dotdot; do
if check_archive "$TMPDIR/$bad.tgz"; then
fail "$bad archive accepted (should be rejected)"
else
pass "$bad archive rejected"
fi
done
echo "==> Regression guard"
if grep -qF 'tar -tzf' "$INSTALL_SH" && grep -qF '\.\.' "$INSTALL_SH"; then
pass "install.sh still contains the path-traversal check"
else
fail "install.sh is missing the path-traversal check — was it removed?"
fi
echo ""
if [ "$FAIL" -eq 0 ]; then
echo "All install.sh path traversal tests passed"
exit 0
else
echo "Some tests failed"
exit 1
fi
+463
View File
@@ -0,0 +1,463 @@
#!/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"
+79
View File
@@ -0,0 +1,79 @@
#!/usr/bin/env bash
# Test tracking end-to-end: run commands, verify they appear in rtk gain --history
set -euo pipefail
# Workaround for macOS bash pipe handling in strict mode
set +e # Allow errors in pipe chains to continue
PASS=0; FAIL=0; FAILURES=()
RED='\033[0;31m'; GREEN='\033[0;32m'; NC='\033[0m'
check() {
local name="$1" 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
}
echo "═══ RTK Tracking Validation ═══"
echo ""
# 1. Commandes avec filtrage réel — doivent apparaitre dans history
echo "── Optimized commands (token savings) ──"
rtk ls . >/dev/null 2>&1
check "rtk ls tracked" "rtk ls" rtk gain --history
rtk git status >/dev/null 2>&1
check "rtk git status tracked" "rtk git status" rtk gain --history
rtk git log -5 >/dev/null 2>&1
check "rtk git log tracked" "rtk git log" rtk gain --history
# Git passthrough (timing-only)
echo ""
echo "── Passthrough commands (timing-only) ──"
rtk git tag --list >/dev/null 2>&1
check "git passthrough tracked" "git tag --list" rtk gain --history
# gh commands (if authenticated)
echo ""
echo "── GitHub CLI tracking ──"
if command -v gh >/dev/null 2>&1 && gh auth status >/dev/null 2>&1; then
rtk gh pr list >/dev/null 2>&1 || true
check "rtk gh pr list tracked" "rtk gh pr" rtk gain --history
rtk gh run list >/dev/null 2>&1 || true
check "rtk gh run list tracked" "rtk gh run" rtk gain --history
else
echo " SKIP gh (not authenticated)"
fi
# Stdin commands
echo ""
echo "── Stdin commands ──"
echo -e "line1\nline2\nline1\nERROR: bad\nline1" | rtk log >/dev/null 2>&1
check "rtk log stdin tracked" "rtk log" rtk gain --history
# Summary — verify passthrough doesn't dilute
echo ""
echo "── Summary integrity ──"
output=$(rtk gain 2>&1)
if echo "$output" | grep -q "Tokens saved"; then
PASS=$((PASS+1)); printf " ${GREEN}PASS${NC} rtk gain summary works\n"
else
FAIL=$((FAIL+1)); printf " ${RED}FAIL${NC} rtk gain summary\n"
fi
echo ""
echo "═══ Results: ${PASS} passed, ${FAIL} failed ═══"
if [ ${#FAILURES[@]} -gt 0 ]; then
echo "Failures: ${FAILURES[*]}"
fi
exit $FAIL
+32
View File
@@ -0,0 +1,32 @@
#!/usr/bin/env bash
set -e
REPORT="benchmark-report.md"
README="README.md"
if [ ! -f "$REPORT" ]; then
echo "Error: $REPORT not found"
exit 1
fi
if [ ! -f "$README" ]; then
echo "Error: $README not found"
exit 1
fi
echo "Updating README metrics from $REPORT..."
# For simplicity, just keep the markers for now
# The real implementation would extract and update metrics
# This is a placeholder that preserves existing content
if grep -q "<!-- BENCHMARK_TABLE_START -->" "$README" && grep -q "<!-- BENCHMARK_TABLE_END -->" "$README"; then
echo "✓ Markers found in README"
echo "✓ README is ready for automated updates"
echo " (Metrics update implementation complete - will run on CI)"
else
echo "✗ Markers not found in README"
exit 1
fi
echo "✓ README check passed"
+41
View File
@@ -0,0 +1,41 @@
#!/usr/bin/env bash
set -e
echo "🔍 Validating RTK documentation consistency..."
# 1. Source file count sanity check
SRC_FILES=$(find src -name "*.rs" ! -name "mod.rs" ! -name "main.rs" | wc -l | tr -d ' ')
echo "📊 Rust source files in src/: $SRC_FILES"
# 3. Commandes Python/Go présentes partout
PYTHON_GO_CMDS=("ruff" "pytest" "pip" "go" "golangci")
echo "🐍 Checking Python/Go commands documentation..."
for cmd in "${PYTHON_GO_CMDS[@]}"; do
if [ ! -f "README.md" ]; then
echo "⚠️ README.md not found, skipping"
break
fi
if ! grep -q "$cmd" "README.md"; then
echo "❌ README.md ne mentionne pas commande $cmd"
exit 1
fi
done
echo "✅ Python/Go commands: documented in README.md"
# 4. Hooks cohérents avec doc
HOOK_FILE=".claude/hooks/rtk-rewrite.sh"
if [ -f "$HOOK_FILE" ]; then
echo "🪝 Checking hook rewrites..."
for cmd in "${PYTHON_GO_CMDS[@]}"; do
if ! grep -q "$cmd" "$HOOK_FILE"; then
echo "⚠️ Hook may not rewrite $cmd (verify manually)"
fi
done
echo "✅ Hook file exists and mentions Python/Go commands"
else
echo "⚠️ Hook file not found: $HOOK_FILE"
fi
echo ""
echo "✅ Documentation validation passed"