diff --git a/.codex_yolo.sh b/.codex_yolo.sh index e018c1f..503d1e6 100755 --- a/.codex_yolo.sh +++ b/.codex_yolo.sh @@ -55,6 +55,9 @@ if [[ "${#}" -gt 0 ]]; then diagnostics|doctor|health) exec "${SCRIPT_DIR}/.codex_yolo_diagnostics.sh" ;; + debt) + exec "${SCRIPT_DIR}/.codex_yolo_debt.sh" "${@:2}" + ;; version) if [[ -f "${SCRIPT_DIR}/VERSION" ]]; then cat "${SCRIPT_DIR}/VERSION" @@ -123,6 +126,7 @@ if [[ "${CODEX_SKIP_UPDATE_CHECK:-0}" != "1" ]]; then curl -fsSL "https://raw.githubusercontent.com/${REPO}/${BRANCH}/.codex_yolo.Dockerfile" -o "${temp_dir}/.codex_yolo.Dockerfile" && \ curl -fsSL "https://raw.githubusercontent.com/${REPO}/${BRANCH}/.codex_yolo_entrypoint.sh" -o "${temp_dir}/.codex_yolo_entrypoint.sh" && \ curl -fsSL "https://raw.githubusercontent.com/${REPO}/${BRANCH}/.codex_yolo_diagnostics.sh" -o "${temp_dir}/.codex_yolo_diagnostics.sh" && \ + curl -fsSL "https://raw.githubusercontent.com/${REPO}/${BRANCH}/.codex_yolo_debt.sh" -o "${temp_dir}/.codex_yolo_debt.sh" && \ curl -fsSL "https://raw.githubusercontent.com/${REPO}/${BRANCH}/default-AGENTS.md" -o "${temp_dir}/default-AGENTS.md" && \ curl -fsSL "https://raw.githubusercontent.com/${REPO}/${BRANCH}/.dockerignore" -o "${temp_dir}/.dockerignore" 2>/dev/null && \ curl -fsSL "https://raw.githubusercontent.com/${REPO}/${BRANCH}/VERSION" -o "${temp_dir}/VERSION"; then @@ -135,10 +139,12 @@ if [[ "${CODEX_SKIP_UPDATE_CHECK:-0}" != "1" ]]; then # Install core files chmod +x "${temp_dir}/.codex_yolo.sh" chmod +x "${temp_dir}/.codex_yolo_diagnostics.sh" + chmod +x "${temp_dir}/.codex_yolo_debt.sh" cp "${temp_dir}/.codex_yolo.sh" "${SCRIPT_DIR}/.codex_yolo.sh" cp "${temp_dir}/.codex_yolo.Dockerfile" "${SCRIPT_DIR}/.codex_yolo.Dockerfile" cp "${temp_dir}/.codex_yolo_entrypoint.sh" "${SCRIPT_DIR}/.codex_yolo_entrypoint.sh" cp "${temp_dir}/.codex_yolo_diagnostics.sh" "${SCRIPT_DIR}/.codex_yolo_diagnostics.sh" + cp "${temp_dir}/.codex_yolo_debt.sh" "${SCRIPT_DIR}/.codex_yolo_debt.sh" cp "${temp_dir}/default-AGENTS.md" "${SCRIPT_DIR}/default-AGENTS.md" cp "${temp_dir}/.dockerignore" "${SCRIPT_DIR}/.dockerignore" 2>/dev/null || true cp "${temp_dir}/VERSION" "${SCRIPT_DIR}/VERSION" diff --git a/.codex_yolo_completion.bash b/.codex_yolo_completion.bash index bcc1629..6990858 100644 --- a/.codex_yolo_completion.bash +++ b/.codex_yolo_completion.bash @@ -8,7 +8,7 @@ _codex_yolo_complete() { prev="${COMP_WORDS[COMP_CWORD-1]}" # codex_yolo specific commands and flags - local codex_yolo_opts="diagnostics doctor health version --version --verbose -v --pull --gh --help" + local codex_yolo_opts="diagnostics doctor health debt version --version --verbose -v --pull --gh --help" # Common codex CLI commands (pass-through) local codex_opts="login --help --yolo --search --device-auth" diff --git a/.codex_yolo_completion.zsh b/.codex_yolo_completion.zsh index 24f5e9f..c901a49 100644 --- a/.codex_yolo_completion.zsh +++ b/.codex_yolo_completion.zsh @@ -8,6 +8,7 @@ _codex_yolo() { 'diagnostics:Run health checks and show diagnostic information' 'doctor:Alias for diagnostics' 'health:Alias for diagnostics' + 'debt:Scan the current repository for technical debt markers' 'version:Show version information' 'login:Log in to OpenAI Codex' ) diff --git a/.codex_yolo_debt.sh b/.codex_yolo_debt.sh new file mode 100755 index 0000000..98b94ca --- /dev/null +++ b/.codex_yolo_debt.sh @@ -0,0 +1,255 @@ +#!/usr/bin/env bash +set -euo pipefail + +MARKER_REGEX='(^|[^[:alnum:]_])(TODO|FIXME|HACK|XXX)(\([^)]+\))?([[:space:]]*[:-]|[[:space:]]+)' + +usage() { + cat <<'USAGE' +Usage: codex_yolo debt [path] + +Scan the current repository (or the optional path) for common technical-debt +markers, classify each finding with deterministic Bash heuristics, assign a +priority score, and print a prioritized report. + +Scanned markers: + TODO, FIXME, HACK, XXX + +Categories: + bug-risk Potential correctness or stability issue + maintainability Cleanup or refactor debt in code + test-gap Missing or deferred test coverage work + docs-config-infra Documentation, configuration, or automation debt +USAGE +} + +normalize_context() { + printf '%s' "$1" | tr '\t' ' ' | sed -E 's/^[[:space:]]+//; s/[[:space:]]+/ /g' +} + +resolve_scan_root() { + local requested_path="${1:-}" + + if [[ -n "${requested_path}" ]]; then + if [[ ! -d "${requested_path}" ]]; then + echo "Error: path not found: ${requested_path}" >&2 + exit 1 + fi + + ( + cd "${requested_path}" + pwd + ) + return + fi + + if git_root="$(git rev-parse --show-toplevel 2>/dev/null)"; then + printf '%s\n' "${git_root}" + else + pwd + fi +} + +scan_matches() { + find . \ + \( -path './.git' \ + -o -path './node_modules' \ + -o -path './dist' \ + -o -path './build' \ + -o -path './coverage' \ + -o -path './.next' \ + -o -path './.venv' \ + -o -path './venv' \ + -o -path './tmp' \ + -o -path './vendor' \ + -o -path './.turbo' \ + -o -path './.cache' \ + \) -prune -o -type f -print0 | + while IFS= read -r -d '' file; do + grep -nHI -E "${MARKER_REGEX}" "${file}" 2>/dev/null || true + done +} + +detect_marker() { + local context="$1" + local upper_context + + upper_context="$(printf '%s' "${context}" | tr '[:lower:]' '[:upper:]')" + if [[ "${upper_context}" =~ ${MARKER_REGEX} ]]; then + printf '%s\n' "${BASH_REMATCH[2]}" + else + printf 'TODO\n' + fi +} + +classify_category() { + local marker="$1" + local file_path="$2" + local context="$3" + local lower_path lower_context + + lower_path="$(printf '%s' "${file_path}" | tr '[:upper:]' '[:lower:]')" + lower_context="$(printf '%s' "${context}" | tr '[:upper:]' '[:lower:]')" + + if [[ "${lower_path}" == *test* ]] || [[ "${lower_path}" == *spec* ]] || + [[ "${lower_context}" == *test* ]] || [[ "${lower_context}" == *coverage* ]] || + [[ "${lower_context}" == *regression* ]] || [[ "${lower_context}" == *fixture* ]] || + [[ "${lower_context}" == *assert* ]] || [[ "${lower_context}" == *flaky* ]]; then + printf 'test-gap\n' + return + fi + + if [[ "${lower_path}" == *.md ]] || [[ "${lower_path}" == *.yml ]] || [[ "${lower_path}" == *.yaml ]] || + [[ "${lower_path}" == *dockerfile* ]] || [[ "${lower_path}" == *config* ]] || + [[ "${lower_path}" == *install.sh ]] || [[ "${lower_path}" == .github/workflows/* ]] || + [[ "${lower_context}" == *docs* ]] || [[ "${lower_context}" == *readme* ]] || + [[ "${lower_context}" == *workflow* ]] || [[ "${lower_context}" == *deploy* ]] || + [[ "${lower_context}" == *config* ]] || [[ "${lower_context}" == *ci* ]]; then + printf 'docs-config-infra\n' + return + fi + + if [[ "${marker}" == "FIXME" ]] || [[ "${marker}" == "XXX" ]] || + [[ "${lower_context}" == *bug* ]] || [[ "${lower_context}" == *broken* ]] || + [[ "${lower_context}" == *error* ]] || [[ "${lower_context}" == *failure* ]] || + [[ "${lower_context}" == *failing* ]] || [[ "${lower_context}" == *crash* ]] || + [[ "${lower_context}" == *panic* ]] || [[ "${lower_context}" == *null* ]] || + [[ "${lower_context}" == *nil* ]] || [[ "${lower_context}" == *race* ]] || + [[ "${lower_context}" == *unsafe* ]]; then + printf 'bug-risk\n' + return + fi + + printf 'maintainability\n' +} + +calculate_score() { + local marker="$1" + local category="$2" + local file_path="$3" + local context="$4" + local score=0 + local lower_path lower_context + local severity_regex='critical|urgent|security|vuln|crash|panic|corrupt|deadlock|prod|production|data[[:space:]]loss' + local signal_regex='bug|broken|error|failure|failing|regression|workaround|temporary|refactor|cleanup|remove|leak|unsafe|null|nil|timeout|retry' + + case "${marker}" in + FIXME) score=70 ;; + XXX) score=65 ;; + HACK) score=55 ;; + TODO) score=45 ;; + esac + + case "${category}" in + bug-risk) score=$((score + 20)) ;; + test-gap) score=$((score + 15)) ;; + docs-config-infra) score=$((score + 10)) ;; + maintainability) score=$((score + 5)) ;; + esac + + lower_path="$(printf '%s' "${file_path}" | tr '[:upper:]' '[:lower:]')" + lower_context="$(printf '%s' "${context}" | tr '[:upper:]' '[:lower:]')" + + if [[ "${lower_context}" =~ ${severity_regex} ]]; then + score=$((score + 15)) + fi + + if [[ "${lower_context}" =~ ${signal_regex} ]]; then + score=$((score + 8)) + fi + + if [[ "${lower_path}" == src/* ]] || [[ "${lower_path}" == lib/* ]] || + [[ "${lower_path}" == app/* ]] || [[ "${lower_path}" == cmd/* ]] || + [[ "${lower_path}" == bin/* ]] || [[ "${lower_path}" == .github/workflows/* ]] || + [[ "${lower_path}" == .codex_yolo.sh ]] || [[ "${lower_path}" == .codex_yolo_entrypoint.sh ]] || + [[ "${lower_path}" == install.sh ]]; then + score=$((score + 5)) + fi + + if (( score > 100 )); then + score=100 + fi + + printf '%s\n' "${score}" +} + +priority_label() { + local score="$1" + + if (( score >= 85 )); then + printf 'critical\n' + elif (( score >= 70 )); then + printf 'high\n' + elif (( score >= 55 )); then + printf 'medium\n' + else + printf 'low\n' + fi +} + +if [[ "${1:-}" == "--help" ]] || [[ "${1:-}" == "-h" ]]; then + usage + exit 0 +fi + +if [[ "$#" -gt 1 ]]; then + echo "Error: expected zero or one path argument" >&2 + echo >&2 + usage >&2 + exit 1 +fi + +scan_root="$(resolve_scan_root "${1:-}")" +findings_file="$(mktemp)" +sorted_file="$(mktemp)" +trap 'rm -f "${findings_file}" "${sorted_file}"' EXIT + +( + cd "${scan_root}" + scan_matches +) | while IFS= read -r match; do + file_path="${match%%:*}" + remainder="${match#*:}" + line_number="${remainder%%:*}" + context="${remainder#*:}" + file_path="${file_path#./}" + context="$(normalize_context "${context}")" + marker="$(detect_marker "${context}")" + category="$(classify_category "${marker}" "${file_path}" "${context}")" + score="$(calculate_score "${marker}" "${category}" "${file_path}" "${context}")" + priority="$(priority_label "${score}")" + + printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ + "${score}" "${priority}" "${category}" "${marker}" "${file_path}" "${line_number}" "${context}" >> "${findings_file}" +done + +if [[ ! -s "${findings_file}" ]]; then + echo "No technical debt markers found in ${scan_root}." + exit 0 +fi + +LC_ALL=C sort -t $'\t' -k1,1nr -k5,5 -k6,6n "${findings_file}" > "${sorted_file}" + +total_findings="$(wc -l < "${sorted_file}" | tr -d ' ')" + +echo "codex_yolo debt report" +echo "Scope: ${scan_root}" +echo "Findings: ${total_findings}" +echo "Markers: TODO, FIXME, HACK, XXX" +echo "Priority score: marker severity + category + risk keywords" +echo "" +printf '%-9s %-5s %-20s %-7s %-28s %s\n' "Priority" "Score" "Category" "Marker" "Location" "Context" +printf '%-9s %-5s %-20s %-7s %-28s %s\n' "--------" "-----" "--------" "------" "--------" "-------" + +while IFS=$'\t' read -r score priority category marker file_path line_number context; do + printf '%-9s %-5s %-20s %-7s %-28s %s\n' \ + "${priority}" "${score}" "${category}" "${marker}" "${file_path}:${line_number}" "${context}" +done < "${sorted_file}" + +echo "" +echo "Category counts:" +for category in bug-risk maintainability test-gap docs-config-infra; do + count="$(awk -F '\t' -v wanted="${category}" '$3 == wanted { total += 1 } END { print total + 0 }' "${sorted_file}")" + if [[ "${count}" != "0" ]]; then + echo " ${category}: ${count}" + fi +done diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 71246ee..b293fcc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,6 +24,7 @@ jobs: bash -n .codex_yolo.sh bash -n .codex_yolo_entrypoint.sh bash -n .codex_yolo_diagnostics.sh + bash -n .codex_yolo_debt.sh bash -n install.sh - name: Run integration tests @@ -88,6 +89,7 @@ jobs: # Check that shell scripts are executable test -x .codex_yolo.sh || (echo ".codex_yolo.sh not executable" && exit 1) test -x .codex_yolo_diagnostics.sh || (echo ".codex_yolo_diagnostics.sh not executable" && exit 1) + test -x .codex_yolo_debt.sh || (echo ".codex_yolo_debt.sh not executable" && exit 1) test -x install.sh || (echo "install.sh not executable" && exit 1) test -x tests/integration_tests.sh || (echo "integration_tests.sh not executable" && exit 1) diff --git a/CHANGELOG.md b/CHANGELOG.md index 99289c3..5d53e61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,20 @@ All notable changes to codex_yolo will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Added +- **Technical Debt Report**: New `codex_yolo debt` host-side command for fast repository triage + - Scans `TODO`, `FIXME`, `HACK`, and `XXX` markers + - Classifies findings into bug-risk, maintainability, test-gap, and docs/config/infra buckets + - Assigns deterministic priority scores and sorts the report by severity + - Runs without building or starting the Docker container + +### Changed +- Installer and auto-update flows now download `.codex_yolo_debt.sh` +- Shell completions now expose `debt` as a first-class subcommand +- Integration tests and CI syntax checks now cover the debt classifier helper + ## [1.1.0] - 2026-01-31 ### Added - Product Perspective diff --git a/EXAMPLES.md b/EXAMPLES.md index 358b625..698081c 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -136,6 +136,27 @@ codex_yolo version codex_yolo --version ``` +### Technical Debt Triage +```bash +# Scan the current repository for technical debt markers +codex_yolo debt + +# Scan a different checkout or subdirectory +codex_yolo debt ../another-repo +``` + +The debt report scans for `TODO`, `FIXME`, `HACK`, and `XXX`, then classifies +each finding into one of four buckets: + +- `bug-risk` +- `maintainability` +- `test-gap` +- `docs-config-infra` + +Scores are deterministic and based on marker type, file path, and keywords in +the surrounding text. Treat the output as prioritized triage rather than a +complete architectural assessment. + ### Common Issues #### "Docker daemon not running" @@ -276,5 +297,6 @@ CODEX_BASE_IMAGE=node:18-slim codex_yolo - Run `codex_yolo --help` for Codex CLI help - Run `codex_yolo diagnostics` for system health check +- Run `codex_yolo debt` for a host-side technical debt report - Visit: https://github.com/laurenceputra/codex_yolo - Report issues: https://github.com/laurenceputra/codex_yolo/issues diff --git a/README.md b/README.md index 6574d8d..ce79672 100644 --- a/README.md +++ b/README.md @@ -22,11 +22,12 @@ Version 1.1.0 brings major improvements to usability, troubleshooting, and devel ### 🚀 Developer Experience - **Shell Completion**: Tab completion for bash and zsh (optional install) +- **Host-side Debt Triage**: Run `codex_yolo debt` to classify `TODO`/`FIXME`/`HACK`/`XXX` markers without building the Docker image - **Comprehensive Examples**: See `EXAMPLES.md` for common use cases and best practices - **Documentation**: Full changelog in `CHANGELOG.md` ### 🔧 Engineering Quality -- **Test Suite**: 14 integration tests ensure reliability +- **Test Suite**: Expanded integration coverage ensures reliability across host-side and Docker-backed workflows - **CI/CD Pipeline**: Automated testing on every change - **Better Code Organization**: Modular, maintainable codebase @@ -93,6 +94,30 @@ CODEX_VERBOSE=1 codex_yolo For more examples and use cases, see [EXAMPLES.md](EXAMPLES.md). +## Technical Debt Report + +Scan the current repository for common debt markers without building or running +the Docker container: + +```bash +codex_yolo debt +``` + +You can also point the report at another directory: + +```bash +codex_yolo debt path/to/repo +``` + +The report scans for `TODO`, `FIXME`, `HACK`, and `XXX`, classifies each +finding as `bug-risk`, `maintainability`, `test-gap`, or +`docs-config-infra`, assigns a deterministic priority score, and prints the +results in descending priority order with file, line, and context. + +This command runs entirely on the host, like `codex_yolo diagnostics`, so it +works even before Docker is installed. The scoring is intentionally heuristic: +use it as a fast triage report, not as a substitute for code review. + ## Login The first run will prompt you to sign in. You can also log in explicitly: diff --git a/TECHNICAL.md b/TECHNICAL.md index ff8901b..10886b1 100644 --- a/TECHNICAL.md +++ b/TECHNICAL.md @@ -20,6 +20,11 @@ codex_yolo is a bash wrapper that runs OpenAI's Codex CLI in an isolated Docker - Configuration verification - Actionable recommendations +**Debt Classifier** (`.codex_yolo_debt.sh`) +- Host-side repository scanning for debt markers +- Deterministic classification and scoring +- Prioritized reporting without requiring Docker + **Installation** (`install.sh`) - Platform detection (macOS, Linux, WSL) - Shell profile integration @@ -69,6 +74,7 @@ Core files update automatically on every run (unless disabled): - `.codex_yolo.Dockerfile` - `.codex_yolo_entrypoint.sh` - `.codex_yolo_diagnostics.sh` +- `.codex_yolo_debt.sh` - `VERSION` Optional files downloaded separately: @@ -87,6 +93,7 @@ Optional files downloaded separately: .codex_yolo.Dockerfile # Container image definition .codex_yolo_entrypoint.sh # Container initialization .codex_yolo_diagnostics.sh # Health check system +.codex_yolo_debt.sh # Host-side technical debt report .codex_yolo_completion.bash # Bash tab completion .codex_yolo_completion.zsh # Zsh tab completion .codex_yolo.conf.example # Configuration template @@ -116,6 +123,17 @@ if [[ "${remote_version}" != "${local_version}" ]]; then fi ``` +**Debt Classifier Heuristics** - Lightweight repository triage +```bash +# Classify by marker, path, and keywords, then sort by score +codex_yolo debt +``` + +The classifier intentionally uses deterministic Bash heuristics instead of a +parser or LLM. This keeps the command fast, portable, and safe to run on the +host, but it also means the report is best used for prioritization rather than +as a definitive audit. + ## Testing Strategy ### Integration Tests @@ -133,13 +151,16 @@ Location: `tests/integration_tests.sh` 8. Configuration files 9. Config file loading 10. Config priority (3 locations) +11. Host-side technical debt classification +12. Clean repository debt scans +13. Docker-free debt dispatch **Running Tests**: ```bash ./tests/integration_tests.sh ``` -Expected output: All tests pass (14/14) +Expected output: all assertions pass, with Docker-specific checks skipped only when Docker is unavailable. ### Manual Testing Checklist @@ -206,11 +227,11 @@ Current: v1.1.0 ### Adding New Features 1. **Update main script** (`.codex_yolo.sh`) - - Add feature implementation - - Update help text if applicable - - Maintain backward compatibility + - Add feature implementation + - Update help text if applicable + - Maintain backward compatibility -2. **Add tests** (`tests/integration_tests.sh`) + 2. **Add tests** (`tests/integration_tests.sh`) - Create new test case - Verify all tests pass - Update test count in summary @@ -425,7 +446,7 @@ Container enables passwordless sudo: - Improved code organization - Integration test infrastructure -**Impact**: +1,800 lines of code, +186% files, 14 automated tests +**Impact**: +1,800 lines of code, +186% files, automated integration coverage across host and container workflows ### v1.0.2 (Previous) diff --git a/install.sh b/install.sh index 681dbf3..9a7535a 100755 --- a/install.sh +++ b/install.sh @@ -122,6 +122,7 @@ curl -fsSL "${raw_base}/.codex_yolo.sh" -o "${INSTALL_DIR}/.codex_yolo.sh" curl -fsSL "${raw_base}/.codex_yolo.Dockerfile" -o "${INSTALL_DIR}/.codex_yolo.Dockerfile" curl -fsSL "${raw_base}/.codex_yolo_entrypoint.sh" -o "${INSTALL_DIR}/.codex_yolo_entrypoint.sh" curl -fsSL "${raw_base}/.codex_yolo_diagnostics.sh" -o "${INSTALL_DIR}/.codex_yolo_diagnostics.sh" +curl -fsSL "${raw_base}/.codex_yolo_debt.sh" -o "${INSTALL_DIR}/.codex_yolo_debt.sh" curl -fsSL "${raw_base}/default-AGENTS.md" -o "${INSTALL_DIR}/default-AGENTS.md" curl -fsSL "${raw_base}/.dockerignore" -o "${INSTALL_DIR}/.dockerignore" 2>/dev/null || true curl -fsSL "${raw_base}/VERSION" -o "${INSTALL_DIR}/VERSION" @@ -132,6 +133,7 @@ curl -fsSL "${raw_base}/EXAMPLES.md" -o "${INSTALL_DIR}/EXAMPLES.md" 2>/dev/null chmod +x "${INSTALL_DIR}/.codex_yolo.sh" chmod +x "${INSTALL_DIR}/.codex_yolo_diagnostics.sh" +chmod +x "${INSTALL_DIR}/.codex_yolo_debt.sh" cat > "${INSTALL_DIR}/env" </dev/null && bash -n "${DIAGNOSTICS_SH}" 2>/dev/null; then +if bash -n "${CODEX_YOLO_SH}" 2>/dev/null && bash -n "${DIAGNOSTICS_SH}" 2>/dev/null && bash -n "${DEBT_SH}" 2>/dev/null; then log_pass "No syntax errors" else log_fail "Syntax errors found" fi -# Test 5: Version command +# Test 6: Version command log_test "Version command" if [[ -f "${SCRIPT_DIR}/../VERSION" ]]; then version_output=$("${CODEX_YOLO_SH}" version 2>&1 || true) @@ -87,7 +96,7 @@ else log_skip "VERSION file not found" fi -# Test 6: --version flag +# Test 7: --version flag log_test "--version flag" version_output=$("${CODEX_YOLO_SH}" --version 2>&1 || true) if echo "${version_output}" | grep -q "version"; then @@ -96,7 +105,7 @@ else log_fail "--version flag didn't work as expected" fi -# Test 7: Diagnostics command (requires Docker) +# Test 8: Diagnostics command (requires Docker) log_test "Diagnostics command" if command -v docker >/dev/null 2>&1; then diag_output=$("${CODEX_YOLO_SH}" diagnostics 2>&1 || true) @@ -109,7 +118,7 @@ else log_skip "Docker not available, skipping diagnostics test" fi -# Test 8: Doctor alias +# Test 9: Doctor alias log_test "Doctor alias for diagnostics" if command -v docker >/dev/null 2>&1; then doctor_output=$("${CODEX_YOLO_SH}" doctor 2>&1 || true) @@ -122,7 +131,7 @@ else log_skip "Docker not available, skipping doctor test" fi -# Test 9: Dry run mode +# Test 10: Dry run mode log_test "Dry run mode" if command -v docker >/dev/null 2>&1; then export CODEX_DRY_RUN=1 @@ -140,7 +149,7 @@ else log_skip "Docker not available, skipping dry run test" fi -# Test 10: Completion files exist +# Test 11: Completion files exist log_test "Completion files exist" bash_completion="${SCRIPT_DIR}/../.codex_yolo_completion.bash" zsh_completion="${SCRIPT_DIR}/../.codex_yolo_completion.zsh" @@ -151,7 +160,7 @@ else log_fail "Missing completion files" fi -# Test 11: Example config file exists +# Test 12: Example configuration file exists log_test "Example configuration file exists" example_config="${SCRIPT_DIR}/../.codex_yolo.conf.example" if [[ -f "${example_config}" ]]; then @@ -160,7 +169,7 @@ else log_fail "Example config not found" fi -# Test 12: Examples documentation exists +# Test 13: Examples documentation exists log_test "Examples documentation exists" examples_doc="${SCRIPT_DIR}/../EXAMPLES.md" if [[ -f "${examples_doc}" ]]; then @@ -169,7 +178,7 @@ else log_fail "EXAMPLES.md not found" fi -# Test 13: Default AGENTS template exists +# Test 14: Default AGENTS template exists log_test "Default AGENTS template exists" default_agents_template="${SCRIPT_DIR}/../default-AGENTS.md" if [[ -f "${default_agents_template}" ]]; then @@ -178,18 +187,18 @@ else log_fail "default-AGENTS.md not found" fi -# Test 14: Config file loading +# Test 15: Config file loading log_test "Config file loading" test_config="/tmp/test_codex_yolo_config" test_script=$(mktemp) test_home=$(mktemp -d) # Setup cleanup trap -cleanup_test_13() { +cleanup_test_15() { rm -f "${test_script}" "${test_config}" rm -rf "${test_home}" } -trap cleanup_test_13 EXIT +trap cleanup_test_15 EXIT cat > "${test_config}" <&1) -cleanup_test_13 +cleanup_test_15 trap - EXIT if echo "${output}" | grep -q "CODEX_VERBOSE=1" && echo "${output}" | grep -q "node:18-slim"; then @@ -224,16 +233,16 @@ else log_info "Output: ${output}" fi -# Test 15: Config priority and install dir support +# Test 16: Config priority and install dir support log_test "Config file priority (install dir config < ~/.codex_yolo/config)" test_script_dir=$(mktemp -d) test_home=$(mktemp -d) test_script=$(mktemp) -cleanup_test_14() { +cleanup_test_16() { rm -rf "${test_script_dir}" "${test_home}" "${test_script}" } -trap cleanup_test_14 EXIT +trap cleanup_test_16 EXIT # Create test configs with different values echo 'TEST_VAR=from_install_dir' > "${test_script_dir}/config" @@ -257,7 +266,7 @@ TESTEOF chmod +x "${test_script}" output=$("${test_script}" 2>&1) -cleanup_test_14 +cleanup_test_16 trap - EXIT if echo "${output}" | grep -q "TEST_VAR=from_config_dir"; then @@ -267,16 +276,16 @@ else log_info "Output: ${output}" fi -# Test 16: SSH mounting with --mount-ssh flag +# Test 17: SSH mounting with --mount-ssh flag log_test "SSH mounting with --mount-ssh flag" if command -v docker >/dev/null 2>&1 && docker info >/dev/null 2>&1; then # Create a temporary home directory for testing test_home=$(mktemp -d) - cleanup_test_15() { + cleanup_test_17() { rm -rf "${test_home}" } - trap cleanup_test_15 EXIT + trap cleanup_test_17 EXIT # Create fake .ssh directory mkdir -p "${test_home}/.ssh" @@ -294,7 +303,7 @@ if command -v docker >/dev/null 2>&1 && docker info >/dev/null 2>&1; then unset CODEX_DRY_RUN unset CODEX_SKIP_UPDATE_CHECK - cleanup_test_15 + cleanup_test_17 trap - EXIT # Check if the output includes SSH mount and warning @@ -308,7 +317,7 @@ else log_skip "Docker not available, skipping --mount-ssh flag test" fi -# Test 17: Wrapper version metadata is embedded in Dockerfile +# Test 18: Wrapper version metadata is embedded in Dockerfile log_test "Dockerfile embeds wrapper version metadata" dockerfile="${SCRIPT_DIR}/../.codex_yolo.Dockerfile" if grep -q 'ARG CODEX_YOLO_WRAPPER_VERSION=' "${dockerfile}" && \ @@ -318,7 +327,7 @@ else log_fail "Dockerfile missing wrapper version metadata support" fi -# Test 18: Main script rebuild logic includes wrapper version mismatch checks +# Test 19: Main script rebuild logic includes wrapper version mismatch checks log_test "Main script rebuilds when wrapper VERSION changes" if grep -q 'CODEX_YOLO_WRAPPER_VERSION=' "${CODEX_YOLO_SH}" && \ grep -q '/opt/codex-yolo-version' "${CODEX_YOLO_SH}" && \ @@ -328,7 +337,7 @@ else log_fail "Wrapper version mismatch rebuild logic missing" fi -# Test 19: Dockerfile includes rg and gh packages +# Test 20: Dockerfile installs rg and gh log_test "Dockerfile installs rg and gh" if grep -q 'gh' "${dockerfile}" && grep -q 'ripgrep' "${dockerfile}"; then log_pass "Dockerfile includes gh and ripgrep packages" @@ -336,7 +345,7 @@ else log_fail "Dockerfile missing gh and/or ripgrep package install" fi -# Test 20: --gh mounting in dry run mode +# Test 21: --gh mounting in dry run mode log_test "GitHub mount with --gh flag" if command -v docker >/dev/null 2>&1 && docker info >/dev/null 2>&1; then test_home=$(mktemp -d) @@ -344,7 +353,7 @@ if command -v docker >/dev/null 2>&1 && docker info >/dev/null 2>&1; then original_home="${HOME}" original_path="${PATH}" - cleanup_test_20() { + cleanup_test_21() { rm -rf "${test_home}" "${fake_bin}" export HOME="${original_home}" export PATH="${original_path}" @@ -352,7 +361,7 @@ if command -v docker >/dev/null 2>&1 && docker info >/dev/null 2>&1; then unset CODEX_SKIP_UPDATE_CHECK unset CODEX_SKIP_VERSION_CHECK } - trap cleanup_test_20 EXIT + trap cleanup_test_21 EXIT mkdir -p "${test_home}/.copilot" "${test_home}/.codex" "${test_home}/.config/gh" cat > "${fake_bin}/gh" <<'TESTEOF' @@ -371,7 +380,7 @@ TESTEOF export CODEX_SKIP_VERSION_CHECK=1 output=$("${CODEX_YOLO_SH}" --gh 2>&1 || true) - cleanup_test_20 + cleanup_test_21 trap - EXIT if echo "${output}" | grep -q "\.copilot" && echo "${output}" | grep -q "\.config/gh" && echo "${output}" | grep -q "Dry run"; then @@ -384,6 +393,98 @@ else log_skip "Docker not available, skipping --gh flag test" fi +# Test 22: Debt command classifies findings and sorts by score +log_test "Debt command classifies findings and sorts by priority" +debt_repo=$(mktemp -d) +mkdir -p "${debt_repo}/lib" "${debt_repo}/src" "${debt_repo}/tests" "${debt_repo}/.github/workflows" +printf '%s\n' '# F'"IXME: crash when cache is empty" > "${debt_repo}/lib/runtime.sh" +printf '%s\n' '# H'"ACK: temporary deploy workaround for stale secret sync" > "${debt_repo}/.github/workflows/deploy.yml" +printf '%s\n' '# T'"ODO: add regression coverage for timeout handling" > "${debt_repo}/tests/auth_test.sh" +printf '%s\n' '# T'"ODO: rename legacy helper after refactor" > "${debt_repo}/src/legacy.sh" + +set +e +debt_output="$(cd "${debt_repo}" && CODEX_SKIP_UPDATE_CHECK=1 "${CODEX_YOLO_SH}" debt 2>&1)" +debt_status=$? +set -e + +score_lines="$(printf '%s\n' "${debt_output}" | awk '/^(critical|high|medium|low)[[:space:]]+[0-9]+/ {print $0}')" +score_values="$(printf '%s\n' "${score_lines}" | awk '{print $2}')" +ordered=1 +score_count=0 +previous_score=101 +while IFS= read -r score; do + [[ -z "${score}" ]] && continue + score_count=$((score_count + 1)) + if (( score > previous_score )); then + ordered=0 + break + fi + previous_score="${score}" +done <<< "${score_values}" + +first_finding="$(printf '%s\n' "${score_lines}" | sed -n '1p')" +last_finding="$(printf '%s\n' "${score_lines}" | sed -n '$p')" +rm -rf "${debt_repo}" + +if [[ ${debt_status} -eq 0 ]] && [[ ${score_count} -eq 4 ]] && [[ ${ordered} -eq 1 ]] && \ + echo "${debt_output}" | grep -q 'bug-risk' && \ + echo "${debt_output}" | grep -q 'docs-config-infra' && \ + echo "${debt_output}" | grep -q 'test-gap' && \ + echo "${debt_output}" | grep -q 'maintainability' && \ + echo "${first_finding}" | grep -q 'lib/runtime.sh:1' && \ + echo "${first_finding}" | grep -q 'bug-risk' && \ + echo "${last_finding}" | grep -q 'maintainability'; then + log_pass "Debt command reports classified findings in descending priority order" +else + log_fail "Debt command did not produce the expected prioritized report" + log_info "Output: ${debt_output}" +fi + +# Test 23: Debt command handles clean repositories +log_test "Debt command handles repositories with no markers" +empty_repo=$(mktemp -d) +mkdir -p "${empty_repo}/src" +printf '%s\n' 'echo \"clean repo\"' > "${empty_repo}/src/clean.sh" + +set +e +empty_output="$(cd "${empty_repo}" && CODEX_SKIP_UPDATE_CHECK=1 "${CODEX_YOLO_SH}" debt 2>&1)" +empty_status=$? +set -e +rm -rf "${empty_repo}" + +if [[ ${empty_status} -eq 0 ]] && echo "${empty_output}" | grep -q 'No technical debt markers found'; then + log_pass "Debt command reports clean repositories without failing" +else + log_fail "Debt command did not handle a clean repository as expected" + log_info "Output: ${empty_output}" +fi + +# Test 24: Debt command does not require Docker dispatch +log_test "Debt command runs without invoking Docker" +dockerless_repo=$(mktemp -d) +fake_bin=$(mktemp -d) +mkdir -p "${dockerless_repo}/lib" +printf '%s\n' '# X'"XX: broken fallback path in parser" > "${dockerless_repo}/lib/parser.sh" +cat > "${fake_bin}/docker" <<'EOF' +#!/usr/bin/env bash +echo "docker should not run" >&2 +exit 99 +EOF +chmod +x "${fake_bin}/docker" + +set +e +dockerless_output="$(cd "${dockerless_repo}" && PATH="${fake_bin}:${PATH}" CODEX_SKIP_UPDATE_CHECK=1 "${CODEX_YOLO_SH}" debt 2>&1)" +dockerless_status=$? +set -e +rm -rf "${dockerless_repo}" "${fake_bin}" + +if [[ ${dockerless_status} -eq 0 ]] && ! echo "${dockerless_output}" | grep -q 'docker should not run'; then + log_pass "Debt command stays host-side and skips Docker checks" +else + log_fail "Debt command unexpectedly invoked Docker logic" + log_info "Output: ${dockerless_output}" +fi + # Summary echo "" echo "=== Test Summary ==="