use super::*;
use crate::playbook::cache;
use crate::playbook::types::*;
use indexmap::IndexMap;
use std::collections::HashMap;
#[tokio::test]
async fn test_PB005_remote_target_rejected() {
let dir = tempfile::tempdir().expect("tempdir creation failed");
let yaml_path = dir.path().join("test.yaml");
std::fs::write(
&yaml_path,
r#"
version: "1.0"
name: remote-test
params: {}
targets:
gpu-box:
host: "gpu-box.local"
ssh_user: noah
stages:
remote_stage:
cmd: "echo remote"
deps: []
outs: []
target: gpu-box
policy:
failure: stop_on_first
validation: checksum
lock_file: true
"#,
)
.expect("unexpected failure");
let config = RunConfig {
playbook_path: yaml_path,
stage_filter: None,
force: false,
dry_run: false,
param_overrides: HashMap::new(),
};
let err = run_playbook(&config).await.unwrap_err();
assert!(err.to_string().contains("Remote execution requires Phase 2"));
}
#[tokio::test]
async fn test_PB005_localhost_target_allowed() {
let dir = tempfile::tempdir().expect("tempdir creation failed");
let out_file = dir.path().join("local.txt");
let yaml_path = dir.path().join("test.yaml");
std::fs::write(
&yaml_path,
format!(
r#"
version: "1.0"
name: localhost-test
params: {{}}
targets:
local:
host: "localhost"
stages:
local_stage:
cmd: "echo local > {}"
deps: []
outs:
- path: {}
target: local
policy:
failure: stop_on_first
validation: checksum
lock_file: true
"#,
out_file.display(),
out_file.display(),
),
)
.expect("unexpected failure");
let config = RunConfig {
playbook_path: yaml_path,
stage_filter: None,
force: false,
dry_run: false,
param_overrides: HashMap::new(),
};
let result = run_playbook(&config).await.expect("async operation failed");
assert_eq!(result.stages_run, 1);
assert_eq!(std::fs::read_to_string(&out_file).expect("fs read failed").trim(), "local");
}
#[tokio::test]
async fn test_PB005_127_0_0_1_target_allowed() {
let dir = tempfile::tempdir().expect("tempdir creation failed");
let out_file = dir.path().join("loopback.txt");
let yaml_path = dir.path().join("test.yaml");
std::fs::write(
&yaml_path,
format!(
r#"
version: "1.0"
name: loopback-test
params: {{}}
targets:
loopback:
host: "127.0.0.1"
stages:
loop_stage:
cmd: "echo loopback > {}"
deps: []
outs:
- path: {}
target: loopback
policy:
failure: stop_on_first
validation: checksum
lock_file: true
"#,
out_file.display(),
out_file.display(),
),
)
.expect("unexpected failure");
let config = RunConfig {
playbook_path: yaml_path,
stage_filter: None,
force: false,
dry_run: false,
param_overrides: HashMap::new(),
};
let result = run_playbook(&config).await.expect("async operation failed");
assert_eq!(result.stages_run, 1);
}
#[tokio::test]
async fn test_PB005_stage_filter() {
let dir = tempfile::tempdir().expect("tempdir creation failed");
let out1 = dir.path().join("one.txt");
let out2 = dir.path().join("two.txt");
let yaml_path = dir.path().join("test.yaml");
std::fs::write(
&yaml_path,
format!(
r#"
version: "1.0"
name: filter-test
params: {{}}
targets: {{}}
stages:
one:
cmd: "echo one > {}"
deps: []
outs:
- path: {}
two:
cmd: "echo two > {}"
deps: []
outs:
- path: {}
policy:
failure: stop_on_first
validation: checksum
lock_file: true
"#,
out1.display(),
out1.display(),
out2.display(),
out2.display(),
),
)
.expect("unexpected failure");
let config = RunConfig {
playbook_path: yaml_path,
stage_filter: Some(vec!["one".to_string()]),
force: false,
dry_run: false,
param_overrides: HashMap::new(),
};
let result = run_playbook(&config).await.expect("async operation failed");
assert_eq!(result.stages_run, 1);
assert!(out1.exists());
assert!(!out2.exists()); }
#[tokio::test]
async fn test_PB005_force_rerun() {
let dir = tempfile::tempdir().expect("tempdir creation failed");
let out_file = dir.path().join("force.txt");
let yaml_path = dir.path().join("test.yaml");
std::fs::write(
&yaml_path,
format!(
r#"
version: "1.0"
name: force-test
params: {{}}
targets: {{}}
stages:
write:
cmd: "echo force > {}"
deps: []
outs:
- path: {}
policy:
failure: stop_on_first
validation: checksum
lock_file: true
"#,
out_file.display(),
out_file.display(),
),
)
.expect("unexpected failure");
let config = RunConfig {
playbook_path: yaml_path.clone(),
stage_filter: None,
force: false,
dry_run: false,
param_overrides: HashMap::new(),
};
let r1 = run_playbook(&config).await.expect("async operation failed");
assert_eq!(r1.stages_run, 1);
let r2 = run_playbook(&config).await.expect("async operation failed");
assert_eq!(r2.stages_cached, 1);
assert_eq!(r2.stages_run, 0);
let force_config = RunConfig {
playbook_path: yaml_path.clone(),
stage_filter: None,
force: true,
dry_run: false,
param_overrides: HashMap::new(),
};
let r3 = run_playbook(&force_config).await.expect("async operation failed");
assert_eq!(r3.stages_run, 1);
assert_eq!(r3.stages_cached, 0);
}
#[tokio::test]
async fn test_PB005_execute_command_stderr() {
let result = execute_command("echo error >&2 && exit 42").await;
assert!(result.is_err());
let err = result.unwrap_err();
let cmd_err = err.downcast_ref::<CommandError>().expect("downcast failed");
assert_eq!(cmd_err.exit_code, Some(42));
assert!(cmd_err.stderr.contains("error"));
}
#[test]
fn test_PB005_show_status_all_stage_statuses() {
let dir = tempfile::tempdir().expect("tempdir creation failed");
let yaml_path = dir.path().join("test.yaml");
std::fs::write(
&yaml_path,
r#"
version: "1.0"
name: multi-status
params: {}
targets: {}
stages:
completed:
cmd: "echo a"
deps: []
outs: []
failed:
cmd: "echo b"
deps: []
outs: []
running:
cmd: "echo c"
deps: []
outs: []
pending:
cmd: "echo d"
deps: []
outs: []
hashing:
cmd: "echo e"
deps: []
outs: []
validating:
cmd: "echo f"
deps: []
outs: []
not_run:
cmd: "echo g"
deps: []
outs: []
policy:
failure: stop_on_first
validation: checksum
lock_file: true
"#,
)
.expect("unexpected failure");
let lock = LockFile {
schema: "1.0".to_string(),
playbook: "multi-status".to_string(),
generated_at: "2026-02-16T14:00:00Z".to_string(),
generator: "batuta test".to_string(),
blake3_version: "1.8".to_string(),
params_hash: None,
stages: IndexMap::from([
(
"completed".to_string(),
StageLock {
status: StageStatus::Completed,
started_at: None,
completed_at: None,
duration_seconds: Some(1.5),
target: None,
deps: vec![],
params_hash: None,
outs: vec![],
cmd_hash: None,
cache_key: None,
},
),
(
"failed".to_string(),
StageLock {
status: StageStatus::Failed,
started_at: None,
completed_at: None,
duration_seconds: Some(0.3),
target: None,
deps: vec![],
params_hash: None,
outs: vec![],
cmd_hash: None,
cache_key: None,
},
),
(
"running".to_string(),
StageLock {
status: StageStatus::Running,
started_at: None,
completed_at: None,
duration_seconds: None,
target: None,
deps: vec![],
params_hash: None,
outs: vec![],
cmd_hash: None,
cache_key: None,
},
),
(
"pending".to_string(),
StageLock {
status: StageStatus::Pending,
started_at: None,
completed_at: None,
duration_seconds: None,
target: None,
deps: vec![],
params_hash: None,
outs: vec![],
cmd_hash: None,
cache_key: None,
},
),
(
"hashing".to_string(),
StageLock {
status: StageStatus::Hashing,
started_at: None,
completed_at: None,
duration_seconds: None,
target: None,
deps: vec![],
params_hash: None,
outs: vec![],
cmd_hash: None,
cache_key: None,
},
),
(
"validating".to_string(),
StageLock {
status: StageStatus::Validating,
started_at: None,
completed_at: None,
duration_seconds: None,
target: None,
deps: vec![],
params_hash: None,
outs: vec![],
cmd_hash: None,
cache_key: None,
},
),
]),
};
cache::save_lock_file(&lock, &yaml_path).expect("unexpected failure");
let result = show_status(&yaml_path);
assert!(result.is_ok());
}
#[tokio::test]
async fn test_PB005_param_overrides() {
let dir = tempfile::tempdir().expect("tempdir creation failed");
let out_file = dir.path().join("param_out.txt");
let yaml_path = dir.path().join("test.yaml");
std::fs::write(
&yaml_path,
format!(
r#"
version: "1.0"
name: param-override-test
params:
greeting: "default"
targets: {{}}
stages:
write:
cmd: "echo '{{{{params.greeting}}}}' > {}"
deps: []
outs:
- path: {}
params:
- greeting
policy:
failure: stop_on_first
validation: checksum
lock_file: true
"#,
out_file.display(),
out_file.display(),
),
)
.expect("unexpected failure");
let config = RunConfig {
playbook_path: yaml_path.clone(),
stage_filter: None,
force: false,
dry_run: false,
param_overrides: HashMap::new(),
};
let r1 = run_playbook(&config).await.expect("async operation failed");
assert_eq!(r1.stages_run, 1);
assert_eq!(std::fs::read_to_string(&out_file).expect("fs read failed").trim(), "default");
let mut overrides = HashMap::new();
overrides
.insert("greeting".to_string(), serde_yaml_ng::Value::String("overridden".to_string()));
let config2 = RunConfig {
playbook_path: yaml_path.clone(),
stage_filter: None,
force: false,
dry_run: false,
param_overrides: overrides,
};
let r2 = run_playbook(&config2).await.expect("async operation failed");
assert_eq!(r2.stages_run, 1);
assert_eq!(std::fs::read_to_string(&out_file).expect("fs read failed").trim(), "overridden");
}