use super::common::*;
#[test]
fn orchestrate_init_validate_plan_and_dry_run_json() {
let tmp_dir = temp_dir("orchestrate").expect("create temp dir");
let config = tmp_dir.join("pipeline.json");
let config_arg = config.to_str().expect("utf-8 path");
let init = run_cli(&["orchestrate", "init", "--output", config_arg, "--json"]);
assert!(init.status.success(), "stderr: {}", init.stderr);
let validate = run_cli(&["orchestrate", "validate", "--file", config_arg, "--json"]);
assert!(validate.status.success(), "stderr: {}", validate.stderr);
let validate_json: Value =
serde_json::from_str(&validate.stdout).expect("orchestrate validate json payload");
assert_eq!(
validate_json["api_version"],
Value::String(String::from("robotrt.orchestrate.validate.v1"))
);
assert_eq!(validate_json["status"], Value::String(String::from("pass")));
let plan = run_cli(&[
"orchestrate",
"plan",
"--file",
config_arg,
"--task",
"release-candidate",
"--json",
]);
assert!(plan.status.success(), "stderr: {}", plan.stderr);
let plan_json: Value =
serde_json::from_str(&plan.stdout).expect("orchestrate plan json payload");
assert_eq!(
plan_json["api_version"],
Value::String(String::from("robotrt.orchestrate.plan.v1"))
);
let planned = plan_json["planned_tasks"]
.as_array()
.expect("planned tasks array");
assert!(!planned.is_empty(), "planned tasks should not be empty");
assert!(
planned.iter().any(|entry| entry == "gate-fast-validated"),
"expected dependency task in plan"
);
assert!(
planned.iter().any(|entry| entry == "release-candidate"),
"expected target task in plan"
);
let dry_run = run_cli(&[
"orchestrate",
"run",
"--file",
config_arg,
"--task",
"release-candidate",
"--dry-run",
"--json",
]);
assert!(dry_run.status.success(), "stderr: {}", dry_run.stderr);
let dry_json: Value = serde_json::from_str(&dry_run.stdout).expect("orchestrate dry-run json");
assert_eq!(dry_json["status"], Value::String(String::from("pass")));
assert_eq!(dry_json["mode"], Value::String(String::from("dry_run")));
}
#[test]
fn orchestrate_validate_rejects_cycle() {
let config = temp_path("orchestrate-cycle", "json");
let config_arg = config.to_str().expect("utf-8 path");
let payload = serde_json::json!({
"api_version": "robotrt.orchestrate.v1",
"project": "robotrt",
"tasks": [
{
"name": "a",
"command": ["bash", "scripts/gate-fast-validated.sh"],
"depends_on": ["b"]
},
{
"name": "b",
"command": ["bash", "scripts/compat-matrix-gate.sh"],
"depends_on": ["a"]
}
]
});
fs::write(
&config,
format!(
"{}\n",
serde_json::to_string_pretty(&payload).expect("serialize cycle payload")
),
)
.expect("write cycle config");
let validate = run_cli(&["orchestrate", "validate", "--file", config_arg, "--json"]);
assert!(!validate.status.success(), "validate unexpectedly passed");
assert!(
validate.stderr.contains("dependency cycle detected")
|| validate.stderr.contains("orchestrate config invalid"),
"unexpected stderr: {}",
validate.stderr
);
}
#[test]
fn orchestrate_up_status_down_service_lifecycle_json() {
let tmp_dir = temp_dir("orchestrate-service").expect("create temp dir");
let config = tmp_dir.join("service.json");
let state = tmp_dir.join("runtime-state.json");
let config_arg = config.to_str().expect("utf-8 config path");
let state_arg = state.to_str().expect("utf-8 state path");
let payload = serde_json::json!({
"api_version": "robotrt.orchestrate.v1",
"project": "robotrt",
"tasks": [
{
"name": "svc",
"command": ["bash", "-lc", "sleep 5"],
"depends_on": [],
"run_mode": "service",
"group": "runtime"
}
]
});
fs::write(
&config,
format!(
"{}\n",
serde_json::to_string_pretty(&payload).expect("serialize service payload")
),
)
.expect("write service config");
let up = run_cli(&[
"orchestrate",
"up",
"--file",
config_arg,
"--state-file",
state_arg,
"--json",
]);
assert!(up.status.success(), "stderr: {}", up.stderr);
let up_json: Value = serde_json::from_str(&up.stdout).expect("orchestrate up json payload");
assert_eq!(up_json["status"], Value::String(String::from("pass")));
let status = run_cli(&["orchestrate", "status", "--state-file", state_arg, "--json"]);
assert!(status.status.success(), "stderr: {}", status.stderr);
let status_json: Value =
serde_json::from_str(&status.stdout).expect("orchestrate status json payload");
let entries = status_json["entries"]
.as_array()
.expect("status entries array");
assert_eq!(entries.len(), 1, "unexpected status entries: {entries:?}");
assert_eq!(entries[0]["task"], Value::String(String::from("svc")));
assert_eq!(entries[0]["alive"], Value::Bool(true));
let down = run_cli(&[
"orchestrate",
"down",
"--task",
"svc",
"--state-file",
state_arg,
"--json",
]);
assert!(down.status.success(), "stderr: {}", down.stderr);
let down_json: Value =
serde_json::from_str(&down.stdout).expect("orchestrate down json payload");
assert_eq!(down_json["status"], Value::String(String::from("pass")));
let status_after_down = run_cli(&[
"orchestrate",
"status",
"--state-file",
state_arg,
"--prune",
"--json",
]);
assert!(
status_after_down.status.success(),
"stderr: {}",
status_after_down.stderr
);
let status_after_json: Value =
serde_json::from_str(&status_after_down.stdout).expect("status after down json payload");
let after_entries = status_after_json["entries"]
.as_array()
.expect("status after down entries array");
assert!(after_entries.is_empty(), "expected empty runtime state");
}
#[test]
fn orchestrate_plan_supports_profile_overlay_and_group_filter() {
let tmp_dir = temp_dir("orchestrate-profile").expect("create temp dir");
let config = tmp_dir.join("pipeline.json");
let overlay = tmp_dir.join("overlay.json");
let config_arg = config.to_str().expect("utf-8 config path");
let overlay_arg = overlay.to_str().expect("utf-8 overlay path");
let config_payload = serde_json::json!({
"api_version": "robotrt.orchestrate.v1",
"project": "robotrt",
"tasks": [
{
"name": "build",
"command": ["bash", "-lc", "true"],
"depends_on": [],
"group": "build"
},
{
"name": "deploy",
"command": ["bash", "-lc", "true"],
"depends_on": ["build"],
"group": "deploy"
}
],
"profiles": {
"ci": {
"tasks": {
"deploy": {
"enabled": false
}
}
}
}
});
fs::write(
&config,
format!(
"{}\n",
serde_json::to_string_pretty(&config_payload).expect("serialize profile payload")
),
)
.expect("write profile config");
let overlay_payload = serde_json::json!({
"api_version": "robotrt.orchestrate.overlay.v1",
"add_tasks": [
{
"name": "lint",
"command": ["bash", "-lc", "true"],
"depends_on": [],
"group": "build"
}
]
});
fs::write(
&overlay,
format!(
"{}\n",
serde_json::to_string_pretty(&overlay_payload).expect("serialize overlay payload")
),
)
.expect("write overlay config");
let validate = run_cli(&[
"orchestrate",
"validate",
"--file",
config_arg,
"--profile",
"ci",
"--overlay",
overlay_arg,
"--json",
]);
assert!(validate.status.success(), "stderr: {}", validate.stderr);
let plan = run_cli(&[
"orchestrate",
"plan",
"--file",
config_arg,
"--profile",
"ci",
"--overlay",
overlay_arg,
"--group",
"build",
"--json",
]);
assert!(plan.status.success(), "stderr: {}", plan.stderr);
let plan_json: Value =
serde_json::from_str(&plan.stdout).expect("orchestrate plan json payload");
let planned = plan_json["planned_tasks"]
.as_array()
.expect("planned tasks array");
assert!(planned.iter().any(|entry| entry == "build"));
assert!(planned.iter().any(|entry| entry == "lint"));
assert!(
planned.iter().all(|entry| entry != "deploy"),
"deploy should be disabled by profile"
);
}
#[test]
fn orchestrate_run_honors_max_parallel_for_independent_tasks() {
let config = temp_path("orchestrate-parallel", "json");
let config_arg = config.to_str().expect("utf-8 path");
let payload = serde_json::json!({
"api_version": "robotrt.orchestrate.v1",
"project": "robotrt",
"max_parallel": 1,
"tasks": [
{
"name": "a",
"command": ["bash", "-lc", "sleep 1"],
"depends_on": []
},
{
"name": "b",
"command": ["bash", "-lc", "sleep 1"],
"depends_on": []
},
{
"name": "c",
"command": ["bash", "-lc", "true"],
"depends_on": ["a", "b"]
}
]
});
fs::write(
&config,
format!(
"{}\n",
serde_json::to_string_pretty(&payload).expect("serialize parallel payload")
),
)
.expect("write parallel config");
let started_at = std::time::Instant::now();
let run = run_cli(&[
"orchestrate",
"run",
"--file",
config_arg,
"--max-parallel",
"2",
"--json",
]);
let elapsed = started_at.elapsed();
assert!(run.status.success(), "stderr: {}", run.stderr);
let run_json: Value = serde_json::from_str(&run.stdout).expect("orchestrate run json payload");
assert_eq!(run_json["status"], Value::String(String::from("pass")));
assert!(
elapsed < std::time::Duration::from_millis(2500),
"expected parallel scheduling to finish quickly, elapsed={elapsed:?}"
);
}