feat: complete Steam execution
This commit is contained in:
+60
-24
@@ -3,7 +3,7 @@
|
||||
use std::env;
|
||||
use std::process::ExitCode;
|
||||
|
||||
use kiln_adapter_native::{NativeAdapter, current_log_path, execute};
|
||||
use kiln_adapter_native::{NativeAdapter, current_log_path_for, execute, submit};
|
||||
use kiln_adapter_steam::SteamAdapter;
|
||||
use kiln_core::adapter::ProviderAdapter;
|
||||
use kiln_core::config::{
|
||||
@@ -27,7 +27,7 @@ Usage:
|
||||
kiln search <query> [--provider native|steam] [--json]
|
||||
kiln info <id-or-name> [--provider native|steam] [--json]
|
||||
kiln scan [--provider native|steam] [--no-cache] [--json]
|
||||
kiln launch <id-or-name> [--dry-run] [--json]
|
||||
kiln launch <id-or-name> [--provider native|steam] [--dry-run] [--json]
|
||||
kiln config defaults
|
||||
kiln config validate [--json]
|
||||
kiln config explain [--json]
|
||||
@@ -332,30 +332,66 @@ fn scan_steam(json_output: bool) -> ExitCode {
|
||||
fn launch(mut arguments: Vec<String>, json_output: bool) -> ExitCode {
|
||||
let dry_run = take_flag(&mut arguments, "--dry-run");
|
||||
let _ = take_flag(&mut arguments, "--no-cache");
|
||||
let [command, selector] = arguments.as_slice() else {
|
||||
return invalid_arguments(json_output);
|
||||
let (selector, provider) = match arguments.as_slice() {
|
||||
[command, selector] if command == "launch" => (selector, None),
|
||||
[command, selector, flag, provider] if command == "launch" && flag == "--provider" => {
|
||||
(selector, Some(provider.as_str()))
|
||||
}
|
||||
_ => return invalid_arguments(json_output),
|
||||
};
|
||||
if command != "launch" {
|
||||
let steam = provider == Some("steam") || provider.is_none() && selector.starts_with("steam:");
|
||||
if provider.is_some_and(|value| !matches!(value, "native" | "steam"))
|
||||
|| provider == Some("native") && selector.starts_with("steam:")
|
||||
{
|
||||
return invalid_arguments(json_output);
|
||||
}
|
||||
with_adapter(json_output, |adapter| {
|
||||
let record = adapter.resolve(selector)?;
|
||||
let plan = adapter.plan_launch(record.id())?;
|
||||
if dry_run {
|
||||
if json_output {
|
||||
println!(
|
||||
"{}",
|
||||
serde_json::to_string(&plan).expect("launch plan serializes")
|
||||
);
|
||||
} else {
|
||||
println!(
|
||||
"{}",
|
||||
serde_json::to_string_pretty(&plan).expect("launch plan serializes")
|
||||
);
|
||||
}
|
||||
return Ok(());
|
||||
if steam {
|
||||
with_steam(json_output, |adapter| {
|
||||
launch_with(adapter, selector, "steam", dry_run, json_output)
|
||||
})
|
||||
} else {
|
||||
with_adapter(json_output, |adapter| {
|
||||
launch_with(adapter, selector, "native", dry_run, json_output)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn launch_with(
|
||||
adapter: &impl ProviderAdapter,
|
||||
selector: &str,
|
||||
provider: &str,
|
||||
dry_run: bool,
|
||||
json_output: bool,
|
||||
) -> Result<(), ContractErrorV1> {
|
||||
let record = adapter.resolve(selector)?;
|
||||
let plan = adapter.plan_launch(record.id())?;
|
||||
if dry_run {
|
||||
let output = if json_output {
|
||||
serde_json::to_string(&plan)
|
||||
} else {
|
||||
serde_json::to_string_pretty(&plan)
|
||||
}
|
||||
let log = current_log_path()?;
|
||||
.expect("launch plan serializes");
|
||||
println!("{output}");
|
||||
return Ok(());
|
||||
}
|
||||
let provider_name = name(provider);
|
||||
let log = current_log_path_for(&provider_name)?;
|
||||
if provider == "steam" {
|
||||
let outcome = submit(&plan, &log)?;
|
||||
if json_output {
|
||||
println!(
|
||||
"{}",
|
||||
json!({"game_id": record.id(), "status": "submitted", "process_id": outcome.process_id, "log": log})
|
||||
);
|
||||
} else {
|
||||
println!(
|
||||
"{} submitted successfully; log: {}",
|
||||
record.id(),
|
||||
log.display()
|
||||
);
|
||||
}
|
||||
} else {
|
||||
let outcome = execute(&plan, &log)?;
|
||||
if json_output {
|
||||
println!(
|
||||
@@ -369,8 +405,8 @@ fn launch(mut arguments: Vec<String>, json_output: bool) -> ExitCode {
|
||||
log.display()
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn with_adapter(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! Isolated CLI contract tests for Phase 1 machine output.
|
||||
|
||||
use std::fs;
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
|
||||
@@ -28,11 +29,23 @@ impl TestDirectory {
|
||||
}
|
||||
|
||||
fn run(&self, arguments: &[&str]) -> std::process::Output {
|
||||
self.run_with_path(
|
||||
arguments,
|
||||
&format!(
|
||||
"{}:{}",
|
||||
self.0.join("bin").display(),
|
||||
std::env::var("PATH").unwrap_or_default()
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn run_with_path(&self, arguments: &[&str], path: &str) -> std::process::Output {
|
||||
Command::new(env!("CARGO_BIN_EXE_kiln"))
|
||||
.args(arguments)
|
||||
.env("XDG_CONFIG_HOME", self.0.join("config"))
|
||||
.env("XDG_STATE_HOME", self.0.join("state"))
|
||||
.env("HOME", self.0.join("home"))
|
||||
.env("PATH", path)
|
||||
.output()
|
||||
.expect("kiln runs")
|
||||
}
|
||||
@@ -66,6 +79,11 @@ impl TestDirectory {
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
let bin = self.0.join("bin");
|
||||
fs::create_dir_all(&bin).unwrap();
|
||||
let steam = bin.join("steam");
|
||||
fs::write(&steam, "#!/bin/sh\nprintf 'steam %s\\n' \"$*\"\n").unwrap();
|
||||
fs::set_permissions(steam, fs::Permissions::from_mode(0o755)).unwrap();
|
||||
fs::write(
|
||||
steamapps.join(format!("appmanifest_{app_id}.acf")),
|
||||
format!(
|
||||
@@ -230,7 +248,7 @@ fn invalid_configuration_and_failed_launch_are_structured() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn steam_provider_cli_discovers_searches_and_resolves_without_launching() {
|
||||
fn steam_provider_cli_discovers_plans_and_submits_without_a_shell() {
|
||||
let directory = TestDirectory::new();
|
||||
directory.add_steam_game("620", "Portal 2");
|
||||
|
||||
@@ -240,6 +258,12 @@ fn steam_provider_cli_discovers_searches_and_resolves_without_launching() {
|
||||
assert_eq!(records[0]["id"], "steam:620");
|
||||
assert_eq!(records[0]["runtime"], "steam-managed");
|
||||
assert_eq!(records[0]["client_required"], true);
|
||||
assert!(
|
||||
records[0]["capabilities"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.contains(&Value::String("launch".into()))
|
||||
);
|
||||
|
||||
let search = directory.run(&["search", "portal", "--provider", "steam", "--json"]);
|
||||
assert!(search.status.success());
|
||||
@@ -260,4 +284,38 @@ fn steam_provider_cli_discovers_searches_and_resolves_without_launching() {
|
||||
let result: Value = serde_json::from_slice(&scan.stdout).unwrap();
|
||||
assert_eq!(result["provider"], "steam");
|
||||
assert_eq!(result["discovered"], 1);
|
||||
|
||||
let dry_run = directory.run(&[
|
||||
"launch",
|
||||
"Portal 2",
|
||||
"--provider",
|
||||
"steam",
|
||||
"--dry-run",
|
||||
"--json",
|
||||
]);
|
||||
assert!(dry_run.status.success());
|
||||
let plan: Value = serde_json::from_slice(&dry_run.stdout).unwrap();
|
||||
assert_eq!(plan["runtime"], "steam-managed");
|
||||
assert_eq!(plan["lifecycle_confidence"], "unknown");
|
||||
assert_eq!(plan["command"]["arguments"][2], "620");
|
||||
|
||||
let launch = directory.run(&["launch", "steam:620", "--json"]);
|
||||
assert!(launch.status.success());
|
||||
let outcome: Value = serde_json::from_slice(&launch.stdout).unwrap();
|
||||
assert_eq!(outcome["status"], "submitted");
|
||||
assert!(outcome["process_id"].as_u64().unwrap() > 0);
|
||||
assert!(
|
||||
fs::read_to_string(directory.0.join("state/kiln/logs/steam.log"))
|
||||
.unwrap()
|
||||
.contains("submitted")
|
||||
);
|
||||
|
||||
fs::remove_file(directory.0.join("bin/steam")).unwrap();
|
||||
let failed = directory.run_with_path(
|
||||
&["launch", "steam:620", "--json"],
|
||||
directory.0.join("bin").to_str().unwrap(),
|
||||
);
|
||||
assert_eq!(failed.status.code(), Some(2));
|
||||
let error: Value = serde_json::from_slice(&failed.stderr).unwrap();
|
||||
assert_eq!(error["code"], "launch-failed");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user