use std::env;
use std::fs;
use std::io::Read;
use std::path::PathBuf;
use std::process::Command;
fn tmp_home(prefix: &str) -> PathBuf {
let mut p = env::temp_dir();
let unique = format!(
"{}_{}_{}",
prefix,
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
);
p.push(unique);
p
}
fn read_all(path: &std::path::Path) -> Vec<u8> {
let mut f = fs::File::open(path).unwrap();
let mut buf = Vec::new();
f.read_to_end(&mut buf).unwrap();
buf
}
const fn bin_path() -> &'static str {
env!("CARGO_BIN_EXE_prompter")
}
#[test]
fn test_init_list_validate_run() {
let home = tmp_home("prompter_it_home");
fs::create_dir_all(&home).unwrap();
let out = Command::new(bin_path())
.env("HOME", &home)
.arg("init")
.output()
.unwrap();
assert!(
out.status.success(),
"init failed: {}",
String::from_utf8_lossy(&out.stderr)
);
let out = Command::new(bin_path())
.env("HOME", &home)
.arg("list")
.output()
.unwrap();
assert!(out.status.success());
let list = String::from_utf8_lossy(&out.stdout);
assert!(list.contains("python.api"));
assert!(list.contains("general.testing"));
let out = Command::new(bin_path())
.env("HOME", &home)
.arg("validate")
.output()
.unwrap();
assert!(
out.status.success(),
"validate failed: {}",
String::from_utf8_lossy(&out.stderr)
);
let out = Command::new(bin_path())
.env("HOME", &home)
.arg("run")
.arg("python.api")
.output()
.unwrap();
assert!(
out.status.success(),
"run failed: {}",
String::from_utf8_lossy(&out.stderr)
);
let output_str = String::from_utf8_lossy(&out.stdout);
assert!(output_str.starts_with("You are an LLM coding agent."));
assert!(output_str.contains("Today is "));
assert!(output_str.contains(", and you are running on a "));
assert!(output_str.contains(" system.\n\n"));
let lib = home.join(".local/prompter/library");
let c_bytes = read_all(&lib.join("a/b/c.md"));
let h_bytes = read_all(&lib.join("f/g/h.md"));
let c_content = String::from_utf8_lossy(&c_bytes);
let h_content = String::from_utf8_lossy(&h_bytes);
assert!(output_str.contains(&*c_content));
assert!(output_str.contains(&*h_content));
assert!(output_str.ends_with(
"Now, read the @AGENTS.md and @CLAUDE.md files in this directory, if they exist."
));
}
#[test]
fn test_missing_file_and_unknown_profile_fail() {
let home = tmp_home("prompter_it_missing");
let cfg_path = home.join(".config/prompter");
let lib_path = home.join(".local/prompter/library");
fs::create_dir_all(&cfg_path).unwrap();
fs::create_dir_all(&lib_path).unwrap();
let cfg = r#"
[root]
depends_on = ["does.not.exist.md", "unknown_profile"]
"#;
fs::write(cfg_path.join("config.toml"), cfg).unwrap();
let out = Command::new(bin_path())
.env("HOME", &home)
.arg("validate")
.output()
.unwrap();
assert!(!out.status.success());
let err = String::from_utf8_lossy(&out.stderr);
assert!(err.contains("Missing file:"));
assert!(err.contains("Unknown profile:"));
assert!(
err.starts_with("error: "),
"validate stderr should use shared error prefix, got: {err}"
);
let out = Command::new(bin_path())
.env("HOME", &home)
.arg("run")
.arg("root")
.output()
.unwrap();
assert!(!out.status.success());
let err = String::from_utf8_lossy(&out.stderr);
assert!(
err.starts_with("error: "),
"run stderr should use shared error prefix, got: {err}"
);
}
#[test]
fn test_recursive_resolution_and_separator() {
let home = tmp_home("prompter_it_recursive");
let cfg_path = home.join(".config/prompter");
let lib_path = home.join(".local/prompter/library");
fs::create_dir_all(&cfg_path).unwrap();
fs::create_dir_all(lib_path.join("a")).unwrap();
fs::create_dir_all(lib_path.join("f")).unwrap();
fs::write(home.join(".local/prompter/library/a/x.md"), b"AX\n").unwrap();
fs::write(home.join(".local/prompter/library/f/y.md"), b"FY\n").unwrap();
let cfg = r#"
[child]
depends_on = ["a/x.md"]
[root]
depends_on = ["child", "f/y.md", "a/x.md"]
"#;
fs::write(cfg_path.join("config.toml"), cfg).unwrap();
let out = Command::new(bin_path())
.env("HOME", &home)
.args(["run", "--separator", "\\n--\\n", "root"]) .output()
.unwrap();
assert!(
out.status.success(),
"run failed: {}",
String::from_utf8_lossy(&out.stderr)
);
let output_str = String::from_utf8_lossy(&out.stdout);
assert!(output_str.starts_with("You are an LLM coding agent."));
assert!(output_str.contains("Today is "));
assert!(output_str.contains(", and you are running on a "));
assert!(output_str.contains(" system.\n\n"));
assert!(output_str.contains("AX\n"));
assert!(output_str.contains("\n--\n"));
assert!(output_str.contains("FY\n"));
assert!(output_str.ends_with(
"Now, read the @AGENTS.md and @CLAUDE.md files in this directory, if they exist."
));
}
#[test]
fn test_system_renders_base_then_extra_profiles() {
let home = tmp_home("prompter_it_system");
let cfg_path = home.join(".config/prompter");
let lib_path = home.join(".local/prompter/library");
fs::create_dir_all(&cfg_path).unwrap();
fs::create_dir_all(lib_path.join("g")).unwrap();
fs::write(home.join(".local/prompter/library/g/base.md"), b"BASE\n").unwrap();
fs::write(home.join(".local/prompter/library/g/extra.md"), b"EXTRA\n").unwrap();
let cfg = r#"
[core.system]
depends_on = ["g/base.md"]
[task]
depends_on = ["g/extra.md"]
"#;
fs::write(cfg_path.join("config.toml"), cfg).unwrap();
let base = Command::new(bin_path())
.env("HOME", &home)
.arg("system")
.output()
.unwrap();
assert!(
base.status.success(),
"system failed: {}",
String::from_utf8_lossy(&base.stderr)
);
let base_str = String::from_utf8_lossy(&base.stdout);
assert!(base_str.contains("BASE\n"));
assert!(!base_str.contains("EXTRA\n"));
let combined = Command::new(bin_path())
.env("HOME", &home)
.args(["system", "task"])
.output()
.unwrap();
assert!(
combined.status.success(),
"system task failed: {}",
String::from_utf8_lossy(&combined.stderr)
);
let combined_str = String::from_utf8_lossy(&combined.stdout);
let base_idx = combined_str.find("BASE\n").expect("base present");
let extra_idx = combined_str.find("EXTRA\n").expect("extra present");
assert!(base_idx < extra_idx, "base must precede appended profiles");
}
#[test]
fn test_run_bare_omits_framing() {
let home = tmp_home("prompter_it_bare");
let cfg_path = home.join(".config/prompter");
let lib_path = home.join(".local/prompter/library");
fs::create_dir_all(&cfg_path).unwrap();
fs::create_dir_all(lib_path.join("a")).unwrap();
fs::create_dir_all(lib_path.join("f")).unwrap();
fs::write(lib_path.join("a/x.md"), b"AX\n").unwrap();
fs::write(lib_path.join("f/y.md"), b"FY\n").unwrap();
let cfg = r#"
post_prompt = "Config post-prompt"
[root]
depends_on = ["a/x.md", "f/y.md"]
"#;
fs::write(cfg_path.join("config.toml"), cfg).unwrap();
let out = Command::new(bin_path())
.env("HOME", &home)
.args(["run", "--bare", "root"])
.output()
.unwrap();
assert!(
out.status.success(),
"run --bare failed: {}",
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(!stdout.starts_with("You are an LLM coding agent."));
assert!(!stdout.contains("Today is "));
assert!(!stdout.contains("Config post-prompt"));
assert!(!stdout.contains("Now, read the @AGENTS.md"));
assert!(stdout.starts_with("AX\n"), "stdout: {stdout}");
assert!(stdout.contains("FY\n"), "stdout: {stdout}");
}
#[test]
fn test_run_bare_honors_explicit_pre_prompt_without_date() {
let home = tmp_home("prompter_it_bare_explicit");
let cfg_path = home.join(".config/prompter");
let lib_path = home.join(".local/prompter/library");
fs::create_dir_all(&cfg_path).unwrap();
fs::create_dir_all(lib_path.join("a")).unwrap();
fs::write(lib_path.join("a/x.md"), b"AX\n").unwrap();
let cfg = r#"
[root]
depends_on = ["a/x.md"]
"#;
fs::write(cfg_path.join("config.toml"), cfg).unwrap();
let out = Command::new(bin_path())
.env("HOME", &home)
.args(["run", "--bare", "--pre-prompt", "EXPLICIT-PRE\n", "root"])
.output()
.unwrap();
assert!(
out.status.success(),
"run --bare --pre-prompt failed: {}",
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.starts_with("EXPLICIT-PRE\n"), "stdout: {stdout}");
assert_eq!(
stdout.matches("Today is ").count(),
0,
"bare output must be date-free for cache stability: {stdout}"
);
assert!(stdout.contains("AX\n"), "stdout: {stdout}");
}
#[test]
fn test_system_bare_omits_framing() {
let home = tmp_home("prompter_it_system_bare");
let cfg_path = home.join(".config/prompter");
let lib_path = home.join(".local/prompter/library");
fs::create_dir_all(&cfg_path).unwrap();
fs::create_dir_all(lib_path.join("g")).unwrap();
fs::write(lib_path.join("g/base.md"), b"BASE\n").unwrap();
let cfg = r#"
[core.system]
depends_on = ["g/base.md"]
"#;
fs::write(cfg_path.join("config.toml"), cfg).unwrap();
let out = Command::new(bin_path())
.env("HOME", &home)
.args(["system", "--bare"])
.output()
.unwrap();
assert!(
out.status.success(),
"system --bare failed: {}",
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(!stdout.contains("Today is "));
assert!(!stdout.contains("Now, read the @AGENTS.md"));
assert_eq!(stdout, "BASE\n");
}
#[test]
fn test_validate_failure_json_uses_shared_envelope() {
let home = tmp_home("prompter_it_validate_json_err");
let cfg_path = home.join(".config/prompter");
let lib_path = home.join(".local/prompter/library");
fs::create_dir_all(&cfg_path).unwrap();
fs::create_dir_all(&lib_path).unwrap();
let cfg = r#"
[A]
depends_on = ["B"]
[B]
depends_on = ["A"]
"#;
fs::write(cfg_path.join("config.toml"), cfg).unwrap();
let out = Command::new(bin_path())
.env("HOME", &home)
.args(["--json", "validate"])
.output()
.unwrap();
assert!(!out.status.success());
let stderr_text = String::from_utf8_lossy(&out.stderr);
let line = stderr_text
.lines()
.next()
.expect("validate --json failure must emit at least one stderr line");
let envelope: serde_json::Value =
serde_json::from_str(line).expect("stderr line must be valid json");
assert_eq!(envelope["ok"], false);
assert_eq!(envelope["command"], "validate");
assert_eq!(envelope["error"]["code"], "ERROR");
assert!(
envelope["error"]["message"]
.as_str()
.is_some_and(|m| m.contains("Cycle detected")),
"envelope missing cycle message: {envelope}"
);
}
#[test]
fn test_list_success_json_uses_shared_envelope() {
let home = tmp_home("prompter_it_list_json_ok");
fs::create_dir_all(&home).unwrap();
let init = Command::new(bin_path())
.env("HOME", &home)
.arg("init")
.output()
.unwrap();
assert!(init.status.success());
let out = Command::new(bin_path())
.env("HOME", &home)
.args(["--json", "list"])
.output()
.unwrap();
assert!(out.status.success());
let stdout_text = String::from_utf8_lossy(&out.stdout);
let line = stdout_text
.lines()
.next()
.expect("list --json must emit at least one stdout line");
let envelope: serde_json::Value =
serde_json::from_str(line).expect("stdout line must be valid json");
assert_eq!(envelope["ok"], true);
assert_eq!(envelope["command"], "list");
assert!(
envelope["data"]["profiles"].is_array(),
"data.profiles missing: {envelope}"
);
}
#[test]
fn test_tree_success_json_uses_shared_envelope() {
let home = tmp_home("prompter_it_tree_json_ok");
fs::create_dir_all(&home).unwrap();
let init = Command::new(bin_path())
.env("HOME", &home)
.arg("init")
.output()
.unwrap();
assert!(init.status.success());
let out = Command::new(bin_path())
.env("HOME", &home)
.args(["--json", "tree"])
.output()
.unwrap();
assert!(out.status.success());
let stdout_text = String::from_utf8_lossy(&out.stdout);
let line = stdout_text
.lines()
.next()
.expect("tree --json must emit at least one stdout line");
let envelope: serde_json::Value =
serde_json::from_str(line).expect("stdout line must be valid json");
assert_eq!(envelope["ok"], true);
assert_eq!(envelope["command"], "tree");
assert!(envelope["data"].is_object(), "data missing: {envelope}");
}
#[test]
fn test_run_success_json_uses_shared_envelope() {
let home = tmp_home("prompter_it_run_json_ok");
fs::create_dir_all(&home).unwrap();
let init = Command::new(bin_path())
.env("HOME", &home)
.arg("init")
.output()
.unwrap();
assert!(init.status.success());
let out = Command::new(bin_path())
.env("HOME", &home)
.args(["--json", "run", "python.api"])
.output()
.unwrap();
assert!(out.status.success());
let stdout_text = String::from_utf8_lossy(&out.stdout);
let line = stdout_text
.lines()
.next()
.expect("run --json must emit at least one stdout line");
let envelope: serde_json::Value =
serde_json::from_str(line).expect("stdout line must be valid json");
assert_eq!(envelope["ok"], true);
assert_eq!(envelope["command"], "run");
assert!(
envelope["data"]["fragments"].is_array(),
"data.fragments missing: {envelope}"
);
}
#[test]
fn test_validate_success_json_uses_shared_envelope() {
let home = tmp_home("prompter_it_validate_json_ok");
fs::create_dir_all(&home).unwrap();
let init = Command::new(bin_path())
.env("HOME", &home)
.arg("init")
.output()
.unwrap();
assert!(init.status.success());
let out = Command::new(bin_path())
.env("HOME", &home)
.args(["--json", "validate"])
.output()
.unwrap();
assert!(out.status.success(), "validate should succeed");
let stdout_text = String::from_utf8_lossy(&out.stdout);
let line = stdout_text
.lines()
.next()
.expect("validate --json success must emit at least one stdout line");
let envelope: serde_json::Value =
serde_json::from_str(line).expect("stdout line must be valid json");
assert_eq!(envelope["ok"], true);
assert_eq!(envelope["command"], "validate");
assert_eq!(envelope["data"]["valid"], true);
}
#[test]
fn test_run_failure_json_uses_shared_envelope() {
let home = tmp_home("prompter_it_run_json_err");
let cfg_path = home.join(".config/prompter");
let lib_path = home.join(".local/prompter/library");
fs::create_dir_all(&cfg_path).unwrap();
fs::create_dir_all(&lib_path).unwrap();
let cfg = r#"
[root]
depends_on = ["does.not.exist.md"]
"#;
fs::write(cfg_path.join("config.toml"), cfg).unwrap();
let out = Command::new(bin_path())
.env("HOME", &home)
.args(["--json", "run", "root"])
.output()
.unwrap();
assert!(!out.status.success());
let stderr_text = String::from_utf8_lossy(&out.stderr);
let line = stderr_text
.lines()
.next()
.expect("run --json failure must emit at least one stderr line");
let envelope: serde_json::Value =
serde_json::from_str(line).expect("stderr line must be valid json");
assert_eq!(envelope["ok"], false);
assert_eq!(envelope["command"], "run");
assert_eq!(envelope["error"]["code"], "ERROR");
assert!(
envelope["error"]["message"].is_string(),
"envelope missing error.message: {envelope}"
);
}
#[test]
fn test_cycle_detection_in_validate() {
let home = tmp_home("prompter_it_cycle");
let cfg_path = home.join(".config/prompter");
let lib_path = home.join(".local/prompter/library");
fs::create_dir_all(&cfg_path).unwrap();
fs::create_dir_all(&lib_path).unwrap();
let cfg = r#"
[A]
depends_on = ["B"]
[B]
depends_on = ["A"]
"#;
fs::write(cfg_path.join("config.toml"), cfg).unwrap();
let out = Command::new(bin_path())
.env("HOME", &home)
.arg("validate")
.output()
.unwrap();
assert!(!out.status.success());
let err = String::from_utf8_lossy(&out.stderr);
assert!(err.contains("Cycle detected"), "stderr: {err}");
}
#[test]
fn test_version_flag() {
let out = Command::new(bin_path())
.args(["meta", "version"])
.output()
.unwrap();
assert!(out.status.success());
let got = String::from_utf8_lossy(&out.stdout).trim().to_string();
let expected = format!("prompter {}", env!("CARGO_PKG_VERSION"));
assert_eq!(got, expected);
}
#[test]
fn test_version_json_flag() {
let out = Command::new(bin_path())
.args(["meta", "--json", "version"])
.output()
.unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("\"version\""));
}
#[test]
fn test_completions_bash() {
let out = Command::new(bin_path())
.args(["meta", "completions", "bash"])
.output()
.unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("prompter"));
assert!(stdout.contains("_prompter"));
}
#[test]
fn test_completions_zsh() {
let out = Command::new(bin_path())
.args(["meta", "completions", "zsh"])
.output()
.unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("prompter"));
}
#[test]
fn test_completions_fish() {
let out = Command::new(bin_path())
.args(["meta", "completions", "fish"])
.output()
.unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("prompter"));
}
#[test]
fn test_doctor_command() {
let out = Command::new(bin_path())
.args(["meta", "doctor"])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("health check"));
}
#[test]
fn test_doctor_command_json() {
let out = Command::new(bin_path())
.args(["meta", "--json", "doctor"])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("\"ok\""));
assert!(stdout.contains("\"checks\""));
}
#[test]
fn test_help_flag() {
let out = Command::new(bin_path()).arg("--help").output().unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
let stderr = String::from_utf8_lossy(&out.stderr);
let combined = format!("{stdout}{stderr}");
assert!(combined.contains("prompter") || combined.contains("Usage:"));
}
#[test]
fn test_run_with_separator() {
let home = tmp_home("prompter_it_sep");
fs::create_dir_all(&home).unwrap();
Command::new(bin_path())
.env("HOME", &home)
.arg("init")
.output()
.unwrap();
let out = Command::new(bin_path())
.env("HOME", &home)
.args(["run", "--separator", "\\n---\\n", "python.api"])
.output()
.unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("\n---\n"));
}
#[test]
fn test_run_with_custom_pre_prompt() {
let home = tmp_home("prompter_it_pre");
fs::create_dir_all(&home).unwrap();
Command::new(bin_path())
.env("HOME", &home)
.arg("init")
.output()
.unwrap();
let out = Command::new(bin_path())
.env("HOME", &home)
.args(["run", "--pre-prompt", "Custom prefix", "python.api"])
.output()
.unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.starts_with("Custom prefix"));
}
#[test]
fn test_run_with_custom_post_prompt() {
let home = tmp_home("prompter_it_post");
fs::create_dir_all(&home).unwrap();
Command::new(bin_path())
.env("HOME", &home)
.arg("init")
.output()
.unwrap();
let out = Command::new(bin_path())
.env("HOME", &home)
.args(["run", "--post-prompt", "Custom suffix", "python.api"])
.output()
.unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.ends_with("Custom suffix"));
}
#[test]
fn test_multiple_profiles_shorthand() {
let home = tmp_home("prompter_it_multi");
let cfg_path = home.join(".config/prompter");
let lib_path = home.join(".local/prompter/library");
fs::create_dir_all(&cfg_path).unwrap();
fs::create_dir_all(lib_path.join("a")).unwrap();
fs::create_dir_all(lib_path.join("b")).unwrap();
fs::create_dir_all(lib_path.join("shared")).unwrap();
fs::write(lib_path.join("shared/common.md"), b"COMMON\n").unwrap();
fs::write(lib_path.join("a/only.md"), b"A_ONLY\n").unwrap();
fs::write(lib_path.join("b/only.md"), b"B_ONLY\n").unwrap();
let cfg = r#"
[profile.a]
depends_on = ["shared/common.md", "a/only.md"]
[profile.b]
depends_on = ["shared/common.md", "b/only.md"]
"#;
fs::write(cfg_path.join("config.toml"), cfg).unwrap();
let out = Command::new(bin_path())
.env("HOME", &home)
.args(["run", "profile.a", "profile.b"])
.output()
.unwrap();
assert!(
out.status.success(),
"run failed: {}",
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8_lossy(&out.stdout);
let common_count = stdout.matches("COMMON").count();
assert_eq!(
common_count, 1,
"shared/common.md should appear exactly once, found {common_count}"
);
assert!(stdout.contains("A_ONLY"));
assert!(stdout.contains("B_ONLY"));
let common_pos = stdout.find("COMMON").unwrap();
let a_pos = stdout.find("A_ONLY").unwrap();
let b_pos = stdout.find("B_ONLY").unwrap();
assert!(
common_pos < a_pos && a_pos < b_pos,
"Files should appear in order: COMMON < A_ONLY < B_ONLY"
);
}
#[test]
fn test_bundle_import_run_and_list() {
let home = tmp_home("prompter_bundle_import_e2e");
let personal_cfg_dir = home.join(".config/prompter");
let personal_lib = home.join(".local/prompter/library");
fs::create_dir_all(&personal_cfg_dir).unwrap();
fs::create_dir_all(personal_lib.join("me")).unwrap();
fs::write(personal_lib.join("me/note.md"), b"PERSONAL\n").unwrap();
let bundle_dir = home.join("work/team-prompts");
fs::create_dir_all(bundle_dir.join("library/shared")).unwrap();
fs::write(bundle_dir.join("library/shared/guide.md"), b"TEAM_GUIDE\n").unwrap();
fs::write(
bundle_dir.join("config.toml"),
r#"
[team.base]
depends_on = ["shared/guide.md"]
"#,
)
.unwrap();
let personal_cfg = format!(
r#"
import = ["{}"]
[my.work]
depends_on = ["team.base", "me/note.md"]
"#,
bundle_dir.join("config.toml").display()
);
fs::write(personal_cfg_dir.join("config.toml"), personal_cfg).unwrap();
let out = Command::new(bin_path())
.env("HOME", &home)
.arg("list")
.output()
.unwrap();
assert!(
out.status.success(),
"list failed: {}",
String::from_utf8_lossy(&out.stderr)
);
let listing = String::from_utf8_lossy(&out.stdout);
assert!(listing.contains("my.work"), "listing: {listing}");
assert!(listing.contains("team.base"), "listing: {listing}");
let out = Command::new(bin_path())
.env("HOME", &home)
.args(["--json", "list"])
.output()
.unwrap();
assert!(out.status.success());
let stdout_text = String::from_utf8_lossy(&out.stdout);
let line = stdout_text.lines().next().unwrap();
let envelope: serde_json::Value = serde_json::from_str(line).unwrap();
let libraries = envelope["data"]["libraries"].as_array().unwrap();
assert_eq!(
libraries.len(),
2,
"expected 2 library roots, got: {envelope}"
);
let out = Command::new(bin_path())
.env("HOME", &home)
.arg("validate")
.output()
.unwrap();
assert!(
out.status.success(),
"validate failed: {}",
String::from_utf8_lossy(&out.stderr)
);
let out = Command::new(bin_path())
.env("HOME", &home)
.args(["run", "my.work"])
.output()
.unwrap();
assert!(
out.status.success(),
"run failed: {}",
String::from_utf8_lossy(&out.stderr)
);
let rendered = String::from_utf8_lossy(&out.stdout);
assert!(rendered.contains("TEAM_GUIDE"), "rendered: {rendered}");
assert!(rendered.contains("PERSONAL"), "rendered: {rendered}");
let team_pos = rendered.find("TEAM_GUIDE").unwrap();
let personal_pos = rendered.find("PERSONAL").unwrap();
assert!(team_pos < personal_pos);
}
#[test]
fn test_bundle_import_duplicate_profile_errors() {
let home = tmp_home("prompter_bundle_dup_e2e");
let personal_cfg_dir = home.join(".config/prompter");
let personal_lib = home.join(".local/prompter/library");
fs::create_dir_all(&personal_cfg_dir).unwrap();
fs::create_dir_all(&personal_lib).unwrap();
let bundle_dir = home.join("work/team-prompts");
fs::create_dir_all(bundle_dir.join("library")).unwrap();
fs::write(
bundle_dir.join("config.toml"),
"\n[clash]\ndepends_on = []\n",
)
.unwrap();
let personal_cfg = format!(
"\nimport = [\"{}\"]\n\n[clash]\ndepends_on = []\n",
bundle_dir.join("config.toml").display()
);
fs::write(personal_cfg_dir.join("config.toml"), personal_cfg).unwrap();
let out = Command::new(bin_path())
.env("HOME", &home)
.arg("validate")
.output()
.unwrap();
assert!(!out.status.success());
let err = String::from_utf8_lossy(&out.stderr);
assert!(err.contains("Duplicate profile `clash`"), "stderr: {err}");
}