1use clap::{Parser, Subcommand, ValueEnum};
7use std::path::PathBuf;
8
9use crate::browser::policy::{PolicyCapability, PolicyPreset};
10use crate::browser::session::{
11 BatchMode, InteractionMode, PreflightAction, VisualClip, VisualFormat,
12};
13
14#[derive(Debug, Parser)]
19#[command(
20 name = "glass",
21 version,
22 about = "Lightweight local-first browser agent using raw Chrome DevTools Protocol"
23)]
24pub struct Cli {
25 #[arg(long, global = true, value_enum, default_value_t = PolicyPreset::Development)]
27 pub policy: PolicyPreset,
28
29 #[arg(long = "policy-allow", global = true, value_enum)]
31 pub policy_allow: Vec<PolicyCapability>,
32
33 #[arg(long = "policy-confirm", global = true, value_enum)]
35 pub policy_confirm: Vec<PolicyCapability>,
36
37 #[arg(long = "policy-confirm-once", global = true, value_enum)]
39 pub policy_confirm_once: Vec<PolicyCapability>,
40
41 #[arg(long, global = true)]
43 pub experimental_extensions: bool,
44
45 #[arg(long = "policy-allow-host", global = true)]
47 pub policy_allow_host: Vec<String>,
48
49 #[arg(long = "policy-deny-host", global = true)]
51 pub policy_deny_host: Vec<String>,
52
53 #[arg(long, global = true, default_value = "default")]
55 pub profile: String,
56
57 #[arg(long, global = true)]
59 pub incognito: bool,
60
61 #[arg(long, global = true)]
64 pub attach: bool,
65
66 #[arg(long = "target-id", global = true)]
69 pub target_id: Option<String>,
70
71 #[arg(long = "frame-id", global = true)]
73 pub frame_id: Option<String>,
74
75 #[arg(long, global = true, default_value_t = 9222)]
77 pub port: u16,
78
79 #[arg(long, global = true)]
81 pub headed: bool,
82
83 #[arg(long, global = true, value_enum, default_value_t = InteractionMode::Human)]
85 pub interaction: InteractionMode,
86
87 #[arg(long, global = true)]
89 pub audit: bool,
90
91 #[arg(long, global = true)]
93 pub trace_on_error: bool,
94
95 #[arg(long = "chrome-path", alias = "chrome", global = true)]
97 pub chrome_path: Option<PathBuf>,
98
99 #[arg(long)]
101 pub mcp: bool,
102
103 #[arg(long, global = true)]
105 pub knowledge_store: Option<PathBuf>,
106
107 #[arg(value_name = "PROMPT")]
109 pub prompt: Option<String>,
110
111 #[command(subcommand)]
112 pub command: Option<Commands>,
113}
114
115#[derive(Debug, Subcommand)]
116pub enum Commands {
117 InstallChromium {
119 #[arg(long)]
121 update: bool,
122 },
123
124 Certify {
126 #[command(subcommand)]
127 action: CertifyCommand,
128 },
129
130 Capabilities,
132
133 Daemon {
135 #[command(subcommand)]
136 action: DaemonCommand,
137 },
138
139 Doctor,
141
142 Profiles {
144 #[command(subcommand)]
145 action: Option<ProfileCommand>,
146 },
147
148 Knowledge {
150 #[command(subcommand)]
151 action: KnowledgeCommand,
152 },
153
154 DeleteProfile { name: String },
156
157 Navigate {
159 url: String,
160 #[arg(long, default_value_t = 20_000)]
161 timeout_ms: u64,
162 #[arg(long)]
163 expected_revision: Option<u64>,
164 },
165
166 Click {
168 target: String,
169 #[arg(long)]
170 expected_revision: Option<u64>,
171 },
172
173 Preflight {
175 target: String,
176 #[arg(long, value_enum, default_value_t = PreflightAction::Click)]
177 action: PreflightAction,
178 },
179
180 ClickAt { x: f64, y: f64 },
182
183 ClickExpectPopup {
185 target: String,
186 #[arg(long)]
187 expected_revision: Option<u64>,
188 },
189
190 DoubleClick {
192 target: String,
193 #[arg(long)]
194 expected_revision: Option<u64>,
195 },
196
197 Hover { target: String },
199
200 Drag {
202 source: String,
203 destination: String,
204 #[arg(long)]
205 expected_revision: Option<u64>,
206 },
207
208 Type {
210 text: String,
211 #[arg(long)]
212 target: Option<String>,
213 #[arg(long)]
214 expected_revision: Option<u64>,
215 },
216
217 Key {
219 key: String,
220 #[arg(long)]
221 expected_revision: Option<u64>,
222 },
223
224 KeyDown {
226 key: String,
227 #[arg(long)]
228 expected_revision: Option<u64>,
229 },
230
231 KeyUp {
233 key: String,
234 #[arg(long)]
235 expected_revision: Option<u64>,
236 },
237
238 Shortcut {
240 shortcut: String,
241 #[arg(long)]
242 expected_revision: Option<u64>,
243 },
244
245 Clear {
247 target: String,
248 #[arg(long)]
249 expected_revision: Option<u64>,
250 },
251
252 Check {
254 target: String,
255 #[arg(long)]
256 expected_revision: Option<u64>,
257 },
258
259 Uncheck {
261 target: String,
262 #[arg(long)]
263 expected_revision: Option<u64>,
264 },
265
266 Select {
268 target: String,
269 value: String,
270 #[arg(long)]
271 expected_revision: Option<u64>,
272 },
273
274 Upload {
276 target: String,
277 #[arg(required = true)]
278 files: Vec<PathBuf>,
279 #[arg(long)]
280 expected_revision: Option<u64>,
281 },
282
283 Screenshot {
285 #[arg(short, long, default_value = "screenshot.png")]
286 output: String,
287 #[arg(long, value_enum, default_value_t = VisualFormat::Png)]
288 format: VisualFormat,
289 #[arg(long)]
290 quality: Option<u8>,
291 #[arg(long, default_value_t = 1.0)]
292 scale: f64,
293 #[arg(long, conflicts_with_all = ["clip", "target"])]
294 full_page: bool,
295 #[arg(long, conflicts_with_all = ["full_page", "target"])]
296 clip: Option<VisualClip>,
297 #[arg(long, conflicts_with_all = ["full_page", "clip"])]
298 target: Option<String>,
299 },
300
301 Text,
303
304 Dom,
306
307 Observe {
309 #[arg(long)]
311 deep_dom: bool,
312 #[arg(long)]
314 screenshot: bool,
315 #[arg(long)]
317 form_values: bool,
318 #[arg(long = "level", alias = "semantic-level", value_parser = parse_semantic_level)]
320 semantic_level: Option<String>,
321 #[arg(long, requires = "semantic_level")]
323 region: Option<String>,
324 },
325
326 Scroll {
328 #[arg(long, default_value_t = 0.0)]
329 dx: f64,
330 #[arg(long, default_value_t = 600.0)]
331 dy: f64,
332 #[arg(long)]
333 expected_revision: Option<u64>,
334 },
335
336 Wait {
338 condition: String,
339 #[arg(long, default_value_t = 10_000)]
340 timeout_ms: u64,
341 },
342
343 Diagnostics {
345 #[arg(long, default_value_t = 1_000)]
346 duration_ms: u64,
347 },
348
349 AcceptDialog,
351
352 DismissDialog,
354
355 DismissConsent,
357
358 Download {
360 destination: PathBuf,
361 #[arg(long, default_value_t = 30_000)]
362 timeout_ms: u64,
363 },
364
365 Targets,
367
368 NewTarget { url: String },
370
371 SelectTarget { id: String },
373
374 CloseTarget { id: String },
376
377 Frames,
379
380 SelectFrame { id: String },
382
383 Evaluate { expression: String },
385
386 Cookies,
388
389 ExportCookies { output: PathBuf },
391
392 ImportCookies { input: PathBuf },
394
395 Pdf {
397 #[arg(short, long, default_value = "page.pdf")]
398 output: String,
399 #[arg(long)]
400 background: bool,
401 },
402
403 FillForm {
405 #[arg(long)]
407 fields: String,
408 #[arg(long)]
410 expected_revision: Option<u64>,
411 },
412
413 Batch {
415 input: Option<PathBuf>,
417 #[arg(long)]
418 atomic: bool,
419 #[arg(long, value_enum, default_value_t = BatchMode::Unguarded)]
421 mode: BatchMode,
422 #[arg(long)]
424 expected_revision: Option<u64>,
425 },
426
427 Workflow {
429 #[command(subcommand)]
431 action: Option<WorkflowAuthoringCommand>,
432 input: Option<PathBuf>,
434 },
435
436 WorkflowResume {
438 workflow: PathBuf,
440 checkpoint: PathBuf,
442 #[arg(long)]
444 inputs: Option<PathBuf>,
445 },
446
447 ResolveIntent {
449 input: Option<PathBuf>,
451 },
452
453 ExecuteIntent {
455 input: Option<PathBuf>,
457 },
458
459 Verify {
461 predicate: String,
463 #[arg(long, default_value_t = 10_000)]
464 timeout_ms: u64,
465 },
466
467 ReconcileRefs {
469 #[arg(long)]
470 from_revision: u64,
471 #[arg(long = "hint")]
473 hints: Vec<String>,
474 #[arg(long)]
476 scope: Option<String>,
477 #[arg(required = true)]
478 refs: Vec<String>,
479 },
480
481 ObserveDelta,
483
484 Checkpoint {
486 #[command(subcommand)]
487 action: CheckpointCommand,
488 },
489
490 ClipboardRead,
492
493 ClipboardWrite { text: String },
495
496 Tui,
498}
499
500fn parse_semantic_level(value: &str) -> Result<String, String> {
501 match value {
502 "summary" | "interactive" | "structured" | "detailed" | "raw" => Ok(value.into()),
503 _ => Err("expected summary, interactive, structured, detailed, or raw".into()),
504 }
505}
506
507#[derive(Debug, Subcommand)]
508pub enum ProfileCommand {
509 List,
510 Create { name: String },
511 Delete { name: String },
512}
513
514#[derive(Debug, Subcommand)]
515pub enum KnowledgeCommand {
516 List,
518 Show { record_id: String },
520 Explain { record_id: String },
522 Stats,
524 Export { output: Option<PathBuf> },
526 Import { input: PathBuf },
528 Invalidate {
530 record_id: String,
531 #[arg(value_enum)]
532 state: KnowledgeInvalidationState,
533 #[arg(long)]
534 reason: Option<String>,
535 #[arg(long)]
536 observed_at: Option<String>,
537 },
538 Purge { origin: String },
540}
541
542#[derive(Debug, Clone, Copy, ValueEnum)]
543pub enum KnowledgeInvalidationState {
544 Stale,
545 Contradicted,
546 Quarantined,
547}
548
549#[derive(Debug, Subcommand)]
550pub enum WorkflowAuthoringCommand {
551 Compile {
553 input: PathBuf,
554 #[arg(short, long)]
555 output: Option<PathBuf>,
556 },
557 Format {
559 input: PathBuf,
560 #[arg(short, long)]
561 output: Option<PathBuf>,
562 },
563 Preview { input: PathBuf },
565 Diff { before: PathBuf, after: PathBuf },
567 Record {
569 #[arg(long)]
571 input: Option<PathBuf>,
572 #[arg(short, long)]
573 output: Option<PathBuf>,
574 },
575 Validate { input: PathBuf },
577 Lint {
579 input: PathBuf,
580 #[arg(long)]
581 warnings_as_errors: bool,
582 },
583}
584
585#[derive(Debug, Subcommand)]
586pub enum DaemonCommand {
587 Start {
589 #[arg(long)]
590 socket: Option<PathBuf>,
591 #[arg(long)]
592 status: Option<PathBuf>,
593 },
594 Status {
596 #[arg(long)]
597 socket: Option<PathBuf>,
598 #[arg(long)]
599 status: Option<PathBuf>,
600 },
601 Stop {
603 #[arg(long)]
604 socket: Option<PathBuf>,
605 #[arg(long)]
606 status: Option<PathBuf>,
607 },
608 Doctor {
610 #[arg(long)]
611 socket: Option<PathBuf>,
612 #[arg(long)]
613 status: Option<PathBuf>,
614 },
615 Logs {
617 #[arg(long)]
618 status: Option<PathBuf>,
619 },
620 AcknowledgeRecovery {
622 #[arg(long)]
623 status: Option<PathBuf>,
624 #[arg(long = "request-id", required = true)]
626 request_ids: Vec<String>,
627 },
628 #[command(hide = true)]
630 Serve {
631 #[arg(long)]
632 socket: PathBuf,
633 #[arg(long)]
634 status: PathBuf,
635 },
636}
637
638#[derive(Debug, Subcommand)]
639pub enum CertifyCommand {
640 Run {
642 #[arg(long)]
644 scenario: PathBuf,
645 #[arg(long)]
647 fixture: PathBuf,
648 #[arg(long)]
650 url: String,
651 #[arg(long, default_value = ".")]
653 workflow_root: PathBuf,
654 #[arg(long)]
656 inputs: Option<PathBuf>,
657 #[arg(short, long)]
659 output: Option<PathBuf>,
660 },
661 Plan {
663 #[arg(long)]
665 scenario: PathBuf,
666 #[arg(long)]
668 fixture: PathBuf,
669 },
670 Release {
672 #[arg(long)]
673 version: String,
674 #[arg(long)]
676 scenarios: PathBuf,
677 #[arg(long)]
679 observations: PathBuf,
680 #[arg(long)]
682 replays: Option<PathBuf>,
683 },
684 Replay {
686 #[arg(long)]
688 scenario: PathBuf,
689 #[arg(long)]
691 input: PathBuf,
692 },
693 ReplayDiff {
695 #[arg(long)]
697 scenario: PathBuf,
698 #[arg(long)]
700 before: PathBuf,
701 #[arg(long)]
703 after: PathBuf,
704 },
705}
706
707#[derive(Debug, Subcommand)]
708pub enum CheckpointCommand {
709 Export,
710 Import { input: Option<PathBuf> },
711}
712
713#[cfg(test)]
714mod tests {
715 use super::*;
716
717 #[test]
718 fn observation_and_human_interaction_are_defaults() {
719 let cli = Cli::try_parse_from(["glass", "observe"]).unwrap();
720
721 assert_eq!(cli.interaction, InteractionMode::Human);
722 assert!(matches!(
723 cli.command,
724 Some(Commands::Observe {
725 deep_dom: false,
726 screenshot: false,
727 form_values: false,
728 semantic_level: None,
729 region: None,
730 })
731 ));
732 }
733
734 #[test]
735 fn screenshot_and_fast_interaction_require_explicit_flags() {
736 let cli =
737 Cli::try_parse_from(["glass", "--interaction", "fast", "observe", "--screenshot"])
738 .unwrap();
739
740 assert_eq!(cli.interaction, InteractionMode::Fast);
741 assert!(matches!(
742 cli.command,
743 Some(Commands::Observe {
744 deep_dom: false,
745 screenshot: true,
746 form_values: false,
747 semantic_level: None,
748 region: None,
749 })
750 ));
751 }
752
753 #[test]
754 fn deep_dom_requires_an_explicit_observation_flag() {
755 let cli = Cli::try_parse_from(["glass", "observe", "--deep-dom"]).unwrap();
756
757 assert!(matches!(
758 cli.command,
759 Some(Commands::Observe {
760 deep_dom: true,
761 screenshot: false,
762 form_values: false,
763 semantic_level: None,
764 region: None,
765 })
766 ));
767 }
768
769 #[test]
770 fn semantic_observation_level_and_region_are_explicit() {
771 let cli = Cli::try_parse_from([
772 "glass",
773 "observe",
774 "--level",
775 "interactive",
776 "--region",
777 "region_main",
778 ])
779 .unwrap();
780 assert!(matches!(
781 cli.command,
782 Some(Commands::Observe {
783 semantic_level: Some(level),
784 region: Some(region),
785 ..
786 }) if level == "interactive" && region == "region_main"
787 ));
788
789 assert!(Cli::try_parse_from(["glass", "observe", "--level", "verbose"]).is_err());
790 }
791
792 #[test]
793 fn screenshot_remains_a_separate_explicit_command() {
794 let cli = Cli::try_parse_from(["glass", "screenshot", "--output", "page.png"]).unwrap();
795
796 assert!(matches!(
797 cli.command,
798 Some(Commands::Screenshot { output, .. }) if output == "page.png"
799 ));
800 }
801
802 #[test]
803 fn double_click_is_an_explicit_action_command() {
804 let cli = Cli::try_parse_from(["glass", "double-click", "r7:b42"]).unwrap();
805
806 assert!(matches!(
807 cli.command,
808 Some(Commands::DoubleClick { target, .. }) if target == "r7:b42"
809 ));
810 }
811
812 #[test]
813 fn click_expect_popup_is_an_explicit_action_command() {
814 let cli = Cli::try_parse_from(["glass", "click-expect-popup", "css=#popup"]).unwrap();
815 assert!(matches!(
816 cli.command,
817 Some(Commands::ClickExpectPopup { target, .. }) if target == "css=#popup"
818 ));
819 }
820
821 #[test]
822 fn wait_has_an_explicit_condition_and_bounded_default() {
823 let cli = Cli::try_parse_from(["glass", "wait", "text=Ready"]).unwrap();
824 assert!(matches!(
825 cli.command,
826 Some(Commands::Wait { condition, timeout_ms: 10_000 }) if condition == "text=Ready"
827 ));
828 }
829
830 #[test]
831 fn topology_commands_are_explicit() {
832 assert!(matches!(
833 Cli::try_parse_from(["glass", "targets"]).unwrap().command,
834 Some(Commands::Targets)
835 ));
836 let cli = Cli::try_parse_from([
837 "glass",
838 "--target-id",
839 "page-1",
840 "--frame-id",
841 "frame-1",
842 "evaluate",
843 "document.title",
844 ])
845 .unwrap();
846 assert_eq!(cli.target_id.as_deref(), Some("page-1"));
847 assert_eq!(cli.frame_id.as_deref(), Some("frame-1"));
848 assert!(matches!(
849 Cli::try_parse_from(["glass", "select-frame", "frame-1"])
850 .unwrap()
851 .command,
852 Some(Commands::SelectFrame { id }) if id == "frame-1"
853 ));
854 }
855
856 #[test]
857 fn complete_input_commands_are_explicit() {
858 assert!(matches!(
859 Cli::try_parse_from(["glass", "drag", "css=#from", "css=#to"])
860 .unwrap()
861 .command,
862 Some(Commands::Drag { source, destination, .. }) if source == "css=#from" && destination == "css=#to"
863 ));
864 assert!(matches!(
865 Cli::try_parse_from(["glass", "shortcut", "Control+A"])
866 .unwrap()
867 .command,
868 Some(Commands::Shortcut { shortcut, .. }) if shortcut == "Control+A"
869 ));
870 assert!(matches!(
871 Cli::try_parse_from([
872 "glass",
873 "fill-form",
874 "--fields",
875 "[]",
876 "--expected-revision",
877 "7"
878 ])
879 .unwrap()
880 .command,
881 Some(Commands::FillForm {
882 fields,
883 expected_revision: Some(7)
884 }) if fields == "[]"
885 ));
886 }
887
888 #[test]
889 fn rejects_unknown_interaction_modes() {
890 assert!(Cli::try_parse_from(["glass", "--interaction", "instant", "observe"]).is_err());
891 }
892
893 #[test]
894 fn attach_and_target_id_are_explicit_global_options() {
895 let cli = Cli::try_parse_from([
896 "glass",
897 "--attach",
898 "--port",
899 "9333",
900 "--target-id",
901 "page-2",
902 "observe",
903 ])
904 .unwrap();
905
906 assert!(cli.attach);
907 assert_eq!(cli.port, 9333);
908 assert_eq!(cli.target_id.as_deref(), Some("page-2"));
909 }
910
911 #[test]
912 fn workflow_command_accepts_optional_json_input() {
913 let cli = Cli::try_parse_from(["glass", "workflow", "workflow.json"]).unwrap();
914 assert!(matches!(
915 cli.command,
916 Some(Commands::Workflow {
917 action: None,
918 input: Some(path)
919 })
920 if path.as_os_str() == "workflow.json"
921 ));
922 let cli = Cli::try_parse_from(["glass", "workflow", "validate", "workflow.yaml"]).unwrap();
923 assert!(matches!(
924 cli.command,
925 Some(Commands::Workflow {
926 action: Some(WorkflowAuthoringCommand::Validate { input }),
927 input: None,
928 }) if input.as_os_str() == "workflow.yaml"
929 ));
930 let cli = Cli::try_parse_from(["glass", "workflow", "preview", "workflow.yaml"]).unwrap();
931 assert!(matches!(
932 cli.command,
933 Some(Commands::Workflow {
934 action: Some(WorkflowAuthoringCommand::Preview { input }),
935 input: None,
936 }) if input.as_os_str() == "workflow.yaml"
937 ));
938 let cli = Cli::try_parse_from([
939 "glass",
940 "workflow",
941 "record",
942 "--input",
943 "events.json",
944 "--output",
945 "draft.json",
946 ])
947 .unwrap();
948 assert!(matches!(
949 cli.command,
950 Some(Commands::Workflow {
951 action: Some(WorkflowAuthoringCommand::Record { input: Some(input), output: Some(output) }),
952 input: None,
953 }) if input.as_os_str() == "events.json" && output.as_os_str() == "draft.json"
954 ));
955 }
956
957 #[test]
958 fn certify_release_command_accepts_versioned_evidence_paths() {
959 let cli = Cli::try_parse_from([
960 "glass",
961 "certify",
962 "release",
963 "--version",
964 "0.2.0",
965 "--scenarios",
966 "scenarios.json",
967 "--observations",
968 "observations.json",
969 ])
970 .unwrap();
971 assert!(matches!(
972 cli.command,
973 Some(Commands::Certify {
974 action: CertifyCommand::Release {
975 version,
976 scenarios,
977 observations,
978 replays: None,
979 },
980 }) if version == "0.2.0"
981 && scenarios.as_os_str() == "scenarios.json"
982 && observations.as_os_str() == "observations.json"
983 ));
984 }
985
986 #[test]
987 fn certify_plan_command_accepts_scenario_and_fixture_paths() {
988 let cli = Cli::try_parse_from([
989 "glass",
990 "certify",
991 "plan",
992 "--scenario",
993 "scenario.json",
994 "--fixture",
995 "fixture.json",
996 ])
997 .unwrap();
998 assert!(matches!(
999 cli.command,
1000 Some(Commands::Certify {
1001 action: CertifyCommand::Plan { scenario, fixture },
1002 }) if scenario.as_os_str() == "scenario.json" && fixture.as_os_str() == "fixture.json"
1003 ));
1004 }
1005
1006 #[test]
1007 fn certify_replay_command_accepts_scenario_and_bundle_paths() {
1008 let cli = Cli::try_parse_from([
1009 "glass",
1010 "certify",
1011 "replay",
1012 "--scenario",
1013 "scenario.json",
1014 "--input",
1015 "replay.json",
1016 ])
1017 .unwrap();
1018 assert!(matches!(
1019 cli.command,
1020 Some(Commands::Certify {
1021 action: CertifyCommand::Replay { scenario, input },
1022 }) if scenario.as_os_str() == "scenario.json" && input.as_os_str() == "replay.json"
1023 ));
1024 }
1025
1026 #[test]
1027 fn certify_replay_diff_command_accepts_two_bundle_paths() {
1028 let cli = Cli::try_parse_from([
1029 "glass",
1030 "certify",
1031 "replay-diff",
1032 "--scenario",
1033 "scenario.json",
1034 "--before",
1035 "before.json",
1036 "--after",
1037 "after.json",
1038 ])
1039 .unwrap();
1040 assert!(matches!(
1041 cli.command,
1042 Some(Commands::Certify {
1043 action: CertifyCommand::ReplayDiff { scenario, before, after },
1044 }) if scenario.as_os_str() == "scenario.json"
1045 && before.as_os_str() == "before.json"
1046 && after.as_os_str() == "after.json"
1047 ));
1048 }
1049
1050 #[test]
1051 fn workflow_resume_command_accepts_checkpoint_and_inputs() {
1052 let cli = Cli::try_parse_from([
1053 "glass",
1054 "workflow-resume",
1055 "workflow.json",
1056 "checkpoint.json",
1057 "--inputs",
1058 "inputs.json",
1059 ])
1060 .unwrap();
1061 assert!(matches!(
1062 cli.command,
1063 Some(Commands::WorkflowResume {
1064 workflow,
1065 checkpoint,
1066 inputs: Some(inputs)
1067 }) if workflow.as_os_str() == "workflow.json"
1068 && checkpoint.as_os_str() == "checkpoint.json"
1069 && inputs.as_os_str() == "inputs.json"
1070 ));
1071 }
1072
1073 #[test]
1074 fn resolve_intent_command_accepts_optional_json_input() {
1075 let cli = Cli::try_parse_from(["glass", "resolve-intent", "intent.json"]).unwrap();
1076 assert!(matches!(
1077 cli.command,
1078 Some(Commands::ResolveIntent { input: Some(path) })
1079 if path.as_os_str() == "intent.json"
1080 ));
1081 assert!(Cli::try_parse_from(["glass", "resolve-intent"]).is_ok());
1082 }
1083
1084 #[test]
1085 fn execute_intent_command_accepts_optional_json_input() {
1086 let cli = Cli::try_parse_from(["glass", "execute-intent", "intent.json"]).unwrap();
1087 assert!(matches!(
1088 cli.command,
1089 Some(Commands::ExecuteIntent { input: Some(path) })
1090 if path.as_os_str() == "intent.json"
1091 ));
1092 assert!(Cli::try_parse_from(["glass", "execute-intent"]).is_ok());
1093 }
1094
1095 #[test]
1096 fn reliability_run_command_requires_fixture_url_and_sources() {
1097 let cli = Cli::try_parse_from([
1098 "glass",
1099 "certify",
1100 "run",
1101 "--scenario",
1102 "scenario.json",
1103 "--fixture",
1104 "fixture.json",
1105 "--url",
1106 "http://127.0.0.1:8000/fixture.html",
1107 "--workflow-root",
1108 "fixtures",
1109 "--inputs",
1110 "inputs.json",
1111 "--output",
1112 "evidence.json",
1113 ])
1114 .unwrap();
1115 assert!(matches!(
1116 cli.command,
1117 Some(Commands::Certify {
1118 action: CertifyCommand::Run {
1119 scenario,
1120 fixture,
1121 url,
1122 workflow_root,
1123 inputs: Some(inputs),
1124 output: Some(output),
1125 }
1126 }) if scenario.as_os_str() == "scenario.json"
1127 && fixture.as_os_str() == "fixture.json"
1128 && url == "http://127.0.0.1:8000/fixture.html"
1129 && workflow_root.as_os_str() == "fixtures"
1130 && inputs.as_os_str() == "inputs.json"
1131 && output.as_os_str() == "evidence.json"
1132 ));
1133 }
1134
1135 #[test]
1136 fn capabilities_command_is_explicitly_offline() {
1137 let cli = Cli::try_parse_from(["glass", "capabilities"]).unwrap();
1138 assert!(matches!(cli.command, Some(Commands::Capabilities)));
1139 }
1140
1141 #[test]
1142 fn experimental_extensions_require_an_explicit_global_opt_in() {
1143 let cli =
1144 Cli::try_parse_from(["glass", "--experimental-extensions", "capabilities"]).unwrap();
1145 assert!(cli.experimental_extensions);
1146 }
1147
1148 #[test]
1149 fn daemon_lifecycle_commands_accept_explicit_local_paths() {
1150 let cli = Cli::try_parse_from([
1151 "glass",
1152 "daemon",
1153 "start",
1154 "--socket",
1155 "/tmp/glass.sock",
1156 "--status",
1157 "/tmp/glass.json",
1158 ])
1159 .unwrap();
1160 assert!(matches!(
1161 cli.command,
1162 Some(Commands::Daemon {
1163 action: DaemonCommand::Start { socket: Some(socket), status: Some(status) }
1164 }) if socket.as_os_str() == "/tmp/glass.sock"
1165 && status.as_os_str() == "/tmp/glass.json"
1166 ));
1167 }
1168
1169 #[test]
1170 fn doctor_command_is_available_without_starting_a_browser() {
1171 let cli = Cli::try_parse_from(["glass", "doctor"]).unwrap();
1172 assert!(matches!(cli.command, Some(Commands::Doctor)));
1173 }
1174
1175 #[test]
1176 fn knowledge_management_commands_parse_without_browser_startup() {
1177 let cli = Cli::try_parse_from(["glass", "knowledge", "list"]).unwrap();
1178 assert!(matches!(
1179 cli.command,
1180 Some(Commands::Knowledge {
1181 action: KnowledgeCommand::List
1182 })
1183 ));
1184 let cli = Cli::try_parse_from(["glass", "knowledge", "explain", "record-1"]).unwrap();
1185 assert!(matches!(
1186 cli.command,
1187 Some(Commands::Knowledge {
1188 action: KnowledgeCommand::Explain { .. }
1189 })
1190 ));
1191 let cli = Cli::try_parse_from([
1192 "glass",
1193 "--knowledge-store",
1194 "knowledge.json",
1195 "knowledge",
1196 "invalidate",
1197 "record-1",
1198 "stale",
1199 ])
1200 .unwrap();
1201 assert!(matches!(
1202 cli.command,
1203 Some(Commands::Knowledge {
1204 action: KnowledgeCommand::Invalidate { .. }
1205 })
1206 ));
1207 }
1208}