use std::fs;
use std::path::{Path, PathBuf};
use std::process::ExitCode;
pub const DEFAULT_ORCHESTRATOR_URL: &str = "https://api.peeramid.xyz";
pub const DEFAULT_ROOM: &str = "demo";
pub const DEFAULT_TOKEN_ENV: &str = "QUORUM_DEMO_TOKEN";
pub struct WorkspaceSpec<'a> {
pub orchestrator_url: &'a str,
pub room: &'a str,
pub token_ref: &'a str,
pub agents: &'a [String],
}
pub fn render_workspace(spec: &WorkspaceSpec<'_>) -> String {
let orchestrator_url = spec.orchestrator_url;
let room = spec.room;
let token_ref = spec.token_ref;
let agents = spec.agents;
let agents_yaml = if agents.is_empty() {
String::from(
" # Edit: list the agent names this room dispatches to, e.g.\n # agents: [\"CortexA\", \"CortexB\"]\n agents: []\n",
)
} else {
let quoted: Vec<String> = agents.iter().map(|a| format!("\"{a}\"")).collect();
format!(" agents: [{}]\n", quoted.join(", "))
};
format!(
r#"# =============================================================================
# quorum workspace — generated by `quorum init`
# =============================================================================
#
# This is a minimal workspace pointing at one remote orchestrator and
# one room. Edit freely — the file format matches the schema documented
# at https://docs.rs/quorum-cli (WorkspaceConfig).
#
# To run a deliberation (export the bearer first if `token` above is an
# ${{ENV}} reference; a file: reference needs no export):
# quorum run --room {room} "<your topic>"
orchestrators:
primary:
mode: remote
address: "{orchestrator_url}"
# Bearer token resolved at runtime — an env var (${{VAR}}) or a
# file (file:/path). Never commit the raw secret here.
token: "{token_ref}"
policies:
default:
{agents_yaml} max_rounds: 3
effort: 0.7
rooms:
{room}:
policy: default
orchestrator: primary
default_room: {room}
"#,
)
}
pub fn run(target: &Path, spec: &WorkspaceSpec<'_>, force: bool) -> ExitCode {
if target.exists() && !force {
eprintln!(
"error: {} already exists. Re-run with --force to overwrite.",
target.display()
);
return ExitCode::FAILURE;
}
let body = render_workspace(spec);
if let Some(parent) = target.parent()
&& !parent.as_os_str().is_empty()
&& let Err(e) = fs::create_dir_all(parent)
{
eprintln!("error: failed to create parent directory {parent:?}: {e}");
return ExitCode::FAILURE;
}
if let Err(e) = fs::write(target, &body) {
eprintln!("error: failed to write {}: {e}", target.display());
return ExitCode::FAILURE;
}
let export_hint = spec
.token_ref
.strip_prefix("${")
.and_then(|s| s.strip_suffix('}'))
.map(|env| format!(" export {env}=<your bearer token>\n"))
.unwrap_or_default();
println!(
"Wrote workspace config to {}\n\nNext steps:\n{export_hint} quorum run --room {} \"<your topic>\"",
target.display(),
spec.room,
);
ExitCode::SUCCESS
}
fn preflight_writable(target: &Path) -> Result<(), String> {
let parent = target
.parent()
.filter(|p| !p.as_os_str().is_empty())
.unwrap_or_else(|| Path::new("."));
if !parent.exists() {
std::fs::create_dir_all(parent).map_err(|e| e.to_string())?;
}
let probe = parent.join(".nsed-write-probe");
std::fs::write(&probe, b"").map_err(|e| e.to_string())?;
let _ = std::fs::remove_file(&probe);
Ok(())
}
fn token_ref_for_onboard(out_dir: Option<&Path>) -> String {
let dir = out_dir
.map(Path::to_path_buf)
.or_else(crate::cli::endpoint::nsed_dir)
.unwrap_or_else(|| PathBuf::from("."));
let path = dir.join("operator.token");
let abs = std::fs::canonicalize(&path).unwrap_or(path);
format!("file:{}", abs.display())
}
fn is_valid_agent_name(name: &str) -> bool {
!name.is_empty()
&& name
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.')
}
fn render_fleet_yaml(agents: &[String]) -> String {
let agent_names: Vec<String> = if agents.is_empty() {
vec!["cortex-a".to_string()]
} else {
agents.iter().map(|s| s.to_string()).collect()
};
let mut agent_entries = String::new();
for name in &agent_names {
agent_entries.push_str(&format!(
" - name: {name}\n provider_id: openai\n model_name: gpt-4o-mini\n",
));
}
format!(
r#"# =============================================================================
# quorum agent fleet — generated by `quorum init --agent-fleet`
# =============================================================================
#
# This file configures the AGENT PROCESS that `quorum serve` runs.
# Each entry under `agents:` becomes one worker connected to the
# orchestrator's NATS bus.
#
# Distinct from `nsed.yaml` (which configures task SUBMISSION via
# `quorum run`/`status`/`trace`/`tui`).
#
# Run:
# quorum serve --nats-url <url-from-quorum-redeem>
#
# By default `quorum serve` reads `./agent.yml` + `~/.nsed/agent.creds`
# from `quorum redeem`. Override with --config / --nats-creds.
providers:
# ── OpenAI-compatible LLMs ───────────────────────────────────────────
# Default option. Works for OpenAI itself, plus any provider that
# speaks the OpenAI wire format (Groq, DeepSeek, Together,
# local llama.cpp, etc.) — just change `base_url`.
# For self-hosted vLLM, point `base_url` at it and uncomment `engine`.
openai:
type: openai
base_url: "https://api.openai.com/v1"
api_key: "${{OPENAI_API_KEY}}" # resolved from your shell env at runtime
# engine: vllm # tool-call parsing strategy. vllm | vllm_xml_responses |
# # gpt-oss (alias harmony). Omit for the default OpenAI style.
# ── Claude CLI ───────────────────────────────────────────────────────
# Uncomment if you have `claude` on $PATH and want the CLI to be the
# agent runtime. No api_key here — Claude CLI handles its own auth.
# claude_cli:
# type: claude
# ── Subprocess agent (any language) via the exec protocol ────────────
# Uncomment to drive an agent from Python/TypeScript/etc. via
# stdin/stdout framing. See docs/reference/exec-agent-protocol.md.
# exec_local:
# type: exec
# ── Subprocess agent via Model Context Protocol ──────────────────────
# Uncomment for an MCP-server-backed agent (Claude Code, generic MCP).
# See docs/reference/mcp-agent-protocol.md.
# mcp_local:
# type: mcp
agents:
{agent_entries}"#,
)
}
pub fn run_agent_fleet(target: &Path, agents: &[String], force: bool) -> ExitCode {
for name in agents {
if !is_valid_agent_name(name) {
eprintln!(
"error: invalid agent name {name:?}. Allowed characters: ASCII letters, digits, `-`, `_`, `.` (and non-empty)."
);
return ExitCode::FAILURE;
}
}
if target.exists() && !force {
eprintln!(
"error: {} already exists. Re-run with --force to overwrite.",
target.display()
);
return ExitCode::FAILURE;
}
let body = render_fleet_yaml(agents);
if let Some(parent) = target.parent()
&& !parent.as_os_str().is_empty()
&& let Err(e) = fs::create_dir_all(parent)
{
eprintln!("error: failed to create parent directory {parent:?}: {e}");
return ExitCode::FAILURE;
}
if let Err(e) = fs::write(target, &body) {
eprintln!("error: failed to write {}: {e}", target.display());
return ExitCode::FAILURE;
}
println!(
"Wrote agent fleet config to {}\n\n\
Next steps:\n\
\x20 1. Edit {} — pick a provider section (only the first is active),\n\
\x20 set the api_key env var (or remove it if your provider doesn't need one).\n\
\x20 2. Make sure ~/.nsed/agent.creds exists (run `quorum redeem <invite>` first).\n\
\x20 3. Run: quorum serve --nats-url <url-from-quorum-redeem>",
target.display(),
target.display(),
);
ExitCode::SUCCESS
}
const ONBOARD_REDEEM_MAX_ATTEMPTS: u32 = 5;
fn write_fleet_body(target: &Path, body: &str, force: bool) -> ExitCode {
if target.exists() && !force {
eprintln!(
"error: {} already exists. Re-run with --force to overwrite.",
target.display()
);
return ExitCode::FAILURE;
}
if let Some(parent) = target.parent()
&& !parent.as_os_str().is_empty()
&& let Err(e) = fs::create_dir_all(parent)
{
eprintln!("error: failed to create parent directory {parent:?}: {e}");
return ExitCode::FAILURE;
}
if let Err(e) = fs::write(target, body) {
eprintln!("error: failed to write {}: {e}", target.display());
return ExitCode::FAILURE;
}
println!("Wrote agent fleet config to {}", target.display());
ExitCode::SUCCESS
}
pub(crate) fn fleet_sections_of(agent_yaml: &str) -> &str {
match agent_yaml.find("# LLM Providers") {
Some(idx) => {
let start = agent_yaml[..idx].rfind("# ====").unwrap_or(idx);
&agent_yaml[start..]
}
None => agent_yaml,
}
}
async fn scaffold_after_redeem(
target: &Path,
spec: &WorkspaceSpec<'_>,
force: bool,
interactive: bool,
) -> ExitCode {
let fleet_yaml = if interactive {
match crate::init::run_agent_setup(spec.orchestrator_url, spec.token_ref).await {
Ok(Some(result)) if !result.agent_config_yaml.is_empty() => result.agent_config_yaml,
Ok(_) => render_fleet_yaml(spec.agents),
Err(e) => {
eprintln!("error: agent setup failed: {e:#}");
return ExitCode::FAILURE;
}
}
} else {
render_fleet_yaml(spec.agents)
};
let quorum_yaml = format!(
"{}\n{}",
render_workspace(spec),
fleet_sections_of(&fleet_yaml)
);
write_fleet_body(target, &quorum_yaml, force)
}
pub struct OnboardSpec<'a> {
pub code: &'a str,
pub orchestrator_url: &'a str,
pub out_dir: Option<&'a Path>,
pub target: &'a Path,
pub room: &'a str,
pub agents: &'a [String],
pub non_interactive: bool,
pub force: bool,
}
pub async fn run_onboard(spec: OnboardSpec<'_>) -> ExitCode {
if let Err(e) = preflight_writable(spec.target) {
eprintln!(
"error: cannot write {} ({e}).\n \
cd to a writable directory (e.g. your home), or pass --config <path>, then re-run.\n \
Your invite has NOT been redeemed yet.",
spec.target.display()
);
return ExitCode::FAILURE;
}
if let Err(e) = crate::cli::commands::redeem::run(
spec.code,
spec.orchestrator_url,
spec.out_dir,
spec.force,
None,
ONBOARD_REDEEM_MAX_ATTEMPTS,
)
.await
{
eprintln!("error: redeem failed: {e:#}");
return ExitCode::FAILURE;
}
let interactive = !spec.non_interactive && std::io::IsTerminal::is_terminal(&std::io::stdin());
if interactive {
return crate::cli::commands::init_wizard::run(spec.target).await;
}
println!("\nScaffolding quorum.yml (workspace + agents)…\n");
let token_ref = token_ref_for_onboard(spec.out_dir);
let workspace = WorkspaceSpec {
orchestrator_url: spec.orchestrator_url,
room: spec.room,
token_ref: &token_ref,
agents: spec.agents,
};
scaffold_after_redeem(spec.target, &workspace, spec.force, false).await
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
fn ws<'a>(
orchestrator_url: &'a str,
room: &'a str,
token_ref: &'a str,
agents: &'a [String],
) -> WorkspaceSpec<'a> {
WorkspaceSpec {
orchestrator_url,
room,
token_ref,
agents,
}
}
#[tokio::test]
async fn scaffold_writes_single_unified_quorum_yml() {
let dir = tempdir().unwrap();
let target = dir.path().join("quorum.yml");
let exit = scaffold_after_redeem(
&target,
&ws(
DEFAULT_ORCHESTRATOR_URL,
DEFAULT_ROOM,
"file:/tmp/operator.token",
&[],
),
false,
false, )
.await;
assert_eq!(exit, ExitCode::SUCCESS);
assert!(target.exists(), "quorum.yml must be written");
let body = fs::read_to_string(&target).unwrap();
assert!(body.contains("orchestrators:"));
assert!(
body.contains("token: \"file:/tmp/operator.token\""),
"workspace must reference the redeemed token file, got:\n{body}"
);
assert!(
body.contains("providers:"),
"fleet providers must be present"
);
let parsed: crate::cli::workspace::QuorumConfig =
serde_yaml::from_str(&body).expect("unified file must parse as QuorumConfig");
assert!(parsed.orchestrators.contains_key("primary"));
}
#[tokio::test]
async fn scaffold_refuses_to_overwrite_existing_without_force() {
let dir = tempdir().unwrap();
let target = dir.path().join("quorum.yml");
fs::write(&target, "existing").unwrap();
let exit = scaffold_after_redeem(
&target,
&ws(
DEFAULT_ORCHESTRATOR_URL,
DEFAULT_ROOM,
"file:/tmp/operator.token",
&[],
),
false, false,
)
.await;
assert_eq!(exit, ExitCode::FAILURE);
assert_eq!(fs::read_to_string(&target).unwrap(), "existing");
}
#[test]
fn render_workspace_embeds_file_token_ref_verbatim() {
let yaml = render_workspace(&ws(
"https://example.test",
"demo",
"file:/home/u/.nsed/operator.token",
&[],
));
assert!(
yaml.contains("token: \"file:/home/u/.nsed/operator.token\""),
"file token ref must be embedded verbatim, got:\n{yaml}"
);
assert!(!yaml.contains("${file:"));
}
#[test]
fn render_workspace_embeds_env_token_ref() {
let yaml = render_workspace(&ws("https://example.test", "demo", "${MY_TOKEN}", &[]));
assert!(yaml.contains("token: \"${MY_TOKEN}\""));
}
#[test]
fn token_ref_for_onboard_points_at_operator_token_in_out_dir() {
let dir = tempdir().unwrap();
fs::write(dir.path().join("operator.token"), "bearer\n").unwrap();
let token_ref = token_ref_for_onboard(Some(dir.path()));
assert!(token_ref.starts_with("file:"));
assert!(
token_ref.ends_with("operator.token"),
"must point at operator.token, got: {token_ref}"
);
let path = token_ref.strip_prefix("file:").unwrap();
assert_eq!(
crate::config::resolve_env_token("token", &format!("file:{path}")),
"bearer"
);
}
#[test]
fn preflight_writable_ok_for_writable_dir_and_creates_missing_parent() {
let dir = tempdir().unwrap();
assert!(preflight_writable(&dir.path().join("nsed.yaml")).is_ok());
assert!(preflight_writable(&dir.path().join("nested/deep/nsed.yaml")).is_ok());
assert!(dir.path().join("nested/deep").exists());
assert!(!dir.path().join(".nsed-write-probe").exists());
}
#[test]
fn preflight_writable_errs_when_parent_is_a_file() {
let dir = tempdir().unwrap();
let file = dir.path().join("not-a-dir");
fs::write(&file, "x").unwrap();
assert!(preflight_writable(&file.join("nsed.yaml")).is_err());
}
#[test]
fn write_fleet_body_writes_creates_parents_and_respects_force() {
let dir = tempdir().unwrap();
let target = dir.path().join("nested/agent.yml");
assert_eq!(
write_fleet_body(&target, "providers: {}\n", false),
ExitCode::SUCCESS
);
assert!(target.exists());
assert_eq!(write_fleet_body(&target, "new", false), ExitCode::FAILURE);
assert_eq!(fs::read_to_string(&target).unwrap(), "providers: {}\n");
assert_eq!(write_fleet_body(&target, "new", true), ExitCode::SUCCESS);
assert_eq!(fs::read_to_string(&target).unwrap(), "new");
}
fn agents(names: &[&str]) -> Vec<String> {
names.iter().map(|s| s.to_string()).collect()
}
#[test]
fn renders_with_provided_inputs() {
let yaml = render_workspace(&ws("https://example.test", "myroom", "${MY_TOKEN}", &[]));
assert!(yaml.contains(r#"address: "https://example.test""#));
assert!(yaml.contains("token: \"${MY_TOKEN}\""));
assert!(yaml.contains("\n myroom:\n policy: default"));
assert!(yaml.contains("default_room: myroom"));
}
#[test]
fn renders_with_defaults() {
let yaml = render_workspace(&ws(
DEFAULT_ORCHESTRATOR_URL,
DEFAULT_ROOM,
"${QUORUM_DEMO_TOKEN}",
&[],
));
assert!(yaml.contains("https://api.peeramid.xyz"));
assert!(yaml.contains("${QUORUM_DEMO_TOKEN}"));
assert!(yaml.contains("\n demo:\n policy: default"));
assert!(yaml.contains("default_room: demo"));
}
#[test]
fn rendered_output_parses_as_yaml() {
let yaml = render_workspace(&ws(
DEFAULT_ORCHESTRATOR_URL,
DEFAULT_ROOM,
"${QUORUM_DEMO_TOKEN}",
&agents(&["CortexA", "CortexB"]),
));
let parsed: serde_yaml::Value =
serde_yaml::from_str(&yaml).expect("rendered yaml must round-trip through serde");
assert!(parsed["orchestrators"]["primary"]["address"].is_string());
assert_eq!(parsed["default_room"].as_str(), Some("demo"));
let agents_node = &parsed["policies"]["default"]["agents"];
assert!(agents_node.is_sequence(), "agents must serialise as a list");
let names: Vec<&str> = agents_node
.as_sequence()
.unwrap()
.iter()
.map(|v| v.as_str().unwrap())
.collect();
assert_eq!(names, vec!["CortexA", "CortexB"]);
}
#[test]
fn renders_with_named_agents() {
let yaml = render_workspace(&ws(
"https://example.test",
"demo",
"${TOK}",
&agents(&["A", "B"]),
));
assert!(
yaml.contains(r#"agents: ["A", "B"]"#),
"agents list must appear in the rendered policy"
);
}
#[test]
fn empty_agents_emits_editable_placeholder() {
let yaml = render_workspace(&ws("https://example.test", "demo", "${TOK}", &[]));
assert!(
yaml.contains("agents: []"),
"empty agents must still emit a parseable agents: [] line so the file round-trips through serde"
);
assert!(
yaml.contains("Edit: list the agent names"),
"empty agents must include an inline edit hint"
);
}
#[test]
fn run_writes_file_and_succeeds_when_target_absent() {
let dir = tempdir().unwrap();
let target = dir.path().join("nsed.yaml");
let exit = run(
&target,
&ws(
DEFAULT_ORCHESTRATOR_URL,
DEFAULT_ROOM,
"${QUORUM_DEMO_TOKEN}",
&[],
),
false,
);
assert_eq!(exit, ExitCode::SUCCESS);
let body = fs::read_to_string(&target).unwrap();
assert!(body.contains("https://api.peeramid.xyz"));
}
#[test]
fn run_refuses_to_overwrite_without_force() {
let dir = tempdir().unwrap();
let target = dir.path().join("nsed.yaml");
fs::write(&target, "existing content").unwrap();
let exit = run(
&target,
&ws(
DEFAULT_ORCHESTRATOR_URL,
DEFAULT_ROOM,
"${QUORUM_DEMO_TOKEN}",
&[],
),
false,
);
assert_eq!(exit, ExitCode::FAILURE);
let body = fs::read_to_string(&target).unwrap();
assert_eq!(body, "existing content", "file must NOT be overwritten");
}
#[test]
fn run_overwrites_with_force() {
let dir = tempdir().unwrap();
let target = dir.path().join("nsed.yaml");
fs::write(&target, "existing content").unwrap();
let exit = run(
&target,
&ws(
DEFAULT_ORCHESTRATOR_URL,
DEFAULT_ROOM,
"${QUORUM_DEMO_TOKEN}",
&[],
),
true,
);
assert_eq!(exit, ExitCode::SUCCESS);
let body = fs::read_to_string(&target).unwrap();
assert!(body.contains("https://api.peeramid.xyz"));
assert!(!body.contains("existing content"));
}
#[test]
fn run_creates_parent_directory_if_missing() {
let dir = tempdir().unwrap();
let target = dir.path().join("nested/path/nsed.yaml");
let exit = run(
&target,
&ws(
DEFAULT_ORCHESTRATOR_URL,
DEFAULT_ROOM,
"${QUORUM_DEMO_TOKEN}",
&[],
),
false,
);
assert_eq!(exit, ExitCode::SUCCESS);
assert!(target.exists());
}
#[test]
fn rendered_output_uses_provided_token_env() {
let yaml = render_workspace(&ws("https://example.test", "demo", "${CUSTOM_TOK}", &[]));
assert!(yaml.contains("${CUSTOM_TOK}"));
assert!(!yaml.contains("${QUORUM_DEMO_TOKEN}"));
}
#[test]
fn fleet_yaml_includes_active_openai_provider() {
let yaml = render_fleet_yaml(&[]);
assert!(yaml.contains("providers:"));
assert!(yaml.contains("openai:"));
assert!(
yaml.contains("api_key: \"${OPENAI_API_KEY}\""),
"default template references env-var; operator overrides"
);
assert!(
yaml.contains("# claude_cli:"),
"claude provider must ship commented"
);
assert!(
yaml.contains("# exec_local:"),
"exec provider must ship commented"
);
assert!(
yaml.contains("# mcp_local:"),
"mcp provider must ship commented"
);
assert!(
yaml.contains("# engine: vllm"),
"engine field must ship commented on the openai provider"
);
}
#[test]
fn fleet_yaml_defaults_to_one_agent_when_none_provided() {
let yaml = render_fleet_yaml(&[]);
assert!(yaml.contains("- name: cortex-a"));
assert!(yaml.contains("model_name: gpt-4o-mini"));
}
#[test]
fn fleet_yaml_emits_one_entry_per_named_agent() {
let names = agents(&["one", "two", "three"]);
let yaml = render_fleet_yaml(&names);
for name in &names {
assert!(
yaml.contains(&format!("- name: {name}")),
"expected entry for {name}, got:\n{yaml}"
);
}
}
#[test]
fn fleet_yaml_parses_as_valid_yaml() {
let yaml = render_fleet_yaml(&agents(&["cortex-a", "cortex-b"]));
let parsed: serde_yaml::Value =
serde_yaml::from_str(&yaml).expect("fleet yaml must round-trip via serde_yaml");
assert!(parsed["providers"]["openai"]["type"].is_string());
assert_eq!(
parsed["providers"]["openai"]["type"].as_str(),
Some("openai")
);
let agents_seq = parsed["agents"].as_sequence().expect("agents is a list");
assert_eq!(agents_seq.len(), 2);
assert_eq!(agents_seq[0]["name"].as_str(), Some("cortex-a"));
}
#[test]
fn run_agent_fleet_writes_file() {
let dir = tempdir().unwrap();
let target = dir.path().join("agent.yml");
let exit = run_agent_fleet(&target, &agents(&["cortex-a"]), false);
assert_eq!(exit, ExitCode::SUCCESS);
assert!(target.exists());
let body = std::fs::read_to_string(&target).unwrap();
assert!(body.contains("providers:"));
assert!(body.contains("- name: cortex-a"));
}
#[test]
fn run_agent_fleet_refuses_to_overwrite_without_force() {
let dir = tempdir().unwrap();
let target = dir.path().join("agent.yml");
std::fs::write(&target, "stale").unwrap();
let exit = run_agent_fleet(&target, &[], false);
assert_eq!(exit, ExitCode::FAILURE);
assert_eq!(std::fs::read_to_string(&target).unwrap(), "stale");
}
#[test]
fn run_agent_fleet_overwrites_with_force() {
let dir = tempdir().unwrap();
let target = dir.path().join("agent.yml");
std::fs::write(&target, "stale").unwrap();
let exit = run_agent_fleet(&target, &[], true);
assert_eq!(exit, ExitCode::SUCCESS);
let body = std::fs::read_to_string(&target).unwrap();
assert!(!body.contains("stale"));
assert!(body.contains("providers:"));
}
#[test]
fn run_agent_fleet_creates_parent_directory() {
let dir = tempdir().unwrap();
let target = dir.path().join("nested/agent.yml");
let exit = run_agent_fleet(&target, &[], false);
assert_eq!(exit, ExitCode::SUCCESS);
assert!(target.exists());
}
#[test]
fn is_valid_agent_name_accepts_documented_alphabet() {
for ok in ["cortex-a", "Cortex_B", "agent.v1", "a", "0", "A1_b.2-x"] {
assert!(is_valid_agent_name(ok), "expected {ok:?} valid");
}
}
#[test]
fn is_valid_agent_name_rejects_yaml_breakers() {
for bad in [
"",
"foo: bar",
"name#with hash",
"has\"quote",
"has'quote",
"line\nbreak",
"tab\there",
"back\\slash",
"space here",
"uni¢ode",
] {
assert!(!is_valid_agent_name(bad), "expected {bad:?} invalid");
}
}
#[test]
fn run_agent_fleet_rejects_invalid_name_and_does_not_write_file() {
let dir = tempdir().unwrap();
let target = dir.path().join("agent.yml");
let exit = run_agent_fleet(&target, &agents(&["bad: name"]), false);
assert_eq!(exit, ExitCode::FAILURE);
assert!(
!target.exists(),
"must not leave a broken agent.yml on disk when validation rejects"
);
}
}