use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::OnceLock;
use tempfile::TempDir;
static SHARED_REPO: OnceLock<(TempDir, PathBuf)> = OnceLock::new();
fn run_decapod(dir: &Path, args: &[&str]) -> std::process::Output {
Command::new(env!("CARGO_BIN_EXE_decapod"))
.current_dir(dir)
.env("DECAPOD_VALIDATE_SKIP_GIT_GATES", "1")
.args(args)
.output()
.expect("run decapod")
}
fn setup_repo() -> &'static (TempDir, PathBuf) {
SHARED_REPO.get_or_init(|| {
let tmp = TempDir::new().expect("tmpdir");
let dir = tmp.path().to_path_buf();
Command::new("git")
.current_dir(&dir)
.args(["init", "-b", "master"])
.output()
.expect("git init");
let init = run_decapod(&dir, &["init", "--force"]);
if !init.status.success() {
eprintln!(
"decapod init failed: {}",
String::from_utf8_lossy(&init.stderr)
);
}
assert!(
init.status.success(),
"decapod init failed: {}",
String::from_utf8_lossy(&init.stderr)
);
let workspace = run_decapod(&dir, &["workspace", "ensure", "--branch", "test/feature"]);
if !workspace.status.success() {
eprintln!(
"workspace ensure failed (may already exist): {}",
String::from_utf8_lossy(&workspace.stderr)
);
}
(tmp, dir)
})
}
#[test]
fn test_capabilities_stability() {
let (_tmp, dir) = setup_repo();
let out = run_decapod(dir, &["capabilities", "--format", "json"]);
assert!(out.status.success(), "capabilities failed");
let json: serde_json::Value =
serde_json::from_str(&String::from_utf8_lossy(&out.stdout)).expect("valid JSON");
assert!(json.get("version").is_some(), "version field missing");
assert!(json.get("capabilities").is_some(), "capabilities missing");
assert!(json.get("subsystems").is_some(), "subsystems missing");
assert!(
json.get("interlock_codes").is_some(),
"interlock_codes missing"
);
let interlock = json["interlock_codes"].as_array().expect("array");
assert!(
interlock.iter().any(|v| v == "workspace_required"),
"workspace_required missing"
);
assert!(
interlock.iter().any(|v| v == "verification_required"),
"verification_required missing"
);
assert!(
interlock.iter().any(|v| v == "store_boundary_violation"),
"store_boundary_violation missing"
);
}
#[test]
fn test_schema_determinism() {
let (_tmp, dir) = setup_repo();
let out = run_decapod(
dir,
&["data", "schema", "--format", "json", "--deterministic"],
);
assert!(out.status.success(), "schema failed");
let json: serde_json::Value =
serde_json::from_str(&String::from_utf8_lossy(&out.stdout)).expect("valid JSON");
assert!(
json.get("command_registry").is_some(),
"command_registry missing"
);
assert!(json.get("subsystems").is_some(), "subsystems missing");
}
#[test]
fn test_validate_terminates_boundedly() {
let (_tmp, dir) = setup_repo();
let out = run_decapod(dir, &["validate"]);
let output = String::from_utf8_lossy(&out.stdout);
let stderr = String::from_utf8_lossy(&out.stderr);
let combined = format!("{output}{stderr}");
assert!(
combined.contains("validate") || combined.contains("gate"),
"validate should run. stdout: {output}, stderr: {stderr}"
);
}
#[test]
fn test_workspace_protection() {
let (_tmp, dir) = setup_repo();
let out = run_decapod(dir, &["workspace", "status"]);
let output = String::from_utf8_lossy(&out.stdout);
println!("WORKSPACE STATUS OUTPUT:\n{}", output);
assert!(
output.contains("is_protected"),
"workspace should report protection status"
);
assert!(output.contains("master"), "should show current branch");
}
#[test]
fn test_session_required() {
let (_tmp, dir) = setup_repo();
let out = run_decapod(dir, &["session", "status"]);
let output = String::from_utf8_lossy(&out.stdout);
assert!(
output.contains("Session") || output.contains("active") || output.contains("inactive"),
"session should report status"
);
}
#[test]
fn test_todo_state_machine() {
let (_tmp, dir) = setup_repo();
run_decapod(dir, &["session", "acquire"]);
let add_out = run_decapod(dir, &["todo", "add", "test task", "--format", "json"]);
assert!(
add_out.status.success(),
"todo add failed: {}",
String::from_utf8_lossy(&add_out.stderr)
);
let add_json: serde_json::Value =
serde_json::from_str(&String::from_utf8_lossy(&add_out.stdout)).expect("valid JSON");
let task_id = add_json["id"].as_str().expect("task id");
let claim_out = run_decapod(dir, &["todo", "claim", "--id", task_id]);
assert!(claim_out.status.success(), "todo claim failed");
let done_out = run_decapod(dir, &["todo", "done", "--id", task_id]);
assert!(done_out.status.success(), "todo done failed");
let list_out = run_decapod(
dir,
&["todo", "list", "--status", "all", "--format", "json"],
);
assert!(list_out.status.success(), "todo list failed");
let list_json: serde_json::Value =
serde_json::from_str(&String::from_utf8_lossy(&list_out.stdout)).expect("valid JSON");
let tasks = list_json["items"].as_array().expect("items array");
let task = tasks.iter().find(|t| t["id"].as_str() == Some(task_id));
if let Some(task) = task {
assert!(
task["status"] == "done" || task["status"] == "verified",
"task should be done or verified"
);
}
}
#[test]
fn test_store_boundary_enforcement() {
let (_tmp, dir) = setup_repo();
let out = run_decapod(dir, &["validate", "--store", "repo"]);
let output = String::from_utf8_lossy(&out.stdout);
let stderr = String::from_utf8_lossy(&out.stderr);
let combined = format!("{output}{stderr}");
assert!(
combined.contains("validate") || combined.contains("gate") || out.status.success(),
"repo store validation should execute. stdout: {output}, stderr: {stderr}"
);
}
#[test]
fn test_error_codes_present() {
let (_tmp, dir) = setup_repo();
let caps = run_decapod(dir, &["capabilities", "--format", "json"]);
let json: serde_json::Value =
serde_json::from_str(&String::from_utf8_lossy(&caps.stdout)).expect("valid JSON");
let interlock = json["interlock_codes"].as_array().expect("array");
let required_codes = [
"workspace_required",
"verification_required",
"store_boundary_violation",
];
for code in required_codes {
assert!(
interlock.iter().any(|v| v == code),
"required error code {code} missing"
);
}
}
#[test]
fn test_workspace_protected_patterns() {
let (_tmp, dir) = setup_repo();
let caps = run_decapod(dir, &["capabilities", "--format", "json"]);
let json: serde_json::Value =
serde_json::from_str(&String::from_utf8_lossy(&caps.stdout)).expect("valid JSON");
let workspace = json.get("workspace").expect("workspace object");
let protected = workspace["protected_patterns"].as_array().expect("array");
assert!(protected.iter().any(|v| v == "main"), "main not protected");
assert!(
protected.iter().any(|v| v == "master"),
"master not protected"
);
}
#[test]
fn test_preflight_schema_stability() {
let (_tmp, dir) = setup_repo();
let out = run_decapod(
dir,
&[
"context",
"preflight",
"--op",
"validate",
"--format",
"json",
],
);
let stdout = String::from_utf8_lossy(&out.stdout);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
out.status.success(),
"preflight should succeed. stderr: {stderr}"
);
let json: serde_json::Value = serde_json::from_str(&stdout).unwrap();
assert!(json.get("op").is_some(), "op field missing");
assert!(json.get("risk_flags").is_some(), "risk_flags missing");
assert!(
json.get("likely_failures").is_some(),
"likely_failures missing"
);
assert!(
json.get("required_capsules").is_some(),
"required_capsules missing"
);
assert!(
json.get("next_best_actions").is_some(),
"next_best_actions missing"
);
assert!(json.get("workspace").is_some(), "workspace missing");
}
#[test]
fn test_impact_schema_stability() {
let (_tmp, dir) = setup_repo();
let out = run_decapod(
dir,
&[
"context",
"impact",
"--changed-files",
"src/a.rs",
"--format",
"json",
],
);
let stdout = String::from_utf8_lossy(&out.stdout);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
out.status.success(),
"impact should succeed. stderr: {stderr}"
);
let json: serde_json::Value = serde_json::from_str(&stdout).unwrap();
assert!(json.get("changed_files").is_some(), "changed_files missing");
assert!(
json.get("will_fail_validate").is_some(),
"will_fail_validate missing"
);
assert!(
json.get("predicted_failures").is_some(),
"predicted_failures missing"
);
assert!(
json.get("validation_predictions").is_some(),
"validation_predictions missing"
);
assert!(
json.get("recommendation").is_some(),
"recommendation missing"
);
assert!(json.get("workspace").is_some(), "workspace missing");
}
#[test]
fn test_preflight_predicts_workspace_required() {
let (_tmp, dir) = setup_repo();
let out = run_decapod(
dir,
&[
"context",
"preflight",
"--op",
"validate",
"--format",
"json",
],
);
let stdout = String::from_utf8_lossy(&out.stdout);
let json: serde_json::Value = serde_json::from_str(&stdout).unwrap();
let risk_flags = json["risk_flags"].as_array().expect("risk_flags array");
assert!(
risk_flags.iter().any(|v| v == "protected_branch"),
"preflight should detect protected_branch risk on master branch"
);
let likely_failures = json["likely_failures"]
.as_array()
.expect("likely_failures array");
let has_workspace_required = likely_failures
.iter()
.any(|f| f.get("code").and_then(|c| c.as_str()) == Some("WORKSPACE_REQUIRED"));
assert!(
has_workspace_required,
"preflight should predict WORKSPACE_REQUIRED"
);
let next_actions = json["next_best_actions"]
.as_array()
.expect("next_best_actions array");
let has_workspace_ensure = next_actions.iter().any(|a| {
a.as_str()
.map(|s| s.contains("workspace ensure"))
.unwrap_or(false)
});
assert!(
has_workspace_ensure,
"preflight should suggest workspace ensure"
);
}
#[test]
fn test_impact_predicts_failure_on_protected_branch() {
let (_tmp, dir) = setup_repo();
let out = run_decapod(
dir,
&[
"context",
"impact",
"--changed-files",
"src/a.rs",
"--format",
"json",
],
);
let stdout = String::from_utf8_lossy(&out.stdout);
let json: serde_json::Value = serde_json::from_str(&stdout).unwrap();
let will_fail = json["will_fail_validate"]
.as_bool()
.expect("will_fail_validate bool");
assert!(
will_fail,
"impact should predict validate will fail on protected branch"
);
let predicted = json["predicted_failures"]
.as_array()
.expect("predicted_failures array");
let has_workspace_required = predicted
.iter()
.any(|f| f.get("code").and_then(|c| c.as_str()) == Some("WORKSPACE_REQUIRED"));
assert!(
has_workspace_required,
"impact should predict WORKSPACE_REQUIRED"
);
}
#[test]
fn test_capabilities_includes_interlock() {
let (_tmp, dir) = setup_repo();
let caps = run_decapod(dir, &["capabilities", "--format", "json"]);
let json: serde_json::Value =
serde_json::from_str(&String::from_utf8_lossy(&caps.stdout)).expect("valid JSON");
let caps_list = json["capabilities"].as_array().expect("capabilities array");
let has_preflight = caps_list
.iter()
.any(|c| c.get("name").and_then(|n| n.as_str()) == Some("preflight.check"));
let has_impact = caps_list
.iter()
.any(|c| c.get("name").and_then(|n| n.as_str()) == Some("impact.predict"));
assert!(has_preflight, "capabilities should include preflight.check");
assert!(has_impact, "capabilities should include impact.predict");
}
#[test]
fn test_demo_interlock_prediction() {
let (_tmp, dir) = setup_repo();
let preflight_out = run_decapod(
dir,
&[
"context",
"preflight",
"--op",
"validate",
"--format",
"json",
],
);
let preflight_stdout = String::from_utf8_lossy(&preflight_out.stdout);
let preflight: serde_json::Value = serde_json::from_str(&preflight_stdout).unwrap();
let risk_flags = preflight["risk_flags"].as_array().expect("array");
assert!(
risk_flags.iter().any(|v| v == "protected_branch"),
"preflight should detect protected_branch"
);
let impact_out = run_decapod(
dir,
&[
"context",
"impact",
"--changed-files",
"src/a.rs",
"--format",
"json",
],
);
let impact_stdout = String::from_utf8_lossy(&impact_out.stdout);
let impact: serde_json::Value = serde_json::from_str(&impact_stdout).unwrap();
let will_fail = impact["will_fail_validate"].as_bool().expect("bool");
assert!(
will_fail,
"impact should predict failure on protected branch"
);
println!("\n=== INTERLOCK DEMO ===");
println!("Preflight detected: {:?}", preflight["risk_flags"]);
println!("Impact predicted: will_fail_validate = {will_fail}");
println!("Recommendation: {}", impact["recommendation"]);
println!("=== END DEMO ===\n");
}