use std::collections::BTreeMap;
use serde_json::json;
use vv_agent::{build_default_registry, ToolCall, ToolContext, ToolDirective, ToolResultStatus};
#[test]
fn todo_handlers_expose_agent_read_and_write_functions() {
let workspace = tempfile::tempdir().expect("workspace");
let mut context = ToolContext::new(workspace.path());
let empty_args = BTreeMap::new();
let read_empty = vv_agent::tools::handlers::todo::todo_read(&mut context, &empty_args);
assert_eq!(read_empty.status, ToolResultStatus::Success);
let read_empty_payload: serde_json::Value =
serde_json::from_str(&read_empty.content).expect("read empty payload");
assert_eq!(read_empty_payload["action"], json!("read"));
assert_eq!(read_empty_payload["count"], json!(0));
assert_eq!(context.shared_state["todo_list"], json!([]));
let read_empty_from_handlers = vv_agent::tools::handlers::todo_read(&mut context, &empty_args);
assert_eq!(read_empty_from_handlers.status, ToolResultStatus::Success);
let write_result = vv_agent::tools::handlers::todo::todo_write(
&mut context,
&BTreeMap::from([(
"todos".to_string(),
json!([{"title": "ship runtime", "status": "completed", "priority": "high"}]),
)]),
);
assert_eq!(write_result.status, ToolResultStatus::Success);
let read_written = vv_agent::tools::handlers::todo::todo_read(&mut context, &empty_args);
let read_written_payload: serde_json::Value =
serde_json::from_str(&read_written.content).expect("read written payload");
assert_eq!(read_written_payload["action"], json!("read"));
assert_eq!(read_written_payload["count"], json!(1));
assert_eq!(
read_written_payload["todos"][0]["title"],
json!("ship runtime")
);
}
#[test]
fn todo_write_updates_shared_state_and_enforces_single_in_progress() {
let workspace = tempfile::tempdir().expect("workspace");
let registry = build_default_registry();
let mut context = ToolContext::new(workspace.path());
let result = registry
.execute(
&ToolCall::new(
"todo_1",
"todo_write",
BTreeMap::from([(
"todos".to_string(),
json!([
{"title": "a", "status": "in_progress", "priority": "high"},
{"title": "b", "status": "in_progress", "priority": "medium"}
]),
)]),
),
&mut context,
)
.expect("todo_write");
assert_eq!(result.status, ToolResultStatus::Error);
assert!(result.content.contains("multiple_in_progress_todos"));
let result = registry
.execute(
&ToolCall::new(
"todo_2",
"todo_write",
BTreeMap::from([(
"todos".to_string(),
json!([
{"title": "a", "status": "in_progress", "priority": "high"},
{"title": "b", "status": "pending", "priority": "medium"}
]),
)]),
),
&mut context,
)
.expect("todo_write");
assert_eq!(result.status, ToolResultStatus::Success);
assert_eq!(
context.shared_state["todo_list"][0]["title"].as_str(),
Some("a")
);
}
#[test]
fn todo_write_rejects_invalid_payloads() {
let workspace = tempfile::tempdir().expect("workspace");
let registry = build_default_registry();
let mut context = ToolContext::new(workspace.path());
let result = registry
.execute(
&ToolCall::new(
"todo_invalid_payload",
"todo_write",
BTreeMap::from([("todos".to_string(), json!("not an array"))]),
),
&mut context,
)
.expect("todo_write");
assert_eq!(result.status, ToolResultStatus::Error);
assert_eq!(result.error_code.as_deref(), Some("invalid_tool_arguments"));
let result = registry
.execute(
&ToolCall::new(
"todo_missing_title",
"todo_write",
BTreeMap::from([(
"todos".to_string(),
json!([{"status": "pending", "priority": "medium"}]),
)]),
),
&mut context,
)
.expect("todo_write");
assert_eq!(result.status, ToolResultStatus::Error);
assert_eq!(result.error_code.as_deref(), Some("invalid_tool_arguments"));
let result = registry
.execute(
&ToolCall::new(
"todo_bad_status",
"todo_write",
BTreeMap::from([(
"todos".to_string(),
json!([{"title": "step", "status": "blocked", "priority": "medium"}]),
)]),
),
&mut context,
)
.expect("todo_write");
assert_eq!(result.status, ToolResultStatus::Error);
assert_eq!(result.error_code.as_deref(), Some("invalid_tool_arguments"));
}
#[test]
fn todo_write_generates_agent_ids_timestamps_and_preserves_created_at() {
let workspace = tempfile::tempdir().expect("workspace");
let registry = build_default_registry();
let mut context = ToolContext::new(workspace.path());
let result = registry
.execute(
&ToolCall::new(
"todo_create",
"todo_write",
BTreeMap::from([(
"todos".to_string(),
json!([{
"title": " Draft plan ",
"status": "pending",
"priority": "medium"
}]),
)]),
),
&mut context,
)
.expect("todo_write");
assert_eq!(result.status, ToolResultStatus::Success);
let payload: serde_json::Value = serde_json::from_str(&result.content).expect("payload");
assert_eq!(payload["action"], "write");
assert_eq!(payload["count"], 1);
let item = payload["todos"][0].as_object().expect("todo item");
let generated_id = item["id"].as_str().expect("id").to_string();
assert_eq!(generated_id.len(), 8);
assert_eq!(item["title"], "Draft plan");
assert_eq!(item["status"], "pending");
assert_eq!(item["priority"], "medium");
let created_at = item["created_at"].as_str().expect("created_at").to_string();
let updated_at = item["updated_at"].as_str().expect("updated_at");
assert!(
created_at.contains('T') && created_at.ends_with("+00:00"),
"created_at should use UTC ISO timestamp format, got {created_at:?}"
);
assert!(
updated_at.contains('T') && updated_at.ends_with("+00:00"),
"updated_at should use UTC ISO timestamp format, got {updated_at:?}"
);
let result = registry
.execute(
&ToolCall::new(
"todo_update",
"todo_write",
BTreeMap::from([(
"todos".to_string(),
json!([{
"id": generated_id,
"title": "Draft plan",
"status": "completed",
"priority": "high"
}]),
)]),
),
&mut context,
)
.expect("todo_write");
assert_eq!(result.status, ToolResultStatus::Success);
let updated_payload: serde_json::Value =
serde_json::from_str(&result.content).expect("updated payload");
assert_eq!(updated_payload["todos"][0]["created_at"], created_at);
assert_eq!(
context.shared_state["todo_list"][0]["created_at"],
json!(created_at)
);
assert_eq!(context.shared_state["todo_list"][0]["status"], "completed");
}
#[test]
fn control_tools_reject_schema_invalid_argument_types() {
let workspace = tempfile::tempdir().expect("workspace");
let registry = build_default_registry();
let mut context = ToolContext::new(workspace.path());
let cases = [
(
"todo_write",
BTreeMap::from([(
"todos".to_string(),
json!([{
"id": false,
"title": "step",
"status": "pending",
"priority": "medium"
}]),
)]),
"/todos/0/id",
),
(
"task_finish",
BTreeMap::from([("require_all_todos_completed".to_string(), json!("false"))]),
"/require_all_todos_completed",
),
(
"ask_user",
BTreeMap::from([("question".to_string(), json!(123))]),
"/question",
),
(
"ask_user",
BTreeMap::from([
("question".to_string(), json!("Choose")),
("allow_custom_options".to_string(), json!("false")),
]),
"/allow_custom_options",
),
(
"activate_skill",
BTreeMap::from([("skill_name".to_string(), json!(123))]),
"/skill_name",
),
];
for (tool_name, arguments, instance_path) in cases {
let result = registry
.execute(
&ToolCall::new(format!("{tool_name}_invalid"), tool_name, arguments),
&mut context,
)
.expect("tool validation");
let payload: serde_json::Value = serde_json::from_str(&result.content).expect("payload");
assert_eq!(result.status, ToolResultStatus::Error);
assert_eq!(result.error_code.as_deref(), Some("invalid_tool_arguments"));
assert_eq!(payload["issues"][0]["instance_path"], instance_path);
assert_eq!(payload["issues"][0]["rule"], "type");
}
}
#[test]
fn task_finish_blocks_when_todos_are_incomplete() {
let workspace = tempfile::tempdir().expect("workspace");
let registry = build_default_registry();
let mut context = ToolContext::new(workspace.path());
registry
.execute(
&ToolCall::new(
"todo_1",
"todo_write",
BTreeMap::from([(
"todos".to_string(),
json!([{"title": "step1", "status": "pending", "priority": "medium"}]),
)]),
),
&mut context,
)
.expect("todo_write");
let result = registry
.execute(
&ToolCall::new(
"finish_1",
"task_finish",
BTreeMap::from([("message".to_string(), json!("done"))]),
),
&mut context,
)
.expect("task_finish");
assert_eq!(result.status, ToolResultStatus::Error);
assert_eq!(result.directive, ToolDirective::Continue);
assert!(result.content.contains("todo_incomplete"));
}
#[test]
fn ask_user_returns_agent_selection_metadata_and_dedupes_options() {
let workspace = tempfile::tempdir().expect("workspace");
let registry = build_default_registry();
let mut context = ToolContext::new(workspace.path());
let result = registry
.execute(
&ToolCall::new(
"ask_1",
"ask_user",
BTreeMap::from([
("question".to_string(), json!("Choose")),
("options".to_string(), json!(["A", "B", "B", ""])),
("selection_type".to_string(), json!("multi")),
("allow_custom_options".to_string(), json!(true)),
]),
),
&mut context,
)
.expect("ask_user");
assert_eq!(result.status, ToolResultStatus::Success);
assert_eq!(result.directive, ToolDirective::WaitUser);
let payload: serde_json::Value = serde_json::from_str(&result.content).expect("payload");
assert_eq!(payload["question"], "Choose");
assert_eq!(payload["selection_type"], "multi");
assert_eq!(payload["allow_custom_options"], true);
assert_eq!(payload["options"], json!(["A", "B"]));
assert_eq!(result.metadata["options"], json!(["A", "B"]));
}
#[test]
fn activate_skill_loads_skill_md_and_updates_shared_state() {
let workspace = tempfile::tempdir().expect("workspace");
let skill_dir = workspace.path().join("skills/demo-skill");
std::fs::create_dir_all(&skill_dir).expect("skill dir");
std::fs::write(
skill_dir.join("SKILL.md"),
r#"---
name: demo-skill
description: Demo skill description
allowed-tools: read_file, write_file
metadata:
owner: agent
---
Use this skill body during execution.
"#,
)
.expect("skill md");
let registry = build_default_registry();
let mut context = ToolContext::new(workspace.path());
context
.shared_state
.insert("available_skills".to_string(), json!(["skills"]));
context.cycle_index = 3;
let result = registry
.execute(
&ToolCall::new(
"skill_1",
"activate_skill",
BTreeMap::from([
("skill_name".to_string(), json!("demo-skill")),
("reason".to_string(), json!("Need demo behavior")),
]),
),
&mut context,
)
.expect("activate_skill");
assert_eq!(result.status, ToolResultStatus::Success);
let payload: serde_json::Value = serde_json::from_str(&result.content).expect("payload");
assert_eq!(payload["status"], "activated");
assert_eq!(payload["skill_name"], "demo-skill");
assert_eq!(
payload["instructions"],
"Use this skill body during execution."
);
assert_eq!(payload["description"], "Demo skill description");
assert_eq!(payload["allowed_tools"], "read_file, write_file");
assert_eq!(payload["reason"], "Need demo behavior");
assert!(payload.get("metadata").is_none());
assert_eq!(context.shared_state["active_skills"], json!(["demo-skill"]));
assert_eq!(
context.shared_state["skill_activation_log"][0]["cycle_index"],
3
);
}
#[test]
fn activate_skill_ignores_package_frontmatter_fields() {
let workspace = tempfile::tempdir().expect("workspace");
let skill_dir = workspace.path().join("skills/package-skill");
std::fs::create_dir_all(&skill_dir).expect("skill dir");
std::fs::write(
skill_dir.join("SKILL.md"),
r#"---
name: package-skill
description: Package fields should stay internal
license: MIT
---
Use this skill without surfacing internal metadata.
"#,
)
.expect("skill md");
let registry = build_default_registry();
let mut context = ToolContext::new(workspace.path());
context
.shared_state
.insert("available_skills".to_string(), json!(["skills"]));
let result = registry
.execute(
&ToolCall::new(
"skill_structured",
"activate_skill",
BTreeMap::from([("skill_name".to_string(), json!("package-skill"))]),
),
&mut context,
)
.expect("activate_skill");
assert_eq!(result.status, ToolResultStatus::Success);
let payload: serde_json::Value = serde_json::from_str(&result.content).expect("payload");
assert_eq!(payload["skill_name"], "package-skill");
assert!(
payload.get("license").is_none(),
"package metadata should not reach model-visible results"
);
assert!(
!result.content.contains("x-internal-note") && !result.content.contains("MIT"),
"activate_skill should not leak structured internal metadata"
);
}
#[test]
fn activate_skill_accepts_inline_entries_and_reports_disallowed_skill() {
let workspace = tempfile::tempdir().expect("workspace");
let registry = build_default_registry();
let mut context = ToolContext::new(workspace.path());
context.shared_state.insert(
"available_skills".to_string(),
json!([
{
"name": "inline-skill",
"description": "Inline description",
"instructions": "Inline body"
}
]),
);
let result = registry
.execute(
&ToolCall::new(
"skill_inline",
"activate_skill",
BTreeMap::from([("skill_name".to_string(), json!("inline-skill"))]),
),
&mut context,
)
.expect("activate inline skill");
assert_eq!(result.status, ToolResultStatus::Success);
let payload: serde_json::Value = serde_json::from_str(&result.content).expect("payload");
assert_eq!(payload["instructions"], "Inline body");
let result = registry
.execute(
&ToolCall::new(
"skill_denied",
"activate_skill",
BTreeMap::from([("skill_name".to_string(), json!("missing"))]),
),
&mut context,
)
.expect("activate missing skill");
assert_eq!(result.status, ToolResultStatus::Error);
assert_eq!(result.error_code.as_deref(), Some("skill_not_allowed"));
}