Skip to main content

vtcode_utility_tool_specs/
lib.rs

1#![cfg_attr(test, allow(missing_docs))]
2//! Passive JSON schemas for utility, file, scheduling, and collaboration tool surfaces.
3
4#![recursion_limit = "256"]
5
6use serde_json::{Value, json};
7
8mod collaboration;
9mod json_schema;
10#[cfg(feature = "mcp")]
11mod mcp_tool;
12mod responses_api;
13
14pub use collaboration::{
15    close_agent_parameters, request_user_input_description, request_user_input_parameters,
16    resume_agent_parameters, send_input_parameters, spawn_agent_parameters,
17    spawn_background_subprocess_parameters, wait_agent_parameters,
18};
19pub use json_schema::{AdditionalProperties, JsonSchema, parse_tool_input_schema};
20#[cfg(feature = "mcp")]
21pub use mcp_tool::{ParsedMcpTool, parse_mcp_tool};
22pub use responses_api::{FreeformTool, FreeformToolFormat, ResponsesApiTool};
23
24pub const SEMANTIC_ANCHOR_GUIDANCE: &str =
25    "Prefer stable semantic @@ anchors such as function, class, method, or impl names.";
26pub const APPLY_PATCH_ALIAS_DESCRIPTION: &str = "Alias for input";
27pub const DEFAULT_APPLY_PATCH_INPUT_DESCRIPTION: &str = "Patch in VT Code format: *** Begin Patch, *** Update File: path, @@ hunk, -/+ lines, *** End Patch";
28
29#[must_use]
30pub fn with_semantic_anchor_guidance(base: &str) -> String {
31    let trimmed = base.trim_end();
32    if trimmed.contains(SEMANTIC_ANCHOR_GUIDANCE) {
33        trimmed.to_string()
34    } else if trimmed.ends_with('.') {
35        format!("{trimmed} {SEMANTIC_ANCHOR_GUIDANCE}")
36    } else {
37        format!("{trimmed}. {SEMANTIC_ANCHOR_GUIDANCE}")
38    }
39}
40
41#[must_use]
42pub fn apply_patch_parameter_schema(input_description: &str) -> Value {
43    json!({
44        "type": "object",
45        "properties": {
46            "input": {
47                "type": "string",
48                "description": with_semantic_anchor_guidance(input_description)
49            },
50            "patch": {
51                "type": "string",
52                "description": APPLY_PATCH_ALIAS_DESCRIPTION
53            }
54        },
55        "anyOf": [
56            {"required": ["input"]},
57            {"required": ["patch"]}
58        ]
59    })
60}
61
62#[must_use]
63pub fn apply_patch_parameters() -> Value {
64    apply_patch_parameter_schema(DEFAULT_APPLY_PATCH_INPUT_DESCRIPTION)
65}
66
67#[must_use]
68pub fn cron_create_parameters() -> Value {
69    json!({
70        "type": "object",
71        "required": ["prompt"],
72        "additionalProperties": false,
73        "properties": {
74            "prompt": {"type": "string", "description": "Prompt to run when the task fires."},
75            "name": {"type": "string", "description": "Optional short label for the task."},
76            "cron": {"type": "string", "description": "Five-field cron expression for recurring tasks."},
77            "delay_minutes": {"type": "integer", "description": "Fixed recurring interval in minutes."},
78            "run_at": {
79                "type": "string",
80                "description": "One-shot fire time in RFC3339 or local datetime form. Use this instead of `cron` or `delay_minutes` for reminders."
81            }
82        }
83    })
84}
85
86#[must_use]
87pub fn cron_list_parameters() -> Value {
88    json!({
89        "type": "object",
90        "properties": {},
91        "additionalProperties": false
92    })
93}
94
95#[must_use]
96pub fn cron_delete_parameters() -> Value {
97    json!({
98        "type": "object",
99        "required": ["id"],
100        "properties": {
101            "id": {"type": "string", "description": "Session scheduled task id to delete."}
102        }
103    })
104}
105
106#[must_use]
107pub fn unified_exec_parameters() -> Value {
108    json!({
109        "type": "object",
110        "properties": {
111            "command": {
112                "description": "Command as a shell string or argv array.",
113                "anyOf": [
114                    {"type": "string"},
115                    {
116                        "type": "array",
117                        "items": {"type": "string"}
118                    }
119                ]
120            },
121            "input": {"type": "string", "description": "stdin for write or continue."},
122            "session_id": {"type": "string", "description": "Session id. Compact alias: `s`."},
123            "spool_path": {"type": "string", "description": "Spool path for inspect."},
124            "query": {"type": "string", "description": "Line filter for inspect or run output."},
125            "head_lines": {"type": "integer", "description": "Head preview lines."},
126            "tail_lines": {"type": "integer", "description": "Tail preview lines."},
127            "max_matches": {"type": "integer", "description": "Max filtered matches.", "default": 200},
128            "literal": {"type": "boolean", "description": "Treat query as literal text.", "default": false},
129            "code": {"type": "string", "description": "Raw Python or JavaScript source for `action=code`. Send the source directly, not JSON or markdown fences."},
130            "language": {
131                "type": "string",
132                "enum": ["python3", "javascript"],
133                "description": "Language for `action=code`. Defaults to `python3`; set `javascript` to run Node-based code execution instead.",
134                "default": "python3"
135            },
136            "action": {
137                "type": "string",
138                "enum": ["run", "write", "poll", "continue", "inspect", "list", "close", "code"],
139                "description": "Optional; inferred from command/code/input/session_id/spool_path. Use `code` to run a fresh Python or JavaScript snippet through the local code executor."
140            },
141            "workdir": {"type": "string", "description": "Working directory. Alias: cwd."},
142            "tty": {"type": "boolean", "description": "Use PTY mode.", "default": false},
143            "shell": {"type": "string", "description": "Shell binary."},
144            "login": {"type": "boolean", "description": "Use a login shell.", "default": false},
145            "sandbox_permissions": {
146                "type": "string",
147                "enum": ["use_default", "with_additional_permissions", "require_escalated"],
148                "description": "Sandbox policy. Use `require_escalated` only when needed."
149            },
150            "additional_permissions": {
151                "type": "object",
152                "description": "Extra sandboxed filesystem access.",
153                "properties": {
154                    "fs_read": {
155                        "type": "array",
156                        "items": {"type": "string"},
157                        "description": "Extra readable paths."
158                    },
159                    "fs_write": {
160                        "type": "array",
161                        "items": {"type": "string"},
162                        "description": "Extra writable paths."
163                    }
164                },
165                "additionalProperties": false
166            },
167            "justification": {"type": "string", "description": "Approval question for `require_escalated`."},
168            "prefix_rule": {
169                "type": "array",
170                "items": {"type": "string"},
171                "description": "Optional persisted approval prefix for `command`."
172            },
173            "timeout_secs": {"type": "integer", "description": "Timeout seconds.", "default": 180},
174            "yield_time_ms": {"type": "integer", "description": "Wait before returning output (ms).", "default": 1000},
175            "confirm": {"type": "boolean", "description": "Confirm destructive ops.", "default": false},
176            "max_output_tokens": {"type": "integer", "description": "Output token cap."},
177            "track_files": {"type": "boolean", "description": "Track file changes.", "default": false}
178        }
179    })
180}
181
182#[must_use]
183pub fn unified_file_parameters() -> Value {
184    json!({
185        "type": "object",
186        "properties": {
187            "action": {
188                "type": "string",
189                "enum": ["read", "write", "edit", "patch", "delete", "move", "copy"],
190                "description": "Optional; inferred from old_str/patch/content/destination/path."
191            },
192            "path": {"type": "string", "description": "File path. Accepts file_path/filepath/target_path/file/p."},
193            "content": {"type": "string", "description": "Content for write."},
194            "old_str": {"type": "string", "description": "Exact text to replace for edit. Max 800 chars or 40 lines; use patch for larger edits."},
195            "new_str": {"type": "string", "description": "Replacement text for edit. Max 800 chars or 40 lines; use patch for larger edits."},
196            "patch": {"type": "string", "description": "Patch text in `*** Update File:` format, not unified diff."},
197            "destination": {"type": "string", "description": "Destination for move or copy."},
198            "offset": {"type": "integer", "description": "Read start line (1-indexed). Compact alias: `o`."},
199            "limit": {"type": "integer", "description": "Read line count. Compact alias: `l`."},
200            "mode": {"type": "string", "description": "Read mode or write mode.", "default": "slice"},
201            "condense": {"type": "boolean", "description": "Condense long output to head/tail. Set false for full content.", "default": true},
202            "raw": {"type": "boolean", "description": "Bypass output spooling and return full content inline. Use when you need exact file content without spooling to disk.", "default": false},
203            "indentation": {
204                "description": "Indentation config. `true` uses defaults.",
205                "anyOf": [
206                    {"type": "boolean"},
207                    {
208                        "type": "object",
209                        "properties": {
210                            "anchor_line": {"type": "integer", "description": "Anchor line; defaults to offset."},
211                            "max_levels": {"type": "integer", "description": "Indent depth cap; 0 means unlimited."},
212                            "include_siblings": {"type": "boolean", "description": "Include sibling blocks."},
213                            "include_header": {"type": "boolean", "description": "Include header lines."},
214                            "max_lines": {"type": "integer", "description": "Optional line cap."}
215                        },
216                        "additionalProperties": false
217                    }
218                ]
219            },
220            "offset_bytes": {"type": "integer", "description": "Byte offset (0-indexed) for byte-range reads. Enables chunked preview of large files.", "minimum": 0},
221            "page_size_bytes": {"type": "integer", "description": "Bytes to read. Accepts alias `length`. Default: 8192.", "minimum": 1}
222        }
223    })
224}
225
226#[must_use]
227pub fn read_file_parameters() -> Value {
228    json!({
229        "type": "object",
230        "properties": {
231            "path": {"type": "string", "description": "File path. Accepts file_path/filepath/target_path/file/p."},
232            "offset": {"type": "integer", "description": "1-indexed line offset. Compact alias: `o`.", "minimum": 1},
233            "limit": {"type": "integer", "description": "Max lines for this chunk. Compact alias: `l`.", "minimum": 1},
234            "mode": {"type": "string", "enum": ["slice", "indentation"], "description": "Read mode.", "default": "slice"},
235            "indentation": {
236                "description": "Indentation-aware block selection.",
237                "anyOf": [
238                    {"type": "boolean"},
239                    {
240                        "type": "object",
241                        "properties": {
242                            "anchor_line": {"type": "integer", "description": "Anchor line; defaults to offset."},
243                            "max_levels": {"type": "integer", "description": "Indent depth cap; 0 means unlimited."},
244                            "include_siblings": {"type": "boolean", "description": "Include sibling blocks."},
245                            "include_header": {"type": "boolean", "description": "Include header lines."},
246                            "max_lines": {"type": "integer", "description": "Optional line cap."}
247                        },
248                        "additionalProperties": false
249                    }
250                ]
251            },
252            "offset_lines": {"type": "integer", "description": "Legacy alias for line offset.", "minimum": 1},
253            "page_size_lines": {"type": "integer", "description": "Legacy alias for line chunk size.", "minimum": 1},
254            "offset_bytes": {"type": "integer", "description": "Byte offset for binary or byte-paged reads.", "minimum": 0},
255            "page_size_bytes": {"type": "integer", "description": "Byte page size for binary or byte-paged reads.", "minimum": 1},
256            "max_bytes": {"type": "integer", "description": "Maximum bytes to return.", "minimum": 1},
257            "max_lines": {"type": "integer", "description": "Maximum lines to return in legacy mode.", "minimum": 1},
258            "chunk_lines": {"type": "integer", "description": "Legacy alias for chunk size in lines.", "minimum": 1},
259            "max_tokens": {"type": "integer", "description": "Optional token budget for large reads.", "minimum": 1},
260            "condense": {"type": "boolean", "description": "Condense long outputs to head/tail. Set false for full content.", "default": true}
261        }
262    })
263}
264
265#[must_use]
266pub fn unified_search_parameters() -> Value {
267    json!({
268        "type": "object",
269        "properties": {
270            "action": {
271                "type": "string",
272                "enum": ["grep", "list", "structural", "outline", "tools", "errors", "agent", "web", "skill"],
273                "description": "Search action: grep (text), list (files), structural (ast-grep pattern search), outline (ast-grep symbol map of a file/directory, no pattern needed), tools, errors, agent, web, skill. For 'web', provide 'query' to search the web, or 'url' to fetch a specific page."
274            },
275            "workflow": {
276                "type": "string",
277                "enum": ["query", "scan", "test", "rewrite", "new", "apply"],
278                "description": "Structural workflow: query (search), scan (config rules), test (rule tests), rewrite (preview), apply (write), new (scaffold).",
279                "default": "query"
280            },
281            "pattern": {"type": "string", "description": "For grep: regex/literal. For list: glob filter. For structural: ast-grep pattern ($VAR=node, $$$ARGS=many). At least one of pattern or kind required for structural query."},
282            "kind": {"type": "string", "description": "Ast-grep node kind (e.g. function_item, call_expression). Supports >, +, ~, :has(), :not() selectors. Use alone or with pattern."},
283            "path": {"type": "string", "description": "Directory or file path to search in. Used by `grep`, `list`, structural `workflow=\"query\"|\"scan\"`, and `outline`. Public structural calls take one root per request even though raw ast-grep `run` can accept multiple paths.", "default": "."},
284            "config_path": {"type": "string", "description": "Ast-grep config path for structural `workflow=\"scan\"` or `workflow=\"test\"`. Defaults to workspace `sgconfig.yml`."},
285            "filter": {"type": "string", "description": "Ast-grep rule or test filter for structural `workflow=\"scan\"` or `workflow=\"test\"`. On `scan`, this maps to `--filter` over rule ids from config."},
286            "lang": {"type": "string", "description": "Language for structural `workflow=\"query\"` or `workflow=\"rewrite\"`, and for `outline`. Set it whenever the code language is known; required for debug_query and recommended for rewrite."},
287            "selector": {"type": "string", "description": "Ast-grep selector when match is a subnode. Supports :has(), :not(), :is(), :nth-child()."},
288            "strictness": {
289                "type": "string",
290                "enum": ["cst", "smart", "ast", "relaxed", "signature", "template"],
291                "description": "Pattern strictness for structural `workflow=\"query\"`."
292            },
293            "view": {
294                "type": "string",
295                "enum": ["digest", "names", "full"],
296                "description": "Output shape for `outline`: digest (symbols grouped by kind, default for single-file queries), names (flat name groups, default for directory queries), full (per-symbol records with ranges/signatures/members). Directory queries also receive a top-level `summary` block with file/symbol counts and a flat `all_symbols` array.",
297                "default": "digest"
298            },
299            "items": {
300                "type": "string",
301                "enum": ["auto", "structure", "exports", "imports", "all"],
302                "description": "Which top-level symbols `outline` includes. `auto` (default) uses structure for file input and exports for directory input.",
303                "default": "auto"
304            },
305            "type": {
306                "description": "Comma-separated symbol types to keep in `outline` (e.g. \"function\", [\"class\",\"enum\"]).",
307                "anyOf": [
308                    {"type": "string"},
309                    {"type": "array", "items": {"type": "string"}}
310                ]
311            },
312            "match": {"type": "string", "description": "Regex for `outline` to filter item names/signatures/first lines."},
313            "pub_members": {"type": "boolean", "description": "In `outline`, show only public members.", "default": false},
314            "follow": {"type": "boolean", "description": "Follow symbolic links while traversing directories. Used by `outline` and structural workflows.", "default": false},
315            "debug_query": {
316                "type": "string",
317                "enum": ["pattern", "ast", "cst", "sexp"],
318                "description": "Print the structural query AST instead of matches for `workflow=\"query\"`. Requires lang."
319            },
320            "globs": {
321                "description": "Optional include/exclude globs for structural `workflow=\"query\"` or `workflow=\"scan\"`. Maps to repeated ast-grep `--globs` flags.",
322                "anyOf": [
323                    {"type": "string"},
324                    {"type": "array", "items": {"type": "string"}}
325                ]
326            },
327            "skip_snapshot_tests": {"type": "boolean", "description": "Skip ast-grep snapshot tests for structural `workflow=\"test\"`.", "default": false},
328            "rewrite": {"type": "string", "description": "Replacement string for structural `workflow=\"rewrite\"`. Meta variables from `pattern` can be referenced (e.g. `$VAR`, `$$$ARGS`). For simple pattern-to-pattern rewrites. Either `rewrite` or `fix_config` is required for `workflow=\"rewrite\"`."},
329            "fix_config": {
330                "type": "object",
331                "description": "Advanced fix configuration for structural `workflow=\"rewrite\"`. Use when replacing only the matched node is not enough, especially for deleting list items or key-value pairs that also need a surrounding comma removed. Either `rewrite` or `fix_config` is required for `workflow=\"rewrite\"`.",
332                "properties": {
333                    "template": {"type": "string", "description": "Replacement template string. Meta variables from `pattern` can be referenced."},
334                    "expand_start": {
335                        "type": "object",
336                        "description": "Expand fix range start backwards. Requires at least one of: regex, kind, pattern.",
337                        "properties": {
338                            "regex": {"type": "string", "description": "Regex to match node text."},
339                            "kind": {"type": "string", "description": "Tree-sitter node kind."},
340                            "pattern": {"type": "string", "description": "Ast-grep pattern."},
341                            "stop_by": {"description": "Expansion stop rule. String (\"line\"|\"end\") or rule object."}
342                        }
343                    },
344                    "expand_end": {
345                        "type": "object",
346                        "description": "Expand fix range end forwards. Requires at least one of: regex, kind, pattern.",
347                        "properties": {
348                            "regex": {"type": "string", "description": "Regex to match node text."},
349                            "kind": {"type": "string", "description": "Tree-sitter node kind."},
350                            "pattern": {"type": "string", "description": "Ast-grep pattern."},
351                            "stop_by": {"description": "Expansion stop rule. String (\"line\"|\"end\") or rule object."}
352                        }
353                    }
354                },
355                "required": ["template"]
356            },
357            "new_subcommand": {"type": "string", "enum": ["project", "rule", "test", "util"], "description": "Subcommand for structural `workflow=\"new\"`. `project` scaffolds sgconfig.yml and directories; `rule` creates a new rule YAML; `test` creates a new test YAML; `util` creates a new utility rule."},
358            "new_name": {"type": "string", "description": "Name for the new rule, test, or utility. Required for `new` subcommands `rule`, `test`, and `util`."},
359            "keyword": {"type": "string", "description": "Keyword for 'tools' search."},
360            "url": {"type": "string", "format": "uri", "description": "URL to fetch content from (for 'web' action). Mutually exclusive with 'query'."},
361            "query": {"type": "string", "description": "Search query for 'web' action. Uses keyless DuckDuckGo. Returns ranked results (title, url, snippet). Mutually exclusive with 'url'."},
362            "prompt": {"type": "string", "description": "The prompt to run on the fetched content (for 'web' action with 'url')."},
363            "name": {"type": "string", "description": "Skill name to load (for 'skill' action)."},
364            "detail_level": {
365                "type": "string",
366                "enum": ["name-only", "name-and-description", "full"],
367                "description": "Detail level for 'tools' action.",
368                "default": "name-and-description"
369            },
370            "mode": {
371                "type": "string",
372                "description": "Mode for 'list' (list|recursive|tree|etc) or 'agent' (debug|analyze|full) action.",
373                "default": "list"
374            },
375            "max_results": {"type": "integer", "description": "Max results to return.", "default": 100},
376            "case_sensitive": {"type": "boolean", "description": "Case-sensitive search.", "default": false},
377            "context_lines": {"type": "integer", "description": "Context lines for `grep` or structural `workflow=\"query\"|\"scan\"` results. Structural maps this to ast-grep `--context`; raw `--before` and `--after` are not exposed separately.", "default": 0},
378            "severities": {
379                "type": "array",
380                "items": {"type": "string", "enum": ["error", "warning", "info", "hint"]},
381                "description": "Post-run severity filter for structural `workflow=\"scan\"`. When present, only findings matching one of the listed severities are returned. Does not override rule severities at the CLI level."
382            },
383            "no_ignore": {
384                "type": "array",
385                "items": {"type": "string", "enum": ["hidden", "dot", "exclude", "global", "parent", "vcs"]},
386                "description": "Ignore file overrides: hidden, dot, exclude, global, parent, vcs."
387            },
388            "threads": {"type": "integer", "description": "Number of threads for ast-grep scan parallelism. 0 means auto. Only for `workflow=\"scan\"`.", "minimum": 0, "maximum": 256, "default": 0},
389            "format": {"type": "string", "enum": ["github", "sarif", "files_with_matches", "count"], "description": "Output format for structural `workflow=\"scan\"`. `github`/`sarif`: CI pipeline formats (raw output). `files_with_matches`: return only unique file paths. `count`: return match counts per file."},
390            "report_style": {"type": "string", "enum": ["rich", "medium", "short"], "description": "Diagnostic report style for structural `workflow=\"scan\"`. Controls verbosity of diagnostic output."},
391            "before_lines": {"type": "integer", "description": "Context lines before each match for structural workflows. Mutually exclusive with `context_lines`.", "minimum": 0, "maximum": 20},
392            "after_lines": {"type": "integer", "description": "Context lines after each match for structural workflows. Mutually exclusive with `context_lines`.", "minimum": 0, "maximum": 20},
393            "builtin_rules": {
394                "type": "array",
395                "items": {"type": "string"},
396                "description": "Built-in ast-grep rules to activate for `workflow=\"scan\"`. Valid values: `unused-suppression` (reports stale ignore directives), `no-suppress-all` (reports suppress-all comments). Use `\"rule:severity\"` format to set severity (e.g. `\"unused-suppression:error\"`). Default severity is hint."
397            },
398            "scope": {"type": "string", "description": "Scope for 'errors' action (archive|all).", "default": "archive"},
399            "max_bytes": {"type": "integer", "description": "Maximum bytes to fetch for 'web' action.", "default": 500000},
400            "timeout_secs": {"type": "integer", "description": "Timeout in seconds.", "default": 30}
401        }
402    })
403}
404
405#[must_use]
406pub fn list_files_parameters() -> Value {
407    json!({
408        "type": "object",
409        "properties": {
410            "path": {"type": "string", "description": "Directory or file path to inspect.", "default": "."},
411            "mode": {
412                "type": "string",
413                "enum": ["list", "recursive", "tree", "find_name", "find_content", "largest", "file", "files"],
414                "description": "Listing mode. Use page/per_page to continue paginated results.",
415                "default": "list"
416            },
417            "pattern": {"type": "string", "description": "Optional glob-style path filter."},
418            "name_pattern": {"type": "string", "description": "Optional name filter for list/find_name modes."},
419            "content_pattern": {"type": "string", "description": "Content query for find_content mode."},
420            "page": {"type": "integer", "description": "1-indexed results page.", "minimum": 1},
421            "per_page": {"type": "integer", "description": "Items per page.", "minimum": 1},
422            "max_results": {"type": "integer", "description": "Maximum total results to consider before pagination.", "minimum": 1},
423            "include_hidden": {"type": "boolean", "description": "Include dotfiles and hidden entries.", "default": false},
424            "response_format": {"type": "string", "enum": ["concise", "detailed"], "description": "Verbosity of the listing output.", "default": "concise"},
425            "case_sensitive": {"type": "boolean", "description": "Case-sensitive name matching.", "default": false}
426        }
427    })
428}
429
430#[cfg(test)]
431mod tests {
432    use super::*;
433    use serde_json::json;
434
435    #[test]
436    fn apply_patch_parameter_schema_keeps_alias_and_guidance_consistent() {
437        let schema = apply_patch_parameter_schema("Patch in VT Code format");
438
439        assert_eq!(
440            schema["properties"]["patch"]["description"],
441            APPLY_PATCH_ALIAS_DESCRIPTION
442        );
443        let input_description = schema["properties"]["input"]["description"]
444            .as_str()
445            .expect("input description");
446        assert!(input_description.contains(SEMANTIC_ANCHOR_GUIDANCE));
447    }
448
449    #[test]
450    fn unified_exec_schema_accepts_string_or_array_commands() {
451        let params = unified_exec_parameters();
452        let command = &params["properties"]["command"];
453        let variants = command["anyOf"].as_array().expect("command anyOf");
454
455        assert_eq!(variants.len(), 2);
456        assert_eq!(variants[0]["type"], "string");
457        assert_eq!(variants[1]["type"], "array");
458        assert_eq!(variants[1]["items"]["type"], "string");
459        assert_eq!(params["properties"]["tty"]["type"], "boolean");
460        assert_eq!(params["properties"]["tty"]["default"], false);
461        assert!(
462            params["properties"]["code"]["description"]
463                .as_str()
464                .expect("code description")
465                .contains("Raw Python or JavaScript source")
466        );
467        assert!(
468            params["properties"]["language"]["description"]
469                .as_str()
470                .expect("language description")
471                .contains("set `javascript`")
472        );
473    }
474
475    #[test]
476    fn unified_search_schema_advertises_structural_and_hides_intelligence() {
477        let params = unified_search_parameters();
478        let actions = params["properties"]["action"]["enum"]
479            .as_array()
480            .expect("action enum");
481
482        assert!(actions.iter().any(|value| value == "structural"));
483        assert!(actions.iter().any(|value| value == "outline"));
484        assert!(!actions.iter().any(|value| value == "intelligence"));
485        assert!(
486            params["properties"]["view"]["enum"]
487                .as_array()
488                .expect("view enum")
489                .iter()
490                .any(|value| value == "digest")
491        );
492        assert!(
493            params["properties"]["items"]["enum"]
494                .as_array()
495                .expect("items enum")
496                .iter()
497                .any(|value| value == "auto")
498        );
499        assert!(
500            params["properties"]["debug_query"]["enum"]
501                .as_array()
502                .expect("debug_query enum")
503                .iter()
504                .any(|value| value == "ast")
505        );
506        assert!(
507            params["properties"]["action"]["description"]
508                .as_str()
509                .expect("action description")
510                .contains("structural")
511        );
512        assert!(
513            params["properties"]["pattern"]["description"]
514                .as_str()
515                .expect("pattern description")
516                .contains("ast-grep pattern")
517        );
518        assert!(
519            params["properties"]["pattern"]["description"]
520                .as_str()
521                .expect("pattern description")
522                .contains("$$$ARGS")
523        );
524        assert!(
525            params["properties"]["pattern"]["description"]
526                .as_str()
527                .expect("pattern description")
528                .contains("glob filter")
529        );
530        assert!(
531            params["properties"]["action"]["description"]
532                .as_str()
533                .expect("action description")
534                .contains("grep")
535        );
536        assert_eq!(params["properties"]["workflow"]["enum"][1], "scan");
537        assert_eq!(params["properties"]["workflow"]["enum"][2], "test");
538        assert!(
539            params["properties"]["config_path"]["description"]
540                .as_str()
541                .expect("config path description")
542                .contains("Defaults to workspace `sgconfig.yml`")
543        );
544        assert!(
545            params["properties"]["skip_snapshot_tests"]["description"]
546                .as_str()
547                .expect("skip snapshot description")
548                .contains("workflow=\"test\"")
549        );
550    }
551
552    #[test]
553    fn legacy_browse_tool_schemas_expose_chunking_and_pagination_fields() {
554        let read_params = read_file_parameters();
555        assert!(read_params["properties"]["offset"].is_object());
556        assert!(read_params["properties"]["limit"].is_object());
557        assert!(read_params["properties"]["page_size_lines"].is_object());
558
559        let list_params = list_files_parameters();
560        assert!(list_params["properties"]["page"].is_object());
561        assert!(list_params["properties"]["per_page"].is_object());
562        assert!(
563            list_params["properties"]["mode"]["enum"]
564                .as_array()
565                .expect("mode enum")
566                .iter()
567                .any(|value| value == "recursive")
568        );
569    }
570
571    #[test]
572    fn semantic_anchor_guidance_is_appended_once() {
573        let base = "Patch in VT Code format.";
574        let with_guidance = with_semantic_anchor_guidance(base);
575
576        assert!(with_guidance.contains(SEMANTIC_ANCHOR_GUIDANCE));
577        assert_eq!(with_semantic_anchor_guidance(&with_guidance), with_guidance);
578    }
579
580    #[test]
581    fn default_apply_patch_parameters_keep_expected_alias_shape() {
582        let schema = apply_patch_parameters();
583
584        assert_eq!(
585            schema["anyOf"],
586            json!([
587                {"required": ["input"]},
588                {"required": ["patch"]}
589            ])
590        );
591    }
592}