# Validation --------------------------------------------------------------- test_that("validate_skill() passes for valid skill", { skill_dir <- create_temp_skill() result <- validate_skill(skill_dir) expect_true(result$valid) expect_length(result$issues, 0) }) test_that("validate_skill() fails when SKILL.md is missing", { dir <- withr::local_tempdir() result <- validate_skill(dir) expect_false(result$valid) expect_match(result$issues, "SKILL.md not found") }) test_that("validate_skill() fails for missing name field", { dir <- withr::local_tempdir() skill_dir <- file.path(dir, "test-skill") dir.create(skill_dir) writeLines( "---\ndescription: A skill.\n---\nBody.", file.path(skill_dir, "SKILL.md") ) result <- validate_skill(skill_dir) expect_false(result$valid) expect_match(result$issues, "Missing or empty 'name'", all = FALSE) }) test_that("validate_skill() fails for missing description field", { dir <- withr::local_tempdir() skill_dir <- file.path(dir, "test-skill") dir.create(skill_dir) writeLines( "---\nname: test-skill\n---\nBody.", file.path(skill_dir, "SKILL.md") ) result <- validate_skill(skill_dir) expect_false(result$valid) expect_match(result$issues, "Missing or empty 'description'", all = FALSE) }) test_that("validate_skill() warns for name with uppercase", { skill_dir <- create_temp_skill(name = "test-skill") # Manually override the name in SKILL.md to have uppercase writeLines( "---\nname: Test-Skill\ndescription: A test.\n---\nBody.", file.path(skill_dir, "SKILL.md") ) result <- validate_skill(skill_dir) expect_true(result$valid) expect_match(result$warnings, "lowercase letters", all = FALSE) }) test_that("validate_skill() warns for name starting with hyphen", { dir <- withr::local_tempdir() skill_dir <- file.path(dir, "-bad-name") dir.create(skill_dir) writeLines( "---\nname: -bad-name\ndescription: A test.\n---\nBody.", file.path(skill_dir, "SKILL.md") ) result <- validate_skill(skill_dir) expect_true(result$valid) expect_match( result$warnings, "must not start or end with a hyphen", all = FALSE ) }) test_that("validate_skill() warns for consecutive hyphens", { dir <- withr::local_tempdir() skill_dir <- file.path(dir, "bad--name") dir.create(skill_dir) writeLines( "---\nname: bad--name\ndescription: A test.\n---\nBody.", file.path(skill_dir, "SKILL.md") ) result <- validate_skill(skill_dir) expect_true(result$valid) expect_match(result$warnings, "consecutive hyphens", all = FALSE) }) test_that("validate_skill() warns for name exceeding 64 characters", { long_name <- paste(rep("a", 65), collapse = "") dir <- withr::local_tempdir() skill_dir <- file.path(dir, long_name) dir.create(skill_dir) writeLines( paste0("---\nname: ", long_name, "\ndescription: A test.\n---\nBody."), file.path(skill_dir, "SKILL.md") ) result <- validate_skill(skill_dir) expect_true(result$valid) expect_match(result$warnings, "too long", all = FALSE) }) test_that("validate_skill() warns when name doesn't match directory", { skill_dir <- create_temp_skill(name = "test-skill") writeLines( "---\nname: other-name\ndescription: A test.\n---\nBody.", file.path(skill_dir, "SKILL.md") ) result <- validate_skill(skill_dir) expect_true(result$valid) expect_match(result$warnings, "does not match directory name", all = FALSE) }) test_that("validate_skill() warns for description exceeding 1024 characters", { long_desc <- paste(rep("a", 1025), collapse = "") skill_dir <- create_temp_skill(description = long_desc) result <- validate_skill(skill_dir) expect_true(result$valid) expect_match(result$warnings, "Description is too long", all = FALSE) }) test_that("validate_skill() warns for unexpected frontmatter fields", { skill_dir <- create_temp_skill(extra_frontmatter = list(bogus = TRUE)) result <- validate_skill(skill_dir) expect_true(result$valid) expect_match(result$warnings, "Unexpected frontmatter", all = FALSE) }) test_that("validate_skill() accepts optional fields", { skill_dir <- create_temp_skill( extra_frontmatter = list( license = "MIT", compatibility = "Requires git", "allowed-tools" = "Read Bash", metadata = list(author = "test") ) ) result <- validate_skill(skill_dir) expect_true(result$valid) }) test_that("validate_skill() warns for compatibility exceeding 500 chars", { long_compat <- paste(rep("a", 501), collapse = "") skill_dir <- create_temp_skill( extra_frontmatter = list(compatibility = long_compat) ) result <- validate_skill(skill_dir) expect_true(result$valid) expect_match(result$warnings, "Compatibility field is too long", all = FALSE) }) # Discovery ---------------------------------------------------------------- test_that("btw_skills_list() finds valid skills", { dir <- withr::local_tempdir() create_temp_skill(name = "my-skill", dir = dir) local_skill_dirs(dir) skills <- btw_skills_list() expect_length(skills, 1) expect_equal(skills[["my-skill"]]$name, "my-skill") }) test_that("btw_skills_list() skips invalid skills with warning", { dir <- withr::local_tempdir() # Create a valid skill create_temp_skill(name = "good-skill", dir = dir) # Create an invalid skill (missing description) bad_dir <- file.path(dir, "bad-skill") dir.create(bad_dir) frontmatter::write_front_matter( list(data = list(name = "bad-skill"), body = "Body."), file.path(bad_dir, "SKILL.md") ) local_skill_dirs(dir) expect_warning( skills <- btw_skills_list(), "Skipping invalid skill" ) expect_length(skills, 1) expect_equal(skills[["good-skill"]]$name, "good-skill") }) test_that("btw_skills_list() later directories override earlier by name", { dir1 <- withr::local_tempdir() dir2 <- withr::local_tempdir() create_temp_skill( name = "shared-skill", description = "From dir1.", dir = dir1 ) create_temp_skill( name = "shared-skill", description = "From dir2.", dir = dir2 ) local_skill_dirs(c(dir1, dir2)) expect_message( skills <- btw_skills_list(), "overrides earlier skill" ) expect_length(skills, 1) expect_equal(skills[["shared-skill"]]$description, "From dir2.") }) test_that("btw_skills_list() includes compatibility and allowed-tools", { dir <- withr::local_tempdir() create_temp_skill( name = "fancy-skill", dir = dir, extra_frontmatter = list( compatibility = "Requires Python 3", `allowed-tools` = "Read Bash" ) ) local_skill_dirs(dir) skills <- btw_skills_list() expect_equal(skills[["fancy-skill"]]$compatibility, "Requires Python 3") expect_equal(skills[["fancy-skill"]]$allowed_tools, "Read Bash") }) test_that("btw_skills_directories() discovers skills from multiple project dirs", { project <- withr::local_tempdir() withr::local_dir(project) project <- getwd() # resolve symlinks (e.g. /private/var on macOS) # Create skills in .btw/skills and .agents/skills btw_dir <- file.path(project, ".btw", "skills") agents_dir <- file.path(project, ".agents", "skills") dir.create(btw_dir, recursive = TRUE) dir.create(agents_dir, recursive = TRUE) dirs <- btw_skills_directories() expect_true(btw_dir %in% dirs) expect_true(agents_dir %in% dirs) }) test_that("btw_skills_directories() includes skills from attached packages", { pkg_skills <- withr::local_tempdir() local_mocked_bindings( attached_package_skill_dirs = function() pkg_skills ) dirs <- btw_skills_directories() expect_true(pkg_skills %in% dirs) }) test_that("attached_package_skill_dirs() returns dirs for attached packages with skills", { pkg_skills <- withr::local_tempdir() local_mocked_bindings( `.packages` = function(...) "fakepkg", system.file = function(..., package = NULL) { if (identical(package, "fakepkg")) pkg_skills else "" }, .package = "base" ) result <- attached_package_skill_dirs() expect_equal(result, pkg_skills) }) test_that("attached_package_skill_dirs() skips packages without a skills dir", { local_mocked_bindings( `.packages` = function(...) "fakepkg", system.file = function(..., package = NULL) "", .package = "base" ) result <- attached_package_skill_dirs() expect_equal(result, character()) }) test_that("attached_package_skill_dirs() excludes btw itself", { pkg_skills <- withr::local_tempdir() local_mocked_bindings( `.packages` = function(...) c("btw", "fakepkg"), system.file = function(..., package = NULL) { if (identical(package, "fakepkg")) pkg_skills else "" }, .package = "base" ) result <- attached_package_skill_dirs() expect_equal(result, pkg_skills) }) test_that("btw_skills_directories() discovers .agents/skills", { project <- withr::local_tempdir() withr::local_dir(project) project <- getwd() agents_dir <- file.path(project, ".agents", "skills") dir.create(agents_dir, recursive = TRUE) dirs <- btw_skills_directories() expect_true(agents_dir %in% dirs) }) test_that("resolve_project_skill_dir() defaults to .btw/skills when none exist", { project <- withr::local_tempdir() withr::local_dir(project) project <- getwd() result <- resolve_project_skill_dir() expect_equal(result, file.path(project, ".btw", "skills")) }) test_that("resolve_project_skill_dir() returns the one that exists", { project <- withr::local_tempdir() withr::local_dir(project) project <- getwd() agents_dir <- file.path(project, ".agents", "skills") dir.create(agents_dir, recursive = TRUE) result <- resolve_project_skill_dir() expect_equal(result, agents_dir) }) test_that("resolve_project_skill_dir() returns first existing when non-interactive", { project <- withr::local_tempdir() withr::local_dir(project) project <- getwd() btw_dir <- file.path(project, ".btw", "skills") agents_dir <- file.path(project, ".agents", "skills") dir.create(btw_dir, recursive = TRUE) dir.create(agents_dir, recursive = TRUE) local_mocked_bindings(is_interactive = function() FALSE) result <- resolve_project_skill_dir() expect_equal(result, btw_dir) }) test_that("find_skill() returns NULL for nonexistent skill", { dir <- withr::local_tempdir() local_skill_dirs(dir) expect_null(find_skill("nonexistent")) }) test_that("find_skill() finds a valid skill", { dir <- withr::local_tempdir() create_temp_skill(name = "found-skill", dir = dir) local_skill_dirs(dir) result <- find_skill("found-skill") expect_type(result, "list") expect_true(file.exists(result$path)) }) # extract_skill_metadata --------------------------------------------------- test_that("extract_skill_metadata() returns parsed frontmatter", { skill_dir <- create_temp_skill( extra_frontmatter = list(license = "MIT") ) metadata <- extract_skill_metadata(file.path(skill_dir, "SKILL.md")) expect_equal(metadata$name, "test-skill") expect_equal(metadata$license, "MIT") }) test_that("extract_skill_metadata() returns empty list for bad files", { tmp <- withr::local_tempfile(lines = "No frontmatter here", fileext = ".md") metadata <- extract_skill_metadata(tmp) expect_equal(metadata, list()) }) test_that("extract_skill_metadata() warns on parse failure", { tmp <- withr::local_tempfile(fileext = ".md") # Write invalid YAML frontmatter writeLines("---\n: invalid yaml: [\n---\nBody.", tmp) expect_warning( metadata <- extract_skill_metadata(tmp), "Failed to parse frontmatter" ) expect_equal(metadata, list()) }) # Resources ---------------------------------------------------------------- test_that("list_skill_resources() finds files recursively", { dir <- withr::local_tempdir() skill_dir <- file.path(dir, "test-skill") dir.create(file.path(skill_dir, "scripts"), recursive = TRUE) dir.create(file.path(skill_dir, "assets", "templates"), recursive = TRUE) writeLines("print('hi')", file.path(skill_dir, "scripts", "run.py")) writeLines( "template", file.path(skill_dir, "assets", "templates", "base.html") ) resources <- list_skill_resources(skill_dir) expect_equal(resources$scripts, "run.py") expect_true("templates/base.html" %in% resources$assets) }) test_that("format_resources_listing() returns empty string for no resources", { resources <- list( scripts = character(0), references = character(0), assets = character(0) ) expect_equal(format_resources_listing(resources, "/tmp"), "") }) # Skill Tool ---------------------------------------------------------------- test_that("btw_tool_skill_impl() returns content and resources", { dir <- withr::local_tempdir() skill_dir <- create_temp_skill(name = "fetch-test", dir = dir) dir.create(file.path(skill_dir, "references")) writeLines("Reference doc.", file.path(skill_dir, "references", "guide.md")) local_skill_dirs(dir) result <- btw_tool_skill_impl("fetch-test") expect_s3_class(result, "ellmer::ContentToolResult") expect_match(result@value, "Test Skill") expect_match(result@value, "References:") expect_equal(result@extra$data$name, "fetch-test") expect_equal(result@extra$data$resources$references, "guide.md") }) test_that("btw_tool_skill_impl() errors for missing skill", { dir <- withr::local_tempdir() local_skill_dirs(dir) expect_error(btw_tool_skill_impl("nonexistent"), "not found") }) test_that("btw_tool_skill_impl(\"\") returns skills listing when skills exist", { dir <- withr::local_tempdir() create_temp_skill( name = "listed-skill", description = "A listed skill.", dir = dir ) local_skill_dirs(dir) result <- btw_tool_skill_impl("") expect_s3_class(result, "ellmer::ContentToolResult") expect_match(result@value, "listed-skill") expect_type(result@extra$data$skills, "list") expect_named(result@extra$data$skills, "listed-skill") }) test_that("btw_tool_skill_impl(\"\") returns no-skills message when none exist", { dir <- withr::local_tempdir() local_skill_dirs(dir) result <- btw_tool_skill_impl("") expect_s3_class(result, "ellmer::ContentToolResult") expect_match(result@value, "No skills are currently available") }) test_that("btw_tool_skill_impl() error for missing skill mentions calling with empty name", { dir <- withr::local_tempdir() create_temp_skill(name = "real-skill", dir = dir) local_skill_dirs(dir) expect_error( btw_tool_skill_impl("nonexistent"), "btw_tool_skill" ) }) # System Prompt ------------------------------------------------------------ test_that("btw_skills_system_prompt() returns empty for no skills", { dir <- withr::local_tempdir() local_skill_dirs(dir) expect_equal(btw_skills_system_prompt(), "") }) test_that("btw_skills_system_prompt() includes skill metadata", { dir <- withr::local_tempdir() create_temp_skill( name = "prompt-test", description = "A skill for testing prompts.", dir = dir, extra_frontmatter = list(compatibility = "Needs R 4.2") ) local_skill_dirs(dir) prompt <- btw_skills_system_prompt() expect_match(prompt, "prompt-test") expect_match(prompt, "A skill for testing prompts.") expect_match(prompt, "Needs R 4.2") }) # select_skill_dir ---------------------------------------------------------- test_that("select_skill_dir() returns single dir directly", { dir <- withr::local_tempdir() skill_dir <- create_temp_skill(name = "only-skill", dir = dir) result <- select_skill_dir(skill_dir) expect_equal(result, skill_dir) }) test_that("select_skill_dir() matches named skill", { dir <- withr::local_tempdir() skill_a <- create_temp_skill(name = "skill-a", dir = dir) skill_b <- create_temp_skill(name = "skill-b", dir = dir) result <- select_skill_dir(c(skill_a, skill_b), skill = "skill-b") expect_equal(result, skill_b) }) test_that("select_skill_dir() aborts when named skill not found", { dir <- withr::local_tempdir() skill_a <- create_temp_skill(name = "skill-a", dir = dir) expect_error( select_skill_dir(skill_a, skill = "nonexistent"), "not found" ) }) test_that("select_skill_dir() aborts for multiple dirs non-interactively", { dir <- withr::local_tempdir() skill_a <- create_temp_skill(name = "skill-a", dir = dir) skill_b <- create_temp_skill(name = "skill-b", dir = dir) local_mocked_bindings(is_interactive = function() FALSE) expect_error( select_skill_dir(c(skill_a, skill_b)), "Multiple skills found" ) }) test_that("select_skill_dir() uses menu for multiple dirs interactively", { dir <- withr::local_tempdir() skill_a <- create_temp_skill(name = "skill-a", dir = dir) skill_b <- create_temp_skill(name = "skill-b", dir = dir) local_mocked_bindings(is_interactive = function() TRUE) local_mocked_bindings(menu = function(...) 2L, .package = "utils") expect_message( result <- select_skill_dir(c(skill_a, skill_b)), "Multiple skills found" ) expect_equal(result, skill_b) }) test_that("select_skill_dir() aborts when no dirs provided", { expect_error(select_skill_dir(character()), "No skills found") }) # install_skill_from_dir --------------------------------------------------- test_that("install_skill_from_dir() installs from directory", { source_dir <- withr::local_tempdir() create_temp_skill(name = "installable", dir = source_dir) target_base <- withr::local_tempdir() withr::local_dir(target_base) expect_message( path <- install_skill_from_dir( file.path(source_dir, "installable"), scope = "project" ), "Installed skill" ) expect_true(dir.exists(path)) expect_true(file.exists(file.path(path, "SKILL.md"))) }) test_that("install_skill_from_dir() refuses invalid skill", { source_dir <- withr::local_tempdir() bad_dir <- file.path(source_dir, "bad-skill") dir.create(bad_dir) writeLines("---\nname: bad-skill\n---\nBody.", file.path(bad_dir, "SKILL.md")) target_base <- withr::local_tempdir() withr::local_dir(target_base) expect_error( install_skill_from_dir(bad_dir, scope = "project"), "Cannot install invalid" ) }) test_that("install_skill_from_dir() errors for nonexistent source", { expect_error(install_skill_from_dir("/nonexistent/path"), "not found") }) test_that("install_skill_from_dir() accepts custom path as scope", { source_dir <- withr::local_tempdir() create_temp_skill(name = "custom-install", dir = source_dir) target <- withr::local_tempdir() custom_dir <- file.path(target, ".openhands", "skills") expect_message( path <- install_skill_from_dir( file.path(source_dir, "custom-install"), scope = custom_dir ), "Installed skill" ) expect_equal(normalizePath(dirname(path)), normalizePath(custom_dir)) expect_true(file.exists(file.path(path, "SKILL.md"))) }) test_that("install_skill_from_dir() treats I('project') as literal path", { source_dir <- withr::local_tempdir() create_temp_skill(name = "literal-test", dir = source_dir) target <- withr::local_tempdir() expect_message( path <- install_skill_from_dir( file.path(source_dir, "literal-test"), scope = I(file.path(target, "project")) ), "Installed skill" ) expect_equal( normalizePath(dirname(path)), normalizePath(file.path(target, "project")) ) }) # btw_skill_install_github ------------------------------------------------- # Helper: create a zip mimicking GitHub's zipball format create_github_zipball <- function(skills, zip_path = NULL) { tmp <- withr::local_tempdir(.local_envir = parent.frame()) repo_dir <- file.path(tmp, "owner-repo-abc1234") dir.create(repo_dir) for (skill in skills) { skill_dir <- file.path(repo_dir, skill$name) dir.create(skill_dir, recursive = TRUE) writeLines( paste0( "---\nname: ", skill$name, "\ndescription: ", skill$description %||% "A test skill.", "\n---\n\n# ", skill$name, "\n\nInstructions.\n" ), file.path(skill_dir, "SKILL.md") ) } if (is.null(zip_path)) { zip_path <- tempfile(fileext = ".zip", tmpdir = tmp) } withr::with_dir(tmp, { utils::zip(zip_path, basename(repo_dir), flags = "-r9Xq") }) zip_path } test_that("btw_skill_install_github() errors for invalid repo format", { expect_error(btw_skill_install_github("badformat"), "owner/repo") expect_error(btw_skill_install_github("a/b/c"), "owner/repo") expect_error(btw_skill_install_github("/repo"), "owner/repo") expect_error(btw_skill_install_github("owner/"), "owner/repo") expect_error(btw_skill_install_github("owner/repo@"), "empty ref") }) test_that("btw_skill_install_github() parses @ref from repo string", { zip_path <- create_github_zipball(list( list(name = "gh-skill", description = "A GitHub skill.") )) local_mocked_bindings( check_installed = function(...) invisible(), .package = "rlang" ) captured_ref <- NULL mock_gh <- function(..., .destfile = NULL) { args <- list(...) captured_ref <<- args$ref file.copy(zip_path, .destfile) } local_mocked_bindings(gh = mock_gh, .package = "gh") target_base <- withr::local_tempdir() withr::local_dir(target_base) expect_message( btw_skill_install_github("owner/repo@v1.0", scope = "project"), "Installed skill" ) expect_equal(captured_ref, "v1.0") }) test_that("btw_skill_install_github() installs single skill", { zip_path <- create_github_zipball(list( list(name = "gh-skill", description = "A GitHub skill.") )) local_mocked_bindings( check_installed = function(...) invisible(), .package = "rlang" ) mock_gh <- function(..., .destfile = NULL) { file.copy(zip_path, .destfile) } local_mocked_bindings(gh = mock_gh, .package = "gh") target_base <- withr::local_tempdir() withr::local_dir(target_base) expect_message( path <- btw_skill_install_github("owner/repo", scope = "project"), "Installed skill" ) expect_true(dir.exists(path)) expect_true(file.exists(file.path(path, "SKILL.md"))) expect_equal(basename(path), "gh-skill") }) test_that("btw_skill_install_github() selects named skill from multiple", { zip_path <- create_github_zipball(list( list(name = "skill-a", description = "Skill A."), list(name = "skill-b", description = "Skill B.") )) local_mocked_bindings( check_installed = function(...) invisible(), .package = "rlang" ) mock_gh <- function(..., .destfile = NULL) { file.copy(zip_path, .destfile) } local_mocked_bindings(gh = mock_gh, .package = "gh") target_base <- withr::local_tempdir() withr::local_dir(target_base) expect_message( path <- btw_skill_install_github( "owner/repo", skill = "skill-b", scope = "project" ), "Installed skill" ) expect_equal(basename(path), "skill-b") }) test_that("btw_skill_install_github() errors when named skill not found", { zip_path <- create_github_zipball(list( list(name = "skill-a", description = "Skill A.") )) local_mocked_bindings( check_installed = function(...) invisible(), .package = "rlang" ) mock_gh <- function(..., .destfile = NULL) { file.copy(zip_path, .destfile) } local_mocked_bindings(gh = mock_gh, .package = "gh") target_base <- withr::local_tempdir() withr::local_dir(target_base) expect_error( btw_skill_install_github("owner/repo", skill = "nonexistent"), "not found" ) }) test_that("btw_skill_install_github() errors when no skills in repo", { # Create a zip with no SKILL.md tmp <- withr::local_tempdir() repo_dir <- file.path(tmp, "owner-repo-abc1234") dir.create(repo_dir) writeLines("Just a README.", file.path(repo_dir, "README.md")) zip_path <- file.path(tmp, "empty.zip") withr::with_dir(tmp, { utils::zip(zip_path, basename(repo_dir), flags = "-r9Xq") }) local_mocked_bindings( check_installed = function(...) invisible(), .package = "rlang" ) mock_gh <- function(..., .destfile = NULL) { file.copy(zip_path, .destfile) } local_mocked_bindings(gh = mock_gh, .package = "gh") target_base <- withr::local_tempdir() withr::local_dir(target_base) expect_error( btw_skill_install_github("owner/repo"), "No skills found" ) }) test_that("btw_skill_install_github() aborts non-interactively with multiple skills", { zip_path <- create_github_zipball(list( list(name = "skill-a", description = "Skill A."), list(name = "skill-b", description = "Skill B.") )) local_mocked_bindings( check_installed = function(...) invisible(), .package = "rlang" ) mock_gh <- function(..., .destfile = NULL) { file.copy(zip_path, .destfile) } local_mocked_bindings(gh = mock_gh, .package = "gh") local_mocked_bindings(is_interactive = function() FALSE) target_base <- withr::local_tempdir() withr::local_dir(target_base) expect_error( btw_skill_install_github("owner/repo"), "Multiple skills found" ) }) test_that("btw_skill_install_github() wraps download errors", { local_mocked_bindings( check_installed = function(...) invisible(), .package = "rlang" ) mock_gh <- function(...) { stop("GitHub API error: Not Found") } local_mocked_bindings(gh = mock_gh, .package = "gh") expect_error( btw_skill_install_github("owner/nonexistent"), "Failed to download" ) }) # btw_skill_install_package ------------------------------------------------ test_that("btw_skill_install_package() installs single skill from package", { # Create a mock package skills directory pkg_skills <- withr::local_tempdir() skill_dir <- file.path(pkg_skills, "pkg-skill") dir.create(skill_dir) writeLines( "---\nname: pkg-skill\ndescription: A package skill.\n---\n\n# Pkg Skill\n", file.path(skill_dir, "SKILL.md") ) local_mocked_bindings( check_installed = function(...) invisible(), .package = "rlang" ) local_mocked_bindings( system.file = function(..., package = NULL) pkg_skills, .package = "base" ) target_base <- withr::local_tempdir() withr::local_dir(target_base) expect_message( path <- btw_skill_install_package("mypkg", scope = "project"), "Installed skill" ) expect_true(dir.exists(path)) expect_equal(basename(path), "pkg-skill") }) test_that("btw_skill_install_package() selects named skill", { pkg_skills <- withr::local_tempdir() for (nm in c("alpha", "beta")) { d <- file.path(pkg_skills, nm) dir.create(d) writeLines( paste0( "---\nname: ", nm, "\ndescription: Skill ", nm, ".\n---\n\n# ", nm, "\n" ), file.path(d, "SKILL.md") ) } local_mocked_bindings( check_installed = function(...) invisible(), .package = "rlang" ) local_mocked_bindings( system.file = function(..., package = NULL) pkg_skills, .package = "base" ) target_base <- withr::local_tempdir() withr::local_dir(target_base) expect_message( path <- btw_skill_install_package( "mypkg", skill = "beta", scope = "project" ), "Installed skill" ) expect_equal(basename(path), "beta") }) test_that("btw_skill_install_package() errors when no skills dir", { local_mocked_bindings( check_installed = function(...) invisible(), .package = "rlang" ) local_mocked_bindings( system.file = function(..., package = NULL) "", .package = "base" ) expect_error( btw_skill_install_package("emptypkg"), "does not bundle any skills" ) }) test_that("btw_skill_install_package() errors when no SKILL.md in subdirs", { pkg_skills <- withr::local_tempdir() dir.create(file.path(pkg_skills, "not-a-skill")) writeLines("just a file", file.path(pkg_skills, "not-a-skill", "README.md")) local_mocked_bindings( check_installed = function(...) invisible(), .package = "rlang" ) local_mocked_bindings( system.file = function(..., package = NULL) pkg_skills, .package = "base" ) expect_error( btw_skill_install_package("mypkg"), "does not bundle any skills" ) }) # Snapshot test for system prompt (existing) -------------------------------- test_that("btw_skills_system_prompt() works", { skip_if_not_snapshot_env() local_mocked_bindings(attached_package_skill_dirs = function() character()) expect_snapshot( cat(btw_skills_system_prompt()), transform = function(x) { gsub(".*?", "SKILL_PATH", x) } ) }) # validate_skill_name() ---------------------------------------------------- test_that("validate_skill() accepts single-character name", { dir <- withr::local_tempdir() skill_dir <- file.path(dir, "a") dir.create(skill_dir) writeLines( "---\nname: a\ndescription: A minimal skill.\n---\nBody.", file.path(skill_dir, "SKILL.md") ) result <- validate_skill(skill_dir) expect_true(result$valid) expect_length(result$issues, 0) }) test_that("validate_skill() flags non-character compatibility", { dir <- withr::local_tempdir() skill_dir <- file.path(dir, "test-skill") dir.create(skill_dir) writeLines( "---\nname: test-skill\ndescription: A test.\ncompatibility: true\n---\nBody.", file.path(skill_dir, "SKILL.md") ) result <- validate_skill(skill_dir) expect_true(result$valid) expect_match(result$warnings, "must be a character string", all = FALSE) }) # find_skill() with invalid skill ------------------------------------------ test_that("find_skill() returns validation errors for invalid skill on disk", { dir <- withr::local_tempdir() # Create a skill that exists on disk but fails validation (missing description) bad_dir <- file.path(dir, "bad-skill") dir.create(bad_dir) writeLines("---\nname: bad-skill\n---\nBody.", file.path(bad_dir, "SKILL.md")) local_skill_dirs(dir) result <- find_skill("bad-skill") expect_type(result, "list") expect_false(result$validation$valid) expect_match(result$validation$errors, "description", all = FALSE) }) # install_skill_from_dir() overwrite ---------------------------------------- test_that("install_skill_from_dir() overwrites with overwrite = TRUE", { source_dir <- withr::local_tempdir() create_temp_skill( name = "overwrite-me", description = "Version 1.", dir = source_dir ) target_base <- withr::local_tempdir() withr::local_dir(target_base) # First install expect_message( path <- install_skill_from_dir( file.path(source_dir, "overwrite-me"), scope = "project" ), "Installed skill" ) # Update source writeLines( "---\nname: overwrite-me\ndescription: Version 2.\n---\nUpdated.", file.path(source_dir, "overwrite-me", "SKILL.md") ) # Re-install with overwrite expect_message( path2 <- install_skill_from_dir( file.path(source_dir, "overwrite-me"), scope = "project", overwrite = TRUE ), "Installed skill" ) expect_equal(path, path2) content <- readLines(file.path(path2, "SKILL.md")) expect_true(any(grepl("Version 2", content))) }) # xml_escape() in system prompt --------------------------------------------- test_that("btw_skills_system_prompt() escapes XML special characters", { dir <- withr::local_tempdir() create_temp_skill( name = "esc-test", description = "Uses & ampersands.", dir = dir ) local_skill_dirs(dir) prompt <- btw_skills_system_prompt() expect_match(prompt, "<tags>", fixed = TRUE) expect_match(prompt, "& ampersands", fixed = TRUE) expect_no_match(prompt, "", fixed = TRUE) }) # maybe_use_build_ignore() ------------------------------------------------- test_that("maybe_use_build_ignore() does nothing outside project dir", { project <- withr::local_tempdir() writeLines("Package: fakepkg", file.path(project, "DESCRIPTION")) writeLines("", file.path(project, ".Rbuildignore")) outside <- withr::local_tempdir() skills_dir <- file.path(outside, ".btw", "skills") # install dir is NOT under project withr::local_dir(project) maybe_use_build_ignore(skills_dir) # .Rbuildignore should be unchanged (empty) expect_equal( trimws(readLines(file.path(project, ".Rbuildignore"))), "" ) }) test_that("maybe_use_build_ignore() does nothing when DESCRIPTION absent", { project <- withr::local_tempdir() withr::local_dir(project) project <- getwd() skills_dir <- file.path(project, ".btw", "skills") dir.create(skills_dir, recursive = TRUE) rbuildignore <- file.path(project, ".Rbuildignore") writeLines(character(), rbuildignore) maybe_use_build_ignore(skills_dir) expect_equal(readLines(rbuildignore), character()) }) test_that("maybe_use_build_ignore() auto-updates .Rbuildignore when it exists", { project <- withr::local_tempdir() withr::local_dir(project) project <- getwd() writeLines("Package: fakepkg", file.path(project, "DESCRIPTION")) rbuildignore <- file.path(project, ".Rbuildignore") writeLines(character(), rbuildignore) skills_dir <- file.path(project, ".btw", "skills") dir.create(skills_dir, recursive = TRUE) expect_message( maybe_use_build_ignore(skills_dir), "\\.Rbuildignore" ) lines <- readLines(rbuildignore) expect_true(any(grepl("btw", lines, fixed = TRUE))) }) test_that("maybe_use_build_ignore() does not add duplicate entry", { project <- withr::local_tempdir() withr::local_dir(project) project <- getwd() writeLines("Package: fakepkg", file.path(project, "DESCRIPTION")) rbuildignore <- file.path(project, ".Rbuildignore") pattern <- escape_for_rbuildignore(".btw") writeLines(pattern, rbuildignore) skills_dir <- file.path(project, ".btw", "skills") dir.create(skills_dir, recursive = TRUE) # Should not add a duplicate maybe_use_build_ignore(skills_dir) lines <- readLines(rbuildignore) expect_equal(sum(lines == pattern), 1L) }) test_that("maybe_use_build_ignore() prompts when .Rbuildignore absent (interactive, yes)", { project <- withr::local_tempdir() withr::local_dir(project) project <- getwd() writeLines("Package: fakepkg", file.path(project, "DESCRIPTION")) skills_dir <- file.path(project, ".agents", "skills") dir.create(skills_dir, recursive = TRUE) rbuildignore <- file.path(project, ".Rbuildignore") expect_false(file.exists(rbuildignore)) local_mocked_bindings(is_interactive = function() TRUE) local_mocked_bindings(menu = function(...) 1L, .package = "utils") suppressMessages(maybe_use_build_ignore(skills_dir)) expect_true(file.exists(rbuildignore)) }) test_that("maybe_use_build_ignore() does nothing when .Rbuildignore absent non-interactively", { project <- withr::local_tempdir() withr::local_dir(project) project <- getwd() writeLines("Package: fakepkg", file.path(project, "DESCRIPTION")) skills_dir <- file.path(project, ".btw", "skills") dir.create(skills_dir, recursive = TRUE) local_mocked_bindings(is_interactive = function() FALSE) maybe_use_build_ignore(skills_dir) expect_false(file.exists(file.path(project, ".Rbuildignore"))) }) test_that("maybe_use_build_ignore() does nothing when user declines prompt", { project <- withr::local_tempdir() withr::local_dir(project) project <- getwd() writeLines("Package: fakepkg", file.path(project, "DESCRIPTION")) skills_dir <- file.path(project, ".btw", "skills") dir.create(skills_dir, recursive = TRUE) local_mocked_bindings(is_interactive = function() TRUE) local_mocked_bindings(menu = function(...) 2L, .package = "utils") suppressMessages(maybe_use_build_ignore(skills_dir)) expect_false(file.exists(file.path(project, ".Rbuildignore"))) }) test_that("install_skill_from_dir() updates .Rbuildignore for R packages", { source_dir <- withr::local_tempdir() create_temp_skill(name = "pkg-skill", dir = source_dir) project <- withr::local_tempdir() withr::local_dir(project) project <- getwd() writeLines("Package: fakepkg", file.path(project, "DESCRIPTION")) writeLines(character(), file.path(project, ".Rbuildignore")) suppressMessages(expect_message( install_skill_from_dir( file.path(source_dir, "pkg-skill"), scope = "project" ), "Installed skill" )) lines <- readLines(file.path(project, ".Rbuildignore")) expect_true(any(grepl("btw", lines, fixed = TRUE))) }) # escape_for_rbuildignore() ------------------------------------------------ test_that("escape_for_rbuildignore() produces correct pattern for dot-prefixed dirs", { expect_equal(escape_for_rbuildignore(".btw"), "^\\.btw$") expect_equal(escape_for_rbuildignore(".agents"), "^\\.agents$") }) test_that("skills prompt is included in btw_client() system prompt", { withr::local_envvar(list(ANTHROPIC_API_KEY = "beep")) local_enable_tools() with_mocked_platform(ide = "rstudio", { chat <- btw_client(path_btw = FALSE) }) system_prompt <- chat$get_system_prompt() expect_match(system_prompt, "## Skills", fixed = TRUE) expect_match(system_prompt, "skill-creator", fixed = TRUE) })