From fe707e15b6f3a521c99defd0e125663f8460cef1 Mon Sep 17 00:00:00 2001 From: Chris McClellan Date: Tue, 14 Apr 2026 13:14:38 -0400 Subject: [PATCH 1/4] feat: retry intercept script when known-error fix succeeds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When scope-intercept wraps a failing command, it now prompts the user to run any available known-error fixes (the same fix infrastructure used by scope doctor), and if a fix succeeds, automatically retries the entire original command. Previously, known errors were detected and help text was shown, but fixes were never run and the command was never retried — the self-healing behaviour only existed in scope doctor. Closes LDE-463 Co-Authored-By: Claude Sonnet 4.6 --- src/bin/scope-intercept.rs | 57 +++++++++++++++++++++++++++++--------- src/shared/mod.rs | 2 +- 2 files changed, 45 insertions(+), 14 deletions(-) diff --git a/src/bin/scope-intercept.rs b/src/bin/scope-intercept.rs index 2fb16fb..e9f3257 100644 --- a/src/bin/scope-intercept.rs +++ b/src/bin/scope-intercept.rs @@ -1,9 +1,13 @@ use clap::Parser; use dev_scope::prelude::*; +use dev_scope::shared::analyze; +use dev_scope::shared::analyze::AnalyzeStatus; use human_panic::setup_panic; use std::env; +use std::io::Cursor; use std::sync::Arc; -use tracing::{Level, debug, enabled, error, info, warn}; +use tokio::io::BufReader; +use tracing::{Level, enabled, error, info, warn}; /// A wrapper CLI that can be used to capture output from a program, check if there are known errors /// and let the user know. @@ -90,20 +94,47 @@ async fn run_command(opts: Cli) -> anyhow::Result { FoundConfig::empty(env::current_dir().unwrap()) }); - let command_output = capture.generate_output(); + let analyze_status = analyze::process_lines( + &found_config.known_error, + &found_config.working_dir, + BufReader::new(Cursor::new(capture.generate_user_output())), + ) + .await?; - for known_error in found_config.known_error.values() { - debug!("Checking known error {}", known_error.name()); - if known_error.regex.is_match(&command_output) { - info!(target: "always", "Known error '{}' found", known_error.name()); - info!(target: "always", "\t==> {}", known_error.help_text); + analyze::report_result(&analyze_status); + + let (capture, exit_code) = if matches!(analyze_status, AnalyzeStatus::KnownErrorFoundFixSucceeded) { + info!(target: "always", "Fix succeeded, retrying command"); + let retry_capture = OutputCapture::capture_output(CaptureOpts { + working_dir: ¤t_dir, + args: &command, + output_dest: OutputDisplay::Visible, + path: &path, + env_vars: Default::default(), + }) + .await?; + + let retry_exit_code = retry_capture.exit_code.unwrap_or(-1); + if accepted_exit_codes.contains(&retry_exit_code) { + return Ok(retry_exit_code); } - } - if found_config.report_upload.is_empty() { - return Ok(exit_code); + (retry_capture, retry_exit_code) + } else { + (capture, exit_code) + }; + + if !found_config.report_upload.is_empty() { + offer_bug_report(&found_config, &command, &capture).await?; } + Ok(exit_code) +} +async fn offer_bug_report( + found_config: &FoundConfig, + command: &[String], + capture: &OutputCapture, +) -> anyhow::Result<()> { let ans = inquire::Confirm::new("Do you want to upload a bug report?") .with_default(false) .with_help_message( @@ -115,13 +146,13 @@ async fn run_command(opts: Cli) -> anyhow::Result { let entrypoint = command.join(" "); let exec_runner = Arc::new(DefaultExecutionProvider::default()); - let builder = DefaultUnstructuredReportBuilder::new(&entrypoint, &capture); + let builder = DefaultUnstructuredReportBuilder::new(&entrypoint, capture); for location in found_config.report_upload.values() { let mut builder = builder.clone(); builder .run_and_append_additional_data( - &found_config, + found_config, exec_runner.clone(), &location.additional_data, ) @@ -140,5 +171,5 @@ async fn run_command(opts: Cli) -> anyhow::Result { } } } - Ok(exit_code) + Ok(()) } diff --git a/src/shared/mod.rs b/src/shared/mod.rs index 3769ee6..ef6cb76 100644 --- a/src/shared/mod.rs +++ b/src/shared/mod.rs @@ -9,7 +9,7 @@ mod capture; mod config_load; mod logging; -pub(crate) mod analyze; +pub mod analyze; pub mod directories; mod models; mod redact; From 00cdc757dd828792d402cd650ae01151b20dcedc Mon Sep 17 00:00:00 2001 From: Chris McClellan Date: Tue, 14 Apr 2026 14:10:45 -0400 Subject: [PATCH 2/4] test: add --yolo flag, E2E and unit tests for intercept retry and doctor known-error fix - Add --yolo / -y flag to scope-intercept to auto-approve fix prompts (required for E2E tests since assert_cmd pipes stdin, no TTY) - Thread yolo through process_lines and prompt_and_run_fix in shared::analyze so callers control whether to prompt or auto-approve - Thread yolo from DoctorRunArgs through DefaultDoctorActionRun to analyze_known_errors, fixing a gap where scope doctor --yolo did not auto-approve known-error fix prompts triggered during check analysis - Add comment to scope analyze explaining why yolo is not applicable there - Add 7 E2E integration tests for scope-intercept (fix succeeds/retries, fix retried but still fails, no fix available, no TTY denial, succeeds first try, no match with exit code preserved, shebang script path) - Add 4 unit tests for DefaultDoctorActionRun::analyze_known_errors (yolo auto-approves, no-TTY denied, no match, match without fix) - Add test fixtures for intercept and doctor known-error scenarios Co-Authored-By: Claude Sonnet 4.6 --- MANUAL_TEST_PLAN.md | 293 ++++++++++++++++++ src/analyze/cli.rs | 5 + src/bin/scope-intercept.rs | 6 + src/doctor/check.rs | 132 +++++++- src/doctor/commands/run.rs | 1 + src/shared/analyze/mod.rs | 90 +++--- tests/common/mod.rs | 16 + tests/scope_intercept.rs | 155 +++++++++ .../.scope/doctor-group.yaml | 14 + .../.scope/known-error.yaml | 13 + .../.scope/known-error.yaml | 13 + .../.scope/known-error.yaml | 8 + .../.scope/known-error.yaml | 13 + 13 files changed, 718 insertions(+), 41 deletions(-) create mode 100644 MANUAL_TEST_PLAN.md create mode 100644 tests/scope_intercept.rs create mode 100644 tests/test-cases/doctor-known-error-with-fix/.scope/doctor-group.yaml create mode 100644 tests/test-cases/doctor-known-error-with-fix/.scope/known-error.yaml create mode 100644 tests/test-cases/intercept-known-error-fix-retry-fails/.scope/known-error.yaml create mode 100644 tests/test-cases/intercept-known-error-no-fix/.scope/known-error.yaml create mode 100644 tests/test-cases/intercept-known-error-with-fix/.scope/known-error.yaml diff --git a/MANUAL_TEST_PLAN.md b/MANUAL_TEST_PLAN.md new file mode 100644 index 0000000..2f28619 --- /dev/null +++ b/MANUAL_TEST_PLAN.md @@ -0,0 +1,293 @@ +# Manual Test Plan: scope-intercept retry on fix (LDE-463) + +## Setup + +```bash +cargo build --bin scope-intercept + +mkdir -p /tmp/test-intercept-retry/.scope +cd /tmp/test-intercept-retry + +INTERCEPT=/path/to/target/debug/scope-intercept +``` + +Note: use `--` to separate scope-intercept flags from commands that have their own flags +(e.g. `scope-intercept --extra-config ... -- bash -c '...'`). + +--- + +## Test 1: Fix succeeds, retry succeeds (happy path) + +**Setup:** +```bash +cat > .scope/known-error.yaml << 'EOF' +apiVersion: scope.github.com/v1alpha +kind: ScopeKnownError +metadata: + name: missing-ready-file + description: Detects when ready.txt is missing and creates it +spec: + pattern: "ready.txt: No such file" + help: The ready.txt file is missing. + fix: + prompt: + text: "Create the ready.txt file?" + commands: + - bash -c 'echo "ready" > ready.txt' +EOF + +rm -f ready.txt +``` + +**Run** (requires a TTY for the fix prompt): +```bash +$INTERCEPT --extra-config /tmp/test-intercept-retry/.scope cat ready.txt +# When prompted "Create the ready.txt file?" → answer y +``` + +**Output:** +``` +cat: ready.txt: No such file or directory +ERROR Command failed, checking for a known error + WARN Known error 'missing-ready-file' found on line 0 + INFO ==> The ready.txt file is missing. + INFO found a fix! +? Create the ready.txt file? (y/N) y + INFO All known errors detected, ignoring rest of output. + INFO Fix succeeded + INFO Fix succeeded, retrying command +ready +EXIT_CODE=0 +``` + +**Result: PASS** — fix runs, command retried, prints "ready", exit 0. + +--- + +## Test 2: Fix succeeds, retry still fails (one retry only) + +**Setup:** +```bash +cat > check.sh << 'SCRIPT' +#!/bin/bash +set -e +cat ready.txt +cat other.txt +SCRIPT +chmod +x check.sh +rm -f ready.txt other.txt +``` + +**Run:** +```bash +$INTERCEPT --extra-config /tmp/test-intercept-retry/.scope bash check.sh +# When prompted → answer y +``` + +**Output:** +``` +cat: ready.txt: No such file or directory +ERROR Command failed, checking for a known error + WARN Known error 'missing-ready-file' found on line 0 + INFO ==> The ready.txt file is missing. + INFO found a fix! +? Create the ready.txt file? (y/N) y + INFO All known errors detected, ignoring rest of output. + INFO Fix succeeded + INFO Fix succeeded, retrying command +ready +cat: other.txt: No such file or directory +EXIT_CODE=1 +``` + +**Result: PASS** — fix runs, retry executes (prints "ready"), fails on `other.txt`, no second analysis, exit 1. + +--- + +## Test 3: Known error found, no fix available + +**Setup:** +```bash +cat > .scope/known-error.yaml << 'EOF' +apiVersion: scope.github.com/v1alpha +kind: ScopeKnownError +metadata: + name: something-broke + description: A known error with no automatic fix +spec: + pattern: "something went wrong" + help: "This is a known issue. Check the wiki for manual steps." +EOF +``` + +**Run:** +```bash +SCOPE_DISABLE_DEFAULT_CONFIG=true $INTERCEPT --extra-config /tmp/test-intercept-retry/.scope -- bash -c 'echo "something went wrong"; exit 1' +``` + +**Output:** +``` +something went wrong +ERROR Command failed, checking for a known error + WARN Known error 'something-broke' found on line 0 + INFO ==> This is a known issue. Check the wiki for manual steps. + INFO All known errors detected, ignoring rest of output. + INFO No automatic fix available +EXIT_CODE=1 +``` + +**Result: PASS** — known error and help text shown, "No automatic fix available", no prompt, no retry, exit 1. + +--- + +## Test 4: User denies the fix + +(Restore fix-enabled config from Test 1, `rm -f ready.txt` first) + +**Run:** +```bash +$INTERCEPT --extra-config /tmp/test-intercept-retry/.scope cat ready.txt +# When prompted "Create the ready.txt file?" → answer n +``` + +**Output:** +``` +cat: ready.txt: No such file or directory +ERROR Command failed, checking for a known error + WARN Known error 'missing-ready-file' found on line 0 + INFO ==> The ready.txt file is missing. + INFO found a fix! +? Create the ready.txt file? (y/N) n + INFO All known errors detected, ignoring rest of output. + WARN User denied fix +EXIT_CODE=1 +``` + +**Result: PASS** — user says No, "User denied fix", no retry, exit 1. + +--- + +## Test 5: Command succeeds on first try + +**Setup:** `echo "ready" > ready.txt` + +**Run:** +```bash +SCOPE_DISABLE_DEFAULT_CONFIG=true $INTERCEPT --extra-config /tmp/test-intercept-retry/.scope cat ready.txt +``` + +**Output:** +``` +ready +EXIT_CODE=0 +``` + +**Result: PASS** — no error messages, no analysis, exit 0. + +--- + +## Test 6: Command fails, no known errors match + +**Run:** +```bash +SCOPE_DISABLE_DEFAULT_CONFIG=true $INTERCEPT --extra-config /tmp/test-intercept-retry/.scope -- bash -c 'echo "totally unexpected failure"; exit 42' +``` + +**Output:** +``` +totally unexpected failure +ERROR Command failed, checking for a known error + INFO No known errors found +EXIT_CODE=42 +``` + +**Result: PASS** — "No known errors found", no retry, original exit code 42 preserved. + +--- + +## Test 7: Shebang usage (primary intended use case) + +scope-intercept is designed to be used as a script's shebang interpreter. The kernel passes the script path as an argument, so `scope-intercept bash` in the shebang causes it to run `bash