use crate::brain::tools::Tool;
use crate::brain::tools::catalog;
use crate::brain::tools::error::ToolError;
use crate::brain::tools::registry::ToolRegistry;
use crate::brain::tools::subagent::ALWAYS_EXCLUDED;
use crate::brain::tools::r#trait::ToolExecutionContext;
use crate::brain::tools::{subagent, suggest_options::SuggestOptionsTool};
use crate::cli::tool_setup::{HEADLESS_PREAMBLE, register_core_agent_tools};
use crate::config::Config;
use crate::db::Database;
use serde_json::json;
use std::sync::Arc;
async fn build_registry(headless: bool) -> (Arc<ToolRegistry>, ()) {
let db = Database::connect_in_memory().await.expect("in-memory db");
db.run_migrations().await.expect("migrations");
let config = Config::default();
let registry = Arc::new(ToolRegistry::new());
let _m = register_core_agent_tools(®istry, &db, &config, headless);
(registry, ())
}
#[tokio::test]
async fn headless_registry_lacks_session_notify_and_suggest_options() {
let (registry, _) = build_registry(true).await;
assert!(
!registry.has_tool("session_notify"),
"session_notify must NOT be registered headless (#129): a one-shot \
process cannot flush a parked notification"
);
assert!(
!registry.has_tool("suggest_options"),
"suggest_options must NOT be registered headless (#129): it renders \
nowhere without a TUI/channel handler"
);
assert!(
registry.has_tool("bash") && registry.has_tool("read_file"),
"core tools must remain registered headless"
);
}
#[tokio::test]
async fn interactive_registry_keeps_session_notify_and_suggest_options() {
let (registry, _) = build_registry(false).await;
assert!(
registry.has_tool("session_notify"),
"interactive registry must keep session_notify"
);
assert!(
registry.has_tool("suggest_options"),
"interactive registry must keep suggest_options"
);
}
#[tokio::test]
async fn interactive_only_tools_hard_error_on_headless_context() {
let mut ctx = ToolExecutionContext::new(uuid::Uuid::new_v4());
ctx.headless = true;
let suggest_err = <SuggestOptionsTool as Tool>::execute(
&SuggestOptionsTool,
json!({ "options": ["Go", "Stop"] }),
&ctx,
)
.await
.expect_err("suggest_options must hard-error headless (#129)");
assert!(
matches!(suggest_err, ToolError::Execution(ref m) if m.contains("not available headless")),
"suggest_options headless error must be a loud ToolError, got: {suggest_err:?}"
);
let notify_err = <subagent::SessionNotifyTool as Tool>::execute(
&subagent::SessionNotifyTool,
json!({ "target_session": uuid::Uuid::new_v4().to_string(), "text": "hi" }),
&ctx,
)
.await
.expect_err("session_notify must hard-error headless (#129)");
assert!(
matches!(notify_err, ToolError::Execution(ref m) if m.contains("not available headless")),
"session_notify headless error must be a loud ToolError, got: {notify_err:?}"
);
}
#[tokio::test]
async fn suggest_options_does_not_trip_guard_on_interactive_context() {
let ctx = ToolExecutionContext::new(uuid::Uuid::new_v4());
let result = <SuggestOptionsTool as Tool>::execute(
&SuggestOptionsTool,
json!({ "options": ["Go"] }),
&ctx,
)
.await;
match result {
Err(ToolError::Execution(m)) if m.contains("not available headless") => {
panic!("headless guard must not fire on an interactive context")
}
_ => {} }
}
#[test]
fn headless_preamble_names_the_final_message_law() {
assert!(
HEADLESS_PREAMBLE.contains("HEADLESS"),
"preamble must identify the surface"
);
assert!(
HEADLESS_PREAMBLE.to_lowercase().contains("final message"),
"preamble must carry the final-message self-containedness law"
);
assert!(
HEADLESS_PREAMBLE.contains("session_notify")
&& HEADLESS_PREAMBLE.contains("suggest_options"),
"preamble must warn the model off the two unavailable tools"
);
}
#[test]
fn child_registry_strips_interactive_only_tools() {
assert!(
ALWAYS_EXCLUDED.contains(&"session_notify"),
"session_notify must be in ALWAYS_EXCLUDED (ruling C)"
);
assert!(
ALWAYS_EXCLUDED.contains(&"suggest_options"),
"suggest_options must be in ALWAYS_EXCLUDED (ruling C)"
);
let parent = ToolRegistry::new();
parent.register(Arc::new(subagent::SessionNotifyTool));
parent.register(Arc::new(SuggestOptionsTool));
let child = subagent::build_child_registry(&parent);
assert!(
!child.has_tool("session_notify") && !child.has_tool("suggest_options"),
"child registry must strip interactive-only tools regardless of parent surface"
);
}
#[test]
fn headless_roster_excludes_interactive_only_tools() {
let headless = catalog::tool_access_prompt(true);
assert!(
!headless.contains("session_notify"),
"headless roster must not advertise session_notify (v0.4.98 ruling a)"
);
assert!(
!headless.contains("suggest_options"),
"headless roster must not advertise suggest_options (v0.4.98 ruling a)"
);
assert!(
headless.contains("spawn_agent") && headless.contains("wait_agent"),
"headless roster must keep the agents group's other members"
);
let interactive = catalog::tool_access_prompt(false);
assert!(
interactive.contains("session_notify"),
"interactive roster keeps session_notify verbatim"
);
assert!(
catalog::CORE_TOOLS.contains(&"suggest_options"),
"suggest_options is core (registered on interactive surfaces), not rostered"
);
}