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