use mentra::{
ContentBlock,
test::{MockRuntime, MockToolCall},
};
use crate::context::{ContextDocument, ContextScope};
use crate::memory::{Memory, MemoryKind};
use super::*;
fn defaults(workspace: &Path, context: &WorkspaceContext) -> mentra::agent::AgentConfig {
configured(workspace, context, None)
}
fn configured(
workspace: &Path,
context: &WorkspaceContext,
system_prompt: Option<&SystemPrompt>,
) -> mentra::agent::AgentConfig {
remembering(workspace, context, system_prompt, &[])
}
fn remembering(
workspace: &Path,
context: &WorkspaceContext,
system_prompt: Option<&SystemPrompt>,
memories: &[Memory],
) -> mentra::agent::AgentConfig {
with_roster(
workspace,
context,
system_prompt,
memories,
ToolRoster::default(),
)
}
fn with_roster(
workspace: &Path,
context: &WorkspaceContext,
system_prompt: Option<&SystemPrompt>,
memories: &[Memory],
roster: ToolRoster,
) -> mentra::agent::AgentConfig {
agent_config(
workspace,
context,
system_prompt,
crate::memory::index_block(memories).as_deref(),
roster,
Compaction::default(),
PathBuf::from("/transcripts"),
)
}
fn deploy_memories() -> Vec<Memory> {
vec![Memory {
name: "deploy-notes".to_string(),
description: "how deploys go out".to_string(),
kind: MemoryKind::Project,
path: PathBuf::from("/mem/deploy-notes.md"),
scope: ContextScope::Global,
}]
}
fn house_rules() -> WorkspaceContext {
WorkspaceContext::from_documents(vec![ContextDocument {
path: PathBuf::from("/repo/AGENTS.md"),
scope: ContextScope::Workspace,
content: "house rules".to_string(),
}])
}
#[test]
fn context_becomes_the_system_prompt_and_the_workspace_is_scoped() {
let agent = defaults(Path::new("/repo"), &house_rules());
assert!(
agent
.system
.expect("a system prompt")
.contains("house rules")
);
assert_eq!(agent.workspace.base_dir, PathBuf::from("/repo"));
}
#[test]
fn an_empty_workspace_context_leaves_the_system_prompt_unset() {
let agent = defaults(Path::new("/repo"), &WorkspaceContext::default());
assert_eq!(agent.system, None);
}
#[test]
fn a_fresh_builder_says_nothing_about_the_system_prompt() {
assert_eq!(WorkspaceBuilder::new("/repo").system_prompt, None);
assert_eq!(
configured(Path::new("/repo"), &house_rules(), None).system,
house_rules().render()
);
}
#[test]
fn an_appended_prompt_comes_after_the_workspace_because_it_is_more_specific() {
let agent = configured(
Path::new("/repo"),
&house_rules(),
Some(&SystemPrompt::Append("answer in Chinese".to_string())),
);
let system = agent.system.expect("a system prompt");
let context = system.find("house rules").expect("the workspace's say");
let host = system.find("answer in Chinese").expect("the host's say");
assert!(context < host, "{system}");
}
#[test]
fn an_appended_prompt_stands_alone_when_the_workspace_says_nothing() {
let agent = configured(
Path::new("/repo"),
&WorkspaceContext::default(),
Some(&SystemPrompt::Append("answer in Chinese".to_string())),
);
assert_eq!(agent.system.as_deref(), Some("answer in Chinese"));
}
#[test]
fn a_replaced_prompt_drops_the_context_block_entirely() {
let agent = configured(
Path::new("/repo"),
&house_rules(),
Some(&SystemPrompt::Replace(
"you are Acme's reviewer".to_string(),
)),
);
assert_eq!(agent.system.as_deref(), Some("you are Acme's reviewer"));
}
#[test]
fn appending_nothing_leaves_the_workspaces_prompt_as_it_was() {
let agent = configured(
Path::new("/repo"),
&house_rules(),
Some(&SystemPrompt::Append(" \n".to_string())),
);
assert_eq!(agent.system, house_rules().render());
}
#[test]
fn replacing_with_nothing_is_how_a_host_asks_for_no_system_prompt() {
let agent = configured(
Path::new("/repo"),
&house_rules(),
Some(&SystemPrompt::Replace(String::new())),
);
assert_eq!(agent.system, None);
}
#[test]
fn the_last_system_prompt_said_is_the_one_that_holds() {
let builder = WorkspaceBuilder::new("/repo")
.with_system_prompt(SystemPrompt::Replace("first".to_string()))
.with_system_prompt(SystemPrompt::Append("second".to_string()));
assert_eq!(
builder.system_prompt,
Some(SystemPrompt::Append("second".to_string()))
);
}
#[test]
fn the_memory_index_lands_after_the_context_documents() {
let agent = remembering(Path::new("/repo"), &house_rules(), None, &deploy_memories());
let system = agent.system.expect("a system prompt");
let context = system.find("house rules").expect("the workspace's say");
let index = system.find("<memories>").expect("the index");
assert!(context < index, "{system}");
assert!(system.contains("deploy-notes — how deploys go out"));
}
#[test]
fn a_replaced_prompt_drops_the_memory_index_too() {
let agent = remembering(
Path::new("/repo"),
&house_rules(),
Some(&SystemPrompt::Replace(
"you are Acme's reviewer".to_string(),
)),
&deploy_memories(),
);
assert_eq!(agent.system.as_deref(), Some("you are Acme's reviewer"));
}
#[test]
fn a_hosts_append_still_lands_after_the_memory_index() {
let agent = remembering(
Path::new("/repo"),
&house_rules(),
Some(&SystemPrompt::Append("answer in Chinese".to_string())),
&deploy_memories(),
);
let system = agent.system.expect("a system prompt");
let index = system.find("<memories>").expect("the index");
let host = system.find("answer in Chinese").expect("the host's say");
assert!(index < host, "{system}");
}
#[test]
fn no_memories_leave_the_prompt_byte_identical() {
let agent = remembering(Path::new("/repo"), &house_rules(), None, &[]);
assert_eq!(agent.system, house_rules().render());
}
#[test]
fn memories_alone_still_make_a_prompt() {
let agent = remembering(
Path::new("/repo"),
&WorkspaceContext::default(),
None,
&deploy_memories(),
);
let system = agent.system.expect("a system prompt");
assert!(system.starts_with("<memories>"), "{system}");
}
#[test]
fn the_memory_index_renders_whatever_the_roster_says() {
let agent = with_roster(
Path::new("/repo"),
&house_rules(),
None,
&deploy_memories(),
ToolRoster::only([crate::tools::SPAWN]),
);
let system = agent.system.expect("a system prompt");
assert!(system.contains("<memories>"), "{system}");
assert!(system.contains("deploy-notes — how deploys go out"));
assert!(
!agent.tool_profile.allows("read"),
"the roster still narrows what the model may actually call"
);
}
#[tokio::test]
async fn the_memory_index_reaches_the_provider_request() {
let mock = MockRuntime::builder()
.model("mock-model", "openai")
.text("noted")
.build()
.expect("the mock runtime builds");
let config = remembering(Path::new("/repo"), &house_rules(), None, &deploy_memories());
let mut session = mock
.runtime()
.create_session_with_config("memories", mock.model(), config)
.expect("session");
session
.append_turn(vec![ContentBlock::Text {
text: "hello".to_string(),
}])
.await
.expect("the scripted turn runs");
let requests = mock.recorded_requests().await;
let system = requests
.last()
.and_then(|request| request.system.as_deref())
.expect("a system prompt reached the provider");
assert!(system.contains("<memories>"), "{system}");
assert!(system.contains("deploy-notes — how deploys go out"));
assert!(
system.find("house rules") < system.find("<memories>"),
"the index comes after the documents: {system}"
);
}
#[test]
fn the_two_doors_spawn_replaces_leave_the_roster() {
let agent = defaults(Path::new("/repo"), &WorkspaceContext::default());
for replaced in ["shell", "background_run", "task"] {
assert!(
!agent.tool_profile.allows(replaced),
"{replaced} is still offered to the model"
);
}
assert!(
agent.tool_profile.allows(crate::tools::SPAWN),
"the door that replaces them has to be open"
);
}
#[test]
fn hiding_is_a_roster_fact_and_not_a_capability_one() {
let agent = defaults(Path::new("/repo"), &WorkspaceContext::default());
assert_eq!(
agent.tool_profile.allowed_tools, None,
"an allow-list here would silently drop every tool nobody thought to name"
);
assert!(agent.tool_profile.allows("read"));
}
#[tokio::test]
async fn the_default_roster_is_exactly_this() {
let runtime = crate::runtime::Runtime::builder()
.with_base_url("http://127.0.0.1:1/v1")
.with_api_key("test-key")
.with_ephemeral_history()
.build()
.expect("builds offline");
let agent = defaults(Path::new("/repo"), &WorkspaceContext::default());
let offered = runtime
.mentra_runtime()
.tools()
.into_iter()
.map(|tool| tool.provider.name)
.filter(|name| agent.tool_profile.allows(name))
.collect::<Vec<_>>();
assert_eq!(
offered,
[
"compact",
"edit",
"glob",
"grep",
"ls",
"read",
crate::tools::SPAWN,
"write",
],
"the model's whole API, in one place"
);
assert!(agent.tool_profile.allows("load_skill"));
}
#[test]
fn the_doors_basis_does_not_surface_stay_shut() {
let agent = defaults(Path::new("/repo"), &WorkspaceContext::default());
for shut in [
"team_spawn",
"team_send",
"team_read_inbox",
"team_broadcast",
"team_request",
"team_respond",
"team_list_requests",
"idle",
"task_create",
"task_claim",
"task_update",
"task_list",
"task_get",
"check_background",
"memory_pin",
"memory_forget",
"memory_search",
] {
assert!(
!agent.tool_profile.allows(shut),
"{shut} is still offered to the model"
);
}
}
#[test]
fn commands_are_available_unless_the_caller_says_otherwise() {
assert!(WorkspaceBuilder::new("/repo").shell.is_granted());
}
#[test]
fn discovery_is_enabled_until_the_caller_turns_it_off() {
assert!(WorkspaceBuilder::new("/repo").discovery_enabled);
assert!(
!WorkspaceBuilder::new("/repo")
.without_discovery()
.discovery_enabled
);
}
#[test]
fn discovery_stays_off_after_later_discovery_setters() {
let builder = WorkspaceBuilder::new("/repo")
.without_discovery()
.with_context(ContextConfig::default())
.with_config(Config::default())
.with_skills(SkillsConfig::default())
.with_memory(MemoryConfig::default())
.with_templates(TemplatesConfig::default())
.with_hooks(HooksConfig::default())
.with_tools(ToolsConfig::default());
#[cfg(feature = "mcp")]
let builder = builder.with_mcp(McpConfig::default());
assert!(
!builder.discovery_enabled,
"a later source-specific setter must not reactivate discovery"
);
}
#[test]
fn a_fresh_builder_carries_a_private_default_runtime_and_an_inherited_model() {
let builder = WorkspaceBuilder::new("/repo");
assert!(matches!(builder.runtime, RuntimeSource::Private(_)));
assert!(matches!(builder.model, WorkspaceModel::Inherited));
}
#[test]
fn builders_return_new_values() {
let base = WorkspaceBuilder::new("/repo");
let derived = base.with_model(ModelSelector::Id("pinned".to_string()));
assert!(matches!(
derived.model,
WorkspaceModel::Selector(ModelSelector::Id(ref id)) if id == "pinned"
));
assert!(matches!(
WorkspaceBuilder::new("/repo").model,
WorkspaceModel::Inherited
));
}
#[test]
fn model_inputs_are_mutually_exclusive_and_last_call_wins() {
let mut resolved = ModelInfo::new("resolved", "openai").with_context_window(200_000);
resolved.display_name = Some("Resolved model".to_string());
let selector_last = WorkspaceBuilder::new("/repo")
.with_resolved_model(resolved.clone())
.with_model(ModelSelector::Id("selected".to_string()));
assert!(matches!(
selector_last.model,
WorkspaceModel::Selector(ModelSelector::Id(ref id)) if id == "selected"
));
let resolved_last = WorkspaceBuilder::new("/repo")
.with_model(ModelSelector::Id("selected".to_string()))
.with_resolved_model(resolved.clone());
assert!(matches!(
resolved_last.model,
WorkspaceModel::Resolved(ref held) if held == &resolved
));
}
#[tokio::test]
async fn a_shared_runtime_is_the_one_the_workspace_borrows() {
let runtime = Arc::new(
crate::runtime::Runtime::builder()
.with_base_url("http://127.0.0.1:1/v1")
.with_api_key("test-key")
.with_ephemeral_history()
.build()
.expect("builds offline"),
);
let builder = WorkspaceBuilder::new("/repo").with_runtime(Arc::clone(&runtime));
match &builder.runtime {
RuntimeSource::Shared(held) => assert!(
Arc::ptr_eq(held, &runtime),
"borrowing must not clone the substrate"
),
RuntimeSource::Private(_) | RuntimeSource::Reusable(_) => {
panic!("with_runtime must switch the source")
}
}
}
#[tokio::test]
async fn reusable_rebuild_refuses_an_unexpected_basis_runtime_owner() {
let root = tempfile::tempdir().expect("workspace");
let mut definition = mentra::provider_core::responses::openai_definition();
definition.descriptor.id = mentra::ProviderId::new("reusable-owner-test");
definition.base_url = Some("http://127.0.0.1:1/".to_string());
let seed = mentra::provider_core::responses::ResponsesProvider::new(
definition,
mentra::provider_core::StaticCredentialSource::new("test-key"),
);
let recipe = RuntimeBuilder::default()
.with_reusable_registered_provider(
"reusable-owner-test",
move || Ok::<_, std::io::Error>(seed.fresh_session_scope()),
|_provider| async { Ok::<_, std::io::Error>(()) },
)
.with_ephemeral_history()
.into_reusable_recipe()
.expect("recipe");
let workspace = Workspace::builder(root.path())
.with_runtime_recipe(recipe)
.without_discovery()
.fresh_only()
.with_resolved_model(ModelInfo::new("test-model", "reusable-owner-test"))
.with_tool_roster(ToolRoster::only(std::iter::empty::<String>()))
.open()
.await
.expect("workspace opens")
.bind_host_tools(Vec::new())
.expect("tool-free checkout binds");
let unexpected_owner = Arc::clone(&workspace.runtime);
let error = workspace
.rebuild_for_reuse()
.await
.expect_err("the extra Basis runtime owner must consume the pool entry");
assert!(matches!(error, RunError::ReusableRuntimeNotUnique));
drop(unexpected_owner);
}
#[test]
fn a_builder_holding_a_credentialed_recipe_does_not_print_it() {
let printed = format!(
"{:?}",
WorkspaceBuilder::new("/repo").with_runtime_builder(
crate::runtime::RuntimeBuilder::default().with_api_key("sk-secret-value")
)
);
assert!(!printed.contains("sk-secret-value"), "{printed}");
assert!(printed.contains("redacted"), "{printed}");
}
#[test]
fn an_unconfigured_workspace_keeps_every_tool_result() {
let agent = defaults(Path::new("/repo"), &WorkspaceContext::default());
assert_eq!(
agent.compaction.keep_recent_tool_results,
usize::MAX,
"mentra's off switch for micro-compaction is what basis defaults to"
);
assert_eq!(agent.compaction.auto_compact_threshold_tokens, Some(50_000));
assert_eq!(agent.compaction.projected_tool_result_budget, None);
}
#[test]
fn what_a_host_says_about_compaction_is_what_the_agent_carries() {
let compaction = Compaction::default()
.with_keep_recent_tool_results(Some(2))
.with_auto_threshold_tokens(Some(400_000))
.with_preserve_recent_user_tokens(1_000);
let agent = agent_config(
Path::new("/repo"),
&WorkspaceContext::default(),
None,
None,
ToolRoster::default(),
compaction,
PathBuf::from("/transcripts"),
);
assert_eq!(agent.compaction.keep_recent_tool_results, 2);
assert_eq!(agent.compaction.projected_tool_result_budget, None);
assert_eq!(
agent.compaction.auto_compact_threshold_tokens,
Some(400_000)
);
assert_eq!(agent.compaction.preserve_recent_user_tokens, 1_000);
}
#[test]
fn with_compaction_returns_a_new_builder() {
let base = WorkspaceBuilder::new("/repo");
let derived =
base.with_compaction(Compaction::default().with_keep_recent_tool_results(Some(1)));
assert_eq!(
derived.compaction,
Compaction::default().with_keep_recent_tool_results(Some(1))
);
assert_eq!(
WorkspaceBuilder::new("/repo").compaction,
Compaction::default(),
"a fresh builder keeps everything"
);
}
fn workspace_of_five_files() -> tempfile::TempDir {
let dir = tempfile::tempdir().expect("tempdir");
for index in 1..=5 {
std::fs::write(
dir.path().join(format!("file{index}.txt")),
format!("marker-{index} {}", "x".repeat(200)),
)
.expect("write file");
}
dir
}
fn reading_all_five() -> MockRuntime {
let mut builder = MockRuntime::builder().model("mock-model", "openai");
for index in 1..=5 {
builder = builder.tool_calls(vec![
MockToolCall::new(
"files",
serde_json::json!({
"operations": [{
"op": "read",
"path": format!("file{index}.txt"),
}],
}),
)
.with_id(format!("read-{index}")),
]);
}
builder
.text("read them all")
.build()
.expect("the mock runtime builds")
}
#[tokio::test]
async fn every_tool_result_the_model_read_is_still_in_front_of_it() {
let workspace = workspace_of_five_files();
let mock = reading_all_five();
let mut session = mock
.runtime()
.create_session_with_config(
"compaction",
mock.model(),
defaults(workspace.path(), &WorkspaceContext::default()),
)
.expect("session");
session
.append_turn(vec![ContentBlock::Text {
text: "read every file".to_string(),
}])
.await
.expect("the scripted turn runs");
let requests = mock.recorded_requests().await;
let last = requests.last().expect("the model was asked something");
let results: Vec<String> = last
.messages
.iter()
.flat_map(|message| &message.content)
.filter_map(|block| match block {
ContentBlock::ToolResult { content, .. } => Some(content.to_display_string()),
_ => None,
})
.collect();
assert_eq!(results.len(), 5, "five reads, five results");
for (index, result) in results.iter().enumerate() {
assert!(
result.contains(&format!("marker-{}", index + 1)),
"the model can no longer see what it read: {result}"
);
}
}
fn offline(path: &Path) -> WorkspaceBuilder {
Workspace::builder(path)
.with_runtime_builder(
RuntimeBuilder::default()
.with_base_url("http://127.0.0.1:1/v1")
.with_api_key("test-key")
.with_ephemeral_history(),
)
.with_model(ModelSelector::Id("test-model".to_string()))
.without_discovery()
}
fn resolved(path: &Path) -> PathBuf {
let canonical = path.canonicalize().expect("canonical path");
dunce::simplified(&canonical).to_path_buf()
}
fn spellings(workspace: &Workspace) -> Vec<&Path> {
vec![
workspace.root(),
workspace.path(),
workspace.agent.workspace.base_dir.as_path(),
workspace.declared_registration.root(),
workspace.hook_registration.key(),
]
}
#[tokio::test]
async fn a_relative_path_is_made_absolute_at_open() {
let workspace = offline(Path::new(".")).open().await.expect("opens");
let here = resolved(&std::env::current_dir().expect("a working directory"));
for spelling in spellings(&workspace) {
assert_eq!(
spelling, here,
"a relative spelling must not survive the open"
);
}
}
#[cfg(unix)]
#[tokio::test]
async fn a_symlinked_spelling_opens_the_directory_it_names() {
let base = tempfile::tempdir().expect("tempdir");
let real = base.path().join("real");
std::fs::create_dir(&real).expect("create the real directory");
let link = base.path().join("link");
std::os::unix::fs::symlink(&real, &link).expect("symlink");
let workspace = offline(&link).open().await.expect("opens");
let canonical = resolved(&real);
for spelling in spellings(&workspace) {
assert_eq!(
spelling, canonical,
"a symlinked spelling must resolve once, at the open"
);
}
}
#[tokio::test]
async fn a_dotted_spelling_is_folded_at_open() {
let base = tempfile::tempdir().expect("tempdir");
let nested = base.path().join("nested");
std::fs::create_dir(&nested).expect("create the nested directory");
let workspace = offline(&nested.join("..").join("nested"))
.open()
.await
.expect("opens");
let canonical = resolved(&nested);
for spelling in spellings(&workspace) {
assert_eq!(spelling, canonical, "`..` must not survive the open");
}
}
#[cfg(windows)]
#[tokio::test]
async fn the_resolved_root_is_not_a_verbatim_windows_path() {
let base = tempfile::tempdir().expect("tempdir");
let workspace = offline(base.path()).open().await.expect("opens");
for spelling in spellings(&workspace) {
assert!(
!spelling.as_os_str().to_string_lossy().starts_with(r"\\?\"),
"a verbatim root denies every absolute path the model names: {}",
spelling.display()
);
assert!(
spelling.is_absolute(),
"simplifying must not cost the root its prefix: {}",
spelling.display()
);
}
}