Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions crates/bashkit/src/scripted_tool/execute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,41 @@ fn push_invocation(
/// Parses `--key value` flags from `ctx.args` using the schema for type coercion.
struct ToolBuiltinAdapter {
name: String,
description: String,
callback: CallbackKind,
schema: serde_json::Value,
log: InvocationLog,
sanitize_errors: bool,
}

impl ToolBuiltinAdapter {
/// Generate help text from the tool's schema, identical to `help <tool>`.
fn help_text(&self) -> String {
let mut out = format!("{} - {}\n", self.name, self.description);
if let Some(usage) = usage_from_schema(&self.schema) {
out.push_str(&format!("Usage: {} {}\n", self.name, usage));
}
out
}
}

#[async_trait]
impl Builtin for ToolBuiltinAdapter {
async fn execute(&self, ctx: Context<'_>) -> Result<ExecResult> {
// Intercept --help before calling the callback — return the same
// quality output as `help <tool>` without invoking the callback.
if ctx.args.iter().any(|a| a == "--help") {
let result = ExecResult::ok(self.help_text());
push_invocation(
&self.log,
&self.name,
ScriptedCommandKind::Help,
ctx.args,
result.exit_code,
);
return Ok(result);
}

let exit_result = match parse_flags(ctx.args, &self.schema) {
Ok(params) => {
let tool_args = ToolArgs {
Expand Down Expand Up @@ -345,6 +371,7 @@ impl ScriptedTool {
let name = tool.def.name.clone();
let builtin: Box<dyn Builtin> = Box::new(ToolBuiltinAdapter {
name: name.clone(),
description: tool.def.description.clone(),
callback: tool.callback.clone(),
schema: tool.def.input_schema.clone(),
log: Arc::clone(&log),
Expand Down
86 changes: 86 additions & 0 deletions crates/bashkit/src/scripted_tool/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1269,4 +1269,90 @@ mod tests {
assert!(resp.stdout.contains("from_impl"));
assert!(resp.stdout.contains("from_fn"));
}

// -- Issue #1278: --help flag tests --

#[tokio::test]
async fn test_tool_help_flag_returns_help_text() {
let tool = build_test_tool();
let resp = tool
.execute(ToolRequest {
commands: "get_user --help".to_string(),
timeout_ms: None,
})
.await;
assert_eq!(resp.exit_code, 0);
assert!(
resp.stdout.contains("get_user"),
"help should include tool name"
);
assert!(
resp.stdout.contains("Fetch user by id"),
"help should include description"
);
assert!(
resp.stdout.contains("--id"),
"help should include parameter flags"
);
}

#[tokio::test]
async fn test_tool_help_flag_does_not_invoke_callback() {
let tool = build_test_tool();
// fail_tool always returns an error, but --help should not invoke it
let resp = tool
.execute(ToolRequest {
commands: "fail_tool --help".to_string(),
timeout_ms: None,
})
.await;
assert_eq!(
resp.exit_code, 0,
"--help should succeed even for fail_tool"
);
assert!(
resp.stdout.contains("Always fails"),
"help should include description"
);
}

#[tokio::test]
async fn test_tool_help_flag_same_as_help_builtin() {
let tool = build_test_tool();
let help_output = tool
.execute(ToolRequest {
commands: "help get_user".to_string(),
timeout_ms: None,
})
.await;
let flag_output = tool
.execute(ToolRequest {
commands: "get_user --help".to_string(),
timeout_ms: None,
})
.await;
assert_eq!(
help_output.stdout, flag_output.stdout,
"`--help` should produce same output as `help <tool>`"
);
}

#[tokio::test]
async fn test_tool_help_flag_stripped_from_args() {
let tool = build_test_tool();
// get_user --help --id 42 should not call the callback with --help in args
let resp = tool
.execute(ToolRequest {
commands: "get_user --help --id 42".to_string(),
timeout_ms: None,
})
.await;
assert_eq!(resp.exit_code, 0);
// Output should be help text, not the callback result
assert!(resp.stdout.contains("Fetch user by id"));
assert!(
!resp.stdout.contains("Alice"),
"callback should NOT be invoked"
);
}
}
Loading