diff options
| author | caner-akca <94343893+caner-akca@users.noreply.github.com> | 2026-09-07 02:27:04 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-09-07 02:27:04 +0300 |
| commit | 4b5e9bda239a0b6903889062d756424578e94691 (patch) | |
| tree | 95f44b7e4b675683edcc9a093e048ec1c10d656e /src/detect/manifest | |
| parent | c7b79294e28fe7c835691a25597fabf226ecfc20 (diff) | |
fix: match claude bash permission prompts at every cursor position (#3615)
Claude marks the selected option with "❯", but bash_permission_prompt did not allow that prefix on numbered branches and did not cover the three-option "don't ask again" layout. Match both two- and three-option Bash approval prompts regardless of the selected option.
refs #2650
Diffstat (limited to 'src/detect/manifest')
| -rw-r--r-- | src/detect/manifest/tests.rs | 102 |
1 files changed, 102 insertions, 0 deletions
diff --git a/src/detect/manifest/tests.rs b/src/detect/manifest/tests.rs index 00730a44..e1073f3c 100644 --- a/src/detect/manifest/tests.rs +++ b/src/detect/manifest/tests.rs @@ -739,6 +739,108 @@ fn claude_blocker_with_background_shell_remains_blocked() { } #[test] +fn claude_bash_prompt_with_dont_ask_again_option_matches_bash_rule() { + // Captured from a Bash approval prompt at its resting cursor position. The + // "don't ask again" choice pushes No to option 3, so the only cursor-free + // option line is one bash_permission_prompt used not to cover, which let + // the narrower generic_permission_prompt claim the prompt instead (#2650). + let screen = concat!( + "──────────────────────────────────────────────────────────────── +", + " Bash command + +", + " curl -sS -o /tmp/probe.html https://example.com +", + " Download example.com to /tmp/probe.html + +", + " This command requires approval + +", + " Do you want to proceed? +", + " ❯ 1. Yes +", + " 2. Yes, and don't ask again for: curl * +", + " 3. No + +", + " Esc to cancel · Tab to amend · ctrl+e to explain +", + ); + let result = osc_explain(Agent::Claude, screen, "", ""); + + assert_eq!(result.state, AgentState::Blocked); + assert_eq!( + result.matched_rule.as_ref().map(|rule| rule.id.as_str()), + Some("bash_permission_prompt") + ); + assert!(result.visible_blocker); +} + +#[test] +fn claude_permission_prompt_matches_at_every_cursor_position() { + // The selected option carries "❯", so no option branch may assume its line + // is cursor-free. Walk the cursor across both option layouts. + let layouts: [&[&str]; 2] = [ + &[" ❯ 1. Yes", " 2. No"], + &[ + " ❯ 1. Yes", + " 2. Yes, and don't ask again for: curl *", + " 3. No", + ], + ]; + + for layout in layouts { + for selected in 0..layout.len() { + let options: Vec<String> = layout + .iter() + .enumerate() + .map(|(index, line)| { + let bare = line.trim_start().trim_start_matches('❯').trim_start(); + if index == selected { + format!(" ❯ {bare}") + } else { + format!(" {bare}") + } + }) + .collect(); + let screen = format!( + concat!( + "──────────────────────────────────────────────────────────────── +", + " Bash command + +", + " curl -sS https://example.com + +", + " Do you want to proceed? +", + "{} + +", + " Esc to cancel · Tab to amend · ctrl+e to explain +", + ), + options.join("\n"), + ); + let result = osc_explain(Agent::Claude, &screen, "", ""); + + assert_eq!( + result.matched_rule.as_ref().map(|rule| rule.id.as_str()), + Some("bash_permission_prompt"), + "{options:?} selected={selected}" + ); + assert_eq!(result.state, AgentState::Blocked, "selected={selected}"); + assert!(result.visible_blocker, "selected={selected}"); + } + } +} + +#[test] fn claude_osc_title_braille_prefix_is_working() { // "⠂" is U+2802, in the braille block U+2800-U+28FF let result = osc_explain(Agent::Claude, "", "⠂ project", ""); |
