use anyhow::Result;
use tokio::fs;
use crate::common::{ManifestBuilder, TestProject};
#[tokio::test]
async fn test_basic_template_substitution() -> Result<()> {
agpm_cli::test_utils::init_test_logging(None);
let project = TestProject::new().await?;
let test_repo = project.create_source_repo("test-repo").await?;
test_repo
.add_resource(
"agents",
"test-agent",
r#"---
title: Test Agent
agpm:
templating: true
---
# {{ agpm.resource.name }}
This agent is installed at: `{{ agpm.resource.install_path }}`
Version: {{ agpm.resource.version }}
"#,
)
.await?;
test_repo.commit_all("Add test agent")?;
test_repo.tag_version("v1.0.0")?;
let repo_url = test_repo.bare_file_url(project.sources_path()).await?;
let manifest = ManifestBuilder::new()
.add_source("test-repo", &repo_url)
.add_agent("test-agent", |d| {
d.source("test-repo").path("agents/test-agent.md").version("v1.0.0")
})
.build();
project.write_manifest(&manifest).await?;
let output = project.run_agpm(&["install"])?;
assert!(output.success);
let installed_path = project.project_path().join(".claude/agents/agpm/test-agent.md");
let content = fs::read_to_string(&installed_path).await?;
assert!(
content.contains("# agents/test-agent"),
"Resource name should be substituted with canonical format"
);
#[cfg(windows)]
let expected_path = "installed at: `.claude\\agents\\agpm\\test-agent.md`";
#[cfg(not(windows))]
let expected_path = "installed at: `.claude/agents/agpm/test-agent.md`";
assert!(
content.contains(expected_path),
"Install path should be substituted with platform-native separators. Content:\n{}",
content
);
assert!(content.contains("Version: v1.0.0"), "Version should be substituted");
assert!(!content.contains("{{ agpm"), "Template syntax should be replaced");
Ok(())
}
#[tokio::test]
async fn test_non_templated_files_with_curly_braces() -> Result<()> {
agpm_cli::test_utils::init_test_logging(None);
let project = TestProject::new().await?;
let test_repo = project.create_source_repo("test-repo").await?;
test_repo
.add_resource(
"snippets",
"javascript-snippet",
r#"---
title: JavaScript Snippet
---
// JavaScript code with arrow functions
const calculateSum = (a, b) => {
return a + b;
};
// Template literal syntax in JavaScript
const message = `Hello, ${name}!`;
// Object destructuring
const { firstName, lastName } = person;
// Array destructuring with rest
const [first, ...rest] = items;
console.log(calculateSum(5, 3));
console.log(message);
console.log(firstName, lastName);
console.log(first, rest);
"#,
)
.await?;
test_repo.commit_all("Add JavaScript snippet")?;
test_repo.tag_version("v1.0.0")?;
let repo_url = test_repo.bare_file_url(project.sources_path()).await?;
let manifest = ManifestBuilder::new()
.add_source("test-repo", &repo_url)
.add_snippet("javascript-snippet", |d| {
d.source("test-repo")
.path("snippets/javascript-snippet.md")
.version("v1.0.0")
.tool("agpm")
})
.build();
project.write_manifest(&manifest).await?;
let output = project.run_agpm(&["install"])?;
assert!(output.success, "Install should succeed");
let installed_path = project.project_path().join(".agpm/snippets/javascript-snippet.md");
let content = fs::read_to_string(&installed_path).await?;
assert!(content.contains("const calculateSum = (a, b) => {"));
assert!(content.contains("const message = `Hello, ${name}!`;"));
assert!(content.contains("const { firstName, lastName } = person;"));
assert!(content.contains("const [first, ...rest] = items;"));
assert!(content.contains("console.log(calculateSum(5, 3));"));
Ok(())
}
#[tokio::test]
async fn test_dependency_references() -> Result<()> {
agpm_cli::test_utils::init_test_logging(None);
let project = TestProject::new().await?;
let test_repo = project.create_source_repo("test-repo").await?;
test_repo
.add_resource(
"snippets",
"helper",
r#"---
title: Helper Functions
agpm:
templating: true
---
# Helper Functions
This file contains helper functions.
## Function List
- sum
- multiply
- divide
"#,
)
.await?;
test_repo
.add_resource(
"agents",
"main-agent",
r#"---
title: Main Agent
dependencies:
snippets:
- path: snippets/helper.md
tool: agpm
name: helper
agpm:
templating: true
---
# Main Agent
This agent uses helper functions from snippets.
{{ agpm.deps.snippets.helper.content }}
## Usage
See helper functions above.
"#,
)
.await?;
test_repo.commit_all("Add agent and snippet")?;
test_repo.tag_version("v1.0.0")?;
let repo_url = test_repo.bare_file_url(project.sources_path()).await?;
let manifest = ManifestBuilder::new()
.add_source("test-repo", &repo_url)
.add_snippet("helper", |d| {
d.source("test-repo").path("snippets/helper.md").version("v1.0.0").tool("agpm")
})
.add_agent("main-agent", |d| {
d.source("test-repo").path("agents/main-agent.md").version("v1.0.0")
})
.build();
project.write_manifest(&manifest).await?;
let output = project.run_agpm(&["install"])?;
assert!(output.success, "Install should succeed. stderr: {}", output.stderr);
let agent_path = project.project_path().join(".claude/agents/agpm/main-agent.md");
let content = fs::read_to_string(&agent_path).await?;
assert!(content.contains("# Helper Functions"));
assert!(content.contains("## Function List"));
assert!(content.contains("- sum"));
assert!(content.contains("- multiply"));
assert!(content.contains("- divide"));
Ok(())
}
#[tokio::test]
async fn test_templating_disabled_preserves_syntax() -> Result<()> {
agpm_cli::test_utils::init_test_logging(None);
let project = TestProject::new().await?;
let test_repo = project.create_source_repo("test-repo").await?;
test_repo
.add_resource(
"agents",
"default-agent",
r#"---
title: Default Agent
---
# {{ agpm.resource.name }}
Install path: {{ agpm.resource.install_path }}
"#,
)
.await?;
test_repo
.add_resource(
"agents",
"explicit-disabled",
r#"---
title: Explicit Disabled Agent
agpm:
templating: false
---
# Agent with Literal Syntax
This file contains literal template syntax: {{ agpm.resource.name }}
"#,
)
.await?;
test_repo.commit_all("Add agents")?;
test_repo.tag_version("v1.0.0")?;
let repo_url = test_repo.bare_file_url(project.sources_path()).await?;
let manifest = ManifestBuilder::new()
.add_source("test-repo", &repo_url)
.add_agent("default-agent", |d| {
d.source("test-repo").path("agents/default-agent.md").version("v1.0.0")
})
.add_agent("explicit-disabled", |d| {
d.source("test-repo").path("agents/explicit-disabled.md").version("v1.0.0")
})
.build();
project.write_manifest(&manifest).await?;
let output = project.run_agpm(&["install"])?;
assert!(output.success);
let default_path = project.project_path().join(".claude/agents/agpm/default-agent.md");
let default_content = fs::read_to_string(&default_path).await?;
assert!(
default_content.contains("# {{ agpm.resource.name }}"),
"Template syntax should remain literal by default"
);
assert!(
default_content.contains("{{ agpm.resource.install_path }}"),
"All template syntax should be preserved by default"
);
let explicit_path = project.project_path().join(".claude/agents/agpm/explicit-disabled.md");
let explicit_content = fs::read_to_string(&explicit_path).await?;
assert!(
explicit_content.contains("{{ agpm.resource.name }}"),
"Template syntax should remain literal when templating is explicitly disabled"
);
Ok(())
}
#[tokio::test]
async fn test_no_template_syntax() -> Result<()> {
agpm_cli::test_utils::init_test_logging(None);
let project = TestProject::new().await?;
let test_repo = project.create_source_repo("test-repo").await?;
test_repo
.add_resource(
"agents",
"plain-agent",
r#"---
title: Plain Agent
agpm:
templating: true
---
# Plain Agent
This agent has no template syntax.
## Features
- Feature 1
- Feature 2
- Feature 3
## Usage
Just use it normally.
"#,
)
.await?;
test_repo.commit_all("Add plain agent")?;
test_repo.tag_version("v1.0.0")?;
let repo_url = test_repo.bare_file_url(project.sources_path()).await?;
let manifest = ManifestBuilder::new()
.add_source("test-repo", &repo_url)
.add_agent("plain-agent", |d| {
d.source("test-repo").path("agents/plain-agent.md").version("v1.0.0")
})
.build();
project.write_manifest(&manifest).await?;
let output = project.run_agpm(&["install"])?;
assert!(output.success, "Install should succeed");
let installed_path = project.project_path().join(".claude/agents/agpm/plain-agent.md");
let content = fs::read_to_string(&installed_path).await?;
assert!(content.contains("# Plain Agent"));
assert!(content.contains("This agent has no template syntax."));
assert!(content.contains("- Feature 1"));
assert!(content.contains("Just use it normally."));
Ok(())
}
#[tokio::test]
async fn test_conditional_rendering() -> Result<()> {
agpm_cli::test_utils::init_test_logging(None);
let project = TestProject::new().await?;
let test_repo = project.create_source_repo("test-repo").await?;
test_repo
.add_resource(
"agents",
"conditional",
r#"---
title: Conditional Agent
agpm:
templating: true
---
# Conditional Content
{% if agpm.resource.source %}
This resource is from source: {{ agpm.resource.source }}
{% else %}
This is a local resource.
{% endif %}
{% if agpm.resource.version %}
Version: {{ agpm.resource.version }}
{% endif %}
"#,
)
.await?;
test_repo.commit_all("Add agent")?;
test_repo.tag_version("v1.0.0")?;
let repo_url = test_repo.bare_file_url(project.sources_path()).await?;
let manifest = ManifestBuilder::new()
.add_source("test-repo", &repo_url)
.add_agent("conditional", |d| {
d.source("test-repo").path("agents/conditional.md").version("v1.0.0")
})
.build();
project.write_manifest(&manifest).await?;
let output = project.run_agpm(&["install"])?;
assert!(output.success);
let installed_path = project.project_path().join(".claude/agents/agpm/conditional.md");
let content = fs::read_to_string(&installed_path).await?;
assert!(
content.contains("This resource is from source: test-repo"),
"Conditional block should render when condition is true"
);
assert!(!content.contains("This is a local resource"), "Alternative block should not render");
assert!(
content.contains("Version: v1.0.0"),
"Optional block should render when variable exists"
);
Ok(())
}
#[tokio::test]
async fn test_loop_over_dependencies() -> Result<()> {
agpm_cli::test_utils::init_test_logging(None);
let project = TestProject::new().await?;
let test_repo = project.create_source_repo("test-repo").await?;
test_repo
.add_resource(
"snippets",
"helper1",
r#"---
title: Helper 1
agpm:
templating: true
---
# Helper 1
This is helper 1.
"#,
)
.await?;
test_repo
.add_resource(
"snippets",
"helper2",
r#"---
title: Helper 2
agpm:
templating: true
---
# Helper 2
This is helper 2.
"#,
)
.await?;
test_repo
.add_resource(
"snippets",
"helper3",
r#"---
title: Helper 3
agpm:
templating: true
---
# Helper 3
This is helper 3.
"#,
)
.await?;
test_repo
.add_resource(
"agents",
"looping-agent",
r#"---
title: Looping Agent
dependencies:
snippets:
- path: snippets/helper1.md
tool: agpm
name: helper1
- path: snippets/helper2.md
tool: agpm
name: helper2
- path: snippets/helper3.md
tool: agpm
name: helper3
agpm:
templating: true
---
# Looping Agent
## Available Helpers
{% for name, snippet in agpm.deps.snippets %}
### {{ name }}
{{ snippet.content }}
{% endfor %}
## Count
There are {{ agpm.deps.snippets | length }} helpers available.
"#,
)
.await?;
test_repo.commit_all("Add resources")?;
test_repo.tag_version("v1.0.0")?;
let repo_url = test_repo.bare_file_url(project.sources_path()).await?;
let manifest = ManifestBuilder::new()
.add_source("test-repo", &repo_url)
.add_snippet("helper1", |d| {
d.source("test-repo").path("snippets/helper1.md").version("v1.0.0").tool("agpm")
})
.add_snippet("helper2", |d| {
d.source("test-repo").path("snippets/helper2.md").version("v1.0.0").tool("agpm")
})
.add_snippet("helper3", |d| {
d.source("test-repo").path("snippets/helper3.md").version("v1.0.0").tool("agpm")
})
.add_agent("looping-agent", |d| {
d.source("test-repo").path("agents/looping-agent.md").version("v1.0.0")
})
.build();
project.write_manifest(&manifest).await?;
let output = project.run_agpm(&["install"])?;
assert!(output.success, "Install should succeed");
let agent_path = project.project_path().join(".claude/agents/agpm/looping-agent.md");
let content = fs::read_to_string(&agent_path).await?;
assert!(content.contains("### helper1"));
assert!(content.contains("# Helper 1"));
assert!(content.contains("### helper2"));
assert!(content.contains("# Helper 2"));
assert!(content.contains("### helper3"));
assert!(content.contains("# Helper 3"));
assert!(content.contains("There are 3 helpers available."));
Ok(())
}
#[tokio::test]
async fn test_non_templated_content_embedding() -> Result<()> {
agpm_cli::test_utils::init_test_logging(None);
let project = TestProject::new().await?;
let test_repo = project.create_source_repo("test-repo").await?;
test_repo
.add_resource(
"snippets",
"code-example",
r#"---
title: Code Example Snippet
agpm:
templating: false
---
# Example Code
This snippet contains literal template syntax that should NOT be rendered:
{{ agpm.resource.name }}
{{ project.language }}
These should remain as-is even when embedded.
"#,
)
.await?;
test_repo
.add_resource(
"agents",
"embedding-agent",
r#"---
title: Embedding Agent
agpm:
templating: true
dependencies:
snippets:
- path: snippets/code-example.md
name: code_example
---
# Agent that Embeds Non-Templated Content
Here's the embedded snippet:
{{ agpm.deps.snippets.code_example.content }}
End of embedded content.
"#,
)
.await?;
test_repo.commit_all("Add resources")?;
test_repo.tag_version("v1.0.0")?;
let repo_url = test_repo.bare_file_url(project.sources_path()).await?;
let manifest = ManifestBuilder::new()
.add_source("test-repo", &repo_url)
.add_agent("embedding-agent", |d| {
d.source("test-repo").path("agents/embedding-agent.md").version("v1.0.0")
})
.build();
project.write_manifest(&manifest).await?;
let output = project.run_agpm(&["install"])?;
assert!(output.success, "Install should succeed");
let agent_path = project.project_path().join(".claude/agents/agpm/embedding-agent.md");
let content = fs::read_to_string(&agent_path).await?;
assert!(
content.contains("{{ agpm.resource.name }}"),
"Template syntax from non-templated snippet should remain literal, got:\n{}",
content
);
assert!(
content.contains("{{ project.language }}"),
"Template syntax from non-templated snippet should remain literal, got:\n{}",
content
);
assert!(content.contains("# Example Code"), "Snippet content should be embedded");
assert!(content.contains("End of embedded content"), "Agent's own content should be present");
Ok(())
}
#[tokio::test]
async fn test_nested_transitive_dependency_rendering() -> Result<()> {
agpm_cli::test_utils::init_test_logging(None);
let project = TestProject::new().await?;
let test_repo = project.create_source_repo("test-repo").await?;
test_repo
.add_resource(
"snippets",
"best-practices",
r#"---
title: Best Practices
agpm:
templating: true
---
# Best Practices
This is the best practices content that should be rendered normally.
Language: {{ agpm.resource.name }}
"#,
)
.await?;
test_repo
.add_resource(
"snippets",
"frontend-engineer-base",
r#"---
title: Frontend Engineer Base
agpm:
templating: false
dependencies:
snippets:
- path: snippets/best-practices.md
name: best_practices
---
# Frontend Engineer Base
Here's the best practices content:
{{ agpm.deps.snippets.best_practices.content }}
This template syntax should remain literal because templating: false.
Even though best_practices dependency should be resolved and available,
the template syntax itself should not be rendered when this snippet is embedded.
"#,
)
.await?;
test_repo
.add_resource(
"agents",
"frontend-engineer",
r#"---
title: Frontend Engineer
agpm:
templating: true
dependencies:
snippets:
- path: snippets/frontend-engineer-base.md
name: frontend_engineer_base
---
# Frontend Engineer
Here's the embedded base content:
{{ agpm.deps.snippets.frontend_engineer_base.content }}
End of agent content.
"#,
)
.await?;
test_repo.commit_all("Add nested dependency resources")?;
test_repo.tag_version("v1.0.0")?;
let repo_url = test_repo.bare_file_url(project.sources_path()).await?;
let manifest = ManifestBuilder::new()
.add_source("test-repo", &repo_url)
.add_agent("frontend-engineer", |d| {
d.source("test-repo").path("agents/frontend-engineer.md").version("v1.0.0")
})
.build();
project.write_manifest(&manifest).await?;
let output = project.run_agpm(&["install"])?;
assert!(output.success, "Install should succeed");
let agent_path = project.project_path().join(".claude/agents/agpm/frontend-engineer.md");
let content = fs::read_to_string(&agent_path).await?;
assert!(
content.contains("{{ agpm.deps.snippets.best_practices.content }}"),
"Template syntax from non-templated snippet should remain literal, got:\n{}",
content
);
assert!(
!content.contains("Language: best-practices"),
"With templating: false, snippet should not render its dependencies"
);
assert!(
!content.contains("This is the best practices content that should be rendered normally."),
"With templating: false, snippet content should not be rendered"
);
assert!(content.contains("# Frontend Engineer"), "Agent title should be present");
assert!(content.contains("End of agent content."), "Agent's own content should be present");
Ok(())
}