Skip to content

Commit e2609fd

Browse files
author
OpenClaw
committed
test: add E2E test that downloads CLI from API and runs console.log(1+1)
Downloads mcp-v8-cli from /api/cli/linux-x86_64, writes it to a temp file, chmod +x, then: 1. Runs: mcp-v8-cli --json exec 'console.log(1+1)' 2. Polls: mcp-v8-cli --json executions get <id> until completed 3. Reads: mcp-v8-cli executions output <id> 4. Asserts output contains '2' The entire flow uses only the binary downloaded from the server — the same one that was embedded at build time. Skips gracefully with a clear message in dev builds where the binary is not embedded.
1 parent 3e4362e commit e2609fd

1 file changed

Lines changed: 136 additions & 1 deletion

File tree

server/tests/cli_endpoints_e2e.rs

Lines changed: 136 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
use reqwest::Client;
1515
use std::process::Stdio;
1616
use tokio::process::Command;
17-
use tokio::time::{sleep, Duration};
17+
use tokio::time::{sleep, timeout, Duration};
1818

1919
// ── Server helper ────────────────────────────────────────────────────────
2020

@@ -374,3 +374,138 @@ async fn test_cli_available_flag_consistent_with_download() {
374374

375375
server.stop().await;
376376
}
377+
378+
// ── Full E2E: download CLI from API, run it, check output ─────────────────
379+
380+
/// Download the CLI binary from /api/cli/linux-x86_64, write it to a temp
381+
/// file, make it executable, then run:
382+
///
383+
/// mcp-v8-cli --url <server> --json exec 'console.log(1+1)'
384+
///
385+
/// Poll until the execution is complete, read the output, and assert it
386+
/// contains "2".
387+
///
388+
/// This test is skipped (with a clear message) when the CLI binary is not
389+
/// embedded in the server build (dev/local builds). It is the definitive
390+
/// end-to-end test for the feature: the same binary that ships in the server
391+
/// is the one used to drive it.
392+
#[tokio::test]
393+
async fn test_downloaded_cli_exec_console_log_outputs_2() {
394+
let mut server = HttpServer::start().await.expect("server start");
395+
let client = Client::new();
396+
397+
// ── Step 1: download the CLI from the server ──────────────────────
398+
let resp = client
399+
.get(format!("{}/api/cli/linux-x86_64", server.base_url))
400+
.send()
401+
.await
402+
.expect("GET /api/cli/linux-x86_64");
403+
404+
if resp.status() == 404 {
405+
// Dev build — binary not embedded; skip gracefully.
406+
eprintln!(
407+
"[SKIP] test_downloaded_cli_exec_console_log_outputs_2: \
408+
CLI not embedded in this build (set MCP_V8_CLI_LINUX_X86_64 at build time)"
409+
);
410+
server.stop().await;
411+
return;
412+
}
413+
414+
assert_eq!(resp.status(), 200, "expected 200 downloading CLI");
415+
416+
let cli_bytes = resp.bytes().await.expect("read CLI binary");
417+
assert!(!cli_bytes.is_empty(), "downloaded CLI is empty");
418+
419+
// ── Step 2: write to a temp file and make executable ─────────────
420+
let cli_path = std::env::temp_dir().join(format!(
421+
"mcp-v8-cli-e2e-{}",
422+
std::process::id()
423+
));
424+
std::fs::write(&cli_path, &cli_bytes).expect("write CLI binary");
425+
426+
#[cfg(unix)]
427+
{
428+
use std::os::unix::fs::PermissionsExt;
429+
std::fs::set_permissions(&cli_path, std::fs::Permissions::from_mode(0o755))
430+
.expect("chmod +x");
431+
}
432+
433+
// ── Step 3: submit 'console.log(1+1)' via the downloaded CLI ─────
434+
let output = timeout(
435+
Duration::from_secs(30),
436+
Command::new(&cli_path)
437+
.args([
438+
"--url", &server.base_url,
439+
"--json",
440+
"exec",
441+
"console.log(1+1)",
442+
])
443+
.stdout(Stdio::piped())
444+
.stderr(Stdio::piped())
445+
.output(),
446+
)
447+
.await
448+
.expect("exec timed out")
449+
.expect("spawn CLI");
450+
451+
assert!(
452+
output.status.success(),
453+
"CLI exec failed: {}",
454+
String::from_utf8_lossy(&output.stderr)
455+
);
456+
457+
let exec_json: serde_json::Value =
458+
serde_json::from_slice(&output.stdout).expect("CLI --json output is not valid JSON");
459+
let exec_id = exec_json["execution_id"]
460+
.as_str()
461+
.expect("execution_id missing from CLI output");
462+
463+
// ── Step 4: poll via the downloaded CLI until completed ───────────
464+
let mut status = String::new();
465+
for _ in 0..60 {
466+
let poll = Command::new(&cli_path)
467+
.args([
468+
"--url", &server.base_url,
469+
"--json",
470+
"executions", "get", exec_id,
471+
])
472+
.stdout(Stdio::piped())
473+
.stderr(Stdio::piped())
474+
.output()
475+
.await
476+
.expect("CLI executions get");
477+
478+
let info: serde_json::Value =
479+
serde_json::from_slice(&poll.stdout).expect("poll output not JSON");
480+
status = info["status"].as_str().unwrap_or("").to_string();
481+
482+
if matches!(status.as_str(), "completed" | "failed" | "timed_out" | "cancelled") {
483+
break;
484+
}
485+
sleep(Duration::from_millis(200)).await;
486+
}
487+
488+
assert_eq!(status, "completed", "execution did not complete: status={status}");
489+
490+
// ── Step 5: read console output via the downloaded CLI ────────────
491+
let output_cmd = Command::new(&cli_path)
492+
.args([
493+
"--url", &server.base_url,
494+
"executions", "output", exec_id,
495+
])
496+
.stdout(Stdio::piped())
497+
.stderr(Stdio::piped())
498+
.output()
499+
.await
500+
.expect("CLI executions output");
501+
502+
let stdout = String::from_utf8_lossy(&output_cmd.stdout);
503+
assert!(
504+
stdout.trim().contains("2"),
505+
"expected output to contain '2', got: {stdout:?}"
506+
);
507+
508+
// ── Cleanup ───────────────────────────────────────────────────────
509+
let _ = std::fs::remove_file(&cli_path);
510+
server.stop().await;
511+
}

0 commit comments

Comments
 (0)