use std::path::{Path, PathBuf};
use studio_worker::{cli, config, run_cli, runtime};
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
fn write_config(dir: &Path, api_uri: &str, feed: &str) -> PathBuf {
let path = dir.join("config.toml");
let body = format!(
r#"api_base_url = "{api_uri}"
vram_threshold_gb = 16.0
auto_start = true
auto_update_enabled = true
auto_update_interval_secs = 60
auto_update_feed = "{feed}"
auto_update_prerelease = false
"#
);
std::fs::write(&path, body).unwrap();
path
}
#[tokio::test]
async fn register_helper_persists_api_base_url_override() {
let dir = tempfile::tempdir().unwrap();
let cfg_path = write_config(dir.path(), "http://placeholder", "http://feed.invalid");
let cfg_path_str = cfg_path.to_string_lossy().to_string();
runtime::register(
Some(&cfg_path_str),
runtime::RegisterArgs {
api_base_url: Some("http://127.0.0.1:0".into()),
..Default::default()
},
)
.await
.unwrap();
let (cfg, _) = config::load(Some(&cfg_path_str)).unwrap();
assert_eq!(cfg.api_base_url, "http://127.0.0.1:0");
}
#[tokio::test]
async fn status_helper_runs_without_panicking() {
let dir = tempfile::tempdir().unwrap();
let cfg_path = write_config(dir.path(), "http://api.invalid", "http://feed.invalid");
let cfg_path_str = cfg_path.to_string_lossy().to_string();
runtime::status(Some(&cfg_path_str)).await.unwrap();
}
#[tokio::test]
async fn check_update_helper_calls_feed_and_prints_outcome() {
let feed = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/releases"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([])))
.mount(&feed)
.await;
let dir = tempfile::tempdir().unwrap();
let cfg_path = write_config(
dir.path(),
"http://api.invalid",
&format!("{}/releases", feed.uri()),
);
let cfg_path_str = cfg_path.to_string_lossy().to_string();
runtime::check_update(Some(&cfg_path_str)).await.unwrap();
}
#[tokio::test]
async fn set_threshold_persists_to_disk() {
let dir = tempfile::tempdir().unwrap();
let cfg_path = write_config(dir.path(), "http://api.invalid", "http://feed.invalid");
let cfg_path_str = cfg_path.to_string_lossy().to_string();
runtime::set_threshold(Some(&cfg_path_str), 33.0).unwrap();
let (cfg, _) = config::load(Some(&cfg_path_str)).unwrap();
assert_eq!(cfg.vram_threshold_gb, 33.0);
}
#[tokio::test]
async fn set_threshold_rejects_negative() {
let dir = tempfile::tempdir().unwrap();
let cfg_path = write_config(dir.path(), "http://api.invalid", "http://feed.invalid");
let cfg_path_str = cfg_path.to_string_lossy().to_string();
let err = runtime::set_threshold(Some(&cfg_path_str), -1.0).unwrap_err();
assert!(err.to_string().contains("threshold must be >= 0"));
}
#[tokio::test]
async fn show_config_prints_resolved_toml() {
let dir = tempfile::tempdir().unwrap();
let cfg_path = write_config(dir.path(), "http://api.invalid", "http://feed.invalid");
let cfg_path_str = cfg_path.to_string_lossy().to_string();
runtime::show_config(Some(&cfg_path_str)).unwrap();
}
#[tokio::test]
async fn run_cli_dispatches_status_subcommand() {
let dir = tempfile::tempdir().unwrap();
let cfg_path = write_config(dir.path(), "http://api.invalid", "http://feed.invalid");
let args = cli::Cli {
config: Some(cfg_path.to_string_lossy().to_string()),
command: cli::Command::Status,
};
run_cli(args).await.unwrap();
}
#[tokio::test]
async fn run_cli_dispatches_set_threshold() {
let dir = tempfile::tempdir().unwrap();
let cfg_path = write_config(dir.path(), "http://api.invalid", "http://feed.invalid");
let p = cfg_path.to_string_lossy().to_string();
run_cli(cli::Cli {
config: Some(p.clone()),
command: cli::Command::SetThreshold { gb: 7.5 },
})
.await
.unwrap();
let (cfg, _) = config::load(Some(&p)).unwrap();
assert_eq!(cfg.vram_threshold_gb, 7.5);
}
#[tokio::test]
async fn run_cli_dispatches_config_subcommand() {
let dir = tempfile::tempdir().unwrap();
let cfg_path = write_config(dir.path(), "http://api.invalid", "http://feed.invalid");
run_cli(cli::Cli {
config: Some(cfg_path.to_string_lossy().to_string()),
command: cli::Command::Config,
})
.await
.unwrap();
}
#[tokio::test]
async fn run_cli_dispatches_register_subcommand() {
let dir = tempfile::tempdir().unwrap();
let cfg_path = write_config(dir.path(), "http://placeholder", "http://feed.invalid");
let p = cfg_path.to_string_lossy().to_string();
run_cli(cli::Cli {
config: Some(p.clone()),
command: cli::Command::Register {
api_base_url: Some("http://cli.invalid".into()),
reset: false,
},
})
.await
.unwrap();
let (cfg, _) = config::load(Some(&p)).unwrap();
assert_eq!(cfg.api_base_url, "http://cli.invalid");
}
#[tokio::test]
async fn run_cli_dispatches_install_and_uninstall_into_temp_xdg() {
let xdg = tempfile::tempdir().unwrap();
let previous = std::env::var("XDG_CONFIG_HOME").ok();
unsafe { std::env::set_var("XDG_CONFIG_HOME", xdg.path()) };
let result_install = run_cli(cli::Cli {
config: None,
command: cli::Command::InstallService,
})
.await;
let result_uninstall = run_cli(cli::Cli {
config: None,
command: cli::Command::UninstallService,
})
.await;
unsafe {
match previous {
Some(v) => std::env::set_var("XDG_CONFIG_HOME", v),
None => std::env::remove_var("XDG_CONFIG_HOME"),
}
}
if let Err(e) = result_install {
eprintln!("install service err (non-fatal in tests): {e:?}");
}
result_uninstall.expect("uninstall should be idempotent");
}
#[tokio::test]
async fn run_cli_dispatches_run_then_aborts() {
let api = wiremock::MockServer::start().await;
let feed = wiremock::MockServer::start().await;
wiremock::Mock::given(method("POST"))
.and(path("/graphics/api/workers/w-test/heartbeat"))
.respond_with(
wiremock::ResponseTemplate::new(200).set_body_json(serde_json::json!({ "ok": true })),
)
.mount(&api)
.await;
wiremock::Mock::given(method("POST"))
.and(path("/graphics/api/workers/w-test/claim"))
.respond_with(wiremock::ResponseTemplate::new(204))
.mount(&api)
.await;
wiremock::Mock::given(method("GET"))
.and(path("/releases"))
.respond_with(wiremock::ResponseTemplate::new(200).set_body_json(serde_json::json!([])))
.mount(&feed)
.await;
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("config.toml");
std::fs::write(
&path,
format!(
r#"api_base_url = "{}"
worker_id = "w-test"
auth_token = "tok-test"
vram_threshold_gb = 16.0
auto_start = true
auto_update_enabled = true
auto_update_interval_secs = 9999
auto_update_feed = "{}/releases"
auto_update_prerelease = false
"#,
api.uri(),
feed.uri()
),
)
.unwrap();
let path_str = path.to_string_lossy().to_string();
let handle = tokio::spawn(async move {
let _ = run_cli(cli::Cli {
config: Some(path_str),
command: cli::Command::Run,
})
.await;
});
tokio::time::sleep(std::time::Duration::from_millis(40)).await;
handle.abort();
let _ = handle.await;
}
#[tokio::test]
async fn run_cli_dispatches_check_update() {
let feed = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/releases"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([])))
.mount(&feed)
.await;
let dir = tempfile::tempdir().unwrap();
let cfg_path = write_config(
dir.path(),
"http://api.invalid",
&format!("{}/releases", feed.uri()),
);
run_cli(cli::Cli {
config: Some(cfg_path.to_string_lossy().to_string()),
command: cli::Command::CheckUpdate,
})
.await
.unwrap();
}
#[cfg(not(feature = "ui"))]
#[tokio::test]
async fn run_cli_ui_subcommand_errors_without_the_ui_feature() {
let dir = tempfile::tempdir().unwrap();
let cfg_path = write_config(dir.path(), "http://api.invalid", "http://feed.invalid");
let err = run_cli(cli::Cli {
config: Some(cfg_path.to_string_lossy().to_string()),
command: cli::Command::Ui,
})
.await
.expect_err("a non-ui build must reject the ui subcommand");
let msg = err.to_string();
assert!(
msg.contains("without the `ui` cargo feature"),
"expected the missing-feature explanation, got: {msg}"
);
assert!(
msg.contains("cargo install studio-worker"),
"expected the remediation hint, got: {msg}"
);
}