Skip to main content

vtcode_utility_tool_specs/
lib.rs

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