use std::path::PathBuf;
use std::process::Command;
use serde_json::Value;
use tempfile::TempDir;
type TestResult = Result<(), Box<dyn std::error::Error>>;
const fn bin() -> &'static str {
env!("CARGO_BIN_EXE_prompter")
}
struct Home {
_root: TempDir,
path: PathBuf,
}
impl Home {
fn new() -> Result<Self, Box<dyn std::error::Error>> {
let root = tempfile::tempdir()?;
let path = root.path().to_path_buf();
Ok(Self { _root: root, path })
}
fn cmd(&self, args: &[&str]) -> Command {
let mut command = Command::new(bin());
command.args(args).env("HOME", &self.path);
command
}
fn write(&self, rel: &str, contents: &[u8]) -> TestResult {
let full = self.path.join(rel);
if let Some(parent) = full.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(full, contents)?;
Ok(())
}
fn join(&self, rel: &str) -> PathBuf {
self.path.join(rel)
}
}
fn first_line(bytes: &[u8]) -> Result<String, Box<dyn std::error::Error>> {
let text = String::from_utf8_lossy(bytes);
let line = text
.lines()
.next()
.ok_or("expected at least one output line")?;
Ok(line.to_owned())
}
fn field<'a>(value: &'a Value, key: &str) -> Result<&'a Value, Box<dyn std::error::Error>> {
value
.get(key)
.ok_or_else(|| format!("missing json field `{key}`").into())
}
#[test]
fn init_list_validate_run() -> TestResult {
let home = Home::new()?;
let init = home.cmd(&["init"]).output()?;
assert!(
init.status.success(),
"init failed: {}",
String::from_utf8_lossy(&init.stderr)
);
let list = home.cmd(&["list"]).output()?;
assert!(list.status.success());
let listing = String::from_utf8_lossy(&list.stdout);
assert!(listing.contains("python.api"), "listing: {listing}");
assert!(listing.contains("general.testing"), "listing: {listing}");
let validate = home.cmd(&["validate"]).output()?;
assert!(
validate.status.success(),
"validate failed: {}",
String::from_utf8_lossy(&validate.stderr)
);
let run = home.cmd(&["run", "python.api"]).output()?;
assert!(
run.status.success(),
"run failed: {}",
String::from_utf8_lossy(&run.stderr)
);
let output = String::from_utf8_lossy(&run.stdout);
assert!(output.starts_with("You are an LLM coding agent."));
assert!(output.contains("Today is "));
assert!(output.contains(", and you are running on a "));
assert!(output.contains(" system.\n\n"));
let c_bytes = std::fs::read(home.join(".local/prompter/library/a/b/c.md"))?;
let h_bytes = std::fs::read(home.join(".local/prompter/library/f/g/h.md"))?;
assert!(output.contains(&*String::from_utf8_lossy(&c_bytes)));
assert!(output.contains(&*String::from_utf8_lossy(&h_bytes)));
assert!(output.ends_with(
"Now, read the @AGENTS.md and @CLAUDE.md files in this directory, if they exist."
));
Ok(())
}
#[test]
fn init_reports_error_when_scaffold_paths_are_blocked() -> TestResult {
let cfg_blocked = Home::new()?;
cfg_blocked.write(".config", b"not a directory")?;
let out = cfg_blocked.cmd(&["init"]).output()?;
assert!(
!out.status.success(),
"init should fail when .config is a file"
);
let err = String::from_utf8_lossy(&out.stderr);
assert!(err.contains("Init failed"), "stderr: {err}");
let lib_blocked = Home::new()?;
lib_blocked.write(".local", b"not a directory")?;
let out = lib_blocked.cmd(&["init"]).output()?;
assert!(
!out.status.success(),
"init should fail when .local is a file"
);
let err = String::from_utf8_lossy(&out.stderr);
assert!(err.contains("Init failed"), "stderr: {err}");
let frag_blocked = Home::new()?;
frag_blocked.write(".local/prompter/library/a", b"occupied by a file")?;
frag_blocked.write(".config/prompter/config.toml", b"[root]\ndepends_on = []\n")?;
let out = frag_blocked.cmd(&["init"]).output()?;
assert!(
!out.status.success(),
"init should fail when a fragment parent path is a file"
);
let err = String::from_utf8_lossy(&out.stderr);
assert!(err.contains("Init failed"), "stderr: {err}");
Ok(())
}
#[test]
fn missing_file_and_unknown_profile_fail() -> TestResult {
let home = Home::new()?;
home.write(".local/prompter/library/.keep", b"")?;
home.write(
".config/prompter/config.toml",
b"[root]\ndepends_on = [\"does.not.exist.md\", \"unknown_profile\"]\n",
)?;
let validate = home.cmd(&["validate"]).output()?;
assert!(!validate.status.success());
let err = String::from_utf8_lossy(&validate.stderr);
assert!(err.contains("Missing file:"), "stderr: {err}");
assert!(err.contains("Unknown profile:"), "stderr: {err}");
assert!(err.starts_with("error: "), "stderr: {err}");
let run = home.cmd(&["run", "root"]).output()?;
assert!(!run.status.success());
let run_err = String::from_utf8_lossy(&run.stderr);
assert!(run_err.starts_with("error: "), "stderr: {run_err}");
Ok(())
}
#[test]
fn recursive_resolution_and_separator() -> TestResult {
let home = Home::new()?;
home.write(".local/prompter/library/a/x.md", b"AX\n")?;
home.write(".local/prompter/library/f/y.md", b"FY\n")?;
home.write(
".config/prompter/config.toml",
b"[child]\ndepends_on = [\"a/x.md\"]\n\n[root]\ndepends_on = [\"child\", \"f/y.md\", \"a/x.md\"]\n",
)?;
let run = home
.cmd(&["run", "--separator", "\\n--\\n", "root"])
.output()?;
assert!(
run.status.success(),
"run failed: {}",
String::from_utf8_lossy(&run.stderr)
);
let output = String::from_utf8_lossy(&run.stdout);
assert!(output.starts_with("You are an LLM coding agent."));
assert!(output.contains("Today is "));
assert!(output.contains(" system.\n\n"));
assert!(output.contains("AX\n"));
assert!(output.contains("\n--\n"));
assert!(output.contains("FY\n"));
assert_eq!(output.matches("AX\n").count(), 1, "output: {output}");
assert!(output.ends_with(
"Now, read the @AGENTS.md and @CLAUDE.md files in this directory, if they exist."
));
Ok(())
}
#[test]
fn system_renders_base_then_extra_profiles() -> TestResult {
let home = Home::new()?;
home.write(".local/prompter/library/g/base.md", b"BASE\n")?;
home.write(".local/prompter/library/g/extra.md", b"EXTRA\n")?;
home.write(
".config/prompter/config.toml",
b"[core.base]\ndepends_on = [\"g/base.md\"]\n\n[task]\ndepends_on = [\"g/extra.md\"]\n",
)?;
let base = home.cmd(&["system"]).output()?;
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 = home.cmd(&["system", "task"]).output()?;
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").ok_or("base present")?;
let extra_idx = combined_str.find("EXTRA\n").ok_or("extra present")?;
assert!(base_idx < extra_idx, "base must precede appended profiles");
Ok(())
}
#[test]
fn run_bare_omits_framing() -> TestResult {
let home = Home::new()?;
home.write(".local/prompter/library/a/x.md", b"AX\n")?;
home.write(".local/prompter/library/f/y.md", b"FY\n")?;
home.write(
".config/prompter/config.toml",
b"post_prompt = \"Config post-prompt\"\n\n[root]\ndepends_on = [\"a/x.md\", \"f/y.md\"]\n",
)?;
let run = home.cmd(&["run", "--bare", "root"]).output()?;
assert!(
run.status.success(),
"run --bare failed: {}",
String::from_utf8_lossy(&run.stderr)
);
let stdout = String::from_utf8_lossy(&run.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}");
Ok(())
}
#[test]
fn run_bare_honors_explicit_pre_prompt_without_date() -> TestResult {
let home = Home::new()?;
home.write(".local/prompter/library/a/x.md", b"AX\n")?;
home.write(
".config/prompter/config.toml",
b"[root]\ndepends_on = [\"a/x.md\"]\n",
)?;
let run = home
.cmd(&["run", "--bare", "--pre-prompt", "EXPLICIT-PRE\n", "root"])
.output()?;
assert!(
run.status.success(),
"run --bare --pre-prompt failed: {}",
String::from_utf8_lossy(&run.stderr)
);
let stdout = String::from_utf8_lossy(&run.stdout);
assert!(stdout.starts_with("EXPLICIT-PRE\n"), "stdout: {stdout}");
assert_eq!(stdout.matches("Today is ").count(), 0, "stdout: {stdout}");
assert!(stdout.contains("AX\n"), "stdout: {stdout}");
Ok(())
}
#[test]
fn system_bare_omits_framing() -> TestResult {
let home = Home::new()?;
home.write(".local/prompter/library/g/base.md", b"BASE\n")?;
home.write(
".config/prompter/config.toml",
b"[core.base]\ndepends_on = [\"g/base.md\"]\n",
)?;
let run = home.cmd(&["system", "--bare"]).output()?;
assert!(
run.status.success(),
"system --bare failed: {}",
String::from_utf8_lossy(&run.stderr)
);
let stdout = String::from_utf8_lossy(&run.stdout);
assert!(!stdout.contains("Today is "));
assert!(!stdout.contains("Now, read the @AGENTS.md"));
assert_eq!(stdout, "BASE\n");
Ok(())
}
#[test]
fn validate_failure_json_uses_shared_envelope() -> TestResult {
let home = Home::new()?;
home.write(".local/prompter/library/.keep", b"")?;
home.write(
".config/prompter/config.toml",
b"[A]\ndepends_on = [\"B\"]\n[B]\ndepends_on = [\"A\"]\n",
)?;
let out = home.cmd(&["--json", "validate"]).output()?;
assert!(!out.status.success());
let envelope: Value = serde_json::from_str(&first_line(&out.stderr)?)?;
assert_eq!(field(&envelope, "ok")?.as_bool(), Some(false));
assert_eq!(field(&envelope, "command")?.as_str(), Some("validate"));
let error = field(&envelope, "error")?;
assert_eq!(field(error, "code")?.as_str(), Some("ERROR"));
let message = field(error, "message")?
.as_str()
.ok_or("error.message must be a string")?;
assert!(message.contains("Cycle detected"), "envelope: {envelope}");
Ok(())
}
#[test]
fn list_success_json_uses_shared_envelope() -> TestResult {
let home = Home::new()?;
assert!(home.cmd(&["init"]).output()?.status.success());
let out = home.cmd(&["--json", "list"]).output()?;
assert!(out.status.success());
let envelope: Value = serde_json::from_str(&first_line(&out.stdout)?)?;
assert_eq!(field(&envelope, "ok")?.as_bool(), Some(true));
assert_eq!(field(&envelope, "command")?.as_str(), Some("list"));
assert!(
field(field(&envelope, "data")?, "profiles")?.is_array(),
"envelope: {envelope}"
);
Ok(())
}
#[test]
fn tree_success_json_uses_shared_envelope() -> TestResult {
let home = Home::new()?;
assert!(home.cmd(&["init"]).output()?.status.success());
let out = home.cmd(&["--json", "tree"]).output()?;
assert!(out.status.success());
let envelope: Value = serde_json::from_str(&first_line(&out.stdout)?)?;
assert_eq!(field(&envelope, "ok")?.as_bool(), Some(true));
assert_eq!(field(&envelope, "command")?.as_str(), Some("tree"));
assert!(
field(&envelope, "data")?.is_object(),
"envelope: {envelope}"
);
Ok(())
}
#[test]
fn run_success_json_uses_shared_envelope() -> TestResult {
let home = Home::new()?;
assert!(home.cmd(&["init"]).output()?.status.success());
let out = home.cmd(&["--json", "run", "python.api"]).output()?;
assert!(out.status.success());
let envelope: Value = serde_json::from_str(&first_line(&out.stdout)?)?;
assert_eq!(field(&envelope, "ok")?.as_bool(), Some(true));
assert_eq!(field(&envelope, "command")?.as_str(), Some("run"));
assert!(
field(field(&envelope, "data")?, "fragments")?.is_array(),
"envelope: {envelope}"
);
Ok(())
}
#[test]
fn validate_success_json_uses_shared_envelope() -> TestResult {
let home = Home::new()?;
assert!(home.cmd(&["init"]).output()?.status.success());
let out = home.cmd(&["--json", "validate"]).output()?;
assert!(out.status.success(), "validate should succeed");
let envelope: Value = serde_json::from_str(&first_line(&out.stdout)?)?;
assert_eq!(field(&envelope, "ok")?.as_bool(), Some(true));
assert_eq!(field(&envelope, "command")?.as_str(), Some("validate"));
assert_eq!(
field(field(&envelope, "data")?, "valid")?.as_bool(),
Some(true)
);
Ok(())
}
#[test]
fn run_failure_json_uses_shared_envelope() -> TestResult {
let home = Home::new()?;
home.write(".local/prompter/library/.keep", b"")?;
home.write(
".config/prompter/config.toml",
b"[root]\ndepends_on = [\"does.not.exist.md\"]\n",
)?;
let out = home.cmd(&["--json", "run", "root"]).output()?;
assert!(!out.status.success());
let envelope: Value = serde_json::from_str(&first_line(&out.stderr)?)?;
assert_eq!(field(&envelope, "ok")?.as_bool(), Some(false));
assert_eq!(field(&envelope, "command")?.as_str(), Some("run"));
let error = field(&envelope, "error")?;
assert_eq!(field(error, "code")?.as_str(), Some("ERROR"));
assert!(field(error, "message")?.is_string(), "envelope: {envelope}");
Ok(())
}
#[test]
fn tree_reports_error_on_unreadable_config() -> TestResult {
let home = Home::new()?;
home.write(".local/prompter/library/.keep", b"")?;
home.write(
".config/prompter/config.toml",
b"this is not valid toml {{{",
)?;
let out = home.cmd(&["tree"]).output()?;
assert!(!out.status.success());
let err = String::from_utf8_lossy(&out.stderr);
assert!(err.starts_with("error: "), "stderr: {err}");
Ok(())
}
#[test]
fn list_reports_error_on_unreadable_config() -> TestResult {
let home = Home::new()?;
home.write(".local/prompter/library/.keep", b"")?;
home.write(
".config/prompter/config.toml",
b"this is not valid toml {{{",
)?;
let out = home.cmd(&["list"]).output()?;
assert!(!out.status.success());
let err = String::from_utf8_lossy(&out.stderr);
assert!(err.starts_with("error: "), "stderr: {err}");
Ok(())
}
#[test]
fn cycle_detection_in_validate() -> TestResult {
let home = Home::new()?;
home.write(".local/prompter/library/.keep", b"")?;
home.write(
".config/prompter/config.toml",
b"[A]\ndepends_on = [\"B\"]\n[B]\ndepends_on = [\"A\"]\n",
)?;
let out = home.cmd(&["validate"]).output()?;
assert!(!out.status.success());
let err = String::from_utf8_lossy(&out.stderr);
assert!(err.contains("Cycle detected"), "stderr: {err}");
Ok(())
}
#[test]
fn version_flag() -> TestResult {
let home = Home::new()?;
let out = home.cmd(&["meta", "version"]).output()?;
assert!(out.status.success());
let got = String::from_utf8_lossy(&out.stdout).trim().to_owned();
assert_eq!(got, format!("prompter {}", env!("CARGO_PKG_VERSION")));
Ok(())
}
#[test]
fn version_json_flag() -> TestResult {
let home = Home::new()?;
let out = home.cmd(&["meta", "--json", "version"]).output()?;
assert!(out.status.success());
assert!(String::from_utf8_lossy(&out.stdout).contains("\"version\""));
Ok(())
}
#[test]
fn completions_bash() -> TestResult {
let home = Home::new()?;
let out = home.cmd(&["meta", "completions", "bash"]).output()?;
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("prompter"));
assert!(stdout.contains("_prompter"));
Ok(())
}
#[test]
fn completions_zsh() -> TestResult {
let home = Home::new()?;
let out = home.cmd(&["meta", "completions", "zsh"]).output()?;
assert!(out.status.success());
assert!(String::from_utf8_lossy(&out.stdout).contains("prompter"));
Ok(())
}
#[test]
fn completions_fish() -> TestResult {
let home = Home::new()?;
let out = home.cmd(&["meta", "completions", "fish"]).output()?;
assert!(out.status.success());
assert!(String::from_utf8_lossy(&out.stdout).contains("prompter"));
Ok(())
}
#[test]
fn doctor_command() -> TestResult {
let home = Home::new()?;
let out = home.cmd(&["meta", "doctor"]).output()?;
assert!(String::from_utf8_lossy(&out.stdout).contains("health check"));
Ok(())
}
#[test]
fn doctor_command_json() -> TestResult {
let home = Home::new()?;
let out = home.cmd(&["meta", "--json", "doctor"]).output()?;
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("\"ok\""), "stdout: {stdout}");
assert!(stdout.contains("\"checks\""), "stdout: {stdout}");
Ok(())
}
#[test]
fn help_flag() -> TestResult {
let home = Home::new()?;
let out = home.cmd(&["--help"]).output()?;
let combined = format!(
"{}{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
assert!(combined.contains("prompter") || combined.contains("Usage:"));
Ok(())
}
#[test]
fn run_with_family_substitutes_variant() -> TestResult {
let home = Home::new()?;
home.write(".local/prompter/library/general/rules.md", b"NEUTRAL\n")?;
home.write(
".local/prompter/library/general/families/gpt/rules.md",
b"GPT_VARIANT\n",
)?;
home.write(
".config/prompter/config.toml",
b"[root]\ndepends_on = [\"general/rules.md\"]\n",
)?;
let out = home
.cmd(&["run", "--bare", "--family", "gpt", "root"])
.output()?;
assert!(
out.status.success(),
"run failed: {}",
String::from_utf8_lossy(&out.stderr)
);
assert_eq!(String::from_utf8_lossy(&out.stdout), "GPT_VARIANT\n");
Ok(())
}
#[test]
fn validate_rejects_orphan_family_variant() -> TestResult {
let home = Home::new()?;
home.write(".local/prompter/library/general/rules.md", b"NEUTRAL\n")?;
home.write(
".local/prompter/library/general/families/gpt/orphan.md",
b"ORPHAN\n",
)?;
home.write(
".config/prompter/config.toml",
b"[root]\ndepends_on = [\"general/rules.md\"]\n",
)?;
let out = home.cmd(&["validate"]).output()?;
assert!(!out.status.success());
let err = String::from_utf8_lossy(&out.stderr);
assert!(err.contains("Orphan family variant"), "stderr: {err}");
Ok(())
}
#[test]
fn run_with_separator() -> TestResult {
let home = Home::new()?;
assert!(home.cmd(&["init"]).output()?.status.success());
let out = home
.cmd(&["run", "--separator", "\\n---\\n", "python.api"])
.output()?;
assert!(out.status.success());
assert!(String::from_utf8_lossy(&out.stdout).contains("\n---\n"));
Ok(())
}
#[test]
fn run_with_custom_pre_prompt() -> TestResult {
let home = Home::new()?;
assert!(home.cmd(&["init"]).output()?.status.success());
let out = home
.cmd(&["run", "--pre-prompt", "Custom prefix", "python.api"])
.output()?;
assert!(out.status.success());
assert!(String::from_utf8_lossy(&out.stdout).starts_with("Custom prefix"));
Ok(())
}
#[test]
fn run_with_custom_post_prompt() -> TestResult {
let home = Home::new()?;
assert!(home.cmd(&["init"]).output()?.status.success());
let out = home
.cmd(&["run", "--post-prompt", "Custom suffix", "python.api"])
.output()?;
assert!(out.status.success());
assert!(String::from_utf8_lossy(&out.stdout).ends_with("Custom suffix"));
Ok(())
}
#[test]
fn multiple_profiles_shorthand() -> TestResult {
let home = Home::new()?;
home.write(".local/prompter/library/shared/common.md", b"COMMON\n")?;
home.write(".local/prompter/library/a/only.md", b"A_ONLY\n")?;
home.write(".local/prompter/library/b/only.md", b"B_ONLY\n")?;
home.write(
".config/prompter/config.toml",
b"[profile.a]\ndepends_on = [\"shared/common.md\", \"a/only.md\"]\n\n[profile.b]\ndepends_on = [\"shared/common.md\", \"b/only.md\"]\n",
)?;
let out = home.cmd(&["run", "profile.a", "profile.b"]).output()?;
assert!(
out.status.success(),
"run failed: {}",
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8_lossy(&out.stdout);
assert_eq!(stdout.matches("COMMON").count(), 1, "stdout: {stdout}");
assert!(stdout.contains("A_ONLY"));
assert!(stdout.contains("B_ONLY"));
let common_pos = stdout.find("COMMON").ok_or("COMMON present")?;
let a_pos = stdout.find("A_ONLY").ok_or("A_ONLY present")?;
let b_pos = stdout.find("B_ONLY").ok_or("B_ONLY present")?;
assert!(common_pos < a_pos && a_pos < b_pos, "stdout: {stdout}");
Ok(())
}
#[test]
fn bundle_import_run_and_list() -> TestResult {
let home = Home::new()?;
home.write(".local/prompter/library/me/note.md", b"PERSONAL\n")?;
home.write("work/team-prompts/library/shared/guide.md", b"TEAM_GUIDE\n")?;
home.write(
"work/team-prompts/config.toml",
b"[team.base]\ndepends_on = [\"shared/guide.md\"]\n",
)?;
let bundle_config = home.join("work/team-prompts/config.toml");
let personal_cfg = format!(
"import = [\"{}\"]\n\n[my.work]\ndepends_on = [\"team.base\", \"me/note.md\"]\n",
bundle_config.display()
);
home.write(".config/prompter/config.toml", personal_cfg.as_bytes())?;
let list = home.cmd(&["list"]).output()?;
assert!(
list.status.success(),
"list failed: {}",
String::from_utf8_lossy(&list.stderr)
);
let listing = String::from_utf8_lossy(&list.stdout);
assert!(listing.contains("my.work"), "listing: {listing}");
assert!(listing.contains("team.base"), "listing: {listing}");
let json_list = home.cmd(&["--json", "list"]).output()?;
assert!(json_list.status.success());
let envelope: Value = serde_json::from_str(&first_line(&json_list.stdout)?)?;
let libraries = field(field(&envelope, "data")?, "libraries")?
.as_array()
.ok_or("data.libraries must be an array")?;
assert_eq!(libraries.len(), 2, "envelope: {envelope}");
let validate = home.cmd(&["validate"]).output()?;
assert!(
validate.status.success(),
"validate failed: {}",
String::from_utf8_lossy(&validate.stderr)
);
let run = home.cmd(&["run", "my.work"]).output()?;
assert!(
run.status.success(),
"run failed: {}",
String::from_utf8_lossy(&run.stderr)
);
let rendered = String::from_utf8_lossy(&run.stdout);
assert!(rendered.contains("TEAM_GUIDE"), "rendered: {rendered}");
assert!(rendered.contains("PERSONAL"), "rendered: {rendered}");
let team_pos = rendered.find("TEAM_GUIDE").ok_or("TEAM_GUIDE present")?;
let personal_pos = rendered.find("PERSONAL").ok_or("PERSONAL present")?;
assert!(team_pos < personal_pos, "rendered: {rendered}");
Ok(())
}
#[test]
fn bundle_import_duplicate_profile_errors() -> TestResult {
let home = Home::new()?;
home.write(".local/prompter/library/.keep", b"")?;
home.write("work/team-prompts/library/.keep", b"")?;
home.write(
"work/team-prompts/config.toml",
b"\n[clash]\ndepends_on = []\n",
)?;
let bundle_config = home.join("work/team-prompts/config.toml");
let personal_cfg = format!(
"\nimport = [\"{}\"]\n\n[clash]\ndepends_on = []\n",
bundle_config.display()
);
home.write(".config/prompter/config.toml", personal_cfg.as_bytes())?;
let out = home.cmd(&["validate"]).output()?;
assert!(!out.status.success());
let err = String::from_utf8_lossy(&out.stderr);
assert!(err.contains("Duplicate profile `clash`"), "stderr: {err}");
Ok(())
}