local_mocked_bindings( btw_can_register_gh_tool = function() FALSE ) describe("btw_client() chat client", { withr::local_envvar(list(ANTHROPIC_API_KEY = "beep")) it("works with `btw.client` option", { local_options( btw.client = ellmer::chat_anthropic( system_prompt = "I like to have my own system prompt." ) ) with_mocked_platform(ide = "rstudio", { chat <- btw_client(path_btw = FALSE) }) expect_match( chat$get_system_prompt(), "I like to have my own system prompt" ) expect_match(chat$get_system_prompt(), "You have access to tools") expect_no_match( getOption("btw.client")$get_system_prompt(), "You have access to tools" ) skip_if_not_macos() expect_snapshot(print(chat), transform = scrub_system_info) }) it("works in the basic case", { data_foo <- mtcars expect_error(btw_client(data_foo), class = "rlib_error_dots_nonempty") chat <- btw_client(path_btw = FALSE) expect_s3_class(chat, "Chat") }) it("modifies `client` argument in place", { client <- ellmer::chat_anthropic( system_prompt = "I like to make my own chat client.", ) chat <- btw_client(client = client, path_btw = FALSE) # Modifies in place expect_identical(chat, client) }) it("accepts a provider string", { expected_client <- ellmer::chat_anthropic() chat <- btw_client(client = "anthropic", path_btw = FALSE) expect_equal(chat$get_provider(), expected_client$get_provider()) }) it("accepts a provider/model string", { expected_client <- ellmer::chat_anthropic( model = "claude-3-7-sonnet-20250219" ) chat <- btw_client(client = "anthropic/claude-3-7-sonnet-20250219") expect_equal(chat$get_provider(), expected_client$get_provider()) }) }) test_that("btw_client() adds `btw.md` context file to system prompt", { withr::local_envvar(list(ANTHROPIC_API_KEY = "beep")) wd <- withr::local_tempdir( tmpdir = file.path(tempdir(), "btw-proj", "subtask") ) withr::local_dir(wd) writeLines( con = file.path(wd, "..", "btw.md"), c( "* Prefer solutions that use {tidyverse}", "* Always use `=` for assignment", "* Always use the native base-R pipe `|>` for piped expressions" ) ) with_mocked_platform(ide = "rstudio", { chat <- btw_client( client = ellmer::chat_anthropic( system_prompt = "I like to have my own system prompt." ) ) }) expect_match(chat$get_system_prompt(), "# Project Context", fixed = TRUE) expect_match( chat$get_system_prompt(), "Always use `=` for assignment", fixed = TRUE ) skip_if_not_macos() expect_snapshot(print(chat), transform = scrub_system_info) }) describe("btw_client() with context files", { withr::local_envvar(list(OPENAI_API_KEY = "beep")) wd <- withr::local_tempdir( tmpdir = file.path(tempdir(), "btw-proj", "subtask") ) withr::local_dir(wd) writeLines( con = file.path(wd, "..", "btw.md"), c( "---", "client:", " provider: openai", " model: gpt-4o", " system_prompt: I like to have my own system prompt", "tools: docs", "---", "", "* Prefer solutions that use {tidyverse}", "* Always use `=` for assignment", "* Always use the native base-R pipe `|>` for piped expressions" ) ) it("uses `btw.md` for client settings and system prompt", { with_mocked_platform(ide = "rstudio", { chat <- btw_client() }) expect_equal(chat$get_model(), "gpt-4o") expect_true(inherits( chat$.__enclos_env__$private$provider, "ellmer::ProviderOpenAI" )) expect_match(chat$get_system_prompt(), "# Project Context", fixed = TRUE) expect_match( chat$get_system_prompt(), "Always use `=` for assignment", fixed = TRUE ) }) it("uses AGENTS.md as an alias of btw.md", { with_mocked_platform(ide = "rstudio", { chat_btw <- btw_client() }) fs::file_move(fs::path(wd, "../btw.md"), fs::path(wd, "../AGENTS.md")) withr::defer( fs::file_move(fs::path(wd, "../AGENTS.md"), fs::path(wd, "../btw.md")) ) with_mocked_platform(ide = "rstudio", { chat_agents <- btw_client() }) expect_equal(chat_agents, chat_btw) }) it("includes llms.txt content in system prompt", { writeLines( con = file.path(wd, "llms.txt"), "EXTRA CONTEXT FROM llms.txt" ) with_mocked_platform(ide = "rstudio", { chat <- btw_client() }) expect_match(chat$get_system_prompt(), "# Project Context", fixed = TRUE) expect_match( chat$get_system_prompt(), "Always use `=` for assignment", fixed = TRUE ) expect_match( chat$get_system_prompt(), "EXTRA CONTEXT FROM llms.txt", fixed = TRUE ) }) it("finds `btw.md` in parent directories", { with_mocked_platform(ide = "rstudio", { chat <- btw_client(path_llms_txt = FALSE) }) fs::file_move("../btw.md", "../btw-context.md") with_mocked_platform(ide = "rstudio", { chat_parent <- btw_client(path_btw = "../btw-context.md", path_llms_txt = FALSE) }) expect_equal( chat_parent$get_system_prompt(), chat$get_system_prompt() ) skip_if_not_macos() expect_snapshot(print(chat), transform = scrub_system_info) }) it("uses `llms.txt` in wd and `btw.md` from parent", { fs::file_move("../btw-context.md", "../btw.md") with_mocked_platform(ide = "rstudio", { chat_parent_llms <- btw_client() }) skip_if_not_macos() expect_snapshot(print(chat_parent_llms), transform = scrub_system_info) }) it("accepts a string for `client`", { btw_md <- withr::local_tempfile(fileext = ".md") writeLines( con = btw_md, c( "---", "client: openai/gpt-4.1-nano", "tools: docs", "---", "", "* Prefer solutions that use {tidyverse}", "* Always use `=` for assignment", "* Always use the native base-R pipe `|>` for piped expressions" ) ) expected_client <- ellmer::chat_openai(model = "gpt-4.1-nano") chat <- btw_client(path_btw = btw_md) expect_equal(chat$get_provider(), expected_client$get_provider()) }) it("throws if `path_btw` is provided but doesn't exist", { expect_error( btw_client(path_btw = tempfile()) ) }) }) describe("btw_client() project vs user settings", { withr::local_envvar(list(OPENAI_API_KEY = "beep", ANTHROPIC_API_KEY = "boop")) project_dir <- withr::local_tempdir("btw-test-project-") withr::local_dir(project_dir) path_user_btw <- withr::local_tempfile(fileext = ".md") local_mocked_bindings( path_find_user = function(filename) { if (filename == "btw.md") path_user_btw else NULL } ) it("falls through to use client settings from user-level btw.md", { writeLines( con = path_user_btw, c( "---", "client:", " provider: openai", " model: gpt-4o", "---", "User level context" ) ) withr::defer(unlink(path_user_btw)) # Create project-level btw.md with different client settings writeLines( con = "btw.md", c( "---", "client:", " provider: anthropic", " model: claude-3-5-sonnet-20241022", "---", "Project level context" ) ) withr::defer(unlink("btw.md")) with_mocked_platform(ide = "rstudio", { chat <- btw_client(path_llms_txt = FALSE) }) # Should use project's client settings expect_equal(chat$get_model(), "claude-3-5-sonnet-20241022") expect_s3_class(chat$get_provider(), "ellmer::ProviderAnthropic") skip_if_not_macos() expect_snapshot(print(chat), transform = scrub_system_info) }) it("falls back to user client settings when project has no client", { # User-level btw.md with client settings writeLines( c( "---", "client:", " provider: openai", " model: gpt-4o", "---", "User level context" ), path_user_btw ) withr::defer(unlink(path_user_btw)) # Project-level btw.md WITHOUT client field writeLines( con = "btw.md", c( "---", "tools: docs", "---", "Project level context only" ) ) withr::defer(unlink("btw.md")) with_mocked_platform(ide = "rstudio", { chat <- btw_client(path_llms_txt = FALSE) }) # Should fall back to user's client settings expect_equal(chat$get_model(), "gpt-4o") expect_s3_class(chat$get_provider(), "ellmer::ProviderOpenAI") skip_if_not_macos() expect_snapshot(print(chat), transform = scrub_system_info) }) it("concatenates user and project prompts with separator", { # User-level btw.md with prompt writeLines( c( "---", "client:", " provider: openai", "---", "USER_GLOBAL_RULES" ), path_user_btw ) withr::defer(unlink(path_user_btw)) # Project-level AGENTS.md with prompt writeLines( con = "AGENTS.md", c( "PROJECT_SPECIFIC_RULES" ) ) withr::defer(unlink("AGENTS.md")) with_mocked_platform(ide = "rstudio", { chat <- btw_client(path_llms_txt = FALSE) }) system_prompt <- chat$get_system_prompt() # Should contain both prompts expect_match(system_prompt, "USER_GLOBAL_RULES", fixed = TRUE) expect_match(system_prompt, "PROJECT_SPECIFIC_RULES", fixed = TRUE) # Should have separator between them expect_match(system_prompt, "\n\n---\n\n", fixed = TRUE) # User prompt should come first user_pos <- gregexpr("USER_GLOBAL_RULES", system_prompt)[[1]][1] project_pos <- gregexpr("PROJECT_SPECIFIC_RULES", system_prompt)[[1]][1] expect_true(user_pos < project_pos) }) it("deep merges options from user and project", { writeLines( c( "---", "client:", " provider: openai", "options:", " cache_size: 100", " timeout: 30", "---", "User level" ), path_user_btw ) withr::defer(unlink(path_user_btw)) writeLines( con = "btw.md", c( "---", "client:", " provider: openai", "options:", " timeout: 60", " feature_x: true", "---", "Project level" ) ) withr::defer(unlink("btw.md")) config <- read_btw_file() # Should have all options, with project overriding user expect_equal(config$options$btw.cache_size, 100) # From user expect_equal(config$options$btw.timeout, 60) # From project (overrides user) expect_equal(config$options$btw.feature_x, TRUE) # From project }) it("uses only project tools when defined", { writeLines( c( "---", "client:", " provider: openai", "tools: [env, files]", "---", "User level" ), path_user_btw ) withr::defer(unlink(path_user_btw)) writeLines( con = "btw.md", c( "---", "client:", " provider: openai", "tools: docs", "---", "Project level" ) ) withr::defer(unlink("btw.md")) with_mocked_platform(ide = "rstudio", { chat <- btw_client(path_llms_txt = FALSE) }) tool_names <- names(chat$get_tools()) # Should have docs tools from project expect_true(any(grepl("btw_tool_docs", tool_names))) # Should NOT have env/files tools from user expect_false(any(grepl("btw_tool_env", tool_names))) expect_false(any(grepl("btw_tool_files", tool_names))) }) it("uses user tools when project has no tools", { writeLines( c( "---", "client:", " provider: openai", "tools: docs", "---", "User level" ), path_user_btw ) withr::defer(unlink(path_user_btw)) writeLines( con = "btw.md", c( "---", "client:", " provider: openai", "---", "Project level" ) ) withr::defer(unlink("btw.md")) with_mocked_platform(ide = "rstudio", { chat <- btw_client(path_llms_txt = FALSE) }) tool_names <- names(chat$get_tools()) # Should have docs tools from user expect_true(any(grepl("btw_tool_docs", tool_names))) }) }) test_that("btw_client() throws for deprecated `model` and `provider` fields in btw.md", { withr::local_envvar(list(OPENAI_API_KEY = "beep")) wd <- withr::local_tempdir( tmpdir = file.path(tempdir(), "btw-test-client-deprecated") ) withr::local_dir(wd) writeLines( con = "btw.md", c( "---", "provider: openai", "---", "", "* Prefer solutions that use {tidyverse}", "* Always use `=` for assignment", "* Always use the native base-R pipe `|>` for piped expressions" ) ) expect_error( btw_client(), "provider" ) writeLines( con = "btw.md", c( "---", "model: gpt-4.1-mini", "---", "", "* Prefer solutions that use {tidyverse}", "* Always use `=` for assignment", "* Always use the native base-R pipe `|>` for piped expressions" ) ) expect_error( btw_client(), "model" ) }) describe("remove_hidden_content()", { it("removes content after single HIDE comment", { expect_equal( remove_hidden_content(c("one", "", "two")), "one" ) }) it("removes content after multiple HIDE comments", { expect_equal( remove_hidden_content(c( "one", "", "two", "", "three" )), "one" ) }) it("removes content between HIDE and /HIDE with no closing", { expect_equal( remove_hidden_content(c( "one", "", "two", "", "three", "" )), "one" ) }) it("removes content with nested HIDE comments and single /HIDE", { expect_equal( remove_hidden_content(c( "one", "", "two", "", "three", "", "four" )), "one" ) }) it("handles properly nested HIDE/HIDE blocks", { expect_equal( remove_hidden_content(c( "one", "", "two", "", "three", "", "four", "", "five" )), c("one", "five") ) }) it("removes all content when HIDE blocks are not properly closed", { expect_equal( remove_hidden_content(c( "one", "", "two", "", "three", "", "four", "" )), "one" ) }) it("returns empty vector when input is empty", { expect_equal( remove_hidden_content(character(0)), character(0) ) }) it("returns original content when no HIDE comments present", { expect_equal( remove_hidden_content(c("one", "two", "three", "four")), c("one", "two", "three", "four") ) }) it("handles single /HIDE without opening HIDE", { expect_equal( remove_hidden_content(c("one", "two", "", "three")), c("one", "two", "", "three") ) }) it("removes everything when HIDE is at the beginning", { expect_equal( remove_hidden_content(c("", "one", "two", "three")), character(0) ) }) it("handles multiple separate HIDE/HIDE blocks", { expect_equal( remove_hidden_content(c( "one", "", "hidden1", "", "two", "", "hidden2", "", "three" )), c("one", "two", "three") ) }) it("handles HIDE comment as last element", { expect_equal( remove_hidden_content(c("one", "two", "")), c("one", "two") ) }) it("handles /HIDE comment as first element", { expect_equal( remove_hidden_content(c("", "one", "two")), c("", "one", "two") ) }) it("handles unmatched /HIDE comment", { expect_equal( remove_hidden_content(c( "one", "", "two", "", "three" )), c("one", "", "two") ) }) it("preserves content between multiple closed HIDE blocks", { expect_equal( remove_hidden_content(c( "start", "", "hidden1", "", "middle", "", "hidden2", "", "end" )), c("start", "middle", "end") ) }) }) test_that("btw_client() accepts a list of tools in `tools` argument", { withr::local_envvar(list(ANTHROPIC_API_KEY = "beep")) chat <- btw_client(tools = btw_tools("docs")) expect_true( all(vapply( chat$get_tools(), inherits, logical(1), "ellmer::ToolDef" )) ) expect_true( all(grepl("btw_tool_docs", names(chat$get_tools()))) ) tool <- ellmer::tool( function(x) x + 1, name = "add_one", description = "Add one", arguments = list( x = ellmer::type_number("A number") ) ) expect_error( btw_client(tools = tool) ) chat <- btw_client(tools = list(tool)) expect_identical(chat$get_tools()[[1]], tool) chat_combo <- btw_client(tools = list("docs_vignette", tool)) expect_identical(chat_combo$get_tools()[[1]], btw_tools("docs_vignette")[[1]]) expect_identical(chat_combo$get_tools()[[2]], tool) chat_no_tools <- btw_client(tools = FALSE) expect_identical(chat_no_tools$get_tools(), list()) chat_btw_tools <- btw_client(tools = "docs") expect_identical(chat_btw_tools$get_tools(), btw_tools("docs")) }) test_that("btw_client() throws for invalid `tools` argument", { withr::local_envvar(list(ANTHROPIC_API_KEY = "beep")) expect_error( btw_client(tools = "not_a_tool") ) expect_error( btw_client(tools = 42) ) expect_error( btw_client(tools = c(btw_tools()[[1]], list(42))), "tools\\[\\[2\\]\\]" ) })