use crate::subagents::{SubagentTask, profiles::validate_subagent_identity_id};
use serde::Deserialize;
use std::path::PathBuf;
pub(crate) const WEB_SEARCH_MAX_QUERIES: usize = 5;
pub(crate) const WEB_SEARCH_DEFAULT_RESULTS: u64 = 5;
pub(crate) const WEB_SEARCH_MAX_RESULTS: u64 = 10;
pub(crate) const WEB_SEARCH_MAX_DOMAINS: usize = 20;
pub(crate) const CODE_SEARCH_DEFAULT_MAX_TOKENS: u64 = 5_000;
pub(crate) const CODE_SEARCH_MIN_MAX_TOKENS: u64 = 1_000;
pub(crate) const CODE_SEARCH_MAX_MAX_TOKENS: u64 = 10_000;
pub(crate) const BROWSER_TIMEOUT_SECS: u64 = 60;
pub(crate) const BROWSER_STDOUT_MAX_BYTES: usize = 64 * 1024;
pub(crate) const BROWSER_STDERR_MAX_BYTES: usize = 16 * 1024;
pub(crate) const BASH_STDOUT_MAX_BYTES: usize = 64 * 1024;
pub(crate) const BASH_STDERR_MAX_BYTES: usize = 16 * 1024;
pub(crate) const FILE_READ_MAX_BYTES: u64 = 1024 * 1024;
pub(crate) const FILE_READ_MAX_LINES: usize = 2_000;
pub(crate) const FILE_READ_MAX_FILES: usize = 8;
pub(crate) const FILE_WRITE_MAX_BYTES: usize = 1024 * 1024;
pub(crate) const FILE_EDIT_TARGET_MAX_BYTES: u64 = 1024 * 1024;
pub(crate) const FILE_EDIT_TEXT_MAX_BYTES: usize = 256 * 1024;
pub(crate) const FIND_DEFAULT_LIMIT: usize = 50;
pub(crate) const FIND_MAX_LIMIT: usize = 200;
pub(crate) const REPO_MAP_DEFAULT_MAX_TOKENS: usize = 4096;
pub(crate) const REPO_MAP_MIN_MAX_TOKENS: usize = 512;
pub(crate) const REPO_MAP_MAX_MAX_TOKENS: usize = 20_000;
pub(crate) const REPO_MAP_MAX_FILES: usize = 1000;
pub(crate) const REPO_MAP_MAX_FILE_BYTES: usize = 512 * 1024;
pub(crate) const REPO_MAP_MAX_SYMBOLS_PER_FILE: usize = 200;
pub(crate) const REPO_MAP_MAX_REFS_PER_FILE: usize = 2000;
pub(crate) const AST_GREP_STDOUT_MAX_BYTES: usize = 64 * 1024;
pub(crate) const AST_GREP_STDERR_MAX_BYTES: usize = 16 * 1024;
pub(crate) const AST_GREP_TIMEOUT_SECS: u64 = 30;
pub(crate) const AST_GREP_DEFAULT_LIMIT: usize = 100;
pub(crate) const AST_GREP_MAX_LIMIT: usize = 500;
pub(crate) const DIAGNOSTICS_DEFAULT_LIMIT: usize = 50;
pub(crate) const DIAGNOSTICS_MAX_LIMIT: usize = 200;
pub(crate) const REFERENCES_DEFAULT_LIMIT: usize = 50;
pub(crate) const REFERENCES_MAX_LIMIT: usize = 100;
pub(crate) const VIEW_IMAGE_RESPONSE_MAX_BYTES: usize = 64 * 1024;
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub(crate) struct ViewImageArgs {
pub(crate) path: String,
pub(crate) prompt: String,
}
impl ViewImageArgs {
pub(crate) fn validate(mut self) -> anyhow::Result<Self> {
self.path = self.path.trim().to_string();
self.prompt = self.prompt.trim().to_string();
if self.path.is_empty() {
anyhow::bail!("path must not be empty");
}
if self.prompt.is_empty() {
anyhow::bail!("prompt must not be empty");
}
if !PathBuf::from(&self.path).is_absolute() {
anyhow::bail!("view_image path must be absolute");
}
Ok(self)
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub(crate) struct ReadArgs {
pub(crate) path: Option<String>,
pub(crate) paths: Option<Vec<String>>,
pub(crate) offset: Option<usize>,
pub(crate) limit: Option<usize>,
}
impl ReadArgs {
pub(crate) fn validate(self) -> anyhow::Result<Self> {
if self.offset == Some(0) {
anyhow::bail!("offset must be at least 1");
}
if self.limit == Some(0) {
anyhow::bail!("limit must be at least 1");
}
if self.limit.is_some_and(|limit| limit > FILE_READ_MAX_LINES) {
anyhow::bail!("limit must be at most {FILE_READ_MAX_LINES} lines");
}
match (&self.path, &self.paths) {
(Some(_), Some(_)) => anyhow::bail!("provide either path or paths, not both"),
(None, None) => anyhow::bail!("read requires paths"),
(Some(path), None) => {
if path.trim().is_empty() {
anyhow::bail!("path must not be empty");
}
}
(None, Some(paths)) => {
if paths.is_empty() {
anyhow::bail!("paths must contain at least one file");
}
if paths.len() > FILE_READ_MAX_FILES {
anyhow::bail!("paths must contain at most {FILE_READ_MAX_FILES} files");
}
if paths.iter().any(|path| path.trim().is_empty()) {
anyhow::bail!("paths must not contain empty items");
}
}
}
Ok(self)
}
pub(crate) fn is_multi_file(&self) -> bool {
self.paths.is_some()
}
pub(crate) fn requested_paths(&self) -> &[String] {
if let Some(paths) = self.paths.as_ref() {
return paths;
}
self.path.as_slice()
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub(crate) struct ListFilesArgs {
pub(crate) path: String,
#[serde(default)]
pub(crate) include_directories: bool,
}
impl ListFilesArgs {
pub(crate) fn validate(mut self) -> anyhow::Result<Self> {
self.path = self.path.trim().to_string();
if self.path.is_empty() {
anyhow::bail!("path must not be empty");
}
Ok(self)
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub(crate) struct BashArgs {
pub(crate) command: Option<String>,
pub(crate) cmd: Option<String>,
pub(crate) timeout: Option<u64>,
}
impl BashArgs {
pub(crate) fn effective_command(&self) -> Option<&str> {
self.command.as_deref().or(self.cmd.as_deref())
}
pub(crate) fn validate(&self) -> anyhow::Result<()> {
let command = self
.effective_command()
.ok_or_else(|| anyhow::anyhow!("missing command"))?;
if command.trim().is_empty() {
anyhow::bail!("command must not be empty");
}
if let Some(timeout) = self.timeout
&& !(1..=300).contains(&timeout)
{
anyhow::bail!("timeout must be between 1 and 300 seconds");
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub(crate) enum BrowserModeArg {
Headless,
Headed,
Connect,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub(crate) struct BrowserArgs {
pub(crate) command: String,
pub(crate) mode: Option<BrowserModeArg>,
#[serde(rename = "connectUrl")]
pub(crate) connect_url: Option<String>,
#[serde(rename = "ignoreHttpsErrors")]
pub(crate) ignore_https_errors: Option<bool>,
}
impl BrowserArgs {
pub(crate) fn validate(mut self) -> anyhow::Result<Self> {
self.command = self.command.trim().to_string();
if self.command.is_empty() {
anyhow::bail!("command must not be empty");
}
if let Some(connect_url) = self.connect_url.take() {
let connect_url = connect_url.trim().to_string();
if !connect_url.is_empty() {
validate_browser_connect_url(&connect_url)?;
self.connect_url = Some(connect_url);
}
}
if self.connect_url.is_some() && self.mode != Some(BrowserModeArg::Connect) {
anyhow::bail!("connectUrl requires mode \"connect\"");
}
if self.mode == Some(BrowserModeArg::Connect) && self.connect_url.is_none() {
anyhow::bail!("connectUrl is required when mode is 'connect'");
}
if self.mode == Some(BrowserModeArg::Connect) && self.ignore_https_errors.is_some() {
anyhow::bail!("ignoreHttpsErrors cannot be used with mode \"connect\"");
}
Ok(self)
}
}
fn validate_browser_connect_url(url: &str) -> anyhow::Result<()> {
if url.starts_with('-') {
anyhow::bail!("connectUrl must be an http(s) or ws(s) URL with a host");
}
if url.contains("://") {
let after_scheme = url.split_once("://").unwrap().1;
if after_scheme.starts_with('/') || after_scheme.starts_with('?') || after_scheme.is_empty()
{
anyhow::bail!("connectUrl must be an http(s) or ws(s) URL with a host");
}
}
let parsed = reqwest::Url::parse(url)
.map_err(|_| anyhow::anyhow!("connectUrl must be an http(s) or ws(s) URL with a host"))?;
if !matches!(parsed.scheme(), "http" | "https" | "ws" | "wss")
|| parsed.host_str().map(|h| h.is_empty()).unwrap_or(true)
{
anyhow::bail!("connectUrl must be an http(s) or ws(s) URL with a host");
}
Ok(())
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub(crate) struct WriteArgs {
pub(crate) path: String,
pub(crate) content: String,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub(crate) struct HashEditArgs {
pub(crate) input: String,
}
impl HashEditArgs {
pub(crate) fn validate(mut self) -> anyhow::Result<Self> {
self.input = self.input.trim().to_string();
if self.input.is_empty() {
anyhow::bail!("input must not be empty");
}
Ok(self)
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub(crate) struct GrepArgs {
pub(crate) pattern: String,
pub(crate) path: Option<String>,
pub(crate) limit: Option<usize>,
pub(crate) context: Option<usize>,
}
impl GrepArgs {
pub(crate) fn validate(self) -> anyhow::Result<Self> {
if self.limit == Some(0) {
anyhow::bail!("limit must be at least 1");
}
Ok(self)
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub(crate) struct AstGrepArgs {
pub(crate) pattern: String,
pub(crate) language: Option<String>,
pub(crate) path: Option<String>,
pub(crate) rewrite: Option<String>,
pub(crate) limit: Option<usize>,
}
impl AstGrepArgs {
pub(crate) fn validate(mut self) -> anyhow::Result<Self> {
self.pattern = self.pattern.trim().to_string();
if self.pattern.is_empty() {
anyhow::bail!("pattern must not be empty");
}
if let Some(limit) = self.limit {
if limit == 0 {
anyhow::bail!("limit must be at least 1");
}
if limit > AST_GREP_MAX_LIMIT {
anyhow::bail!("limit must be at most {AST_GREP_MAX_LIMIT}");
}
}
if let Some(language) = &self.language
&& language.trim().is_empty()
{
anyhow::bail!("language must not be empty when provided");
}
self.rewrite = self
.rewrite
.take()
.filter(|rewrite| !rewrite.trim().is_empty());
Ok(self)
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub(crate) struct DiagnosticsArgs {
pub(crate) path: Option<String>,
pub(crate) limit: Option<usize>,
}
impl DiagnosticsArgs {
pub(crate) fn validate(mut self) -> anyhow::Result<Self> {
if let Some(path) = self.path.take() {
let path = path.trim().to_string();
if path.is_empty() {
anyhow::bail!("path must not be empty when provided");
}
self.path = Some(path);
}
if self.limit == Some(0) {
anyhow::bail!("limit must be at least 1");
}
if self
.limit
.is_some_and(|limit| limit > DIAGNOSTICS_MAX_LIMIT)
{
anyhow::bail!("limit must be at most {DIAGNOSTICS_MAX_LIMIT}");
}
Ok(self)
}
pub(crate) fn limit(&self) -> usize {
self.limit.unwrap_or(DIAGNOSTICS_DEFAULT_LIMIT)
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub(crate) struct ReferencesArgs {
pub(crate) path: String,
pub(crate) line: u32,
pub(crate) column: u32,
#[serde(default)]
pub(crate) include_declaration: bool,
pub(crate) limit: Option<usize>,
}
impl ReferencesArgs {
pub(crate) fn validate(mut self) -> anyhow::Result<Self> {
self.path = self.path.trim().to_string();
if self.path.is_empty() {
anyhow::bail!("path must not be empty");
}
if self.line == 0 {
anyhow::bail!("line must be at least 1");
}
if self.column == 0 {
anyhow::bail!("column must be at least 1");
}
if self.limit == Some(0) {
anyhow::bail!("limit must be at least 1");
}
if self.limit.is_some_and(|limit| limit > REFERENCES_MAX_LIMIT) {
anyhow::bail!("limit must be at most {REFERENCES_MAX_LIMIT}");
}
Ok(self)
}
pub(crate) fn limit(&self) -> usize {
self.limit.unwrap_or(REFERENCES_DEFAULT_LIMIT)
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub(crate) struct FindArgs {
pub(crate) query: String,
pub(crate) path: Option<String>,
pub(crate) kind: Option<FindKindArg>,
pub(crate) limit: Option<usize>,
pub(crate) offset: Option<usize>,
}
impl FindArgs {
pub(crate) fn validate(mut self) -> anyhow::Result<Self> {
self.query = self.query.trim().to_string();
if self.query.is_empty() {
anyhow::bail!("query must not be empty");
}
if self.query.len() > 512 {
anyhow::bail!("query must be at most 512 characters");
}
if self.limit == Some(0) {
anyhow::bail!("limit must be at least 1");
}
if self.limit.is_some_and(|limit| limit > FIND_MAX_LIMIT) {
anyhow::bail!("limit must be at most {FIND_MAX_LIMIT}");
}
Ok(self)
}
}
#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub(crate) enum FindKindArg {
Files,
Directories,
Mixed,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub(crate) struct RepoMapArgs {
pub(crate) path: Option<String>,
pub(crate) query: Option<String>,
#[serde(rename = "maxTokens")]
pub(crate) max_tokens: Option<usize>,
}
impl RepoMapArgs {
pub(crate) fn validate(mut self) -> anyhow::Result<Self> {
if let Some(path) = self.path.take() {
let path = path.trim().to_string();
if path.is_empty() {
anyhow::bail!("path must not be empty");
}
self.path = Some(path);
}
if let Some(query) = self.query.take() {
let query = query.trim().to_string();
if !query.is_empty() {
if query.len() > 512 {
anyhow::bail!("query must be at most 512 characters");
}
self.query = Some(query);
}
}
if let Some(max_tokens) = self.max_tokens
&& !(REPO_MAP_MIN_MAX_TOKENS..=REPO_MAP_MAX_MAX_TOKENS).contains(&max_tokens)
{
anyhow::bail!(
"maxTokens must be between {REPO_MAP_MIN_MAX_TOKENS} and {REPO_MAP_MAX_MAX_TOKENS}"
);
}
Ok(self)
}
pub(crate) fn max_tokens(&self) -> usize {
self.max_tokens
.unwrap_or(REPO_MAP_DEFAULT_MAX_TOKENS)
.clamp(REPO_MAP_MIN_MAX_TOKENS, REPO_MAP_MAX_MAX_TOKENS)
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub(crate) struct SubagentsArgs {
pub(crate) tasks: Vec<SubagentTaskArgs>,
pub(crate) concurrency: Option<usize>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub(crate) struct SubagentTaskArgs {
pub(crate) intent: String,
pub(crate) agent: Option<String>,
pub(crate) identity: Option<String>,
pub(crate) context: Option<String>,
pub(crate) cwd: Option<String>,
}
impl TryFrom<SubagentsArgs> for crate::subagents::SubagentsArgs {
type Error = anyhow::Error;
fn try_from(args: SubagentsArgs) -> anyhow::Result<Self> {
let tasks = args
.tasks
.into_iter()
.map(|task| {
if let Some(identity) = task.identity.as_deref() {
validate_subagent_identity_id(identity)?;
}
Ok(SubagentTask {
intent: task.intent,
agent: task.agent,
identity: task.identity,
context: task.context,
cwd: task.cwd.map(PathBuf::from),
})
})
.collect::<anyhow::Result<Vec<_>>>()?;
Self::from_validated_parts(tasks, args.concurrency)
}
}
#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub(crate) enum RecencyFilter {
Day,
Week,
Month,
Year,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub(crate) struct WebSearchArgs {
pub(crate) queries: Vec<String>,
#[serde(rename = "numResults")]
pub(crate) num_results: Option<u64>,
#[serde(rename = "includeContent")]
pub(crate) include_content: Option<bool>,
#[serde(rename = "recencyFilter")]
pub(crate) recency_filter: Option<RecencyFilter>,
#[serde(rename = "domainFilter")]
pub(crate) domain_filter: Option<Vec<String>>,
}
impl WebSearchArgs {
pub(crate) fn validate(mut self) -> anyhow::Result<Self> {
self.queries = self
.queries
.into_iter()
.map(|query| query.trim().to_string())
.collect();
if self.queries.is_empty() {
anyhow::bail!("web_search requires at least one query");
}
if self.queries.len() > WEB_SEARCH_MAX_QUERIES {
anyhow::bail!("queries must contain at most {WEB_SEARCH_MAX_QUERIES} items");
}
for query in &self.queries {
if query.is_empty() {
anyhow::bail!("queries must not contain empty items");
}
if query.chars().count() > 512 {
anyhow::bail!("each query must be at most 512 characters");
}
}
if let Some(num_results) = self.num_results
&& !(1..=WEB_SEARCH_MAX_RESULTS).contains(&num_results)
{
anyhow::bail!("numResults must be between 1 and {WEB_SEARCH_MAX_RESULTS}");
}
if let Some(domains) = self.domain_filter.take() {
if domains.len() > WEB_SEARCH_MAX_DOMAINS {
anyhow::bail!("domainFilter must contain at most {WEB_SEARCH_MAX_DOMAINS} domains");
}
let domains = domains
.into_iter()
.map(|domain| validate_domain_filter(domain.trim()))
.collect::<anyhow::Result<Vec<_>>>()?;
self.domain_filter = Some(domains);
}
Ok(self)
}
pub(crate) fn normalized_queries(&self) -> Vec<String> {
self.queries.clone()
}
pub(crate) fn num_results(&self) -> u64 {
self.num_results.unwrap_or(WEB_SEARCH_DEFAULT_RESULTS)
}
pub(crate) fn include_content(&self) -> bool {
self.include_content.unwrap_or(false)
}
}
fn validate_domain_filter(domain: &str) -> anyhow::Result<String> {
let domain = domain.trim();
let Some(domain_without_prefix) = domain.strip_prefix('-') else {
return validate_domain_name(domain).map(|()| domain.to_string());
};
if domain_without_prefix.starts_with('-') {
anyhow::bail!("domainFilter exclusions may use only one leading '-'");
}
validate_domain_name(domain_without_prefix)?;
Ok(format!("-{domain_without_prefix}"))
}
fn validate_domain_name(domain: &str) -> anyhow::Result<()> {
if domain.is_empty() {
anyhow::bail!("domainFilter entries must not be empty");
}
if domain.len() > 253 {
anyhow::bail!("domainFilter entries must be at most 253 characters");
}
if domain.contains("://")
|| domain.contains('/')
|| domain.contains('\\')
|| domain.contains(':')
|| domain.chars().any(char::is_whitespace)
{
anyhow::bail!(
"domainFilter entries must be bare domains without schemes, ports, paths, or whitespace"
);
}
if domain.starts_with('.') || domain.ends_with('.') || domain.contains("..") {
anyhow::bail!("domainFilter entries must not contain empty labels");
}
for label in domain.split('.') {
if label.is_empty() {
anyhow::bail!("domainFilter entries must not contain empty labels");
}
if label.len() > 63 {
anyhow::bail!("domainFilter labels must be at most 63 characters");
}
if label.starts_with('-') || label.ends_with('-') {
anyhow::bail!("domainFilter labels must not start or end with '-'");
}
if !label
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || byte == b'-')
{
anyhow::bail!(
"domainFilter entries must contain only ASCII letters, digits, dots, and hyphens"
);
}
}
Ok(())
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub(crate) struct CodeSearchArgs {
pub(crate) query: String,
#[serde(rename = "maxTokens")]
pub(crate) max_tokens: Option<u64>,
}
impl CodeSearchArgs {
pub(crate) fn validate(mut self) -> anyhow::Result<Self> {
self.query = self.query.trim().to_string();
if self.query.is_empty() {
anyhow::bail!("query must not be empty");
}
if self.query.len() > 512 {
anyhow::bail!("query must be at most 512 characters");
}
if let Some(max_tokens) = self.max_tokens
&& !(CODE_SEARCH_MIN_MAX_TOKENS..=CODE_SEARCH_MAX_MAX_TOKENS).contains(&max_tokens)
{
anyhow::bail!(
"maxTokens must be between {CODE_SEARCH_MIN_MAX_TOKENS} and {CODE_SEARCH_MAX_MAX_TOKENS}"
);
}
Ok(self)
}
pub(crate) fn max_tokens(&self) -> u64 {
self.max_tokens.unwrap_or(CODE_SEARCH_DEFAULT_MAX_TOKENS)
}
}
#[cfg(test)]
mod lsp_arg_tests {
use super::*;
use serde_json::json;
#[test]
fn diagnostics_args_validate_path_and_limit() {
let args =
serde_json::from_value::<DiagnosticsArgs>(json!({"path":" src/lib.rs ","limit":50}))
.unwrap()
.validate()
.unwrap();
assert_eq!(args.path.as_deref(), Some("src/lib.rs"));
assert_eq!(args.limit(), 50);
assert!(
serde_json::from_value::<DiagnosticsArgs>(json!({"path":" "}))
.unwrap()
.validate()
.is_err()
);
assert!(
serde_json::from_value::<DiagnosticsArgs>(json!({"limit":0}))
.unwrap()
.validate()
.is_err()
);
assert!(
serde_json::from_value::<DiagnosticsArgs>(json!({"limit":DIAGNOSTICS_MAX_LIMIT + 1}))
.unwrap()
.validate()
.is_err()
);
}
#[test]
fn references_args_validate_position_path_and_limit() {
let args = serde_json::from_value::<ReferencesArgs>(
json!({"path":" lib.rs ","line":1,"column":2}),
)
.unwrap()
.validate()
.unwrap();
assert_eq!(args.path, "lib.rs");
assert_eq!(args.limit(), REFERENCES_DEFAULT_LIMIT);
assert!(!args.include_declaration);
assert!(
serde_json::from_value::<ReferencesArgs>(json!({"path":"","line":1,"column":1}))
.unwrap()
.validate()
.is_err()
);
assert!(
serde_json::from_value::<ReferencesArgs>(json!({"path":"lib.rs","line":0,"column":1}))
.unwrap()
.validate()
.is_err()
);
assert!(
serde_json::from_value::<ReferencesArgs>(json!({"path":"lib.rs","line":1,"column":0}))
.unwrap()
.validate()
.is_err()
);
assert_eq!(REFERENCES_MAX_LIMIT, crate::lsp::MAX_REFERENCES);
let max = serde_json::from_value::<ReferencesArgs>(
json!({"path":"lib.rs","line":1,"column":1,"limit":REFERENCES_MAX_LIMIT}),
)
.unwrap()
.validate()
.unwrap();
assert_eq!(max.limit(), 100);
assert!(
serde_json::from_value::<ReferencesArgs>(
json!({"path":"lib.rs","line":1,"column":1,"limit":REFERENCES_MAX_LIMIT + 1})
)
.unwrap()
.validate()
.is_err()
);
}
#[test]
fn web_search_requires_queries_array_and_trims_items() {
let args = serde_json::from_value::<WebSearchArgs>(json!({"queries":[" rust ", "cargo"]}))
.unwrap()
.validate()
.unwrap();
assert_eq!(args.normalized_queries(), vec!["rust", "cargo"]);
assert!(serde_json::from_value::<WebSearchArgs>(json!({"query":"rust"})).is_err());
assert!(serde_json::from_value::<WebSearchArgs>(json!({})).is_err());
assert!(
serde_json::from_value::<WebSearchArgs>(json!({"queries":[]}))
.unwrap()
.validate()
.is_err()
);
assert!(
serde_json::from_value::<WebSearchArgs>(json!({"queries":[" "]}))
.unwrap()
.validate()
.is_err()
);
let max_ascii =
serde_json::from_value::<WebSearchArgs>(json!({"queries":["a".repeat(512)]}))
.unwrap()
.validate()
.unwrap();
assert_eq!(max_ascii.queries[0].chars().count(), 512);
assert!(
serde_json::from_value::<WebSearchArgs>(json!({"queries":["a".repeat(513)]}))
.unwrap()
.validate()
.is_err()
);
let max_unicode =
serde_json::from_value::<WebSearchArgs>(json!({"queries":["é".repeat(512)]}))
.unwrap()
.validate()
.unwrap();
assert_eq!(max_unicode.queries[0].chars().count(), 512);
assert!(
serde_json::from_value::<WebSearchArgs>(json!({"queries":["é".repeat(513)]}))
.unwrap()
.validate()
.is_err()
);
}
}