use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json;
use std::path::{Path, PathBuf};
const COORDINATION_DEFAULT: &str = include_str!("../assets/agent-skills/coordination.md");
const SUPERVISOR_DEFAULT: &str = include_str!("../assets/agent-skills/supervisor.md");
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Source {
Embedded,
AgentsStandard,
User,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub enum SkillFormat {
Standardized,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct StandardizedSkillMetadata {
pub name: String,
pub description: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub license: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub compatibility: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SkillTemplate {
pub name: String,
pub content: String,
pub source: Source,
pub format: SkillFormat,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<StandardizedSkillMetadata>,
#[serde(skip_serializing_if = "Option::is_none")]
pub resource_paths: Option<Vec<PathBuf>>,
}
#[derive(Debug, thiserror::Error)]
pub enum SkillError {
#[error("unknown skill '{name}' — no embedded default or user override exists")]
UnknownSkill {
name: String,
},
#[error("skill '{name}' validation failed: {reason}")]
ValidationError {
name: String,
reason: String,
},
#[error("cannot read skill directory at '{}' — check directory permissions", path.display())]
DirectoryReadError {
path: PathBuf,
source: std::io::Error,
},
#[error("cannot read user override skill file at '{}' — check file permissions", path.display())]
UserOverrideRead {
path: PathBuf,
source: std::io::Error,
},
}
fn embedded_default(skill_name: &str) -> Option<&'static str> {
match skill_name {
"coordination" => Some(COORDINATION_DEFAULT),
"supervisor" => Some(SUPERVISOR_DEFAULT),
_ => None,
}
}
pub fn resolve(skill_name: &str) -> Result<SkillTemplate, SkillError> {
resolve_with_config_dir(skill_name, None)
}
fn try_load_standardized_skill(
skill_name: &str,
config_dir_override: Option<&Path>,
) -> Result<Option<SkillTemplate>, SkillError> {
if let Some(config_dir) = config_dir_override
&& let Some(skill) = try_load_user_override(skill_name, config_dir)?
{
return Ok(Some(skill));
}
try_load_from_agents_dir(skill_name)
}
fn try_load_user_override(
skill_name: &str,
config_dir: &Path,
) -> Result<Option<SkillTemplate>, SkillError> {
let skill_dir = config_dir
.join("git-paw")
.join("agent-skills")
.join(skill_name);
if skill_dir.is_dir() {
let skill_md_path = skill_dir.join("SKILL.md");
if skill_md_path.exists() {
return load_skill_from_directory(&skill_dir, skill_name, Source::User);
}
}
Ok(None)
}
fn try_load_from_agents_dir(skill_name: &str) -> Result<Option<SkillTemplate>, SkillError> {
let Ok(mut current_dir) = std::env::current_dir() else {
return Ok(None);
};
for _ in 0..5 {
let agents_dir = current_dir.join(".agents").join("skills").join(skill_name);
if agents_dir.is_dir() {
let skill_md_path = agents_dir.join("SKILL.md");
if skill_md_path.exists() {
return load_skill_from_directory(&agents_dir, skill_name, Source::AgentsStandard);
}
}
if !current_dir.pop() {
break;
}
}
Ok(None)
}
fn load_skill_from_directory(
skill_dir: &Path,
skill_name: &str,
source: Source,
) -> Result<Option<SkillTemplate>, SkillError> {
let skill_md_path = skill_dir.join("SKILL.md");
let content = match std::fs::read_to_string(&skill_md_path) {
Ok(content) => content,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(source_err) => {
let error = match source {
Source::User => SkillError::UserOverrideRead {
path: skill_md_path.clone(),
source: source_err,
},
_ => SkillError::DirectoryReadError {
path: skill_dir.to_path_buf(),
source: source_err,
},
};
return Err(error);
}
};
let (metadata, content_without_frontmatter) = parse_standardized_metadata(&content)?;
let mut resource_paths = Vec::new();
for subdir in ["scripts", "references", "assets"] {
let subdir_path = skill_dir.join(subdir);
if subdir_path.exists() && subdir_path.is_dir() {
resource_paths.push(subdir_path);
}
}
Ok(Some(SkillTemplate {
name: skill_name.to_string(),
content: content_without_frontmatter,
source,
format: SkillFormat::Standardized,
metadata,
resource_paths: if resource_paths.is_empty() {
None
} else {
Some(resource_paths)
},
}))
}
fn parse_standardized_metadata(
content: &str,
) -> Result<(Option<StandardizedSkillMetadata>, String), SkillError> {
let lines: Vec<&str> = content.lines().collect();
if lines.len() < 2 || !lines[0].trim().starts_with("---") {
return Ok((None, content.to_string()));
}
let mut frontmatter_end = None;
for (i, line) in lines.iter().enumerate().skip(1) {
if line.trim().starts_with("---") {
frontmatter_end = Some(i);
break;
}
}
let Some(frontmatter_end) = frontmatter_end else {
return Ok((None, content.to_string())); };
let frontmatter_lines = &lines[1..frontmatter_end];
let frontmatter_yaml = frontmatter_lines.join("\n");
let metadata: StandardizedSkillMetadata = match serde_yaml::from_str(&frontmatter_yaml) {
Ok(meta) => meta,
Err(e) => {
return Err(SkillError::ValidationError {
name: "unknown".to_string(),
reason: format!("invalid YAML frontmatter: {e}"),
});
}
};
if metadata.name.is_empty() {
return Err(SkillError::ValidationError {
name: "unknown".to_string(),
reason: "missing required 'name' field in frontmatter".to_string(),
});
}
if metadata.description.is_empty() {
return Err(SkillError::ValidationError {
name: metadata.name.clone(),
reason: "missing required 'description' field in frontmatter".to_string(),
});
}
let content_without_frontmatter = lines[frontmatter_end + 1..].join("\n");
Ok((Some(metadata), content_without_frontmatter))
}
fn resolve_with_config_dir(
skill_name: &str,
config_dir: Option<&Path>,
) -> Result<SkillTemplate, SkillError> {
if let Some(skill) = try_load_standardized_skill(skill_name, config_dir)? {
return Ok(skill);
}
if let Some(content) = embedded_default(skill_name) {
let (metadata, content_without_frontmatter) = parse_standardized_metadata(content)?;
return Ok(SkillTemplate {
name: skill_name.to_string(),
content: content_without_frontmatter,
source: Source::Embedded,
format: SkillFormat::Standardized,
metadata,
resource_paths: None,
});
}
Err(SkillError::UnknownSkill {
name: skill_name.to_string(),
})
}
fn slugify_branch(branch: &str) -> String {
crate::broker::messages::slugify_branch(branch)
}
pub fn build_boot_block(branch_id: &str, broker_url: &str) -> String {
let template = include_str!("../assets/boot-block-template.md");
let slugified_branch = slugify_branch(branch_id);
template
.replace("{{BRANCH_ID}}", &slugified_branch)
.replace("{{GIT_PAW_BROKER_URL}}", broker_url)
}
pub fn render(
template: &SkillTemplate,
branch: &str,
broker_url: &str,
project: &str,
test_command: Option<&str>,
) -> String {
let branch_id = slugify_branch(branch);
let test_command_value = test_command.unwrap_or("(not configured)");
let mut output = template
.content
.replace("{{BRANCH_ID}}", &branch_id)
.replace("{{PROJECT_NAME}}", project)
.replace("{{GIT_PAW_BROKER_URL}}", broker_url)
.replace("{{TEST_COMMAND}}", test_command_value);
if let Some(metadata) = &template.metadata {
output = output
.replace("{{SKILL_NAME}}", &metadata.name)
.replace("{{SKILL_DESCRIPTION}}", &metadata.description);
}
let mut start = 0;
while let Some(open) = output[start..].find("{{") {
let abs_open = start + open;
if let Some(close) = output[abs_open..].find("}}") {
let placeholder = &output[abs_open..abs_open + close + 2];
eprintln!(
"warning: unsubstituted placeholder {placeholder} in skill '{}'",
template.name
);
start = abs_open + close + 2;
} else {
break;
}
}
output
}
#[cfg(test)]
mod tests {
use super::*;
use serial_test::serial;
#[test]
fn embedded_coordination_is_reachable() {
let tmpl = resolve("coordination").expect("should resolve coordination");
assert_eq!(tmpl.source, Source::Embedded);
assert!(!tmpl.content.is_empty());
}
#[test]
fn embedded_coordination_contains_all_operations() {
let tmpl = resolve("coordination").unwrap();
assert!(tmpl.content.contains("agent.status"));
assert!(tmpl.content.contains("agent.artifact"));
assert!(tmpl.content.contains("agent.blocked"));
assert!(
tmpl.content
.contains("{{GIT_PAW_BROKER_URL}}/messages/{{BRANCH_ID}}")
);
}
#[test]
fn embedded_coordination_documents_supervisor_messages() {
let tmpl = resolve("coordination").unwrap();
assert!(tmpl.content.contains("agent.verified"));
assert!(tmpl.content.contains("agent.feedback"));
assert!(tmpl.content.contains("re-publish"));
}
#[test]
#[serial(directory_changes)]
fn standard_location_skill_loading() {
let dir = tempfile::tempdir().unwrap();
let project_dir = dir.path().join("my-project");
std::fs::create_dir_all(&project_dir).unwrap();
let skill_dir = project_dir
.join(".agents")
.join("skills")
.join("coordination");
std::fs::create_dir_all(&skill_dir).unwrap();
let skill_md_content = "---\nname: coordination\ndescription: Custom coordination skill\n---\n\ncustom skill content";
std::fs::write(skill_dir.join("SKILL.md"), skill_md_content).unwrap();
let original_dir = std::env::current_dir().unwrap();
std::env::set_current_dir(&project_dir).unwrap();
let tmpl = resolve("coordination").expect("should resolve");
assert_eq!(tmpl.source, Source::AgentsStandard);
assert!(tmpl.content.contains("custom skill content"));
std::env::set_current_dir(original_dir).unwrap();
}
#[test]
fn unknown_skill_returns_error() {
let result = resolve("nonexistent");
assert!(
matches!(result, Err(SkillError::UnknownSkill { ref name }) if name == "nonexistent"),
"expected UnknownSkill error, got {result:?}"
);
}
#[test]
fn branch_id_is_substituted() {
let tmpl = SkillTemplate {
name: "test".into(),
content: "agent_id:\"{{BRANCH_ID}}\"".into(),
source: Source::Embedded,
format: SkillFormat::Standardized,
metadata: None,
resource_paths: None,
};
let output = render(
&tmpl,
"feat/http-broker",
"http://127.0.0.1:9119",
"git-paw",
None,
);
assert!(output.contains("feat-http-broker"));
assert!(!output.contains("{{BRANCH_ID}}"));
}
#[test]
fn broker_url_placeholder_substituted() {
let tmpl = SkillTemplate {
name: "test".into(),
content: "curl {{GIT_PAW_BROKER_URL}}/status".into(),
source: Source::Embedded,
format: SkillFormat::Standardized,
metadata: None,
resource_paths: None,
};
let output = render(&tmpl, "feat/x", "http://127.0.0.1:9119", "git-paw", None);
assert!(output.contains("http://127.0.0.1:9119/status"));
assert!(!output.contains("{{GIT_PAW_BROKER_URL}}"));
}
#[test]
fn slug_substitution_matches_slugify_branch() {
let tmpl = SkillTemplate {
name: "test".into(),
content: "id={{BRANCH_ID}}".into(),
source: Source::Embedded,
format: SkillFormat::Standardized,
metadata: None,
resource_paths: None,
};
let output = render(
&tmpl,
"Feature/HTTP_Broker",
"http://127.0.0.1:9119",
"git-paw",
None,
);
let expected = slugify_branch("Feature/HTTP_Broker");
assert_eq!(output, format!("id={expected}"));
}
#[test]
fn render_is_deterministic() {
let tmpl = resolve("coordination").unwrap();
let a = render(&tmpl, "feat/x", "http://127.0.0.1:9119", "git-paw", None);
let b = render(&tmpl, "feat/x", "http://127.0.0.1:9119", "git-paw", None);
assert_eq!(a, b);
}
#[test]
#[serial(directory_changes)]
fn render_performs_no_io() {
let dir = tempfile::tempdir().unwrap();
let project_dir = dir.path().join("my-project");
std::fs::create_dir_all(&project_dir).unwrap();
let skill_dir = project_dir
.join(".agents")
.join("skills")
.join("coordination");
std::fs::create_dir_all(&skill_dir).unwrap();
let skill_md_content = "---\nname: coordination\ndescription: Test coordination skill\n---\n\nuser {{BRANCH_ID}}";
std::fs::write(skill_dir.join("SKILL.md"), skill_md_content).unwrap();
let original_dir = std::env::current_dir().unwrap();
std::env::set_current_dir(&project_dir).unwrap();
let tmpl = resolve("coordination").unwrap();
assert_eq!(tmpl.source, Source::AgentsStandard);
std::fs::remove_dir_all(skill_dir).unwrap();
let output = render(&tmpl, "feat/x", "http://127.0.0.1:9119", "git-paw", None);
assert!(output.contains("feat-x"));
std::env::set_current_dir(original_dir).unwrap();
}
#[test]
fn unknown_placeholder_survives() {
let tmpl = SkillTemplate {
name: "test".into(),
content: "url={{UNKNOWN_THING}}".into(),
source: Source::Embedded,
format: SkillFormat::Standardized,
metadata: None,
resource_paths: None,
};
let output = render(&tmpl, "feat/x", "http://127.0.0.1:9119", "git-paw", None);
assert!(
output.contains("{{UNKNOWN_THING}}"),
"unknown placeholder should survive in output"
);
}
#[test]
fn no_unknown_placeholders_after_render() {
let tmpl = resolve("coordination").unwrap();
let output = render(&tmpl, "feat/x", "http://127.0.0.1:9119", "git-paw", None);
assert!(
!output.contains("{{"),
"no double-curly placeholders should remain: {output}"
);
}
#[test]
fn embedded_supervisor_is_reachable() {
let tmpl = resolve("supervisor").expect("should resolve supervisor");
assert_eq!(tmpl.source, Source::Embedded);
assert!(!tmpl.content.is_empty());
}
#[test]
fn supervisor_skill_contains_role_definition() {
let tmpl = resolve("supervisor").unwrap();
assert!(tmpl.content.contains("do NOT write code"));
}
#[test]
fn supervisor_skill_contains_broker_status() {
let tmpl = resolve("supervisor").unwrap();
assert!(tmpl.content.contains("{{GIT_PAW_BROKER_URL}}/status"));
}
#[test]
fn supervisor_skill_contains_verified_and_feedback() {
let tmpl = resolve("supervisor").unwrap();
assert!(tmpl.content.contains("agent.verified"));
assert!(tmpl.content.contains("agent.feedback"));
}
#[test]
fn supervisor_skill_contains_tmux_commands() {
let tmpl = resolve("supervisor").unwrap();
assert!(tmpl.content.contains("tmux capture-pane"));
assert!(tmpl.content.contains("tmux send-keys"));
assert!(tmpl.content.contains("paw-{{PROJECT_NAME}}"));
}
#[test]
fn supervisor_skill_contains_spec_audit_procedure() {
let tmpl = resolve("supervisor").unwrap();
assert!(
tmpl.content.contains("Spec Audit"),
"supervisor skill should contain Spec Audit section"
);
assert!(
tmpl.content.contains("openspec/changes/"),
"should reference openspec/changes/ for spec file discovery"
);
assert!(
tmpl.content.contains("grep"),
"should instruct to grep for matching tests"
);
}
#[test]
fn supervisor_skill_spec_audit_after_test_before_verified() {
let tmpl = resolve("supervisor").unwrap();
let test_pos = tmpl.content.find("Regression check").unwrap_or(0);
let audit_pos = tmpl.content.find("Spec Audit").unwrap_or(0);
let verify_pos = tmpl.content.find("Verify or feedback").unwrap_or(0);
assert!(
audit_pos > test_pos,
"spec audit should appear after test/regression check"
);
assert!(
audit_pos < verify_pos,
"spec audit should appear before verify/feedback"
);
}
#[test]
fn project_name_is_substituted() {
let tmpl = SkillTemplate {
name: "test".into(),
content: "session=paw-{{PROJECT_NAME}}".into(),
source: Source::Embedded,
format: SkillFormat::Standardized,
metadata: None,
resource_paths: None,
};
let output = render(&tmpl, "feat/x", "http://127.0.0.1:9119", "my-app", None);
assert!(output.contains("paw-my-app"));
assert!(!output.contains("{{PROJECT_NAME}}"));
}
#[test]
fn branch_id_and_project_name_both_substituted() {
let tmpl = SkillTemplate {
name: "test".into(),
content: "agent={{BRANCH_ID}} session=paw-{{PROJECT_NAME}}".into(),
source: Source::Embedded,
format: SkillFormat::Standardized,
metadata: None,
resource_paths: None,
};
let output = render(&tmpl, "feat/http-broker", "url", "git-paw", None);
assert!(output.contains("feat-http-broker"));
assert!(output.contains("paw-git-paw"));
assert!(!output.contains("{{BRANCH_ID}}"));
assert!(!output.contains("{{PROJECT_NAME}}"));
}
#[test]
#[serial(directory_changes)]
fn standardized_skill_format_is_detected() {
let dir = tempfile::tempdir().unwrap();
let project_dir = dir.path().join("my-project");
std::fs::create_dir_all(&project_dir).unwrap();
let skill_dir = project_dir
.join(".agents")
.join("skills")
.join("test-standardized");
std::fs::create_dir_all(&skill_dir).unwrap();
let skill_md_content = "---\nname: test-standardized\ndescription: A test standardized skill\n---\n\nThis is the skill content with {{BRANCH_ID}} placeholder.";
std::fs::write(skill_dir.join("SKILL.md"), skill_md_content).unwrap();
let original_dir = std::env::current_dir().unwrap();
std::env::set_current_dir(&project_dir).unwrap();
let tmpl = resolve("test-standardized").expect("should resolve");
assert_eq!(tmpl.format, SkillFormat::Standardized);
assert!(tmpl.content.contains("This is the skill content"));
assert!(tmpl.content.contains("{{BRANCH_ID}}"));
assert!(tmpl.metadata.is_some());
let metadata = tmpl.metadata.as_ref().unwrap();
assert_eq!(metadata.name, "test-standardized");
assert_eq!(metadata.description, "A test standardized skill");
std::env::set_current_dir(original_dir).unwrap();
}
#[test]
fn standardized_skill_with_resources_loads_paths() {
let dir = tempfile::tempdir().unwrap();
let skills_parent_dir = dir.path().join("git-paw").join("agent-skills");
let specific_skill_dir = skills_parent_dir.join("test-with-resources");
std::fs::create_dir_all(&specific_skill_dir).unwrap();
std::fs::create_dir_all(specific_skill_dir.join("scripts")).unwrap();
std::fs::create_dir_all(specific_skill_dir.join("references")).unwrap();
std::fs::create_dir_all(specific_skill_dir.join("assets")).unwrap();
let skill_md_content = "---\nname: test-with-resources\ndescription: Skill with resources\n---\n\nMain content here.";
std::fs::write(specific_skill_dir.join("SKILL.md"), skill_md_content).unwrap();
let tmpl = resolve_with_config_dir("test-with-resources", Some(dir.path()))
.expect("should resolve");
assert_eq!(tmpl.format, SkillFormat::Standardized);
assert!(tmpl.resource_paths.is_some());
let resource_paths = tmpl.resource_paths.as_ref().unwrap();
assert_eq!(resource_paths.len(), 3);
assert!(resource_paths.iter().any(|p| p.ends_with("scripts")));
assert!(resource_paths.iter().any(|p| p.ends_with("references")));
assert!(resource_paths.iter().any(|p| p.ends_with("assets")));
}
#[test]
#[serial(directory_changes)]
fn standard_location_loading() {
let temp_dir = tempfile::tempdir().unwrap();
let project_dir = temp_dir.path().join("my-project");
std::fs::create_dir_all(&project_dir).unwrap();
let standard_skill_dir = project_dir
.join(".agents")
.join("skills")
.join("test-skill");
std::fs::create_dir_all(&standard_skill_dir).unwrap();
let standard_content = "---\nname: test-skill\ndescription: Standard location skill\n---\n\nContent from .agents/skills/";
std::fs::write(standard_skill_dir.join("SKILL.md"), standard_content).unwrap();
let original_dir = std::env::current_dir().unwrap();
std::env::set_current_dir(&project_dir).unwrap();
let tmpl = resolve("test-skill").expect("should resolve");
assert_eq!(tmpl.source, Source::AgentsStandard);
assert!(tmpl.content.contains("Content from .agents/skills/"));
std::env::set_current_dir(original_dir).unwrap();
}
#[test]
fn standardized_skill_metadata_placeholders_are_substituted() {
let metadata = StandardizedSkillMetadata {
name: "test-skill".to_string(),
description: "Test description".to_string(),
license: None,
compatibility: None,
metadata: None,
};
let tmpl = SkillTemplate {
name: "test".into(),
content: "Name: {{SKILL_NAME}}, Desc: {{SKILL_DESCRIPTION}}".into(),
source: Source::Embedded,
format: SkillFormat::Standardized,
metadata: Some(metadata),
resource_paths: None,
};
let output = render(&tmpl, "feat/x", "http://127.0.0.1:9119", "git-paw", None);
assert!(output.contains("Name: test-skill, Desc: Test description"));
assert!(!output.contains("{{SKILL_NAME}}"));
assert!(!output.contains("{{SKILL_DESCRIPTION}}"));
}
#[test]
fn test_command_placeholder_substitutes_when_set() {
let tmpl = SkillTemplate {
name: "supervisor".into(),
content: "Run `{{TEST_COMMAND}}` after each merge.".into(),
source: Source::Embedded,
format: SkillFormat::Standardized,
metadata: None,
resource_paths: None,
};
let output = render(
&tmpl,
"supervisor",
"http://127.0.0.1:9119",
"git-paw",
Some("just check"),
);
assert_eq!(output, "Run `just check` after each merge.");
assert!(!output.contains("{{TEST_COMMAND}}"));
}
#[test]
fn test_command_placeholder_falls_back_when_unset() {
let tmpl = SkillTemplate {
name: "supervisor".into(),
content: "Baseline: {{TEST_COMMAND}}".into(),
source: Source::Embedded,
format: SkillFormat::Standardized,
metadata: None,
resource_paths: None,
};
let output = render(
&tmpl,
"supervisor",
"http://127.0.0.1:9119",
"git-paw",
None,
);
assert_eq!(output, "Baseline: (not configured)");
assert!(!output.contains("{{TEST_COMMAND}}"));
}
#[test]
fn supervisor_template_no_unsubstituted_placeholders_when_test_command_set() {
let tmpl = resolve("supervisor").expect("supervisor skill resolves");
let output = render(
&tmpl,
"supervisor",
"http://127.0.0.1:9119",
"git-paw",
Some("just check"),
);
assert!(
!output.contains("{{TEST_COMMAND}}"),
"supervisor template still contains a literal {{TEST_COMMAND}} after render"
);
assert!(
!output.contains("{{"),
"supervisor template has unsubstituted {{...}} placeholder after render"
);
}
#[test]
fn invalid_standardized_skill_frontmatter_returns_error() {
let dir = tempfile::tempdir().unwrap();
let project_dir = dir.path().join("my-project");
std::fs::create_dir_all(&project_dir).unwrap();
let skill_dir = project_dir
.join(".agents")
.join("skills")
.join("invalid-skill");
std::fs::create_dir_all(&skill_dir).unwrap();
let skill_md_content = "---\nname: invalid-skill\n---\n\nContent here.";
std::fs::write(skill_dir.join("SKILL.md"), skill_md_content).unwrap();
let original_dir = std::env::current_dir().unwrap();
std::env::set_current_dir(&project_dir).unwrap();
let result = resolve("invalid-skill");
assert!(matches!(result, Err(SkillError::ValidationError { .. })));
std::env::set_current_dir(original_dir).unwrap();
}
#[test]
fn skill_template_is_cloneable() {
let tmpl = resolve("coordination").unwrap();
let cloned = tmpl.clone();
assert_eq!(tmpl.name, cloned.name);
assert_eq!(tmpl.content, cloned.content);
assert_eq!(tmpl.source, cloned.source);
}
#[test]
fn boot_block_contains_all_four_essential_events() {
let block = build_boot_block("feat/errors", "http://localhost:9119");
assert!(
block.contains("### 1. REGISTER"),
"Missing REGISTER section"
);
assert!(block.contains("### 2. DONE"), "Missing DONE section");
assert!(block.contains("### 3. BLOCKED"), "Missing BLOCKED section");
assert!(
block.contains("### 4. QUESTION"),
"Missing QUESTION section"
);
}
#[test]
fn boot_block_substitutes_branch_id_placeholder() {
let block = build_boot_block("Feature/HTTP_Broker", "http://localhost:9119");
assert!(
block.contains("feature-http_broker"),
"Branch ID not properly slugified"
);
assert!(
!block.contains("{{BRANCH_ID}}"),
"BRANCH_ID placeholder not substituted"
);
}
#[test]
fn boot_block_substitutes_broker_url_placeholder() {
let block = build_boot_block("feat/x", "http://127.0.0.1:9119");
assert!(
block.contains("http://127.0.0.1:9119/publish"),
"Broker URL not substituted"
);
assert!(
!block.contains("{{GIT_PAW_BROKER_URL}}"),
"GIT_PAW_BROKER_URL placeholder not substituted"
);
}
#[test]
fn boot_block_contains_paste_handling_instructions() {
let block = build_boot_block("feat/x", "http://localhost:9119");
assert!(
block.contains("PASTE HANDLING"),
"Missing paste handling section"
);
assert!(
block.contains("additional Enter key"),
"Missing Enter key instruction"
);
assert!(
block.contains("[Pasted text #N]"),
"Missing paste text reference"
);
}
#[test]
fn boot_block_question_section_emphasizes_waiting() {
let block = build_boot_block("feat/x", "http://localhost:9119");
assert!(
block.contains("DO NOT CONTINUE UNTIL YOU RECEIVE AN ANSWER!"),
"Missing wait emphasis"
);
assert!(
block.contains("WAIT for the answer before continuing"),
"Missing wait instruction"
);
}
#[test]
fn boot_block_is_deterministic() {
let a = build_boot_block("feat/x", "http://localhost:9119");
let b = build_boot_block("feat/x", "http://localhost:9119");
assert_eq!(a, b, "Boot block generation should be deterministic");
}
#[test]
fn boot_block_handles_complex_branch_names() {
let block = build_boot_block("fix/topological-cycle-fallback", "http://localhost:9119");
assert!(
block.contains("fix-topological-cycle-fallback"),
"Complex branch name not properly slugified"
);
}
#[test]
fn boot_block_contains_pre_expanded_curl_commands() {
let block = build_boot_block("feat/test", "http://127.0.0.1:9119");
assert!(
block.contains("curl -s -X POST http://127.0.0.1:9119/publish"),
"Curl commands not pre-expanded"
);
assert!(
block.contains("\"agent_id\":\"feat-test\""),
"Agent ID not substituted in curl commands"
);
}
}