inst/tinytest/test_agent_context.R

# Tests for agent_context()

# --- Setup ---
root <- file.path(tempdir(), "test_agent_context")
unlink(root, recursive = TRUE)
dir.create(root, recursive = TRUE, showWarnings = FALSE)

project_dir <- file.path(root, "demopkg")
dir.create(project_dir, recursive = TRUE, showWarnings = FALSE)

workspace_dir <- file.path(root, "workspace")
dir.create(workspace_dir, recursive = TRUE, showWarnings = FALSE)

mem_base <- file.path(root, "claude_mem")
mem_proj_dir <- file.path(mem_base, "-home-user-demopkg", "memory")
dir.create(mem_proj_dir, recursive = TRUE, showWarnings = FALSE)

codex_home <- file.path(root, "codex_home")
codex_mem_dir <- file.path(codex_home, "memories")
dir.create(codex_mem_dir, recursive = TRUE, showWarnings = FALSE)

# Fake claude global path (points to nonexistent file by default).
# Use this in all tests to avoid leaking the real ~/.claude/CLAUDE.md.
fake_claude_global <- file.path(root, "fake_home_claude.md")

# Helper that injects the test fixtures
ac <- function(agent, workspace = workspace_dir, ...) {
    old_codex_home <- Sys.getenv("CODEX_HOME", unset = NA_character_)
    Sys.setenv(CODEX_HOME = codex_home)
    on.exit({
        if (is.na(old_codex_home)) {
            Sys.unsetenv("CODEX_HOME")
        } else {
            Sys.setenv(CODEX_HOME = old_codex_home)
        }
    }, add = TRUE)

    saber::agent_context(agent = agent,
                         project_dir = project_dir,
                         workspace_dir = workspace,
                         memory_base = mem_base,
                         claude_global_path = fake_claude_global,
                         ...)
}

write_lines_to <- function(path, lines) {
    writeLines(lines, path)
}

# --- Empty case: no files at all ---
expect_equal(ac("corteza"), "")

# --- AGENTS.md only, codex agent (codex autoloads it -> skipped) ---
write_lines_to(file.path(project_dir, "AGENTS.md"), "Project rules.")
result <- ac("codex")
expect_false(grepl("AGENTS.md", result))
expect_false(grepl("Project rules", result))

# --- AGENTS.md only, claude agent (claude doesn't autoload AGENTS.md) ---
result <- ac("claude")
expect_true(grepl("AGENTS.md", result))
expect_true(grepl("Project rules", result))

# --- CLAUDE.md only, claude agent (autoloaded -> skipped) ---
file.remove(file.path(project_dir, "AGENTS.md"))
write_lines_to(file.path(project_dir, "CLAUDE.md"), "Claude project rules.")
result <- ac("claude")
expect_false(grepl("Claude project rules", result))

# --- CLAUDE.md only, codex agent (loads it as fallback) ---
result <- ac("codex")
expect_true(grepl("CLAUDE.md", result))
expect_true(grepl("Claude project rules", result))

# --- Both files exist and differ, corteza receives both, CLAUDE.md first ---
write_lines_to(file.path(project_dir, "AGENTS.md"), "Agents version.")
result <- ac("corteza")
expect_true(grepl("Claude project rules", result))
expect_true(grepl("Agents version", result))
expect_true(regexpr("## CLAUDE.md", result, fixed = TRUE) <
            regexpr("## AGENTS.md", result, fixed = TRUE))

# --- Memory loading ---
write_lines_to(file.path(mem_proj_dir, "MEMORY.md"),
               c("- [Test](t.md) - a test memory"))
write_lines_to(file.path(codex_mem_dir, "reciprocal.md"),
               "saber is meant to be reciprocal")
result <- ac("corteza")
expect_true(grepl("## Memory", result))
expect_true(grepl("test memory", result))
expect_true(grepl("## Codex Memories", result))
expect_true(grepl("saber is meant to be reciprocal", result))

# --- Claude memory skipped for claude by default, Codex memory is included ---
result <- ac("claude")
expect_false(grepl("## Memory", result))
expect_true(grepl("saber is meant to be reciprocal", result))

# --- Codex memory skipped for codex by default, Claude memory is included ---
result <- ac("codex")
expect_true(grepl("test memory", result))
expect_false(grepl("saber is meant to be reciprocal", result))

# --- Corteza receives both memory systems ---
result <- ac("corteza")
expect_true(grepl("test memory", result))
expect_true(grepl("saber is meant to be reciprocal", result))

# --- Memory force-included for claude includes both memory systems ---
result <- ac("claude", include_memory = TRUE)
expect_true(grepl("## Memory", result))
expect_true(grepl("saber is meant to be reciprocal", result))

# --- Memory can be disabled for all memory systems ---
result <- ac("corteza", include_memory = FALSE)
expect_false(grepl("test memory", result))
expect_false(grepl("saber is meant to be reciprocal", result))

# --- Legacy llamar identifier remains a corteza alias ---
result <- ac("llamar")
expect_true(grepl("test memory", result))
expect_true(grepl("saber is meant to be reciprocal", result))

# --- SOUL.md and USER.md from workspace ---
write_lines_to(file.path(workspace_dir, "SOUL.md"), "I am the soul.")
write_lines_to(file.path(workspace_dir, "USER.md"), "User preferences here.")
result <- ac("corteza")
expect_true(grepl("SOUL.md", result))
expect_true(grepl("I am the soul", result))

