# Test tool creation ----------------------------------------------------------
# Note: btw_agent_tool() returns raw tools that get wrapped with _intent
# argument later by as_ellmer_tools(). These tests check the unwrapped tools.
test_that("btw_agent_tool() returns NULL for invalid name", {
tmp_dir <- withr::local_tempdir()
agent_file <- file.path(tmp_dir, "agent-invalid.md")
writeLines(
"---\nname: 123invalid\ndescription: Test\n---\nPrompt.",
agent_file
)
expect_warning(
result <- btw_agent_tool(agent_file),
"Invalid agent name"
)
expect_null(result)
})
test_that("btw_agent_tool() returns NULL for reserved name", {
tmp_dir <- withr::local_tempdir()
reserved_name <- names(.btw_tools)[1]
agent_file <- file.path(tmp_dir, sprintf("agent-%s.md", reserved_name))
writeLines(
sprintf("---\nname: %s\ndescription: Test\n---\nPrompt.", reserved_name),
agent_file
)
expect_warning(
result <- btw_agent_tool(agent_file),
"reserved"
)
expect_null(result)
})
test_that("btw_agent_tool() errors for non-existent file", {
expect_error(
btw_agent_tool("/nonexistent/path/agent-test.md"),
"Agent file not found"
)
})
# Test integration with btw_tools() -------------------------------------------
# These tests check the full integration through btw_tools() which applies
# all the necessary wrapping including _intent argument.
test_that("custom agents can be discovered and loaded", {
skip_if_not_installed("ellmer")
tmp_dir <- withr::local_tempdir()
btw_dir <- file.path(tmp_dir, ".btw")
dir.create(btw_dir)
local_test_agent_file(btw_dir, "integration_test")
# Get tools from that directory
tools <- withr::with_dir(tmp_dir, custom_agent_discover_tools())
expect_type(tools, "list")
expect_true("btw_tool_agent_integration_test" %in% names(tools))
tool_def <- tools[["btw_tool_agent_integration_test"]]
expect_equal(tool_def$name, "btw_tool_agent_integration_test")
expect_equal(tool_def$group, "agent")
expect_type(tool_def$tool, "closure")
# Calling tool() should return a tool object (before wrapping)
tool <- tool_def$tool()
expect_equal(tool@name, "btw_tool_agent_integration_test")
expect_equal(tool@description, "A test agent")
})
test_that("custom_agent_discover_tools() returns empty list when no agents", {
tmp_dir <- withr::local_tempdir()
btw_dir <- file.path(tmp_dir, ".btw")
dir.create(btw_dir)
tools <- withr::with_dir(tmp_dir, custom_agent_discover_tools())
expect_length(tools, 0)
})
test_that("custom_agent_discover_tools() skips files with invalid names", {
skip_if_not_installed("ellmer")
tmp_dir <- withr::local_tempdir()
btw_dir <- file.path(tmp_dir, ".btw")
dir.create(btw_dir)
# Create valid agent
local_test_agent_file(btw_dir, "valid_agent")
# Create agent with invalid name
content_invalid <- "---
name: 123invalid
description: Invalid
---
Invalid agent."
writeLines(content_invalid, file.path(btw_dir, "agent-invalid.md"))
expect_warning(
tools <- withr::with_dir(tmp_dir, custom_agent_discover_tools()),
"Invalid agent name"
)
# Should only have the valid agent
expect_length(tools, 1)
expect_true("btw_tool_agent_valid_agent" %in% names(tools))
})
test_that("custom_agent_discover_tools() skips files with missing name", {
skip_if_not_installed("ellmer")
tmp_dir <- withr::local_tempdir()
btw_dir <- file.path(tmp_dir, ".btw")
dir.create(btw_dir)
# Create agent without name
content_no_name <- "---
description: No name
---
Agent without name."
writeLines(content_no_name, file.path(btw_dir, "agent-noname.md"))
# Should warn about missing name
expect_warning(
tools <- withr::with_dir(tmp_dir, custom_agent_discover_tools()),
"Agent file has no name"
)
expect_length(tools, 0)
})
test_that("custom_agent_discover_tools() warns on error loading agent", {
tmp_dir <- withr::local_tempdir()
btw_dir <- file.path(tmp_dir, ".btw")
dir.create(btw_dir)
# Create file with malformed YAML
content_bad_yaml <- "---
name: bad_yaml
description: [invalid yaml structure
---
Bad YAML."
writeLines(content_bad_yaml, file.path(btw_dir, "agent-bad.md"))
expect_warning(
tools <- withr::with_dir(tmp_dir, custom_agent_discover_tools()),
"Error loading custom agent"
)
})
test_that("custom_agent_discover_tools() handles multiple agents", {
skip_if_not_installed("ellmer")
tmp_dir <- withr::local_tempdir()
btw_dir <- file.path(tmp_dir, ".btw")
dir.create(btw_dir)
local_test_agent_file(btw_dir, "agent_one")
local_test_agent_file(btw_dir, "agent_two")
local_test_agent_file(btw_dir, "agent_three")
tools <- withr::with_dir(tmp_dir, custom_agent_discover_tools())
expect_length(tools, 3)
expect_true("btw_tool_agent_agent_one" %in% names(tools))
expect_true("btw_tool_agent_agent_two" %in% names(tools))
expect_true("btw_tool_agent_agent_three" %in% names(tools))
})
# Test error handling ---------------------------------------------------------
test_that("validate_agent_name() includes helpful messages", {
expect_warning(
validate_agent_name(NULL, "test.md"),
"Add.*name: agent_name"
)
expect_warning(
validate_agent_name(names(.btw_tools)[1], "test.md"),
"reserved"
)
expect_warning(
validate_agent_name("123bad", "test.md"),
"must be valid R identifiers"
)
expect_warning(
validate_agent_name("has-dash", "test.md"),
"must start with a letter"
)
})
test_that("btw_agent_tool() returns valid tool for valid config", {
tmp_dir <- withr::local_tempdir()
agent_file <- file.path(tmp_dir, "agent-config_test.md")
writeLines(
c(
"---",
"name: config_test",
"description: A config test agent",
"tools:",
" - files",
" - docs",
"---",
"Test prompt"
),
agent_file
)
tool <- btw_agent_tool(agent_file)
# The tool should be created (not NULL)
expect_false(is.null(tool))
# Check basic properties
expect_equal(tool@name, "btw_tool_agent_config_test")
expect_equal(tool@description, "A config test agent")
})
# Test agent name variations --------------------------------------------------
test_that("validate_agent_name() handles various valid patterns", {
valid_patterns <- c(
"a", # Single letter
"A", # Capital letter
"agent_123", # With numbers
"AgentName", # CamelCase
"agent_name_v2", # Multiple underscores
"MyAgent123" # Mixed
)
for (name in valid_patterns) {
expect_true(validate_agent_name(name, "test.md"), info = name)
}
})
test_that("validate_agent_name() rejects edge cases", {
invalid_cases <- c(
"_agent", # Starts with underscore
"1agent", # Starts with number
"agent-name", # Contains dash
"agent name", # Contains space
"agent.name", # Contains dot
"agent$name", # Contains special char
"" # Empty string
)
for (name in invalid_cases) {
expect_warning(
result <- validate_agent_name(name, "test.md"),
info = name
)
expect_false(result, info = name)
}
})
# Internal closure structure is an implementation detail.
# Tool behavior is tested through integration tests below.
# ---- Custom Agent Configuration (Behavioral) --------------------------------
test_that("custom_agent_client_from_config creates chat with custom system prompt", {
withr::local_dir(withr::local_tempdir())
dir.create(".btw")
# Create agent file
writeLines(
c(
"---",
"name: code_reviewer",
"description: Expert code reviewer",
"tools:",
" - files",
"---",
"",
"You are an expert code reviewer. Focus on:",
"- Code quality and best practices",
"- Performance issues",
"- Security vulnerabilities"
),
".btw/agent-code-reviewer.md"
)
# Load config and create chat
agent_config <- read_agent_md_file(".btw/agent-code-reviewer.md")
chat <- custom_agent_client_from_config(agent_config)
expect_true(inherits(chat, "Chat"))
# Verify system prompt
system_prompt <- chat$get_system_prompt()
expect_match(system_prompt, "expert code reviewer", ignore.case = TRUE)
expect_match(system_prompt, "Code quality", ignore.case = TRUE)
# Verify tools
tool_names <- map_chr(chat$get_tools(), function(t) t@name)
expect_true(all(grepl("^btw_tool_files_", tool_names)))
expect_false(any(grepl("^btw_tool_docs_", tool_names)))
})
test_that("custom_agent_client_from_config respects tool restrictions", {
withr::local_dir(withr::local_tempdir()) # avoid any user/global btw.md files
agent_config <- list(
name = "docs_agent",
description = "Documentation expert",
tools = "docs",
system_prompt = "You help with documentation.",
tools_default = NULL,
tools_allowed = NULL,
client = NULL
)
chat <- custom_agent_client_from_config(agent_config)
tool_names <- map_chr(chat$get_tools(), function(t) t@name)
expect_true(all(grepl("^btw_tool_docs_", tool_names)))
expect_false(any(grepl("^btw_tool_files_", tool_names)))
})
test_that("custom_agent_client_from_config concatenates system prompts", {
withr::local_dir(withr::local_tempdir()) # avoid any user/global btw.md files
agent_config <- list(
name = "test",
client = NULL,
tools = "files",
system_prompt = "Custom instructions for this agent.",
tools_default = NULL,
tools_allowed = NULL
)
chat <- custom_agent_client_from_config(agent_config)
system_prompt <- chat$get_system_prompt()
# Should include base prompt
expect_match(system_prompt, "Task Execution", ignore.case = TRUE)
# Should include custom prompt
expect_match(system_prompt, "Custom instructions", ignore.case = TRUE)
# Should have separator
expect_match(system_prompt, "---")
})
test_that("custom_agent_client_from_config uses subagent_resolve_client", {
# Test explicit client
custom_client <- ellmer::chat_anthropic(model = "claude-opus-4-20241120")
agent_config <- list(
name = "test",
client = custom_client,
tools = "files",
system_prompt = "Test"
)
chat <- custom_agent_client_from_config(agent_config)
expect_identical(chat, custom_client)
# Test option fallback
withr::local_options(
btw.subagent.client = "anthropic/claude-sonnet-4-20250514"
)
agent_config$client <- NULL
chat2 <- custom_agent_client_from_config(agent_config)
expect_equal(chat2$get_model(), "claude-sonnet-4-20250514")
})
# ---- Multiple Custom Agents -------------------------------------------------
test_that("multiple custom agents can be discovered and registered", {
withr::local_dir(withr::local_tempdir())
dir.create(".btw")
# Create two agents
local_test_agent_file(".btw", "agent_one")
local_test_agent_file(".btw", "agent_two")
# Use custom_agent_discover_tools() to get internal btw tool structure
tools <- custom_agent_discover_tools()
expect_type(tools, "list")
expect_true("btw_tool_agent_agent_one" %in% names(tools))
expect_true("btw_tool_agent_agent_two" %in% names(tools))
# Verify they have correct structure
agent_one_def <- tools[["btw_tool_agent_agent_one"]]
agent_two_def <- tools[["btw_tool_agent_agent_two"]]
expect_equal(agent_one_def$name, "btw_tool_agent_agent_one")
expect_equal(agent_one_def$group, "agent")
expect_type(agent_one_def$tool, "closure")
expect_equal(agent_two_def$name, "btw_tool_agent_agent_two")
expect_equal(agent_two_def$group, "agent")
expect_type(agent_two_def$tool, "closure")
})
# Test custom_icon() -----------------------------------------------------------
describe("custom_icon()", {
it("returns NULL for NULL or empty input", {
expect_null(custom_icon(NULL))
expect_null(custom_icon(""))
})
it("handles raw SVG input", {
svg <- ''
result <- custom_icon(svg)
expect_s3_class(result, "html")
expect_true(grepl("'
result <- custom_icon(svg)
expect_s3_class(result, "html")
})
it("handles SVG case-insensitively", {
svg <- ''
result <- custom_icon(svg)
expect_s3_class(result, "html")
})
it("uses shiny::icon() for plain icon names", {
skip_if_not_installed("shiny")
result <- custom_icon("home")
expect_s3_class(result, "shiny.tag")
# Font Awesome 6 uses "fa-house" for "home"
expect_true(grepl("fa-ho", as.character(result)))
})
it("warns and returns NULL for unknown icon names", {
skip_if_not_installed("shiny")
expect_warning(
result <- custom_icon("some-unknown-icon-name"),
"is not supported"
)
expect_null(result)
})
it("warns for unknown package prefix", {
expect_warning(
result <- custom_icon("unknownpkg::someicon"),
"Unknown icon package"
)
expect_null(result)
})
it("warns for invalid specification format", {
expect_warning(
result <- custom_icon("too::many::colons"),
"Invalid icon specification"
)
expect_null(result)
})
it("warns when package is not installed", {
# Use a package that definitely isn't installed
expect_warning(
result <- custom_icon("notarealpackage123::home"),
"Unknown icon package"
)
expect_null(result)
})
})
describe("custom_icon() with fontawesome package", {
skip_if_not_installed("fontawesome")
it("uses fontawesome::fa() for fontawesome:: prefix", {
result <- custom_icon("fontawesome::home")
expect_s3_class(result, "fontawesome")
expect_true(grepl("svg", as.character(result)))
})
it("warns for invalid fontawesome icon", {
expect_warning(
result <- custom_icon("fontawesome::nonexistent-icon-xyz"),
"Error creating icon"
)
expect_null(result)
})
})
describe("custom_icon() with bsicons package", {
skip_if_not_installed("bsicons")
it("uses bsicons::bs_icon() for bsicons:: prefix", {
result <- custom_icon("bsicons::house")
# bsicons returns an "html" class object
expect_s3_class(result, "html")
expect_true(grepl("svg", as.character(result)))
})
it("warns for invalid bsicons icon", {
expect_warning(
result <- custom_icon("bsicons::nonexistent-icon-xyz"),
"Error creating icon"
)
expect_null(result)
})
})
describe("custom_icon() with phosphoricons package", {
skip_if_not_installed("phosphoricons")
it("uses phosphoricons::ph() for phosphoricons:: prefix", {
result <- custom_icon("phosphoricons::house")
expect_s3_class(result, "shiny.tag")
expect_true(grepl("svg", as.character(result)))
})
})
describe("custom_icon() with shiny:: prefix", {
skip_if_not_installed("shiny")
it("uses shiny::icon() for shiny:: prefix", {
result <- custom_icon("shiny::home")
expect_s3_class(result, "shiny.tag")
# Font Awesome 6 uses "fa-house" for "home"
expect_true(grepl("fa-ho", as.character(result)))
})
})
describe("custom_icon() integration with btw_agent_tool()", {
it("applies custom icon from config", {
skip_if_not_installed("shiny")
tmp_dir <- withr::local_tempdir()
agent_file <- file.path(tmp_dir, "agent-icon_test.md")
writeLines(
c(
"---",
"name: icon_test",
"description: Test icon configuration",
"icon: robot",
"---",
"Test prompt"
),
agent_file
)
tool <- btw_agent_tool(agent_file)
expect_false(is.null(tool))
expect_s3_class(tool@annotations$icon, "shiny.tag")
})
it("applies SVG icon from config", {
tmp_dir <- withr::local_tempdir()
agent_file <- file.path(tmp_dir, "agent-svg_test.md")
writeLines(
c(
"---",
"name: svg_test",
"description: Test SVG icon",
'icon: \'\'',
"---",
"Test prompt"
),
agent_file
)
tool <- btw_agent_tool(agent_file)
expect_false(is.null(tool))
expect_s3_class(tool@annotations$icon, "html")
})
it("falls back to default icon when custom_icon returns NULL", {
tmp_dir <- withr::local_tempdir()
agent_file <- file.path(tmp_dir, "agent-no_icon.md")
writeLines(
c(
"---",
"name: no_icon",
"description: Test without icon",
"---",
"Test prompt"
),
agent_file
)
tool <- btw_agent_tool(agent_file)
expect_false(is.null(tool))
# Should have the default agent icon
expect_false(is.null(tool@annotations$icon))
})
it("falls back to default icon for unknown package prefix", {
tmp_dir <- withr::local_tempdir()
agent_file <- file.path(tmp_dir, "agent-bad_icon.md")
writeLines(
c(
"---",
"name: bad_icon",
"description: Test with invalid icon",
"icon: unknownpkg::someicon",
"---",
"Test prompt"
),
agent_file
)
expect_warning(
tool <- btw_agent_tool(agent_file),
"Unknown icon package"
)
expect_false(is.null(tool))
# Should fall back to default agent icon
expect_false(is.null(tool@annotations$icon))
})
it("falls back to default icon for unknown shiny icon names", {
skip_if_not_installed("shiny")
tmp_dir <- withr::local_tempdir()
agent_file <- file.path(tmp_dir, "agent-unknown_icon.md")
writeLines(
c(
"---",
"name: unknown_icon",
"description: Test with unknown shiny icon",
"icon: some-unknown-icon-xyz",
"---",
"Test prompt"
),
agent_file
)
expect_warning(
tool <- btw_agent_tool(agent_file),
"is not supported"
)
expect_false(is.null(tool))
# Should fall back to default agent icon
expect_false(is.null(tool@annotations$icon))
})
})