#[path = "install_support.rs"]
mod support;
use std::fs;
use support::{create_derived_fixture, create_fixture, run_pray, temporary_directory};
#[test]
fn verify_reports_custom_implementation() {
let repo = temporary_directory("pray-verify");
create_fixture(&repo);
assert!(run_pray(&repo, &["install"]).status.success());
let rendered_path = repo.join("INSTRUCTIONS.md");
let mut rendered = fs::read_to_string(&rendered_path).expect("rendered file exists");
rendered = rendered.replace("Testing guidance", "Changed guidance");
fs::write(&rendered_path, rendered).expect("rendered file rewritten");
let verify = run_pray(&repo, &["verify"]);
assert!(!verify.status.success());
assert_eq!(verify.status.code(), Some(6));
let stderr = String::from_utf8_lossy(&verify.stderr);
assert!(stderr.contains("custom_implementation") || stderr.contains("verify error"));
assert!(stderr.contains("sample/base::testing-basics"));
assert!(stderr.contains("pray install"));
}
#[test]
fn verify_warns_on_orphan_markers_and_strict_fails() {
let repo = temporary_directory("pray-verify-orphan-marker");
create_fixture(&repo);
assert!(run_pray(&repo, &["install"]).status.success());
let rendered_path = repo.join("INSTRUCTIONS.md");
let mut rendered = fs::read_to_string(&rendered_path).expect("rendered file exists");
rendered.push_str("<!-- pray:abc123 -->\nOrphan marker body\n<!-- pray:abc123 -->\n");
fs::write(&rendered_path, rendered).expect("rendered file rewritten");
let verify = run_pray(&repo, &["verify"]);
assert!(verify.status.success());
let stderr = String::from_utf8_lossy(&verify.stderr);
assert!(stderr.contains("orphan_marker"));
let strict_verify = run_pray(&repo, &["verify", "--strict"]);
assert!(!strict_verify.status.success());
assert_eq!(strict_verify.status.code(), Some(6));
let strict_stderr = String::from_utf8_lossy(&strict_verify.stderr);
assert!(strict_stderr.contains("orphan_marker"));
}
#[test]
fn install_preserves_unmanaged_content_when_patching_rendered_files() {
let repo = temporary_directory("pray-install-patch-preserve");
create_fixture(&repo);
assert!(run_pray(&repo, &["install"]).status.success());
let rendered_path = repo.join("INSTRUCTIONS.md");
let rendered = fs::read_to_string(&rendered_path).expect("rendered file exists");
let rendered = rendered.replace(
"## Shared instructions\n\n",
"## Shared instructions\n\nUser note: keep this line.\n\n",
);
fs::write(&rendered_path, rendered).expect("rendered file rewritten");
let install = run_pray(&repo, &["install"]);
assert!(
install.status.success(),
"install failed: {}",
String::from_utf8_lossy(&install.stderr)
);
let rendered = fs::read_to_string(&rendered_path).expect("rendered file exists");
assert!(rendered.contains("User note: keep this line."));
assert!(rendered.contains("Testing guidance"));
}
#[test]
fn install_rejects_conflicting_managed_changes_when_conflict_policy_is_fail() {
let repo = temporary_directory("pray-install-conflict");
create_fixture(&repo);
assert!(run_pray(&repo, &["install"]).status.success());
let rendered_path = repo.join("INSTRUCTIONS.md");
let rendered = fs::read_to_string(&rendered_path).expect("rendered file exists");
let rendered = rendered.replace("Testing guidance", "Conflicting guidance");
fs::write(&rendered_path, rendered).expect("rendered file rewritten");
let install = run_pray(&repo, &["install"]);
assert!(!install.status.success());
assert!(String::from_utf8_lossy(&install.stderr).contains("conflict"));
let rendered = fs::read_to_string(&rendered_path).expect("rendered file exists");
assert!(rendered.contains("Conflicting guidance"));
}
#[test]
fn verify_reports_missing_managed_span_with_package_context_and_recovery_guidance() {
let repo = temporary_directory("pray-verify-missing-span");
create_fixture(&repo);
assert!(run_pray(&repo, &["install"]).status.success());
let rendered_path = repo.join("INSTRUCTIONS.md");
let rendered = fs::read_to_string(&rendered_path).expect("rendered file exists");
let rendered = rendered.replace("<!-- pray:", "<!-- removed pray:");
fs::write(&rendered_path, rendered).expect("rendered file rewritten");
let verify = run_pray(&repo, &["verify"]);
assert!(!verify.status.success());
assert_eq!(verify.status.code(), Some(6));
let stderr = String::from_utf8_lossy(&verify.stderr);
assert!(stderr.contains("removed_prayer"));
assert!(stderr.contains("INSTRUCTIONS.md"));
assert!(stderr.contains("sample/base::testing-basics"));
assert!(stderr.contains("pray install"));
}
#[test]
fn install_reports_missing_required_local_file_with_recovery_guidance() {
let repo = temporary_directory("pray-install-missing-local");
create_fixture(&repo);
assert!(run_pray(&repo, &["install"]).status.success());
fs::remove_file(repo.join(".agents/project.md")).expect("remove local file");
let install = run_pray(&repo, &["install"]);
assert!(!install.status.success());
assert_eq!(install.status.code(), Some(3));
let stderr = String::from_utf8_lossy(&install.stderr);
assert!(stderr.contains("Prayfile lists"));
assert!(stderr.contains(".agents/project.md"));
assert!(stderr.contains("Create the file"));
assert!(stderr.contains("pray install"));
}
#[test]
fn beta_flow_rejects_corrupted_lockfile_after_clean_install() {
let repo = temporary_directory("pray-beta-lockfile");
create_fixture(&repo);
let install = run_pray(&repo, &["install"]);
assert!(
install.status.success(),
"install failed: {}",
String::from_utf8_lossy(&install.stderr)
);
let verify = run_pray(&repo, &["verify"]);
assert!(
verify.status.success(),
"verify failed: {}",
String::from_utf8_lossy(&verify.stderr)
);
let drift = run_pray(&repo, &["drift"]);
assert!(
drift.status.success(),
"drift failed: {}",
String::from_utf8_lossy(&drift.stderr)
);
let format = run_pray(&repo, &["format"]);
assert!(
format.status.success(),
"format failed: {}",
String::from_utf8_lossy(&format.stderr)
);
fs::write(repo.join("Prayfile.lock"), "this is not a valid lockfile\n")
.expect("corrupt lockfile");
let corrupted_verify = run_pray(&repo, &["verify"]);
assert!(!corrupted_verify.status.success());
assert_eq!(corrupted_verify.status.code(), Some(2));
let stderr = String::from_utf8_lossy(&corrupted_verify.stderr);
assert!(stderr.contains("lockfile parse error") || stderr.contains("parse error"));
}
#[test]
fn install_repairs_corrupted_rendered_output_and_lockfile() {
let repo = temporary_directory("pray-install-repair");
create_fixture(&repo);
let install = run_pray(&repo, &["install"]);
assert!(
install.status.success(),
"install failed: {}",
String::from_utf8_lossy(&install.stderr)
);
let original_lockfile = fs::read_to_string(repo.join("Prayfile.lock")).expect("lockfile");
let original_rendered = fs::read_to_string(repo.join("INSTRUCTIONS.md")).expect("rendered");
fs::write(repo.join("Prayfile.lock"), "this is not a valid lockfile\n")
.expect("corrupt lockfile");
fs::write(repo.join("INSTRUCTIONS.md"), "broken rendered output\n").expect("corrupt rendered");
let reinstall = run_pray(&repo, &["install"]);
assert!(
reinstall.status.success(),
"reinstall failed: {}",
String::from_utf8_lossy(&reinstall.stderr)
);
assert_eq!(
fs::read_to_string(repo.join("Prayfile.lock")).expect("restored lockfile"),
original_lockfile
);
assert_eq!(
fs::read_to_string(repo.join("INSTRUCTIONS.md")).expect("restored rendered"),
original_rendered
);
let verify = run_pray(&repo, &["verify"]);
assert!(
verify.status.success(),
"verify failed after repair: {}",
String::from_utf8_lossy(&verify.stderr)
);
}
#[test]
fn install_locked_rejects_lockfile_drift() {
let repo = temporary_directory("pray-install-locked");
create_fixture(&repo);
assert!(run_pray(&repo, &["install"]).status.success());
let original_lockfile = fs::read_to_string(repo.join("Prayfile.lock")).expect("lockfile");
let original_rendered = fs::read_to_string(repo.join("INSTRUCTIONS.md")).expect("rendered");
fs::write(
repo.join(".agents/project.md"),
"Local guidance\nExtra local guidance\n",
)
.expect("rewrite local file");
let locked = run_pray(&repo, &["install", "--locked"]);
assert!(!locked.status.success());
assert_eq!(locked.status.code(), Some(6));
let stderr = String::from_utf8_lossy(&locked.stderr);
assert!(stderr.contains("lockfile needs update"));
assert!(stderr.contains("pray install"));
assert_eq!(
fs::read_to_string(repo.join("Prayfile.lock")).expect("preserved lockfile"),
original_lockfile
);
assert_eq!(
fs::read_to_string(repo.join("INSTRUCTIONS.md")).expect("preserved rendered"),
original_rendered
);
}
#[test]
fn install_lockfile_uses_project_relative_paths() {
let repo = temporary_directory("pray-install-lockfile-paths");
create_fixture(&repo);
assert!(run_pray(&repo, &["install"]).status.success());
let lockfile = fs::read_to_string(repo.join("Prayfile.lock")).expect("lockfile");
assert!(
!lockfile.contains("/Users/"),
"lockfile must not contain absolute paths: {lockfile}"
);
assert!(lockfile.contains(r#"path = "./packages/base""#));
assert!(lockfile.contains(r#"artifact = "path:./packages/base""#));
}
#[test]
fn install_frozen_rejects_stale_rendered_output() {
let repo = temporary_directory("pray-install-frozen");
create_fixture(&repo);
assert!(run_pray(&repo, &["install"]).status.success());
let rendered_path = repo.join("INSTRUCTIONS.md");
let rendered = fs::read_to_string(&rendered_path).expect("rendered file exists");
let rendered = rendered.replace("Testing guidance", "Changed guidance");
fs::write(&rendered_path, rendered).expect("rendered file rewritten");
let frozen = run_pray(&repo, &["install", "--frozen"]);
assert!(!frozen.status.success());
assert_eq!(frozen.status.code(), Some(5));
let stderr = String::from_utf8_lossy(&frozen.stderr);
assert!(stderr.contains("stale"));
assert!(stderr.contains("pray plan"));
}
#[test]
fn install_offline_accepts_explicit_local_paths() {
let repo = temporary_directory("pray-install-offline");
create_fixture(&repo);
let offline = run_pray(&repo, &["install", "--offline"]);
assert!(
offline.status.success(),
"offline install failed: {}",
String::from_utf8_lossy(&offline.stderr)
);
}
#[test]
fn install_offline_accepts_derived_package_paths_when_files_exist() {
let repo = temporary_directory("pray-install-offline-derived");
create_derived_fixture(&repo);
let offline = run_pray(&repo, &["install", "--offline"]);
assert!(
offline.status.success(),
"offline install failed: {}",
String::from_utf8_lossy(&offline.stderr)
);
assert!(repo.join("INSTRUCTIONS.md").is_file());
}
#[test]
fn install_reports_all_package_resolution_errors() {
let repo = temporary_directory("pray-install-multi-error");
fs::write(
repo.join("Prayfile"),
r#"
prayfile "1"
target :tool_a do
output "INSTRUCTIONS.md"
end
agent "missing/one", "~> 1.0", path: "packages/one"
agent "missing/two", "~> 1.0", path: "packages/two"
render mode: :managed, conflict: :fail, churn: :minimal
"#,
)
.expect("write Prayfile");
fs::create_dir_all(repo.join("packages/one")).expect("one directory");
fs::create_dir_all(repo.join("packages/two")).expect("two directory");
let install = run_pray(&repo, &["install"]);
assert!(!install.status.success());
assert_eq!(install.status.code(), Some(3));
let stderr = String::from_utf8_lossy(&install.stderr);
assert!(stderr.contains("missing/one:"));
assert!(stderr.contains("missing/two:"));
assert!(stderr.contains("no prayspec file found"));
}