# --- USER.md loads for claude when claude_global_path doesn't exist ---
result <- ac("claude")
expect_true(grepl("User preferences here", result))

# --- claude_global_path file is loaded for codex/corteza ---
write_lines_to(fake_claude_global, "Fake claude global content.")
result <- ac("corteza")
expect_true(grepl("Fake claude global content", result))
# When claude global exists, USER.md is the fallback (not loaded)
expect_false(grepl("User preferences here", result))

# --- claude_global_path is skipped for claude (autoloaded) ---
result <- ac("claude")
expect_false(grepl("Fake claude global content", result))
# But USER.md still loads for claude
expect_true(grepl("User preferences here", result))

# Reset for remaining tests
file.remove(fake_claude_global)

# --- include_soul = FALSE skips SOUL.md ---
result <- ac("corteza", include_soul = FALSE)
expect_false(grepl("I am the soul", result))

# --- workspace_dir = NULL skips SOUL.md and USER.md ---
result <- ac("corteza", workspace = NULL)
expect_false(grepl("SOUL", result))
expect_false(grepl("USER", result))

# --- NULL agent loads everything ---
result <- ac(NULL)
expect_true(grepl("## Memory", result))
expect_true(grepl("Claude project rules", result))
expect_true(grepl("I am the soul", result))

# --- include_project = FALSE skips even when files exist ---
result <- ac("corteza", include_project = FALSE)
expect_false(grepl("Claude project rules", result))

# --- Symlink dedup: AGENTS.md symlink to CLAUDE.md, claude agent skips both ---
file.remove(file.path(project_dir, "AGENTS.md"))
file.symlink("CLAUDE.md", file.path(project_dir, "AGENTS.md"))
result <- ac("claude")
# AGENTS.md is a symlink to CLAUDE.md, and claude autoloads CLAUDE.md,
# so AGENTS.md should also be skipped (same canonical file)
expect_false(grepl("Claude project rules", result))

# --- Project merge: sections the agent already holds are not repeated ---
merge_dir <- file.path(root, "mergepkg")
dir.create(merge_dir)
project_only <- function(agent, ...) {
    saber::agent_context(agent = agent, project_dir = merge_dir,
                         include_memory = FALSE, include_global = FALSE,
                         include_soul = FALSE, ...)
}
count_of <- function(text, x) lengths(regmatches(x, gregexpr(text, x, fixed = TRUE)))
shared <- c("# Demo", "", "Intro line.", "",
            "## Build", "", "```bash", "# not a heading", "make", "```", "",
            "## Style", "", "Use snake_case.")
claude_md <- file.path(merge_dir, "CLAUDE.md")
agents_md <- file.path(merge_dir, "AGENTS.md")

# Identical copies: the autoloading agents get nothing, corteza gets one.
write_lines_to(claude_md, shared)
write_lines_to(agents_md, c(shared, ""))
expect_equal(project_only("claude"), "")
expect_equal(project_only("codex"), "")
result <- project_only("corteza")
expect_equal(count_of("Use snake_case.", result), 1L)
expect_true(grepl("## CLAUDE.md", result, fixed = TRUE))
expect_false(grepl("## AGENTS.md", result, fixed = TRUE))
# One file is still emitted verbatim.
expect_true(grepl(paste(shared, collapse = "\n"), result, fixed = TRUE))

# A drifted copy: only the sections that differ are added.
write_lines_to(agents_md, c(shared[1:11],
                            "## Style", "", "Use snake_case and 4 spaces.", "",
                            "## Release", "", "Tag after CRAN accepts."))
result <- project_only("claude")
expect_true(grepl("## AGENTS.md (sections not already loaded)", result,
                  fixed = TRUE))
expect_true(grepl("Use snake_case and 4 spaces.", result, fixed = TRUE))
expect_true(grepl("Tag after CRAN accepts.", result, fixed = TRUE))
expect_false(grepl("Intro line.", result, fixed = TRUE))
# The fenced comment did not split the Build section, so it dropped whole.
expect_false(grepl("make", result, fixed = TRUE))
expect_false(grepl("# not a heading", result, fixed = TRUE))

result <- project_only("codex")
expect_true(grepl("## CLAUDE.md (sections not already loaded)", result,
                  fixed = TRUE))
expect_true(grepl("Use snake_case.", result, fixed = TRUE))
expect_false(grepl("Tag after CRAN accepts.", result, fixed = TRUE))
expect_false(grepl("Intro line.", result, fixed = TRUE))

# corteza and a forced include get the union once.
for (result in list(project_only("corteza"),
                    project_only("claude", include_project = TRUE))) {
    expect_equal(count_of("Intro line.", result), 1L)
    expect_equal(count_of("make", result), 1L)
    expect_true(grepl("Use snake_case.", result, fixed = TRUE))
    expect_true(grepl("Use snake_case and 4 spaces.", result, fixed = TRUE))
    expect_true(grepl("Tag after CRAN accepts.", result, fixed = TRUE))
}

# --- Cleanup ---
unlink(root, recursive = TRUE)

Try the saber package in your browser

Any scripts or data that you put into this service are public.

saber documentation built on Nov. 10, 2026, 9:06 a.m.