use std::path::Path;
use std::time::Duration;
use serde_json::{Value, json};
use crate::models::Tool;
use crate::tools::spec::{ToolError, ToolResult, required_str};
pub const JS_EXECUTION_TOOL_NAME: &str = "js_execution";
const JS_EXECUTION_TOOL_TYPE: &str = "code_execution_20250825";
#[must_use]
pub fn js_execution_tool_definition() -> Tool {
Tool {
tool_type: Some(JS_EXECUTION_TOOL_TYPE.to_string()),
name: JS_EXECUTION_TOOL_NAME.to_string(),
description:
"Execute JavaScript code in a local sandboxed Node.js runtime and return stdout/stderr/return_code as JSON."
.to_string(),
input_schema: json!({
"type": "object",
"properties": {
"code": { "type": "string", "description": "JavaScript source code to execute." }
},
"required": ["code"]
}),
allowed_callers: Some(vec!["direct".to_string()]),
defer_loading: Some(false),
input_examples: None,
strict: None,
cache_control: None,
}
}
pub async fn execute_js_execution_tool(
input: &Value,
workspace: &Path,
) -> Result<ToolResult, ToolError> {
let code = required_str(input, "code")?;
let node = crate::dependencies::resolve_node().ok_or_else(|| {
ToolError::execution_failed(
"js_execution: no Node.js runtime found on PATH (tried `node`). \
Install Node 18+ and ensure `node` is on PATH, then restart \
deepseek-tui."
.to_string(),
)
})?;
let temp_dir = tempfile::tempdir()
.map_err(|e| ToolError::execution_failed(format!("tempdir failed: {e}")))?;
let script_path = temp_dir.path().join("js_execution.js");
tokio::fs::write(&script_path, code)
.await
.map_err(|e| ToolError::execution_failed(format!("tempfile write failed: {e}")))?;
let mut cmd = tokio::process::Command::new(&node);
cmd.arg(&script_path);
cmd.current_dir(workspace);
let output = tokio::time::timeout(Duration::from_secs(120), cmd.output())
.await
.map_err(|_| ToolError::Timeout { seconds: 120 })
.and_then(|res| res.map_err(|e| ToolError::execution_failed(e.to_string())))?;
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
let return_code = output.status.code().unwrap_or(-1);
let success = output.status.success();
let payload = json!({
"type": "code_execution_result",
"stdout": stdout,
"stderr": stderr,
"return_code": return_code,
"content": [],
});
Ok(ToolResult {
content: serde_json::to_string(&payload).unwrap_or_else(|_| payload.to_string()),
success,
metadata: Some(payload),
})
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
fn node_present() -> bool {
crate::dependencies::resolve_node().is_some()
}
#[test]
fn tool_definition_advertises_js_execution_name_and_required_code_field() {
let tool = js_execution_tool_definition();
assert_eq!(tool.name, JS_EXECUTION_TOOL_NAME);
assert_eq!(tool.tool_type.as_deref(), Some(JS_EXECUTION_TOOL_TYPE));
let required = tool
.input_schema
.get("required")
.and_then(|v| v.as_array())
.expect("schema must declare a `required` array");
assert!(
required.iter().any(|v| v.as_str() == Some("code")),
"input_schema must require `code`",
);
}
#[tokio::test]
async fn execute_js_runs_node_and_returns_stdout_payload() {
if !node_present() {
return;
}
let tmp = tempdir().expect("tempdir");
let result = execute_js_execution_tool(
&json!({ "code": "process.stdout.write('hello from node')" }),
tmp.path(),
)
.await
.expect("execute");
assert!(result.success, "successful node run must report success");
assert!(
result.content.contains("hello from node"),
"stdout payload must surface the printed text; got {}",
result.content
);
}
#[tokio::test]
async fn execute_js_surfaces_runtime_error_with_nonzero_exit() {
if !node_present() {
return;
}
let tmp = tempdir().expect("tempdir");
let result = execute_js_execution_tool(
&json!({ "code": "throw new Error('intentional fail')" }),
tmp.path(),
)
.await
.expect("execute should not Err — runtime errors land in stderr/exit code");
assert!(
!result.success,
"non-zero exit must report success=false in the result payload"
);
assert!(
result.content.contains("intentional fail"),
"stderr payload must surface the error message; got {}",
result.content
);
}
#[tokio::test]
async fn execute_js_rejects_input_without_code_field() {
let tmp = tempdir().expect("tempdir");
let err = execute_js_execution_tool(&json!({}), tmp.path())
.await
.expect_err("missing `code` must reject before any node spawn");
let msg = err.to_string();
assert!(
msg.contains("code"),
"error must name the missing `code` field; got {msg}"
);
}
}