use clap::Parser;
use meld::agent::{AgentIdentity, AgentRole, AgentStorage, XdgAgentStorage};
use meld::cli::{Cli, Commands, ContextCommands, RunContext};
use meld::config::{xdg, AgentConfig, ProviderConfig, ProviderType};
use meld::context::frame::{Basis, Frame};
use meld::error::ApiError;
use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;
use tempfile::TempDir;
use crate::integration::with_xdg_env;
fn create_test_agent(
agent_id: &str,
role: AgentRole,
prompt_path: Option<&str>,
) -> Result<PathBuf, ApiError> {
let agents_dir = XdgAgentStorage::new().agents_dir()?;
fs::create_dir_all(&agents_dir)
.map_err(|e| ApiError::ConfigError(format!("Failed to create agents directory: {}", e)))?;
let config_path = agents_dir.join(format!("{}.toml", agent_id));
let mut agent_config = AgentConfig {
agent_id: agent_id.to_string(),
role,
system_prompt: None,
system_prompt_path: prompt_path.map(|s| s.to_string()),
metadata: Default::default(),
};
if role != AgentRole::Reader {
agent_config.metadata.insert(
"user_prompt_file".to_string(),
"Analyze the file at {path}".to_string(),
);
agent_config.metadata.insert(
"user_prompt_directory".to_string(),
"Analyze the directory at {path}".to_string(),
);
}
let toml = toml::to_string(&agent_config)
.map_err(|e| ApiError::ConfigError(format!("Failed to serialize agent config: {}", e)))?;
fs::write(&config_path, toml)
.map_err(|e| ApiError::ConfigError(format!("Failed to write agent config: {}", e)))?;
Ok(config_path)
}
fn create_test_provider(
provider_name: &str,
provider_type: ProviderType,
) -> Result<PathBuf, ApiError> {
let providers_dir = xdg::providers_dir()?;
fs::create_dir_all(&providers_dir).map_err(|e| {
ApiError::ConfigError(format!("Failed to create providers directory: {}", e))
})?;
let config_path = providers_dir.join(format!("{}.toml", provider_name));
let provider_config = ProviderConfig {
provider_name: Some(provider_name.to_string()),
provider_type,
model: "test-model".to_string(),
api_key: None,
endpoint: None,
default_options: meld::provider::CompletionOptions::default(),
};
let toml = toml::to_string(&provider_config).map_err(|e| {
ApiError::ConfigError(format!("Failed to serialize provider config: {}", e))
})?;
fs::write(&config_path, toml)
.map_err(|e| ApiError::ConfigError(format!("Failed to write provider config: {}", e)))?;
Ok(config_path)
}
#[test]
fn test_context_get_with_path() {
let temp_dir = TempDir::new().unwrap();
with_xdg_env(&temp_dir, || {
let workspace_root = temp_dir.path().join("workspace");
fs::create_dir_all(&workspace_root).unwrap();
let test_file = workspace_root.join("test.txt");
fs::write(&test_file, "test content").unwrap();
let run_context = RunContext::new(workspace_root.clone(), None).unwrap();
run_context
.execute(&Commands::Scan { force: true })
.unwrap();
let result = run_context.execute(&Commands::Context {
command: ContextCommands::Get {
node: None,
path: Some(test_file),
agent: None,
frame_type: None,
max_frames: 10,
ordering: "recency".to_string(),
combine: false,
separator: "\n\n---\n\n".to_string(),
format: "text".to_string(),
include_metadata: false,
include_deleted: false,
},
});
assert!(result.is_ok());
let output = result.unwrap();
assert!(output.contains("Node:"));
assert!(output.contains("test.txt"));
});
}
#[test]
fn test_context_get_with_node_id() {
let temp_dir = TempDir::new().unwrap();
with_xdg_env(&temp_dir, || {
let workspace_root = temp_dir.path().join("workspace");
fs::create_dir_all(&workspace_root).unwrap();
let test_file = workspace_root.join("test.txt");
fs::write(&test_file, "test content").unwrap();
let run_context = RunContext::new(workspace_root.clone(), None).unwrap();
run_context
.execute(&Commands::Scan { force: true })
.unwrap();
let status_output = run_context
.execute(&Commands::Status {
format: "json".to_string(),
workspace_only: true,
agents_only: false,
providers_only: false,
breakdown: false,
test_connectivity: false,
})
.unwrap();
let status_json: serde_json::Value = serde_json::from_str(&status_output).unwrap();
let root_hash = status_json["workspace"]["tree"]["root_hash"]
.as_str()
.unwrap();
let result = run_context.execute(&Commands::Context {
command: ContextCommands::Get {
node: Some(root_hash.to_string()),
path: None,
agent: None,
frame_type: None,
max_frames: 10,
ordering: "recency".to_string(),
combine: false,
separator: "\n\n---\n\n".to_string(),
format: "text".to_string(),
include_metadata: false,
include_deleted: false,
},
});
assert!(result.is_ok());
let output = result.unwrap();
assert!(output.contains("Node:"));
});
}
#[test]
fn test_context_get_invalid_path() {
let temp_dir = TempDir::new().unwrap();
with_xdg_env(&temp_dir, || {
let workspace_root = temp_dir.path().join("workspace");
fs::create_dir_all(&workspace_root).unwrap();
let run_context = RunContext::new(workspace_root.clone(), None).unwrap();
let test_path = workspace_root.join("nonexistent.txt");
fs::write(&test_path, "test content").unwrap();
let result = run_context.execute(&Commands::Context {
command: ContextCommands::Get {
node: None,
path: Some(test_path),
agent: None,
frame_type: None,
max_frames: 10,
ordering: "recency".to_string(),
combine: false,
separator: "\n\n---\n\n".to_string(),
format: "text".to_string(),
include_metadata: false,
include_deleted: false,
},
});
assert!(result.is_err());
match result {
Err(ApiError::PathNotInTree(_)) => {}
_ => panic!("Expected PathNotInTree error, got: {:?}", result),
}
});
}
#[test]
fn test_context_get_json_format() {
let temp_dir = TempDir::new().unwrap();
with_xdg_env(&temp_dir, || {
let workspace_root = temp_dir.path().join("workspace");
fs::create_dir_all(&workspace_root).unwrap();
let test_file = workspace_root.join("test.txt");
fs::write(&test_file, "test content").unwrap();
let run_context = RunContext::new(workspace_root.clone(), None).unwrap();
run_context
.execute(&Commands::Scan { force: true })
.unwrap();
let result = run_context.execute(&Commands::Context {
command: ContextCommands::Get {
node: None,
path: Some(test_file),
agent: None,
frame_type: None,
max_frames: 10,
ordering: "recency".to_string(),
combine: false,
separator: "\n\n---\n\n".to_string(),
format: "json".to_string(),
include_metadata: false,
include_deleted: false,
},
});
assert!(result.is_ok());
let output = result.unwrap();
let _json: serde_json::Value = serde_json::from_str(&output).unwrap();
assert!(output.contains("node_id"));
assert!(output.contains("frames"));
});
}
#[test]
fn test_context_get_json_metadata_projection_filters_internal_keys() {
let temp_dir = TempDir::new().unwrap();
with_xdg_env(&temp_dir, || {
let workspace_root = temp_dir.path().join("workspace");
fs::create_dir_all(&workspace_root).unwrap();
let test_file = workspace_root.join("test.txt");
fs::write(&test_file, "test content").unwrap();
let run_context = RunContext::new(workspace_root.clone(), None).unwrap();
run_context
.execute(&Commands::Scan { force: true })
.unwrap();
{
let mut registry = run_context.api().agent_registry().write();
registry.register(AgentIdentity::new(
"writer-metadata".to_string(),
AgentRole::Writer,
));
}
let node_id = run_context
.api()
.node_store()
.find_by_path(&test_file)
.unwrap()
.unwrap()
.node_id;
let mut metadata = HashMap::new();
metadata.insert("provider".to_string(), "test-provider".to_string());
metadata.insert("model".to_string(), "test-model".to_string());
metadata.insert("provider_type".to_string(), "ollama".to_string());
metadata.insert(
"prompt_digest".to_string(),
"digest-summarize-file".to_string(),
);
metadata.insert(
"context_digest".to_string(),
"digest-context-file".to_string(),
);
metadata.insert("prompt_link_id".to_string(), "prompt-link-1".to_string());
metadata.insert("deleted".to_string(), "true".to_string());
let frame = Frame::new(
Basis::Node(node_id),
b"metadata projection".to_vec(),
"context-writer-metadata".to_string(),
"writer-metadata".to_string(),
metadata,
)
.unwrap();
run_context
.api()
.put_frame(node_id, frame, "writer-metadata".to_string())
.unwrap();
let output = run_context
.execute(&Commands::Context {
command: ContextCommands::Get {
node: None,
path: Some(test_file),
agent: None,
frame_type: None,
max_frames: 10,
ordering: "recency".to_string(),
combine: false,
separator: "\n\n---\n\n".to_string(),
format: "json".to_string(),
include_metadata: true,
include_deleted: true,
},
})
.unwrap();
let parsed: serde_json::Value = serde_json::from_str(&output).unwrap();
let frames = parsed["frames"].as_array().unwrap();
assert_eq!(frames.len(), 1);
assert_eq!(frames[0]["agent_id"].as_str(), Some("writer-metadata"));
let metadata_obj = frames[0]["metadata"].as_object().unwrap();
assert!(!metadata_obj.contains_key("agent_id"));
assert!(!metadata_obj.contains_key("deleted"));
assert_eq!(
metadata_obj.get("provider").and_then(|v| v.as_str()),
Some("test-provider")
);
assert_eq!(
metadata_obj.get("model").and_then(|v| v.as_str()),
Some("test-model")
);
assert_eq!(
metadata_obj.get("provider_type").and_then(|v| v.as_str()),
Some("ollama")
);
assert_eq!(
metadata_obj.get("prompt_digest").and_then(|v| v.as_str()),
Some("digest-summarize-file")
);
assert_eq!(
metadata_obj.get("context_digest").and_then(|v| v.as_str()),
Some("digest-context-file")
);
assert_eq!(
metadata_obj.get("prompt_link_id").and_then(|v| v.as_str()),
Some("prompt-link-1")
);
assert!(!metadata_obj.contains_key("prompt"));
});
}
#[test]
fn test_context_get_combine() {
let temp_dir = TempDir::new().unwrap();
with_xdg_env(&temp_dir, || {
let workspace_root = temp_dir.path().join("workspace");
fs::create_dir_all(&workspace_root).unwrap();
let test_file = workspace_root.join("test.txt");
fs::write(&test_file, "test content").unwrap();
let run_context = RunContext::new(workspace_root.clone(), None).unwrap();
run_context
.execute(&Commands::Scan { force: true })
.unwrap();
let result = run_context.execute(&Commands::Context {
command: ContextCommands::Get {
node: None,
path: Some(test_file),
agent: None,
frame_type: None,
max_frames: 10,
ordering: "recency".to_string(),
combine: true,
separator: " | ".to_string(),
format: "text".to_string(),
include_metadata: false,
include_deleted: false,
},
});
assert!(result.is_ok());
});
}
#[test]
fn test_context_generate_requires_provider() {
let temp_dir = TempDir::new().unwrap();
with_xdg_env(&temp_dir, || {
let workspace_root = temp_dir.path().join("workspace");
fs::create_dir_all(&workspace_root).unwrap();
let prompts_dir = xdg::prompts_dir().unwrap();
let prompt_path = prompts_dir.join("test.md");
fs::write(&prompt_path, "Test prompt").unwrap();
create_test_agent("test-agent", AgentRole::Writer, Some("prompts/test.md")).unwrap();
let test_file = workspace_root.join("test.txt");
fs::write(&test_file, "test content").unwrap();
let run_context = RunContext::new(workspace_root.clone(), None).unwrap();
run_context
.execute(&Commands::Scan { force: true })
.unwrap();
let result = run_context.execute(&Commands::Context {
command: ContextCommands::Generate {
node: None,
path: Some(test_file),
path_positional: None,
agent: Some("test-agent".to_string()),
provider: None,
frame_type: None,
force: false,
no_recursive: false,
},
});
assert!(result.is_err());
match result {
Err(ApiError::ProviderNotConfigured(_)) => {}
_ => panic!("Expected ProviderNotConfigured error"),
}
});
}
#[test]
fn test_context_generate_requires_agent_or_default() {
let temp_dir = TempDir::new().unwrap();
with_xdg_env(&temp_dir, || {
let workspace_root = temp_dir.path().join("workspace");
fs::create_dir_all(&workspace_root).unwrap();
let prompts_dir = xdg::prompts_dir().unwrap();
let prompt_path = prompts_dir.join("test.md");
fs::write(&prompt_path, "Test prompt").unwrap();
create_test_agent("test-agent", AgentRole::Writer, Some("prompts/test.md")).unwrap();
create_test_provider("test-provider", ProviderType::Ollama).unwrap();
let test_file = workspace_root.join("test.txt");
fs::write(&test_file, "test content").unwrap();
let run_context = RunContext::new(workspace_root.clone(), None).unwrap();
run_context
.execute(&Commands::Scan { force: true })
.unwrap();
let result = run_context.execute(&Commands::Context {
command: ContextCommands::Generate {
node: None,
path: Some(test_file),
path_positional: None,
agent: None,
provider: Some("test-provider".to_string()),
frame_type: None,
force: false,
no_recursive: false,
},
});
if let Err(e) = result {
assert!(!e.to_string().contains("No Writer agents found"));
}
});
}
#[test]
fn test_context_generate_multiple_agents_requires_flag() {
let temp_dir = TempDir::new().unwrap();
with_xdg_env(&temp_dir, || {
let workspace_root = temp_dir.path().join("workspace");
fs::create_dir_all(&workspace_root).unwrap();
let prompts_dir = xdg::prompts_dir().unwrap();
let prompt_path = prompts_dir.join("test.md");
fs::write(&prompt_path, "Test prompt").unwrap();
create_test_agent("agent1", AgentRole::Writer, Some("prompts/test.md")).unwrap();
create_test_agent("agent2", AgentRole::Writer, Some("prompts/test.md")).unwrap();
create_test_provider("test-provider", ProviderType::Ollama).unwrap();
let test_file = workspace_root.join("test.txt");
fs::write(&test_file, "test content").unwrap();
let run_context = RunContext::new(workspace_root.clone(), None).unwrap();
run_context
.execute(&Commands::Scan { force: true })
.unwrap();
let result = run_context.execute(&Commands::Context {
command: ContextCommands::Generate {
node: None,
path: Some(test_file),
path_positional: None,
agent: None,
provider: Some("test-provider".to_string()),
frame_type: None,
force: false,
no_recursive: false,
},
});
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Multiple Writer agents found"));
});
}
#[test]
fn test_context_get_invalid_ordering() {
let temp_dir = TempDir::new().unwrap();
with_xdg_env(&temp_dir, || {
let workspace_root = temp_dir.path().join("workspace");
fs::create_dir_all(&workspace_root).unwrap();
let test_file = workspace_root.join("test.txt");
fs::write(&test_file, "test content").unwrap();
let run_context = RunContext::new(workspace_root.clone(), None).unwrap();
run_context
.execute(&Commands::Scan { force: true })
.unwrap();
let result = run_context.execute(&Commands::Context {
command: ContextCommands::Get {
node: None,
path: Some(test_file),
agent: None,
frame_type: None,
max_frames: 10,
ordering: "invalid".to_string(),
combine: false,
separator: "\n\n---\n\n".to_string(),
format: "text".to_string(),
include_metadata: false,
include_deleted: false,
},
});
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Invalid ordering"));
});
}
#[test]
fn test_context_get_invalid_format() {
let temp_dir = TempDir::new().unwrap();
with_xdg_env(&temp_dir, || {
let workspace_root = temp_dir.path().join("workspace");
fs::create_dir_all(&workspace_root).unwrap();
let test_file = workspace_root.join("test.txt");
fs::write(&test_file, "test content").unwrap();
let run_context = RunContext::new(workspace_root.clone(), None).unwrap();
run_context
.execute(&Commands::Scan { force: true })
.unwrap();
let result = run_context.execute(&Commands::Context {
command: ContextCommands::Get {
node: None,
path: Some(test_file),
agent: None,
frame_type: None,
max_frames: 10,
ordering: "recency".to_string(),
combine: false,
separator: "\n\n---\n\n".to_string(),
format: "invalid".to_string(),
include_metadata: false,
include_deleted: false,
},
});
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Invalid format"));
});
}
#[test]
fn test_context_generate_rejects_async_flag() {
let parse_result = Cli::try_parse_from([
"meld",
"context",
"generate",
"--path",
"./foo.txt",
"--async",
]);
assert!(parse_result.is_err());
}
#[test]
fn test_context_generate_mutually_exclusive_node_path() {
let temp_dir = TempDir::new().unwrap();
with_xdg_env(&temp_dir, || {
let workspace_root = temp_dir.path().join("workspace");
fs::create_dir_all(&workspace_root).unwrap();
let _run_context = RunContext::new(workspace_root.clone(), None).unwrap();
});
}