use std::path::Path;
pub use crate::layers::{PromptAgent, PromptAgentKind, PromptContext, PromptError};
use crate::layers::{compose, resolve_layers};
const STARTUP_DEFAULT: &str = include_str!("../prompts/startup.md");
const STARTUP_AGENTINFINITY: &str = include_str!("../prompts/startup-agentinfinity.md");
pub const BUNDLED_PROMPT_FILES: &[(&str, &str)] = &[
("base.md", include_str!("../prompts/base.md")),
("agent0.md", include_str!("../prompts/agent0.md")),
("clone.md", include_str!("../prompts/clone.md")),
(
"agentinfinity.md",
include_str!("../prompts/agentinfinity.md"),
),
("startup.md", STARTUP_DEFAULT),
("startup-agentinfinity.md", STARTUP_AGENTINFINITY),
];
pub fn startup_prompt_for<A: PromptAgent>(agent: A) -> &'static str {
match agent.prompt_agent_kind() {
PromptAgentKind::Agentinfinity => STARTUP_AGENTINFINITY,
PromptAgentKind::Agent0 | PromptAgentKind::Clone => STARTUP_DEFAULT,
}
}
pub fn render_prompt<A: PromptAgent>(
ctx: PromptContext<A>,
cwd: &Path,
) -> Result<String, PromptError> {
compose(&resolve_layers(ctx, cwd, &[])?)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::runtime_config;
use std::sync::{Mutex, MutexGuard, OnceLock};
use tempfile::TempDir;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum TestAgent {
Agent0,
Clone(u32),
Agentinfinity,
}
impl std::fmt::Display for TestAgent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Agent0 => f.write_str("agent0"),
Self::Clone(n) => write!(f, "agent{n}"),
Self::Agentinfinity => f.write_str("agentinfinity"),
}
}
}
impl PromptAgent for TestAgent {
fn prompt_agent_kind(self) -> PromptAgentKind {
match self {
Self::Agent0 => PromptAgentKind::Agent0,
Self::Clone(_) => PromptAgentKind::Clone,
Self::Agentinfinity => PromptAgentKind::Agentinfinity,
}
}
fn prompt_agent_name(self) -> String {
match self {
Self::Agent0 => "agent0".to_string(),
Self::Clone(n) => format!("agent{n}"),
Self::Agentinfinity => "agentinfinity".to_string(),
}
}
fn prompt_agent_env_n(self) -> String {
match self {
Self::Agent0 => "0".to_string(),
Self::Clone(n) => n.to_string(),
Self::Agentinfinity => "infinity".to_string(),
}
}
}
struct PromptTestEnv {
_tmp: TempDir,
_guard: MutexGuard<'static, ()>,
prior_xdg: Option<String>,
prior_machine_type: Option<String>,
}
impl PromptTestEnv {
fn new() -> Self {
let guard = test_lock().lock().unwrap_or_else(|err| err.into_inner());
let tmp = TempDir::new().unwrap();
let prior_xdg = std::env::var("XDG_CONFIG_HOME").ok();
let prior_machine_type = std::env::var("MACHINE_TYPE").ok();
unsafe {
std::env::set_var("XDG_CONFIG_HOME", tmp.path());
std::env::remove_var("MACHINE_TYPE");
}
std::fs::create_dir_all(runtime_config::config_dir()).unwrap();
Self {
_tmp: tmp,
_guard: guard,
prior_xdg,
prior_machine_type,
}
}
}
fn test_lock() -> &'static Mutex<()> {
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
LOCK.get_or_init(|| Mutex::new(()))
}
impl Drop for PromptTestEnv {
fn drop(&mut self) {
unsafe {
match &self.prior_xdg {
Some(value) => std::env::set_var("XDG_CONFIG_HOME", value),
None => std::env::remove_var("XDG_CONFIG_HOME"),
}
match &self.prior_machine_type {
Some(value) => std::env::set_var("MACHINE_TYPE", value),
None => std::env::remove_var("MACHINE_TYPE"),
}
}
}
}
fn ctx_for(agent: TestAgent) -> PromptContext<TestAgent> {
PromptContext::new(agent, "/tmp/netsky-test")
}
#[test]
fn renders_all_agents_without_addendum() {
let _env = PromptTestEnv::new();
let nowhere = std::path::PathBuf::from("/dev/null/does-not-exist");
for agent in [
TestAgent::Agent0,
TestAgent::Clone(1),
TestAgent::Clone(8),
TestAgent::Agentinfinity,
] {
let out = render_prompt(ctx_for(agent), &nowhere).unwrap();
assert!(!out.is_empty(), "empty prompt for {agent}");
assert!(out.contains("---"), "missing separator for {agent}");
assert!(!out.contains("{{"), "unsubstituted placeholder for {agent}");
}
}
#[test]
fn clone_prompt_substitutes_n() {
let nowhere = std::path::PathBuf::from("/dev/null/does-not-exist");
let out = render_prompt(ctx_for(TestAgent::Clone(5)), &nowhere).unwrap();
assert!(out.contains("agent5"));
assert!(!out.contains("{{ n }}"));
}
#[test]
fn cwd_addendum_is_appended() {
let _env = PromptTestEnv::new();
let tmp = tempfile::tempdir().unwrap();
std::fs::write(tmp.path().join("0.md"), "USER POLICY HERE").unwrap();
let out = render_prompt(ctx_for(TestAgent::Agent0), tmp.path()).unwrap();
assert!(out.contains("USER POLICY HERE"));
}
#[test]
fn netsky_toml_addendum_overrides_default_path() {
let _env = PromptTestEnv::new();
let tmp = tempfile::tempdir().unwrap();
std::fs::write(tmp.path().join("0.md"), "OLD POLICY").unwrap();
std::fs::create_dir_all(tmp.path().join("addenda")).unwrap();
std::fs::write(tmp.path().join("addenda/0-personal.md"), "NEW POLICY").unwrap();
std::fs::write(
tmp.path().join("netsky.toml"),
"schema_version = 1\n[addendum]\nagent0 = \"addenda/0-personal.md\"\n",
)
.unwrap();
let out = render_prompt(ctx_for(TestAgent::Agent0), tmp.path()).unwrap();
assert!(out.contains("NEW POLICY"));
assert!(!out.contains("OLD POLICY"));
}
#[test]
fn missing_netsky_toml_falls_back_to_legacy_addendum() {
let _env = PromptTestEnv::new();
let tmp = tempfile::tempdir().unwrap();
std::fs::write(tmp.path().join("0.md"), "LEGACY ADDENDUM").unwrap();
let out = render_prompt(ctx_for(TestAgent::Agent0), tmp.path()).unwrap();
assert!(out.contains("LEGACY ADDENDUM"));
}
#[test]
fn netsky_toml_without_addendum_section_falls_back() {
let _env = PromptTestEnv::new();
let tmp = tempfile::tempdir().unwrap();
std::fs::write(tmp.path().join("0.md"), "FALLBACK POLICY").unwrap();
std::fs::write(
tmp.path().join("netsky.toml"),
"schema_version = 1\n[owner]\nname = \"Alice\"\n",
)
.unwrap();
let out = render_prompt(ctx_for(TestAgent::Agent0), tmp.path()).unwrap();
assert!(out.contains("FALLBACK POLICY"));
}
#[test]
fn netsky_toml_addendum_absolute_path_used_as_is() {
let _env = PromptTestEnv::new();
let tmp = tempfile::tempdir().unwrap();
let abs_addendum = tmp.path().join("absolute-addendum.md");
std::fs::write(&abs_addendum, "ABSOLUTE POLICY").unwrap();
std::fs::write(
tmp.path().join("netsky.toml"),
format!(
"schema_version = 1\n[addendum]\nagent0 = \"{}\"\n",
abs_addendum.display()
),
)
.unwrap();
let out = render_prompt(ctx_for(TestAgent::Agent0), tmp.path()).unwrap();
assert!(out.contains("ABSOLUTE POLICY"));
}
#[test]
fn runtime_addendum_layers_append_after_cwd_addendum() {
let _env = PromptTestEnv::new();
let tmp = tempfile::tempdir().unwrap();
std::fs::write(tmp.path().join("0.md"), "CWD POLICY").unwrap();
std::fs::write(runtime_config::owner_path(), "github_username = \"cody\"\n").unwrap();
std::fs::write(runtime_config::addendum_path(), "BASE POLICY\n").unwrap();
std::fs::write(runtime_config::active_host_path(), "work\n").unwrap();
std::fs::write(runtime_config::host_addendum_path("work"), "WORK POLICY\n").unwrap();
let out = render_prompt(ctx_for(TestAgent::Agent0), tmp.path()).unwrap();
let cwd = out.find("CWD POLICY").unwrap();
let base = out.find("BASE POLICY").unwrap();
let host = out.find("WORK POLICY").unwrap();
assert!(cwd < base);
assert!(base < host);
}
#[test]
fn machine_type_env_overrides_active_host_cache() {
let _env = PromptTestEnv::new();
let tmp = tempfile::tempdir().unwrap();
std::fs::write(runtime_config::owner_path(), "github_username = \"cody\"\n").unwrap();
std::fs::write(runtime_config::active_host_path(), "personal\n").unwrap();
std::fs::write(
runtime_config::host_addendum_path("personal"),
"PERSONAL POLICY\n",
)
.unwrap();
std::fs::write(runtime_config::host_addendum_path("work"), "WORK POLICY\n").unwrap();
unsafe {
std::env::set_var("MACHINE_TYPE", "work");
}
let out = render_prompt(ctx_for(TestAgent::Agent0), tmp.path()).unwrap();
assert!(out.contains("WORK POLICY"));
assert!(!out.contains("PERSONAL POLICY"));
}
#[test]
fn startup_prompt_switches_for_watchdog() {
assert_eq!(startup_prompt_for(TestAgent::Agent0), STARTUP_DEFAULT);
assert_eq!(startup_prompt_for(TestAgent::Clone(3)), STARTUP_DEFAULT);
assert_eq!(
startup_prompt_for(TestAgent::Agentinfinity),
STARTUP_AGENTINFINITY
);
}
}