1use serde_json::{json, Value};
4
5use crate::envelope::print_success_json;
6use crate::error::{CliError, ErrorKind};
7
8pub const COMMANDS: &[&str] = &[
10 "doctor",
11 "commands",
12 "schema",
13 "version",
14 "goto",
15 "view",
16 "press",
17 "click-at",
18 "write",
19 "keys",
20 "type",
21 "wait",
22 "hover",
23 "drag",
24 "fill-form",
25 "upload",
26 "back",
27 "forward",
28 "reload",
29 "eval",
30 "grab",
31 "print-pdf",
32 "monitor",
33 "run",
34 "exec",
35 "extract",
36 "text",
37 "scroll",
38 "cookie",
39 "attr",
40 "assert",
41 "console",
42 "net",
43 "page",
44 "dialog",
45 "scrape",
46 "batch-scrape",
47 "crawl",
48 "map",
49 "search",
50 "parse",
51 "qr",
52 "find-paths",
53 "mitm",
54 "workflow",
55 "config",
56 "emulate",
57 "resize",
58 "perf",
59 "lighthouse",
60 "screencast",
61 "heap",
62 "extension",
63 "devtools3p",
64 "webmcp",
65 "completions",
66];
67
68pub const PARITY_DEFAULT_ON_REQUIRED: &[&str] = &[
70 "goto",
71 "view",
72 "press",
73 "write",
74 "keys",
75 "type",
76 "wait",
77 "hover",
78 "drag",
79 "fill-form",
80 "upload",
81 "back",
82 "forward",
83 "reload",
84 "eval",
85 "grab",
86 "console",
87 "net",
88 "page",
89 "dialog",
90 "emulate",
91 "resize",
92 "perf",
93 "lighthouse",
94 "run",
95 "exec",
96 "doctor",
97 "commands",
98 "schema",
99 "version",
100 "completions",
101];
102
103pub const DEVTOOLS_TOOL_MAP: &[(&str, &str)] = &[
105 ("click", "press"),
106 ("drag", "drag"),
107 ("fill", "write"),
108 ("fill_form", "fill-form"),
109 ("handle_dialog", "dialog"),
110 ("hover", "hover"),
111 ("press_key", "keys"),
112 ("type_text", "type"),
113 ("upload_file", "upload"),
114 ("click_at", "click-at"),
115 ("navigate_page", "goto|back|forward|reload"),
116 ("new_page", "page new"),
117 ("list_pages", "page list"),
118 ("select_page", "page select"),
119 ("close_page", "page close"),
120 ("wait_for", "wait"),
121 ("emulate", "emulate"),
122 ("resize_page", "resize"),
123 ("performance_start_trace", "perf start"),
124 ("performance_stop_trace", "perf stop"),
125 ("performance_analyze_insight", "perf insight"),
126 ("list_network_requests", "net list"),
127 ("get_network_request", "net get"),
128 ("evaluate_script", "eval"),
129 ("list_console_messages", "console list"),
130 ("get_console_message", "console get"),
131 ("take_screenshot", "grab"),
132 ("take_snapshot", "view"),
133 ("lighthouse_audit", "lighthouse"),
134 ("screencast_start", "screencast start"),
135 ("screencast_stop", "screencast stop"),
136 ("take_heapsnapshot", "heap take"),
137 ("close_heapsnapshot", "heap close"),
138 ("compare_heapsnapshots", "heap compare"),
139 ("get_heapsnapshot_summary", "heap summary"),
140 ("get_heapsnapshot_details", "heap details"),
141 ("get_heapsnapshot_class_nodes", "heap class-nodes"),
142 ("get_heapsnapshot_dominators", "heap dominators"),
143 ("get_heapsnapshot_duplicate_strings", "heap dup-strings"),
144 ("get_heapsnapshot_edges", "heap edges"),
145 ("get_heapsnapshot_retainers", "heap retainers"),
146 ("get_heapsnapshot_retaining_paths", "heap paths"),
147 ("get_heapsnapshot_object_details", "heap object-details"),
148 ("install_extension", "extension install"),
149 ("list_extensions", "extension list"),
150 ("reload_extension", "extension reload"),
151 ("trigger_extension_action", "extension trigger"),
152 ("uninstall_extension", "extension uninstall"),
153 ("list_3p_developer_tools", "devtools3p list"),
154 ("execute_3p_developer_tool", "devtools3p exec"),
155 ("list_webmcp_tools", "webmcp list"),
156 ("execute_webmcp_tool", "webmcp exec"),
157];
158
159pub fn list_commands(json: bool) -> Result<(), CliError> {
160 let map: Vec<Value> = DEVTOOLS_TOOL_MAP
161 .iter()
162 .map(|(tool, cli)| json!({ "tool": tool, "cli": cli }))
163 .collect();
164 let data = json!({
165 "commands": COMMANDS,
166 "schema_version": 1,
167 "parity_default_on": PARITY_DEFAULT_ON_REQUIRED,
168 "devtools_tool_map": map,
169 "binary": "browser-automation-cli",
170 });
171 if json {
172 print_success_json(data)?;
173 } else {
174 for c in COMMANDS {
175 println!("{c}");
176 }
177 }
178 Ok(())
179}
180
181fn schema_object(description: &str, properties: Value, required: &[&str]) -> Value {
182 json!({
183 "type": "object",
184 "description": description,
185 "properties": properties,
186 "required": required,
187 "additionalProperties": false,
188 })
189}
190
191fn schema_for(cmd: &str) -> Option<Value> {
192 let props = match cmd {
193 "doctor" => schema_object(
194 "Diagnose local Chrome install and one-shot readiness",
195 json!({
196 "offline": { "type": "boolean", "description": "Skip network probes" },
197 "quick": { "type": "boolean", "description": "Skip live launch test" },
198 "fix": { "type": "boolean", "description": "Apply safe repairs when possible" },
199 "json": { "type": "boolean" }
200 }),
201 &[],
202 ),
203 "commands" => schema_object(
204 "List available commands",
205 json!({ "json": { "type": "boolean" } }),
206 &[],
207 ),
208 "schema" => schema_object(
209 "JSON Schema fragment for one command",
210 json!({
211 "cmd": { "type": "string", "description": "Command name from `commands`" }
212 }),
213 &["cmd"],
214 ),
215 "version" => schema_object("Print CLI version (JSON when --json)", json!({}), &[]),
216 "goto" => schema_object(
217 "Navigate to URL and wait for load (one-shot)",
218 json!({
219 "url": { "type": "string", "description": "Absolute URL or about:blank" },
220 "init_script": { "type": "string", "description": "JS to evaluate before navigation (tool-ref initScript)" },
221 "handle_before_unload": { "type": "boolean", "description": "Accept beforeunload dialogs automatically" },
222 "navigation_timeout_ms": { "type": "integer", "description": "Navigation timeout override in milliseconds" }
223 }),
224 &["url"],
225 ),
226 "view" => schema_object(
227 "Accessibility snapshot with @eN refs",
228 json!({
229 "verbose": { "type": "boolean" },
230 "path": { "type": "string", "description": "Optional file to write tree text" }
231 }),
232 &[],
233 ),
234 "press" => schema_object(
235 "Click element by CSS selector or @eN",
236 json!({
237 "target": { "type": "string" },
238 "dblclick": { "type": "boolean" },
239 "include_snapshot": { "type": "boolean" }
240 }),
241 &["target"],
242 ),
243 "click-at" => schema_object(
244 "Click at page CSS coordinates (requires --experimental-vision)",
245 json!({
246 "x": { "type": "number" },
247 "y": { "type": "number" },
248 "dblclick": { "type": "boolean" },
249 "include_snapshot": { "type": "boolean" }
250 }),
251 &["x", "y"],
252 ),
253 "write" => schema_object(
254 "Smart fill: text, select option, checkbox/radio true|false",
255 json!({
256 "target": { "type": "string" },
257 "value": { "type": "string" },
258 "include_snapshot": { "type": "boolean" }
259 }),
260 &["target", "value"],
261 ),
262 "keys" => schema_object(
263 "Press a CDP key name",
264 json!({ "key": { "type": "string" } }),
265 &["key"],
266 ),
267 "type" => schema_object(
268 "Type text into a target",
269 json!({
270 "target": { "type": "string" },
271 "text": { "type": "string" },
272 "clear": { "type": "boolean" },
273 "submit": { "type": "string", "description": "Optional key after type (e.g. Enter)" },
274 "focus_only": { "type": "boolean", "description": "Focus target without typing" },
275 "include_snapshot": { "type": "boolean" }
276 }),
277 &["text"],
278 ),
279 "wait" => schema_object(
280 "Wait ms and/or text and/or CSS selector and/or load state",
281 json!({
282 "ms": { "type": "integer", "minimum": 0 },
283 "text": {
284 "oneOf": [
285 { "type": "string" },
286 { "type": "array", "items": { "type": "string" } }
287 ],
288 "description": "Repeatable --text values; any match wins (OR)"
289 },
290 "selector": { "type": "string" },
291 "state": {
292 "type": "string",
293 "enum": ["load", "domcontentloaded", "networkidle", "none"]
294 },
295 "wait_timeout_ms": { "type": "integer", "minimum": 0 },
296 "include_snapshot": { "type": "boolean" }
297 }),
298 &[],
299 ),
300 "hover" => schema_object(
301 "Hover element by CSS selector or @eN",
302 json!({ "target": { "type": "string" } }),
303 &["target"],
304 ),
305 "drag" => schema_object(
306 "Drag from one target to another",
307 json!({
308 "from": { "type": "string" },
309 "to": { "type": "string" }
310 }),
311 &["from", "to"],
312 ),
313 "fill-form" => schema_object(
314 "Fill multiple fields from JSON array",
315 json!({
316 "json": {
317 "type": "string",
318 "description": "JSON array of {target,value} objects"
319 }
320 }),
321 &["json"],
322 ),
323 "upload" => schema_object(
324 "Upload a regular file to a file input",
325 json!({
326 "target": { "type": "string" },
327 "path": { "type": "string" }
328 }),
329 &["target", "path"],
330 ),
331 "back" => schema_object("History back", json!({}), &[]),
332 "forward" => schema_object("History forward", json!({}), &[]),
333 "reload" => schema_object(
334 "Reload page",
335 json!({ "ignore_cache": { "type": "boolean" } }),
336 &[],
337 ),
338 "eval" => schema_object(
339 "Evaluate JavaScript expression or function declaration",
340 json!({
341 "expression": { "type": "string" },
342 "args": { "type": "string", "description": "JSON array of function args" },
343 "dialog_action": { "type": "string", "description": "accept|dismiss during evaluate" },
344 "file_path": { "type": "string", "description": "Optional path to write result" }
345 }),
346 &["expression"],
347 ),
348 "grab" => schema_object(
349 "Screenshot (png/jpeg/webp)",
350 json!({
351 "path": { "type": "string" },
352 "format": { "type": "string", "enum": ["png", "jpeg", "webp"] },
353 "full_page": { "type": "boolean" },
354 "quality": { "type": "integer" },
355 "element": { "type": "string", "description": "CSS selector or @eN" }
356 }),
357 &[],
358 ),
359 "print-pdf" => schema_object(
360 "Print current page to PDF via CDP Page.printToPDF (one-shot)",
361 json!({
362 "path": { "type": "string", "description": "Output PDF path" },
363 "url": { "type": "string", "description": "Optional URL to navigate before print" }
364 }),
365 &[],
366 ),
367 "monitor" => schema_object(
368 "One-shot change check against a baseline file (hash/text)",
369 json!({
370 "action": { "type": "string", "enum": ["check"] },
371 "url": { "type": "string" },
372 "baseline": { "type": "string", "description": "Baseline file path" },
373 "write_baseline": { "type": "boolean" },
374 "engine": { "type": "string", "enum": ["http", "browser"] }
375 }),
376 &["action", "url", "baseline"],
377 ),
378 "run" => schema_object(
379 "Execute NDJSON multi-step script in one process",
380 json!({ "script": { "type": "string", "description": "Path to .jsonl script" } }),
381 &["script"],
382 ),
383 "exec" => schema_object(
384 "Single-step inline command (same surface as run steps)",
385 json!({
386 "args": {
387 "type": "array",
388 "items": { "type": "string" },
389 "description": "e.g. [\"goto\", \"about:blank\"] or [\"wait\", \"--ms\", \"100\"]"
390 }
391 }),
392 &["args"],
393 ),
394 "extract" => schema_object(
395 "Extract text or attribute from target, or LLM extract with --llm",
396 json!({
397 "target": { "type": "string", "description": "CSS/@eN, http(s) URL, or file path for --llm" },
398 "attr": { "type": "string" },
399 "llm": { "type": "boolean" },
400 "question": { "type": "string" },
401 "schema_json": { "type": "string", "description": "Path to JSON schema file" }
402 }),
403 &["target"],
404 ),
405 "text" => schema_object(
406 "Extract visible text from target (PRD §7)",
407 json!({
408 "target": { "type": "string" }
409 }),
410 &["target"],
411 ),
412 "scroll" => schema_object(
413 "Scroll window or element by pixel deltas",
414 json!({
415 "target": { "type": "string", "description": "Optional CSS/@eN" },
416 "delta_x": { "type": "number" },
417 "delta_y": { "type": "number" },
418 "dx": { "type": "number", "description": "Alias for delta_x" },
419 "dy": { "type": "number", "description": "Alias for delta_y" }
420 }),
421 &[],
422 ),
423 "cookie" => schema_object(
424 "Cookie list/set/clear for the one-shot browser process",
425 json!({
426 "action": { "type": "string", "enum": ["list", "set", "clear"] },
427 "url": { "type": "string" },
428 "json": { "type": "string", "description": "JSON array for set" }
429 }),
430 &["action"],
431 ),
432 "attr" => schema_object(
433 "Read one attribute from target",
434 json!({
435 "target": { "type": "string" },
436 "name": { "type": "string" }
437 }),
438 &["target", "name"],
439 ),
440 "assert" => schema_object(
441 "Assertion helpers (url/text/console)",
442 json!({
443 "kind": { "type": "string", "enum": ["url", "text", "console"] },
444 "value": { "type": "string" },
445 "url": { "type": "string" },
446 "url_contains": { "type": "string" },
447 "text": { "type": "string" },
448 "text_contains": { "type": "string" },
449 "contains": { "type": "boolean" },
450 "target": { "type": "string" },
451 "level": { "type": "string" },
452 "max": { "type": "integer" }
453 }),
454 &[],
455 ),
456 "console" => schema_object(
457 "List/get/clear/dump captured console messages (needs --capture-console)",
458 json!({
459 "action": { "type": "string", "enum": ["list", "get", "clear", "dump"] },
460 "id": { "type": "integer", "minimum": 0 },
461 "path": { "type": "string" }
462 }),
463 &["action"],
464 ),
465 "net" => schema_object(
466 "List or get captured network requests (needs --capture-network)",
467 json!({
468 "action": { "type": "string", "enum": ["list", "get"] },
469 "id": { "type": "integer", "minimum": 0 },
470 "request_path": { "type": "string" },
471 "response_path": { "type": "string" }
472 }),
473 &["action"],
474 ),
475 "page" => schema_object(
476 "Page info or multi-tab list|new|select|close",
477 json!({
478 "action": {
479 "type": "string",
480 "enum": ["info", "list", "new", "select", "close"]
481 },
482 "url": { "type": "string" },
483 "index": { "type": "integer", "minimum": 0 }
484 }),
485 &[],
486 ),
487 "dialog" => schema_object(
488 "Accept or dismiss open dialog",
489 json!({
490 "action": { "type": "string", "enum": ["accept", "dismiss"] },
491 "text": { "type": "string" }
492 }),
493 &["action"],
494 ),
495 "scrape" => schema_object(
496 "Navigate and return body text / formats (local HTTP or CDP scrape)",
497 json!({
498 "url": { "type": "string" },
499 "format": {
500 "type": "string",
501 "enum": [
502 "text", "markdown", "html", "raw-html", "links", "metadata",
503 "screenshot", "summary", "product", "branding"
504 ],
505 "description": "Default text; browser engine also applies format via outerHTML"
506 },
507 "engine": {
508 "type": "string",
509 "enum": ["http", "browser"],
510 "description": "Default browser (CDP)"
511 },
512 "only_main_content": { "type": "boolean" },
513 "webhook_url": {
514 "type": "string",
515 "description": "Optional one-shot operator POST of result data (not product telemetry)"
516 }
517 }),
518 &["url"],
519 ),
520 "batch-scrape" => schema_object(
521 "Scrape many URLs from a file (HTTP engine, one-shot JoinSet)",
522 json!({
523 "urls_file": { "type": "string", "description": "Path to file with one URL per line" },
524 "format": {
525 "type": "string",
526 "enum": ["text", "markdown", "html", "links", "metadata"]
527 },
528 "concurrency": { "type": "integer", "minimum": 1 }
529 }),
530 &["urls_file"],
531 ),
532 "crawl" => schema_object(
533 "Crawl from a seed URL (HTTP BFS, one-shot)",
534 json!({
535 "url": { "type": "string" },
536 "limit": { "type": "integer", "minimum": 1 },
537 "max_depth": { "type": "integer", "minimum": 0 },
538 "format": { "type": "string" },
539 "same_host": { "type": "boolean" }
540 }),
541 &["url"],
542 ),
543 "map" => schema_object(
544 "Map site URLs from a seed (HTTP)",
545 json!({
546 "url": { "type": "string" },
547 "limit": { "type": "integer", "minimum": 1 },
548 "max_depth": { "type": "integer", "minimum": 0 }
549 }),
550 &["url"],
551 ),
552 "search" => schema_object(
553 "Local search (HTTP SERP links or URL map)",
554 json!({
555 "query": { "type": "string" },
556 "limit": { "type": "integer", "minimum": 1 }
557 }),
558 &["query"],
559 ),
560 "parse" => schema_object(
561 "Parse a local file (html/md/txt/pdf/docx/xlsx)",
562 json!({
563 "path": { "type": "string" },
564 "redact_pii": { "type": "boolean" }
565 }),
566 &["path"],
567 ),
568 "qr" => schema_object(
569 "QR encode/decode one-shot (no Chrome)",
570 json!({
571 "action": { "type": "string", "enum": ["encode", "decode"] },
572 "text": { "type": "string" },
573 "format": { "type": "string", "enum": ["png", "svg", "terminal"] },
574 "path": { "type": "string" }
575 }),
576 &["action"],
577 ),
578 "find-paths" => schema_object(
579 "Discover filesystem paths (fd-like; no Chrome)",
580 json!({
581 "pattern": { "type": "string" },
582 "paths": { "type": "array", "items": { "type": "string" } },
583 "extension": { "type": "string" },
584 "hidden": { "type": "boolean" },
585 "no_ignore": { "type": "boolean" },
586 "max_depth": { "type": "integer" },
587 "type": { "type": "string", "enum": ["f", "d"] },
588 "limit": { "type": "integer" }
589 }),
590 &[],
591 ),
592 "mitm" => schema_object(
593 "MITM capture / CA / HAR (one-shot local 127.0.0.1)",
594 json!({
595 "action": {
596 "type": "string",
597 "enum": [
598 "status", "list", "get", "har", "export",
599 "domains", "apis", "init-ca", "start"
600 ]
601 },
602 "id": { "type": "string" },
603 "out": { "type": "string" },
604 "seconds": { "type": "integer", "minimum": 1 },
605 "limit": { "type": "integer", "minimum": 1 }
606 }),
607 &["action"],
608 ),
609 "workflow" => schema_object(
610 "Workflow journal DAG (petgraph + SQLite under XDG state)",
611 json!({
612 "action": { "type": "string", "enum": ["run", "resume", "status"] },
613 "manifest": { "type": "string", "description": "JSON workflow manifest path" },
614 "journal": { "type": "string" },
615 "name": { "type": "string" }
616 }),
617 &["action"],
618 ),
619 "config" => schema_object(
620 "XDG config and path management (no product env at runtime)",
621 json!({
622 "action": {
623 "type": "string",
624 "enum": ["path", "init", "show", "set", "get"]
625 },
626 "key": {
627 "type": "string",
628 "description": "For set/get: lang|timeout|artifacts_dir|ignore_robots|namespace|encryption_key|color|log_level|chrome_path|lighthouse_path|openrouter_api_key|llm_base_url|llm_model"
629 },
630 "value": { "type": "string" }
631 }),
632 &["action"],
633 ),
634 "emulate" => schema_object(
635 "Emulate UA locale timezone network geo media CPU viewport headers",
636 json!({
637 "user_agent": { "type": "string" },
638 "locale": { "type": "string" },
639 "timezone": { "type": "string" },
640 "offline": { "type": "boolean" },
641 "latitude": { "type": "number" },
642 "longitude": { "type": "number" },
643 "media": { "type": "string" },
644 "network_conditions": { "type": "string" },
645 "cpu_throttling_rate": { "type": "number" },
646 "color_scheme": { "type": "string" },
647 "extra_headers": { "type": "string" },
648 "viewport": { "type": "string" }
649 }),
650 &[],
651 ),
652 "resize" => schema_object(
653 "Resize viewport",
654 json!({
655 "width": { "type": "integer" },
656 "height": { "type": "integer" },
657 "scale": { "type": "number" },
658 "mobile": { "type": "boolean" }
659 }),
660 &["width", "height"],
661 ),
662 "perf" => schema_object(
663 "Performance start|stop|insight",
664 json!({
665 "action": { "type": "string", "enum": ["start", "stop", "insight"] },
666 "path": { "type": "string" },
667 "reload": { "type": "boolean" },
668 "name": { "type": "string" }
669 }),
670 &["action"],
671 ),
672 "lighthouse" => schema_object(
673 "External lighthouse audit with JSON scores",
674 json!({
675 "url": { "type": "string" },
676 "out_dir": { "type": "string" },
677 "device": { "type": "string" },
678 "mode": { "type": "string" },
679 "lighthouse_path": { "type": "string" }
680 }),
681 &["url"],
682 ),
683 "screencast" => schema_object(
684 "Screencast start|stop (requires --experimental-screencast)",
685 json!({
686 "action": { "type": "string", "enum": ["start", "stop"] },
687 "path": { "type": "string" }
688 }),
689 &["action"],
690 ),
691 "heap" => schema_object(
692 "Heap snapshot tools (deep ops need --category-memory)",
693 json!({
694 "action": { "type": "string" },
695 "path": { "type": "string" },
696 "base": { "type": "string" },
697 "current": { "type": "string" },
698 "id": { "type": "integer" },
699 "node": { "type": "integer" }
700 }),
701 &["action"],
702 ),
703 "extension" => schema_object(
704 "Extension tools (requires --category-extensions)",
705 json!({
706 "action": { "type": "string" },
707 "path": { "type": "string" },
708 "id": { "type": "string" }
709 }),
710 &["action"],
711 ),
712 "devtools3p" => schema_object(
713 "Third-party tools surface (requires --category-third-party)",
714 json!({
715 "action": { "type": "string", "enum": ["list", "exec"] },
716 "name": { "type": "string" },
717 "params": { "type": "string" },
718 "url": { "type": "string" }
719 }),
720 &["action"],
721 ),
722 "webmcp" => schema_object(
723 "Web surface tools (requires --category-webmcp)",
724 json!({
725 "action": { "type": "string", "enum": ["list", "exec"] },
726 "name": { "type": "string" },
727 "input": { "type": "string" },
728 "url": { "type": "string" }
729 }),
730 &["action"],
731 ),
732 "completions" => schema_object(
733 "Generate shell completions (no Chrome)",
734 json!({
735 "shell": {
736 "type": "string",
737 "enum": ["bash", "zsh", "fish", "elvish", "powershell"]
738 }
739 }),
740 &["shell"],
741 ),
742 _ => return None,
743 };
744 Some(props)
745}
746
747pub fn schema_for_cmd(cmd: &str, json: bool) -> Result<(), CliError> {
748 if !COMMANDS.contains(&cmd) {
749 return Err(CliError::with_suggestion(
750 ErrorKind::Usage,
751 format!("unknown command for schema: {cmd}"),
752 "use `browser-automation-cli commands --json` to list commands",
753 ));
754 }
755 let fragment = schema_for(cmd)
756 .unwrap_or_else(|| schema_object(&format!("Schema fragment for `{cmd}`"), json!({}), &[]));
757 let data = json!({
758 "command": cmd,
759 "schema_version": 1,
760 "schema": fragment,
761 "type": fragment.get("type").cloned().unwrap_or(json!("object")),
762 "description": fragment.get("description").cloned().unwrap_or(json!("")),
763 "properties": fragment.get("properties").cloned().unwrap_or(json!({})),
764 "required": fragment.get("required").cloned().unwrap_or(json!([])),
765 });
766 if json {
767 print_success_json(data)?;
768 } else {
769 println!(
770 "{}",
771 serde_json::to_string_pretty(&data).unwrap_or_default()
772 );
773 }
774 Ok(())
775}
776
777#[cfg(test)]
778mod tests {
779 use super::*;
780
781 #[test]
782 fn parity_default_on_subset_of_commands() {
783 for req in PARITY_DEFAULT_ON_REQUIRED {
784 assert!(
785 COMMANDS.contains(req),
786 "parity command missing from COMMANDS: {req}"
787 );
788 }
789 }
790
791 #[test]
792 fn commands_unique() {
793 let mut seen = std::collections::BTreeSet::new();
794 for c in COMMANDS {
795 assert!(seen.insert(*c), "duplicate command: {c}");
796 }
797 }
798}