1#![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 = ¶ms["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}