#![cfg(feature = "sprites-live-tests")]
use everruns_integrations_sprites::client::SpritesClient;
use serde_json::json;
const SPRITES_WORKSPACE_PATH: &str = "/home/sprite";
struct SpriteGuard {
sprite_name: String,
}
impl SpriteGuard {
fn new(sprite_name: String) -> Self {
Self { sprite_name }
}
}
impl Drop for SpriteGuard {
fn drop(&mut self) {
let name = self.sprite_name.clone();
let Some(api_token) = get_api_token() else {
eprintln!("[cleanup] No API token, cannot delete sprite {name}");
return;
};
let handle = std::thread::spawn(move || {
let rt = tokio::runtime::Runtime::new().expect("cleanup runtime");
let client = SpritesClient::new(api_token);
rt.block_on(async {
eprintln!("[cleanup] Deleting sprite {name}");
match client.delete_sprite(&name).await {
Ok(()) => eprintln!("[cleanup] Sprite {name} deleted"),
Err(e) => eprintln!("[cleanup] Failed to delete sprite {name}: {e}"),
}
});
});
let _ = handle.join();
}
}
fn get_api_token() -> Option<String> {
std::env::var("SPRITES_API_TOKEN")
.ok()
.filter(|k| !k.trim().is_empty())
}
macro_rules! require_api_token {
() => {
match get_api_token() {
Some(token) => token,
None => panic!(
"SPRITES_API_TOKEN not set — live tests require real credentials (fail-closed policy)"
),
}
};
}
async fn create_test_sprite(api_token: String, label: &str) -> (SpritesClient, SpriteGuard) {
let client = SpritesClient::new(api_token);
let suffix = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis()
% 1_000_000;
let name = format!("everruns-test-{label}-{suffix}");
let info = client
.create_sprite(
&name,
json!({
"guest": {"cpus": 1, "memory_mb": 256},
"metadata": {"everruns-test": label}
}),
)
.await
.expect("Failed to create sprite");
assert_eq!(info.name, name);
eprintln!("[test] Created sprite {name}");
let guard = SpriteGuard::new(name);
(client, guard)
}
#[tokio::test]
async fn test_live_sprite_lifecycle() {
let api_token = require_api_token!();
let (client, guard) = create_test_sprite(api_token, "lifecycle").await;
let name = &guard.sprite_name;
let result = client
.exec(name, "echo hello-everruns", None)
.await
.expect("exec failed");
assert_eq!(result.exit_code, 0);
assert!(
result.stdout.contains("hello-everruns"),
"Unexpected output: {}",
result.stdout
);
let content = b"print('hello from everruns live test')\n";
client
.write_file(
name,
&format!("{SPRITES_WORKSPACE_PATH}/test_live.py"),
content,
)
.await
.expect("file write failed");
let downloaded = client
.read_file(name, &format!("{SPRITES_WORKSPACE_PATH}/test_live.py"))
.await
.expect("file read failed");
assert_eq!(
downloaded, content,
"Downloaded content doesn't match uploaded"
);
let cp = client
.create_checkpoint(name)
.await
.expect("checkpoint failed");
assert!(!cp.id.is_empty(), "Checkpoint ID should not be empty");
let info = client.get_sprite(name).await.expect("get_sprite failed");
assert_eq!(info.name, *name);
client
.delete_sprite(name)
.await
.expect("delete_sprite failed");
}
#[tokio::test]
async fn test_live_exec_exit_code() {
let api_token = require_api_token!();
let (client, guard) = create_test_sprite(api_token, "exit-code").await;
let name = &guard.sprite_name;
let result = client
.exec(name, "exit 42", None)
.await
.expect("exec failed");
assert_ne!(result.exit_code, 0, "Expected nonzero exit code, got 0");
}
#[tokio::test]
async fn test_live_checkpoint_restore() {
let api_token = require_api_token!();
let (client, guard) = create_test_sprite(api_token, "checkpoint").await;
let name = &guard.sprite_name;
client
.write_file(
name,
&format!("{SPRITES_WORKSPACE_PATH}/before.txt"),
b"original",
)
.await
.expect("write failed");
let cp = client
.create_checkpoint(name)
.await
.expect("checkpoint failed");
client
.write_file(
name,
&format!("{SPRITES_WORKSPACE_PATH}/before.txt"),
b"modified",
)
.await
.expect("overwrite failed");
client
.restore_checkpoint(name, &cp.id)
.await
.expect("restore failed");
let exec = client
.exec(name, "echo after-restore", None)
.await
.expect("post-restore exec failed");
assert_eq!(exec.exit_code, 0);
assert!(
exec.stdout.contains("after-restore"),
"Unexpected post-restore output: {}",
exec.stdout
);
let content = client
.read_file(name, &format!("{SPRITES_WORKSPACE_PATH}/before.txt"))
.await
.expect("read failed");
let content = String::from_utf8_lossy(&content);
assert!(
content == "original" || content == "modified",
"File should remain readable after restore, got: {content}"
);
}