use serde_json::Value;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
static SCRIPT_COUNTER: AtomicU64 = AtomicU64::new(0);
fn assay_binary() -> PathBuf {
PathBuf::from(env!("CARGO_BIN_EXE_assay"))
}
fn write_script(name: &str, content: &str) -> PathBuf {
let nonce = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_nanos();
let seq = SCRIPT_COUNTER.fetch_add(1, Ordering::Relaxed);
let dir = std::env::temp_dir().join(format!("assay-tool-mode-{nonce}-{seq}"));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join(name);
std::fs::write(&path, content).unwrap();
path
}
fn temp_dir(prefix: &str) -> PathBuf {
let nonce = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_nanos();
let seq = SCRIPT_COUNTER.fetch_add(1, Ordering::Relaxed);
let dir = std::env::temp_dir().join(format!("{prefix}-{nonce}-{seq}"));
std::fs::create_dir_all(&dir).unwrap();
dir
}
fn run_tool_mode(script_path: &Path) -> std::process::Output {
Command::new(assay_binary())
.args(["run", "--mode", "tool", script_path.to_str().unwrap()])
.output()
.unwrap()
}
fn run_tool_mode_with_state_dir(script_path: &Path, state_dir: &Path) -> std::process::Output {
Command::new(assay_binary())
.args(["run", "--mode", "tool", script_path.to_str().unwrap()])
.env("ASSAY_STATE_DIR", state_dir)
.output()
.unwrap()
}
fn run_normal_mode(script_path: &Path) -> std::process::Output {
Command::new(assay_binary())
.args(["run", script_path.to_str().unwrap()])
.output()
.unwrap()
}
fn run_resume(token: &str, approve: &str, state_dir: &Path) -> std::process::Output {
Command::new(assay_binary())
.args(["resume", "--token", token, "--approve", approve])
.env("ASSAY_STATE_DIR", state_dir)
.output()
.unwrap()
}
fn approval_script() -> PathBuf {
write_script(
"approval.lua",
r#"
local oc = require("assay.openclaw")
local c = oc.client("http://localhost:1", { token = "t" })
local approved = c.gates:approve("Deploy?", { env = "prod" })
if approved then
return { status = "deployed" }
else
return { status = "rejected" }
end
"#,
)
}
fn extract_resume_token(json: &Value) -> String {
json["requiresApproval"]["resumeToken"]
.as_str()
.unwrap()
.to_string()
}
fn resume_state_path(state_dir: &Path, token: &str) -> PathBuf {
state_dir.join("resume").join(format!("{token}.json"))
}
fn stdout_json(output: &std::process::Output) -> Value {
serde_json::from_slice(&output.stdout).unwrap()
}
#[test]
fn test_tool_mode_success() {
let script = write_script(
"success.lua",
r#"return { message = "ok", values = {1, 2, 3} }"#,
);
let output = run_tool_mode(&script);
assert!(output.status.success());
let json = stdout_json(&output);
assert_eq!(json["ok"], Value::Bool(true));
assert_eq!(json["status"], Value::String("ok".into()));
assert_eq!(json["requiresApproval"], Value::Null);
assert_eq!(json["output"]["message"], Value::String("ok".into()));
assert_eq!(json["output"]["values"], serde_json::json!([1, 2, 3]));
}
#[test]
fn test_tool_mode_error() {
let script = write_script("error.lua", r#"error("boom")"#);
let output = run_tool_mode(&script);
assert!(
output.status.success(),
"tool mode should exit zero on envelope"
);
let json = stdout_json(&output);
assert_eq!(json["ok"], Value::Bool(false));
assert_eq!(json["status"], Value::String("error".into()));
assert!(json["error"].as_str().unwrap().contains("boom"));
}
#[test]
fn test_tool_mode_nil_return() {
let script = write_script("nil.lua", "local x = 1\n");
let output = run_tool_mode(&script);
assert!(output.status.success());
let json = stdout_json(&output);
assert_eq!(json["ok"], Value::Bool(true));
assert_eq!(json["output"], Value::Null);
}
#[test]
fn test_tool_mode_string_return() {
let script = write_script("string.lua", r#"return "hello from tool mode""#);
let output = run_tool_mode(&script);
assert!(output.status.success());
let json = stdout_json(&output);
assert_eq!(json["output"], Value::String("hello from tool mode".into()));
}
#[test]
fn test_tool_mode_env_var() {
let script = write_script("env.lua", "return { enabled = true }");
let output = Command::new(assay_binary())
.args(["run", script.to_str().unwrap()])
.env("ASSAY_MODE", "tool")
.output()
.unwrap();
assert!(output.status.success());
let json = stdout_json(&output);
assert_eq!(json["ok"], Value::Bool(true));
assert_eq!(json["output"]["enabled"], Value::Bool(true));
}
#[test]
fn test_tool_mode_stderr_separation() {
let script = write_script(
"stderr.lua",
r#"
log.info("logged to stderr")
return { ok = true }
"#,
);
let output = run_tool_mode(&script);
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stdout.contains("\"output\""),
"stdout should contain only envelope: {stdout}"
);
assert!(
!stdout.contains("logged to stderr"),
"stdout should not contain logs: {stdout}"
);
assert!(
stderr.contains("logged to stderr"),
"stderr should contain logs: {stderr}"
);
}
#[test]
fn test_tool_mode_timeout() {
let script = write_script("timeout.lua", "sleep(30)\n");
let output = Command::new(assay_binary())
.args([
"run",
"--mode",
"tool",
"--timeout",
"2",
script.to_str().unwrap(),
])
.output()
.unwrap();
assert!(
output.status.success(),
"timeout envelope should still exit zero"
);
let json = stdout_json(&output);
assert_eq!(json["ok"], Value::Bool(false));
assert_eq!(json["status"], Value::String("timeout".into()));
assert_eq!(
json["error"],
Value::String("execution timed out after 2s".into())
);
}
#[test]
fn test_tool_mode_truncates_large_output() {
let payload = "a".repeat(600_000);
let script = write_script("truncate.lua", &format!(r#"return "{payload}""#));
let output = run_tool_mode(&script);
assert!(output.status.success());
assert!(
output.stdout.len() <= 524_288,
"stdout should stay under cap"
);
let json = stdout_json(&output);
assert_eq!(json["ok"], Value::Bool(true));
assert_eq!(json["truncated"], Value::Bool(true));
assert!(json["output"].as_str().unwrap().len() < payload.len());
}
#[test]
fn test_tool_mode_approval_halt() {
let state_dir = temp_dir("assay-resume-state");
let script = approval_script();
let output = run_tool_mode_with_state_dir(&script, &state_dir);
assert!(output.status.success());
let json = stdout_json(&output);
assert_eq!(json["ok"], Value::Bool(true));
assert_eq!(json["status"], Value::String("needs_approval".into()));
assert_eq!(json["output"], Value::Null);
assert_eq!(
json["requiresApproval"]["prompt"],
Value::String("Deploy?".into())
);
assert_eq!(
json["requiresApproval"]["context"],
serde_json::json!({ "env": "prod" })
);
let token = extract_resume_token(&json);
assert_eq!(token.len(), 32);
assert!(token.chars().all(|ch| ch.is_ascii_hexdigit()));
assert!(resume_state_path(&state_dir, &token).exists());
}
#[test]
fn test_resume_approve_yes() {
let state_dir = temp_dir("assay-resume-state");
let script = approval_script();
let first = run_tool_mode_with_state_dir(&script, &state_dir);
assert!(first.status.success());
let first_json = stdout_json(&first);
let token = extract_resume_token(&first_json);
let resumed = run_resume(&token, "yes", &state_dir);
assert!(resumed.status.success());
let json = stdout_json(&resumed);
assert_eq!(json["ok"], Value::Bool(true));
assert_eq!(json["status"], Value::String("ok".into()));
assert_eq!(json["output"]["status"], Value::String("deployed".into()));
}
#[test]
fn test_resume_approve_no() {
let state_dir = temp_dir("assay-resume-state");
let script = approval_script();
let first = run_tool_mode_with_state_dir(&script, &state_dir);
assert!(first.status.success());
let first_json = stdout_json(&first);
let token = extract_resume_token(&first_json);
let resumed = run_resume(&token, "no", &state_dir);
assert!(resumed.status.success());
let json = stdout_json(&resumed);
assert_eq!(json["ok"], Value::Bool(true));
assert_eq!(json["status"], Value::String("ok".into()));
assert_eq!(json["output"]["status"], Value::String("rejected".into()));
}
#[test]
fn test_resume_expired_token() {
let state_dir = temp_dir("assay-resume-state");
let script = approval_script();
let token = "expiredtokenexpiredtokenexpired12";
let resume_dir = state_dir.join("resume");
std::fs::create_dir_all(&resume_dir).unwrap();
std::fs::write(
resume_state_path(&state_dir, token),
serde_json::json!({
"script_path": script,
"approval_prompt": "Deploy?",
"approval_context": { "env": "prod" },
"created_at": 0,
"ttl_secs": 1
})
.to_string(),
)
.unwrap();
let output = run_resume(token, "yes", &state_dir);
assert!(output.status.success());
let json = stdout_json(&output);
assert_eq!(json["ok"], Value::Bool(false));
assert_eq!(json["status"], Value::String("error".into()));
assert!(json["error"].as_str().unwrap().contains("expired"));
}
#[test]
fn test_resume_invalid_token() {
let state_dir = temp_dir("assay-resume-state");
let output = run_resume("missing-token", "yes", &state_dir);
assert!(output.status.success());
let json = stdout_json(&output);
assert_eq!(json["ok"], Value::Bool(false));
assert_eq!(json["status"], Value::String("error".into()));
assert!(
json["error"]
.as_str()
.unwrap()
.contains("invalid resume token")
);
}
#[test]
fn test_resume_cleanup() {
let state_dir = temp_dir("assay-resume-state");
let script = approval_script();
let first = run_tool_mode_with_state_dir(&script, &state_dir);
assert!(first.status.success());
let first_json = stdout_json(&first);
let token = extract_resume_token(&first_json);
let state_path = resume_state_path(&state_dir, &token);
assert!(state_path.exists());
let resumed = run_resume(&token, "yes", &state_dir);
assert!(resumed.status.success());
assert!(!state_path.exists());
}
#[test]
fn test_tool_mode_normal_mode() {
let script = write_script(
"normal.lua",
r#"
log.info("plain mode")
return { ignored = true }
"#,
);
let output = Command::new(assay_binary())
.args(["run", script.to_str().unwrap()])
.output()
.unwrap();
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stdout.trim().is_empty(),
"normal mode should not emit JSON envelope: {stdout}"
);
assert!(
stderr.contains("plain mode"),
"normal mode log should remain on stderr: {stderr}"
);
}
#[test]
fn test_approval_non_tool_mode() {
let script = approval_script();
let output = run_normal_mode(&script);
assert!(!output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stdout.trim().is_empty(),
"normal mode should not emit JSON envelope: {stdout}"
);
assert!(stderr.contains("openclaw: approval_required:"));
}