Skip to main content

probador/
commands.rs

1//! CLI command definitions using clap
2
3use clap::{Parser, Subcommand, ValueEnum};
4use std::path::PathBuf;
5
6/// Probador: CLI for Probar - Rust-native testing framework for WASM games
7#[derive(Parser, Debug)]
8#[command(name = "probador")]
9#[command(author, version, about, long_about = None)]
10#[command(propagate_version = true)]
11pub struct Cli {
12    /// Verbosity level (-v, -vv, -vvv)
13    #[arg(short, long, action = clap::ArgAction::Count, global = true)]
14    pub verbose: u8,
15
16    /// Quiet mode (suppress non-error output)
17    #[arg(short, long, global = true)]
18    pub quiet: bool,
19
20    /// Color output (auto, always, never)
21    #[arg(long, default_value = "auto", global = true)]
22    pub color: ColorArg,
23
24    /// Subcommand to run
25    #[command(subcommand)]
26    pub command: Commands,
27}
28
29/// CLI subcommands
30#[derive(Subcommand, Debug)]
31pub enum Commands {
32    /// Run tests
33    Test(TestArgs),
34
35    /// Record test execution
36    Record(RecordArgs),
37
38    /// Generate reports
39    Report(ReportArgs),
40
41    /// Generate coverage heatmaps
42    Coverage(CoverageArgs),
43
44    /// Initialize a new Probar project
45    Init(InitArgs),
46
47    /// Show configuration
48    Config(ConfigArgs),
49
50    /// Start WASM development server
51    Serve(ServeArgs),
52
53    /// Build WASM package
54    Build(BuildArgs),
55
56    /// Watch for changes and rebuild
57    Watch(WatchArgs),
58
59    /// Run state machine playbooks
60    Playbook(PlaybookArgs),
61
62    /// Run WASM compliance checks (C001-C010)
63    ///
64    /// Validates WASM application against Probar's compliance checklist:
65    /// - C001: Code execution verified (not just mocked HTML)
66    /// - C002: Console errors cause test failure
67    /// - C003: Custom elements tested
68    /// - C004: Both threading/non-threading modes tested
69    /// - C005: Low memory scenario tested
70    /// - C006: COOP/COEP headers present
71    /// - C007: Replay hash matches
72    /// - C008: Proper cache handling
73    /// - C009: WASM under size limit
74    /// - C010: No panic paths in WASM
75    Comply(ComplyArgs),
76
77    /// Verify audio-visual synchronization against EDL ground truth
78    ///
79    /// Extracts audio from rendered video, detects tick onsets, and compares
80    /// against EDL declarations. Fails when drift exceeds tolerance.
81    AvSync(AvSyncArgs),
82
83    /// Verify audio quality (levels, clipping, silence)
84    ///
85    /// Extracts audio from rendered video and analyzes peak/RMS levels,
86    /// detects digital clipping, and identifies silence regions.
87    Audio(AudioArgs),
88
89    /// Verify video quality (codec, resolution, FPS, duration)
90    ///
91    /// Probes video files with ffprobe and validates metadata against
92    /// expected properties.
93    Video(VideoArgs),
94
95    /// Verify animation timing and easing curves
96    ///
97    /// Compares declared animation events against actual timing data
98    /// from rendered output.
99    Animation(AnimationArgs),
100
101    /// Run browser/WASM stress tests (Section H: Points 116-125)
102    ///
103    /// Validates system stability under concurrency stress:
104    /// - atomics: `SharedArrayBuffer` lock contention (> 10k ops/sec)
105    /// - worker-msg: Worker message throughput (> 5k msg/sec)
106    /// - render: Render loop stability (60 FPS under load)
107    /// - trace: Renacer tracing overhead (< 5%)
108    /// - full: All stress tests combined
109    Stress(StressArgs),
110
111    /// LLM inference testing: correctness, load testing, and reporting
112    ///
113    /// Test OpenAI-compatible LLM inference endpoints (realizar, ollama, llama.cpp):
114    /// - test: Run correctness tests from a YAML config
115    /// - load: Run concurrent load tests with latency/throughput metrics
116    /// - report: Generate Markdown/JSON reports from results
117    Llm(LlmArgs),
118}
119
120/// Arguments for the av-sync command
121#[derive(Parser, Debug)]
122pub struct AvSyncArgs {
123    /// AV sync subcommand
124    #[command(subcommand)]
125    pub subcommand: AvSyncSubcommand,
126}
127
128/// AV sync subcommands
129#[derive(Subcommand, Debug)]
130pub enum AvSyncSubcommand {
131    /// Check a single video against its EDL
132    Check(AvSyncCheckArgs),
133
134    /// Batch check a directory of videos
135    Report(AvSyncReportArgs),
136}
137
138/// Arguments for av-sync check
139#[derive(Parser, Debug)]
140pub struct AvSyncCheckArgs {
141    /// Path to the video file
142    pub video: PathBuf,
143
144    /// Path to EDL JSON file (default: <video>.edl.json)
145    #[arg(long)]
146    pub edl: Option<PathBuf>,
147
148    /// Tolerance in milliseconds (default: 20)
149    #[arg(long, default_value = "20")]
150    pub tolerance_ms: f64,
151
152    /// Output format
153    #[arg(long, default_value = "text")]
154    pub format: AvSyncOutputFormat,
155
156    /// Show per-tick details
157    #[arg(long)]
158    pub detailed: bool,
159}
160
161/// Arguments for av-sync report
162#[derive(Parser, Debug)]
163pub struct AvSyncReportArgs {
164    /// Directory containing rendered videos and EDL files
165    pub dir: PathBuf,
166
167    /// Tolerance in milliseconds (default: 20)
168    #[arg(long, default_value = "20")]
169    pub tolerance_ms: f64,
170
171    /// Output format
172    #[arg(long, default_value = "text")]
173    pub format: AvSyncOutputFormat,
174
175    /// Output file (default: stdout)
176    #[arg(short, long)]
177    pub output: Option<PathBuf>,
178}
179
180/// Output format for av-sync commands
181#[derive(ValueEnum, Clone, Debug, Default)]
182pub enum AvSyncOutputFormat {
183    /// Human-readable text
184    #[default]
185    Text,
186    /// JSON output
187    Json,
188}
189
190/// Arguments for the audio command
191#[derive(Parser, Debug)]
192pub struct AudioArgs {
193    /// Audio subcommand
194    #[command(subcommand)]
195    pub subcommand: AudioSubcommand,
196}
197
198/// Audio subcommands
199#[derive(Subcommand, Debug)]
200pub enum AudioSubcommand {
201    /// Check audio quality of a single video
202    Check(AudioCheckArgs),
203}
204
205/// Arguments for audio check
206#[derive(Parser, Debug)]
207pub struct AudioCheckArgs {
208    /// Path to the video file
209    pub video: PathBuf,
210
211    /// Sample rate for extraction (default: 48000)
212    #[arg(long, default_value = "48000")]
213    pub sample_rate: u32,
214
215    /// Minimum RMS level in dBFS (default: -40)
216    #[arg(long, default_value = "-40", allow_hyphen_values = true)]
217    pub min_rms_dbfs: f64,
218
219    /// Maximum peak level in dBFS (default: -0.1)
220    #[arg(long, default_value = "-0.1", allow_hyphen_values = true)]
221    pub max_peak_dbfs: f64,
222
223    /// Fail if clipping detected (default: true)
224    #[arg(long, default_value = "true")]
225    pub no_clipping: bool,
226
227    /// Output format
228    #[arg(long, default_value = "text")]
229    pub format: OutputFormat,
230}
231
232/// Arguments for the video command
233#[derive(Parser, Debug)]
234pub struct VideoArgs {
235    /// Video subcommand
236    #[command(subcommand)]
237    pub subcommand: VideoSubcommand,
238}
239
240/// Video subcommands
241#[derive(Subcommand, Debug)]
242pub enum VideoSubcommand {
243    /// Check video quality of a single file
244    Check(VideoCheckArgs),
245}
246
247/// Arguments for video check
248#[derive(Parser, Debug)]
249pub struct VideoCheckArgs {
250    /// Path to the video file
251    pub video: PathBuf,
252
253    /// Expected width
254    #[arg(long)]
255    pub width: Option<u32>,
256
257    /// Expected height
258    #[arg(long)]
259    pub height: Option<u32>,
260
261    /// Expected FPS
262    #[arg(long)]
263    pub fps: Option<f64>,
264
265    /// Expected codec
266    #[arg(long)]
267    pub codec: Option<String>,
268
269    /// Minimum duration in seconds
270    #[arg(long)]
271    pub min_duration: Option<f64>,
272
273    /// Maximum duration in seconds
274    #[arg(long)]
275    pub max_duration: Option<f64>,
276
277    /// Require audio stream
278    #[arg(long)]
279    pub require_audio: bool,
280
281    /// Output format
282    #[arg(long, default_value = "text")]
283    pub format: OutputFormat,
284}
285
286/// Arguments for the animation command
287#[derive(Parser, Debug)]
288pub struct AnimationArgs {
289    /// Animation subcommand
290    #[command(subcommand)]
291    pub subcommand: AnimationSubcommand,
292}
293
294/// Animation subcommands
295#[derive(Subcommand, Debug)]
296pub enum AnimationSubcommand {
297    /// Verify animation timeline against observed events
298    Check(AnimationCheckArgs),
299}
300
301/// Arguments for animation check
302#[derive(Parser, Debug)]
303pub struct AnimationCheckArgs {
304    /// Path to the animation timeline JSON
305    pub timeline: PathBuf,
306
307    /// Path to the observed events JSON
308    pub observed: PathBuf,
309
310    /// Tolerance in milliseconds (default: 20)
311    #[arg(long, default_value = "20")]
312    pub tolerance_ms: f64,
313
314    /// Output format
315    #[arg(long, default_value = "text")]
316    pub format: OutputFormat,
317}
318
319/// Output format for check commands
320#[derive(ValueEnum, Clone, Debug, Default)]
321pub enum OutputFormat {
322    /// Human-readable text
323    #[default]
324    Text,
325    /// JSON output
326    Json,
327}
328
329/// Arguments for the stress command
330#[derive(Parser, Debug)]
331#[allow(clippy::struct_excessive_bools)]
332pub struct StressArgs {
333    /// Stress test mode
334    #[arg(long, default_value = "atomics")]
335    pub mode: String,
336
337    /// Test duration in seconds
338    #[arg(short, long, default_value = "30")]
339    pub duration: u64,
340
341    /// Number of concurrent workers
342    #[arg(short, long, default_value = "4")]
343    pub concurrency: u32,
344
345    /// Output format (text, json)
346    #[arg(short, long, default_value = "text")]
347    pub output: String,
348
349    /// Run atomics stress test
350    #[arg(long)]
351    pub atomics: bool,
352
353    /// Run worker message stress test
354    #[arg(long)]
355    pub worker_msg: bool,
356
357    /// Run render loop stress test
358    #[arg(long)]
359    pub render: bool,
360
361    /// Run tracing overhead stress test
362    #[arg(long)]
363    pub trace: bool,
364
365    /// Run full system stress test
366    #[arg(long)]
367    pub full: bool,
368}
369
370impl StressArgs {
371    /// Get the stress mode from arguments
372    #[must_use]
373    pub fn get_mode(&self) -> String {
374        if self.atomics {
375            "atomics".to_string()
376        } else if self.worker_msg {
377            "worker-msg".to_string()
378        } else if self.render {
379            "render".to_string()
380        } else if self.trace {
381            "trace".to_string()
382        } else if self.full {
383            "full".to_string()
384        } else {
385            self.mode.clone()
386        }
387    }
388}
389
390/// Arguments for the llm command
391#[derive(Parser, Debug)]
392pub struct LlmArgs {
393    /// LLM subcommand
394    #[command(subcommand)]
395    pub subcommand: LlmSubcommand,
396}
397
398/// LLM subcommands
399#[derive(Subcommand, Debug)]
400pub enum LlmSubcommand {
401    /// Run correctness tests against an LLM endpoint
402    Test(LlmTestArgs),
403    /// Run concurrent load test against an LLM endpoint
404    Load(LlmLoadArgs),
405    /// Run full benchmark lifecycle (start, warmup, measure, compare, teardown)
406    Bench(LlmBenchArgs),
407    /// Generate reports from test results
408    Report(LlmReportArgs),
409    /// ML experiment tracking with data audits, budget gates, and early stopping
410    Experiment(ExperimentArgs),
411    /// Pre-flight data quality audit for training data
412    DataAudit(DataAuditArgs),
413    /// Sweep concurrency levels to find optimal operating point
414    Sweep(LlmSweepArgs),
415    /// Generate synthetic JSONL dataset for workload-driven benchmarking
416    GenDataset(LlmGenDatasetArgs),
417    /// Compute weighted performance scores for runtime comparison
418    Score(LlmScoreArgs),
419}
420
421/// Arguments for `probador llm test`
422#[derive(Parser, Debug)]
423pub struct LlmTestArgs {
424    /// Path to the YAML test configuration file
425    #[arg(short, long)]
426    pub config: PathBuf,
427
428    /// Base URL of the LLM API server
429    #[arg(short, long)]
430    pub url: String,
431
432    /// Model name to include in requests
433    #[arg(short, long, default_value = "default")]
434    pub model: String,
435
436    /// Runtime name for reporting (e.g., realizar, ollama, llamacpp)
437    #[arg(long, default_value = "unknown")]
438    pub runtime_name: String,
439
440    /// Output file path for JSON results
441    #[arg(short, long)]
442    pub output: Option<PathBuf>,
443}
444
445/// Arguments for `probador llm load`
446#[derive(Parser, Debug)]
447pub struct LlmLoadArgs {
448    /// Base URL of the LLM API server
449    #[arg(short, long)]
450    pub url: String,
451
452    /// Model name to include in requests
453    #[arg(short, long, default_value = "default")]
454    pub model: String,
455
456    /// Number of concurrent workers
457    #[arg(short, long, default_value = "4")]
458    pub concurrency: usize,
459
460    /// Test duration (e.g., 30s, 2m, 1h)
461    #[arg(short, long, default_value = "30s")]
462    pub duration: String,
463
464    /// Runtime name for reporting
465    #[arg(long, default_value = "unknown")]
466    pub runtime_name: String,
467
468    /// Prompt profile: micro, short, medium, long
469    #[arg(long)]
470    pub prompt_profile: Option<String>,
471
472    /// Path to YAML prompt file
473    #[arg(long)]
474    pub prompt_file: Option<PathBuf>,
475
476    /// Warmup duration before measurement (e.g., 5s, 10s). Default: no warmup.
477    #[arg(long, default_value = "0s")]
478    pub warmup: String,
479
480    /// Output file path for JSON results
481    #[arg(short, long)]
482    pub output: Option<PathBuf>,
483
484    /// Use SSE streaming for real per-token timing (TTFT, TPOT, ITL).
485    /// Use --stream false to disable.
486    #[arg(long, default_value = "true", action = clap::ArgAction::Set)]
487    pub stream: bool,
488
489    /// Target request rate (req/s). Omit for max throughput (closed-loop).
490    #[arg(long)]
491    pub rate: Option<f64>,
492
493    /// Rate distribution: poisson (default) or constant. Only used with --rate.
494    #[arg(long, default_value = "poisson")]
495    pub rate_distribution: String,
496
497    /// Number of transformer layers in the model (e.g., 28 for Qwen 1.5B).
498    /// Computes per-layer decode time for cross-runtime comparison.
499    #[arg(long)]
500    pub num_layers: Option<u32>,
501
502    /// Inline correctness validation: none, basic, contains:X, pattern:X
503    #[arg(long, default_value = "none")]
504    pub validate: String,
505
506    /// Exit non-zero if quality pass rate drops below this threshold (e.g., 0.95)
507    #[arg(long)]
508    pub fail_on_quality: Option<f64>,
509
510    /// Multiplier of median ITL for spike detection (default: 5.0)
511    #[arg(long, default_value = "5.0")]
512    pub spike_threshold: f64,
513
514    /// Enable GPU telemetry collection via nvidia-smi
515    #[arg(long)]
516    pub gpu_telemetry: bool,
517
518    /// GPU telemetry polling interval (e.g., 1s, 2s)
519    #[arg(long, default_value = "1s")]
520    pub gpu_poll_interval: String,
521
522    /// Expected GPU clock speed in MHz for throttle detection (auto-detect if omitted)
523    #[arg(long)]
524    pub expected_clock_mhz: Option<u32>,
525
526    /// Skip the pre-flight health check (not recommended)
527    #[arg(long)]
528    pub skip_health_check: bool,
529
530    /// Path to JSONL dataset file for workload-driven benchmarking
531    #[arg(long)]
532    pub dataset: Option<PathBuf>,
533
534    /// Override `max_tokens` for all requests (e.g., --max-tokens 128)
535    #[arg(long)]
536    pub max_tokens: Option<u32>,
537
538    /// Max tokens distribution: uniform:MIN,MAX or fixed:N.
539    /// Creates heterogeneous traffic with staggered completion times.
540    /// Example: --max-tokens-distribution uniform:16,128
541    #[arg(long)]
542    pub max_tokens_distribution: Option<String>,
543}
544
545/// Arguments for `probador llm bench` (full benchmark lifecycle)
546#[derive(Parser, Debug)]
547pub struct LlmBenchArgs {
548    /// Base URL of the LLM API server
549    #[arg(short, long)]
550    pub url: String,
551
552    /// Model name to include in requests
553    #[arg(short, long, default_value = "default")]
554    pub model: String,
555
556    /// Shell command to start the server (optional)
557    #[arg(long)]
558    pub start: Option<String>,
559
560    /// Maximum time to wait for server readiness (e.g., 120s)
561    #[arg(long, default_value = "120s")]
562    pub health_timeout: String,
563
564    /// Prompt profile: micro, short, medium, long
565    #[arg(long, default_value = "medium")]
566    pub prompt_profile: String,
567
568    /// Path to YAML prompt file (overrides --prompt-profile)
569    #[arg(long)]
570    pub prompt_file: Option<PathBuf>,
571
572    /// Warmup duration before measurement (e.g., 10s)
573    #[arg(long, default_value = "10s")]
574    pub warmup: String,
575
576    /// Per-run measurement duration (e.g., 60s)
577    #[arg(short, long, default_value = "60s")]
578    pub duration: String,
579
580    /// Number of concurrent workers
581    #[arg(short, long, default_value = "1")]
582    pub concurrency: usize,
583
584    /// Number of measurement runs
585    #[arg(long, default_value = "3")]
586    pub runs: usize,
587
588    /// Cooldown between runs (e.g., 5s)
589    #[arg(long, default_value = "5s")]
590    pub cooldown: String,
591
592    /// Baseline JSON file for regression detection
593    #[arg(long)]
594    pub baseline: Option<PathBuf>,
595
596    /// Percentage threshold for regression detection (exit 1 if exceeded)
597    #[arg(long)]
598    pub fail_on_regression: Option<f64>,
599
600    /// Runtime name for reporting (e.g., apr-gguf-gpu)
601    #[arg(long, default_value = "unknown")]
602    pub runtime_name: String,
603
604    /// Output file path for JSON results
605    #[arg(short, long)]
606    pub output: Option<PathBuf>,
607
608    /// Use SSE streaming for real per-token timing (TTFT, TPOT, ITL).
609    /// Use --stream false to disable.
610    #[arg(long, default_value = "true", action = clap::ArgAction::Set)]
611    pub stream: bool,
612
613    /// Trace level for brick profiler data (GH-114): brick, step, layer
614    #[arg(long)]
615    pub trace_level: Option<String>,
616
617    /// Number of transformer layers in the model (e.g., 28 for Qwen 1.5B).
618    /// Computes per-layer decode time for cross-runtime comparison.
619    #[arg(long)]
620    pub num_layers: Option<u32>,
621}
622
623/// Arguments for `probador llm report`
624#[derive(Parser, Debug)]
625pub struct LlmReportArgs {
626    /// Directory containing JSON result files
627    #[arg(short, long)]
628    pub results: PathBuf,
629
630    /// Output path for the performance Markdown table
631    #[arg(short, long, default_value = "performance.md")]
632    pub output: PathBuf,
633
634    /// Also update a README.md with the latest results
635    #[arg(long)]
636    pub update_readme: Option<PathBuf>,
637}
638
639/// Arguments for `probador llm score`
640#[derive(Parser, Debug)]
641pub struct LlmScoreArgs {
642    /// Directory containing probador JSON result files
643    #[arg(short, long)]
644    pub results: PathBuf,
645
646    /// Filter by concurrency level (scores computed per-concurrency)
647    #[arg(short, long)]
648    pub concurrency: Option<usize>,
649
650    /// Filter results by platform (matches `runtime_name` substring)
651    #[arg(long)]
652    pub platform: Option<String>,
653
654    /// Output file (default: stdout)
655    #[arg(short, long)]
656    pub output: Option<PathBuf>,
657
658    /// Output format: json, markdown, table
659    #[arg(long, default_value = "table")]
660    pub format: String,
661
662    /// Exit non-zero if any runtime scores below this grade (e.g., C+)
663    #[arg(long)]
664    pub fail_on_grade: Option<String>,
665
666    /// Include per-layer decode efficiency scores
667    #[arg(long)]
668    pub by_layer: bool,
669
670    /// Include per-prompt-profile scores (short/medium/long)
671    #[arg(long)]
672    pub by_profile: bool,
673
674    /// Include correctness scores (from inline quality validation)
675    #[arg(long)]
676    pub by_correctness: bool,
677
678    /// Include per-output-length scores (short/medium/long output)
679    #[arg(long)]
680    pub by_output_length: bool,
681
682    /// Include VRAM memory efficiency scores
683    #[arg(long)]
684    pub by_memory: bool,
685
686    /// Include cold start time scores
687    #[arg(long)]
688    pub by_cold_start: bool,
689
690    /// Include power efficiency (tok/s per watt) scores
691    #[arg(long)]
692    pub by_power: bool,
693
694    /// Include concurrency scaling curve scores
695    #[arg(long)]
696    pub by_scaling: bool,
697}
698
699/// Arguments for `probador llm experiment`
700#[derive(Parser, Debug)]
701pub struct ExperimentArgs {
702    /// Experiment subcommand
703    #[command(subcommand)]
704    pub subcommand: ExperimentSubcommand,
705}
706
707/// Experiment subcommands
708#[derive(Subcommand, Debug)]
709pub enum ExperimentSubcommand {
710    /// Initialize a new experiment
711    Init(ExperimentInitArgs),
712    /// Show experiment status
713    Status(ExperimentStatusArgs),
714    /// Compare two runs within an experiment
715    Compare(ExperimentCompareArgs),
716}
717
718/// Arguments for `probador llm experiment init`
719#[derive(Parser, Debug)]
720pub struct ExperimentInitArgs {
721    /// Experiment name
722    pub name: String,
723
724    /// Description of the experiment
725    #[arg(short, long)]
726    pub description: Option<String>,
727
728    /// Maximum GPU-hours budget
729    #[arg(long)]
730    pub max_gpu_hours: Option<f64>,
731
732    /// Maximum cost budget (USD)
733    #[arg(long)]
734    pub max_cost_usd: Option<f64>,
735
736    /// Cost per GPU-hour for budget calculation
737    #[arg(long, default_value = "3.50")]
738    pub cost_per_gpu_hour: f64,
739
740    /// Output file for experiment state
741    #[arg(short, long, default_value = "experiment.json")]
742    pub output: PathBuf,
743}
744
745/// Arguments for `probador llm experiment status`
746#[derive(Parser, Debug)]
747pub struct ExperimentStatusArgs {
748    /// Path to experiment JSON file
749    #[arg(short, long, default_value = "experiment.json")]
750    pub file: PathBuf,
751}
752
753/// Arguments for `probador llm experiment compare`
754#[derive(Parser, Debug)]
755pub struct ExperimentCompareArgs {
756    /// Path to experiment JSON file
757    #[arg(short = 'f', long, default_value = "experiment.json")]
758    pub file: PathBuf,
759
760    /// First run ID
761    pub run_a: String,
762
763    /// Second run ID
764    pub run_b: String,
765
766    /// Metric to compare (e.g., `eval_accuracy`, `eval_loss`)
767    #[arg(short, long, default_value = "eval_loss")]
768    pub metric: String,
769
770    /// Whether lower values are better (true for loss, false for accuracy)
771    #[arg(long)]
772    pub lower_is_better: bool,
773}
774
775/// Arguments for `probador llm data-audit`
776#[derive(Parser, Debug)]
777pub struct DataAuditArgs {
778    /// Path to JSONL training data file
779    pub file: PathBuf,
780
781    /// Maximum class imbalance ratio before failing (e.g., 3.0 = 3:1)
782    #[arg(long, default_value = "3.0")]
783    pub max_imbalance: f64,
784}
785
786/// Arguments for `probador llm sweep`
787#[derive(Parser, Debug)]
788pub struct LlmSweepArgs {
789    /// Base URL of the LLM API server
790    #[arg(short, long)]
791    pub url: String,
792
793    /// Model name to include in requests
794    #[arg(short, long, default_value = "default")]
795    pub model: String,
796
797    /// Concurrency levels to sweep (comma-separated)
798    #[arg(long, default_value = "1,2,4,8,16")]
799    pub concurrency_levels: String,
800
801    /// Per-level test duration (e.g., 30s)
802    #[arg(short, long, default_value = "30s")]
803    pub duration: String,
804
805    /// Warmup duration per level (e.g., 5s)
806    #[arg(long, default_value = "5s")]
807    pub warmup: String,
808
809    /// Use SSE streaming
810    #[arg(long, default_value = "true", action = clap::ArgAction::Set)]
811    pub stream: bool,
812
813    /// Runtime name for reporting
814    #[arg(long, default_value = "unknown")]
815    pub runtime_name: String,
816
817    /// P99 latency multiplier vs c=1 to declare saturated
818    #[arg(long, default_value = "2.0")]
819    pub saturation_threshold: f64,
820
821    /// Stop sweep early when saturated
822    #[arg(long, default_value = "true", action = clap::ArgAction::Set)]
823    pub early_stop: bool,
824
825    /// Prompt profile: micro, short, medium, long
826    #[arg(long)]
827    pub prompt_profile: Option<String>,
828
829    /// Path to YAML prompt file
830    #[arg(long)]
831    pub prompt_file: Option<PathBuf>,
832
833    /// Output file for sweep results JSON
834    #[arg(short, long)]
835    pub output: Option<PathBuf>,
836
837    /// Number of transformer layers
838    #[arg(long)]
839    pub num_layers: Option<u32>,
840}
841
842/// Arguments for `probador llm gen-dataset`
843#[derive(Parser, Debug)]
844pub struct LlmGenDatasetArgs {
845    /// Distribution type: uniform, lognormal
846    #[arg(long, default_value = "lognormal")]
847    pub distribution: String,
848
849    /// Mean input token count
850    #[arg(long, default_value = "128")]
851    pub input_mean: f64,
852
853    /// Stddev of input token count
854    #[arg(long, default_value = "64")]
855    pub input_stddev: f64,
856
857    /// Mean output (`max_tokens`) count
858    #[arg(long, default_value = "128")]
859    pub output_mean: f64,
860
861    /// Stddev of output (`max_tokens`) count
862    #[arg(long, default_value = "96")]
863    pub output_stddev: f64,
864
865    /// Number of entries to generate
866    #[arg(long, default_value = "1000")]
867    pub count: usize,
868
869    /// Output JSONL file path
870    #[arg(short, long, default_value = "dataset.jsonl")]
871    pub output: PathBuf,
872}
873
874/// Arguments for the test command
875#[derive(Parser, Debug)]
876#[allow(clippy::struct_excessive_bools)]
877pub struct TestArgs {
878    /// Filter tests by pattern
879    #[arg(short, long)]
880    pub filter: Option<String>,
881
882    /// Number of parallel test jobs
883    #[arg(short = 'j', long, default_value = "0")]
884    pub parallel: usize,
885
886    /// Enable coverage collection
887    #[arg(long)]
888    pub coverage: bool,
889
890    /// Enable mutation testing
891    #[arg(long)]
892    pub mutants: bool,
893
894    /// Fail fast on first error
895    #[arg(long)]
896    pub fail_fast: bool,
897
898    /// Watch mode - rerun on changes
899    #[arg(short, long)]
900    pub watch: bool,
901
902    /// Test timeout in milliseconds
903    #[arg(long, default_value = "30000")]
904    pub timeout: u64,
905
906    /// Output directory for results
907    #[arg(short, long, default_value = "target/probar")]
908    pub output: PathBuf,
909
910    /// Skip compile check before running tests
911    /// By default, probar runs `cargo test --no-run` to verify compilation
912    /// before executing playbook tests. Use this flag to bypass that check.
913    #[arg(long)]
914    pub skip_compile: bool,
915}
916
917/// Arguments for the record command
918#[derive(Parser, Debug)]
919pub struct RecordArgs {
920    /// Test to record
921    pub test: String,
922
923    /// Output format
924    #[arg(short, long, default_value = "gif")]
925    pub format: RecordFormat,
926
927    /// Output file path
928    #[arg(short, long)]
929    pub output: Option<PathBuf>,
930
931    /// Frame rate for recording (for GIF/MP4)
932    #[arg(long, default_value = "10")]
933    pub fps: u8,
934
935    /// Recording quality (1-100)
936    #[arg(long, default_value = "80")]
937    pub quality: u8,
938}
939
940/// Recording output format
941#[derive(ValueEnum, Clone, Debug, Default)]
942pub enum RecordFormat {
943    /// Animated GIF
944    #[default]
945    Gif,
946    /// PNG screenshots
947    Png,
948    /// SVG vector graphics
949    Svg,
950    /// MP4 video
951    Mp4,
952}
953
954/// Arguments for the report command
955#[derive(Parser, Debug)]
956pub struct ReportArgs {
957    /// Report format
958    #[arg(short, long, default_value = "html")]
959    pub format: ReportFormat,
960
961    /// Output directory
962    #[arg(short, long, default_value = "target/probar/reports")]
963    pub output: PathBuf,
964
965    /// Open report in browser after generation
966    #[arg(long)]
967    pub open: bool,
968}
969
970/// Report output format
971#[derive(ValueEnum, Clone, Debug, Default)]
972pub enum ReportFormat {
973    /// HTML report
974    #[default]
975    Html,
976    /// `JUnit` XML
977    Junit,
978    /// LCOV coverage
979    Lcov,
980    /// Cobertura XML coverage
981    Cobertura,
982    /// JSON
983    Json,
984}
985
986/// Arguments for the coverage command
987#[derive(Parser, Debug)]
988pub struct CoverageArgs {
989    /// Output PNG file path
990    #[arg(long)]
991    pub png: Option<PathBuf>,
992
993    /// Output JSON file path
994    #[arg(long)]
995    pub json: Option<PathBuf>,
996
997    /// Color palette (viridis, magma, heat)
998    #[arg(long, default_value = "viridis")]
999    pub palette: PaletteArg,
1000
1001    /// Include legend in PNG output
1002    #[arg(long)]
1003    pub legend: bool,
1004
1005    /// Highlight coverage gaps in red
1006    #[arg(long)]
1007    pub gaps: bool,
1008
1009    /// Title for the heatmap
1010    #[arg(long)]
1011    pub title: Option<String>,
1012
1013    /// PNG width in pixels
1014    #[arg(long, default_value = "800")]
1015    pub width: u32,
1016
1017    /// PNG height in pixels
1018    #[arg(long, default_value = "600")]
1019    pub height: u32,
1020
1021    /// Coverage data input file (JSON)
1022    #[arg(short, long)]
1023    pub input: Option<PathBuf>,
1024}
1025
1026/// Color palette argument
1027#[derive(ValueEnum, Clone, Debug, Default)]
1028pub enum PaletteArg {
1029    /// Viridis (colorblind-friendly)
1030    #[default]
1031    Viridis,
1032    /// Magma (dark to bright)
1033    Magma,
1034    /// Heat (black-red-yellow-white)
1035    Heat,
1036}
1037
1038/// Arguments for the init command
1039#[derive(Parser, Debug)]
1040pub struct InitArgs {
1041    /// Project directory (default: current directory)
1042    #[arg(default_value = ".")]
1043    pub path: PathBuf,
1044
1045    /// Force initialization even if files exist
1046    #[arg(short, long)]
1047    pub force: bool,
1048}
1049
1050/// Arguments for the config command
1051#[derive(Parser, Debug)]
1052pub struct ConfigArgs {
1053    /// Show current configuration
1054    #[arg(long)]
1055    pub show: bool,
1056
1057    /// Set a configuration value (key=value)
1058    #[arg(long)]
1059    pub set: Option<String>,
1060
1061    /// Reset to default configuration
1062    #[arg(long)]
1063    pub reset: bool,
1064}
1065
1066/// Arguments for the serve command
1067#[derive(Parser, Debug)]
1068#[allow(clippy::struct_excessive_bools)]
1069pub struct ServeArgs {
1070    /// Subcommand for serve (tree, viz, score)
1071    #[command(subcommand)]
1072    pub subcommand: Option<ServeSubcommand>,
1073
1074    /// Directory to serve (default: current directory)
1075    #[arg(short = 'd', long = "dir", default_value = ".")]
1076    pub directory: PathBuf,
1077
1078    /// HTTP port to listen on
1079    #[arg(short, long, default_value = "8080")]
1080    pub port: u16,
1081
1082    /// WebSocket port for hot reload
1083    #[arg(long, default_value = "8081")]
1084    pub ws_port: u16,
1085
1086    /// Open browser automatically
1087    #[arg(long)]
1088    pub open: bool,
1089
1090    /// Enable CORS for cross-origin requests
1091    #[arg(long)]
1092    pub cors: bool,
1093
1094    /// Enable Cross-Origin Isolation (COOP/COEP headers)
1095    ///
1096    /// Required for `SharedArrayBuffer` and parallel WASM with Web Workers.
1097    /// Sets Cross-Origin-Opener-Policy: same-origin and
1098    /// Cross-Origin-Embedder-Policy: require-corp headers.
1099    #[arg(long)]
1100    pub cross_origin_isolated: bool,
1101
1102    /// Enable debug mode with verbose request/response logging
1103    #[arg(long)]
1104    pub debug: bool,
1105
1106    /// Enable content linting (HTML/CSS/JS validation)
1107    #[arg(long)]
1108    pub lint: bool,
1109
1110    /// Enable file watching for hot reload (default: true)
1111    #[arg(long, default_value = "true")]
1112    pub watch: bool,
1113
1114    /// Validate module imports before serving
1115    ///
1116    /// Scans HTML files for JS/WASM imports and verifies they resolve
1117    /// with correct MIME types. Fails if any imports are broken.
1118    #[arg(long)]
1119    pub validate: bool,
1120
1121    /// Monitor requests and warn about issues (404s, MIME mismatches)
1122    #[arg(long)]
1123    pub monitor: bool,
1124
1125    /// Exclude directories from validation (e.g., `node_modules`)
1126    ///
1127    /// Can be specified multiple times: --exclude `node_modules` --exclude vendor
1128    #[arg(long, value_name = "DIR")]
1129    pub exclude: Vec<String>,
1130}
1131
1132/// Serve subcommands
1133#[derive(Subcommand, Debug, Clone)]
1134pub enum ServeSubcommand {
1135    /// Display file tree of served directory
1136    Tree(TreeArgs),
1137
1138    /// Interactive TUI visualization of served files
1139    Viz(VizArgs),
1140
1141    /// Generate project testing score (0-100)
1142    Score(ScoreArgs),
1143}
1144
1145/// Arguments for the tree subcommand
1146#[derive(Parser, Debug, Clone)]
1147pub struct TreeArgs {
1148    /// Directory to display (default: current directory)
1149    #[arg(default_value = ".")]
1150    pub path: PathBuf,
1151
1152    /// Maximum depth to display (0 = root only)
1153    #[arg(long)]
1154    pub depth: Option<usize>,
1155
1156    /// Filter files by glob pattern (e.g., "*.html")
1157    #[arg(long)]
1158    pub filter: Option<String>,
1159
1160    /// Show file sizes
1161    #[arg(long, default_value = "true")]
1162    pub sizes: bool,
1163
1164    /// Show MIME types
1165    #[arg(long, default_value = "true")]
1166    pub mime_types: bool,
1167}
1168
1169/// Arguments for the viz subcommand
1170#[derive(Parser, Debug, Clone)]
1171pub struct VizArgs {
1172    /// Directory to visualize (default: current directory)
1173    #[arg(default_value = ".")]
1174    pub path: PathBuf,
1175
1176    /// HTTP port for TUI server
1177    #[arg(short, long, default_value = "8080")]
1178    pub port: u16,
1179}
1180
1181/// Arguments for the score subcommand
1182#[derive(Parser, Debug, Clone)]
1183pub struct ScoreArgs {
1184    /// Project directory to score (default: current directory)
1185    #[arg(default_value = ".")]
1186    pub path: PathBuf,
1187
1188    /// Minimum score threshold (exit non-zero if below)
1189    #[arg(long)]
1190    pub min: Option<u32>,
1191
1192    /// Output format
1193    #[arg(long, default_value = "text")]
1194    pub format: ScoreOutputFormat,
1195
1196    /// Output HTML report to file
1197    #[arg(long)]
1198    pub report: Option<PathBuf>,
1199
1200    /// Show detailed breakdown of all criteria
1201    #[arg(long)]
1202    pub detailed: bool,
1203
1204    /// Append score to history file (JSONL)
1205    #[arg(long)]
1206    pub history: Option<PathBuf>,
1207
1208    /// Show score trend over time
1209    #[arg(long)]
1210    pub trend: bool,
1211
1212    /// Run LIVE browser validation (starts server, launches headless browser)
1213    ///
1214    /// This actually tests if the app works rather than just checking for files.
1215    /// Requires Chrome/Chromium installed. Recommended for accurate scoring.
1216    #[arg(long)]
1217    pub live: bool,
1218
1219    /// Port for live validation server (default: random available port)
1220    #[arg(long, default_value = "0")]
1221    pub port: u16,
1222}
1223
1224/// Output format for score command
1225#[derive(ValueEnum, Clone, Debug, Default)]
1226pub enum ScoreOutputFormat {
1227    /// Human-readable text
1228    #[default]
1229    Text,
1230    /// JSON output for CI integration
1231    Json,
1232}
1233
1234/// Arguments for the build command
1235#[derive(Parser, Debug)]
1236pub struct BuildArgs {
1237    /// Package directory (default: current directory)
1238    #[arg(default_value = ".")]
1239    pub path: PathBuf,
1240
1241    /// Build target (web, bundler, nodejs, no-modules)
1242    #[arg(short, long, default_value = "web")]
1243    pub target: WasmTarget,
1244
1245    /// Build in release mode
1246    #[arg(long)]
1247    pub release: bool,
1248
1249    /// Output directory (default: pkg)
1250    #[arg(short, long)]
1251    pub out_dir: Option<PathBuf>,
1252
1253    /// Enable profiling (adds names section to WASM)
1254    #[arg(long)]
1255    pub profiling: bool,
1256
1257    // ========================================================================
1258    // Zero-Artifact Architecture (PROBAR-SPEC-009-P7)
1259    // ========================================================================
1260    /// Generate web artifacts from brick definitions (PROBAR-SPEC-009-P7)
1261    ///
1262    /// Path to Rust file containing #[brick] definitions.
1263    /// Generates: index.html, style.css, main.js, worker.js, audio-worklet.js
1264    #[arg(long)]
1265    pub bricks: Option<PathBuf>,
1266
1267    /// Application name for generated artifacts
1268    #[arg(long, default_value = "app")]
1269    pub app_name: String,
1270
1271    /// WASM module path for generated HTML
1272    #[arg(long, default_value = "./pkg/app.js")]
1273    pub wasm_module: String,
1274
1275    /// Model path for worker (if applicable)
1276    #[arg(long)]
1277    pub model_path: Option<String>,
1278
1279    /// Page title for generated HTML
1280    #[arg(long)]
1281    pub title: Option<String>,
1282
1283    /// Verify generated artifacts match brick assertions
1284    #[arg(long)]
1285    pub verify: bool,
1286}
1287
1288/// WASM build target
1289#[derive(ValueEnum, Clone, Debug, Default)]
1290pub enum WasmTarget {
1291    /// ES modules for web browsers
1292    #[default]
1293    Web,
1294    /// `CommonJS` for bundlers like webpack
1295    Bundler,
1296    /// `Node.js` modules
1297    Nodejs,
1298    /// No ES modules (legacy)
1299    NoModules,
1300}
1301
1302impl WasmTarget {
1303    /// Get `wasm-pack` target string
1304    #[must_use]
1305    pub const fn as_str(&self) -> &'static str {
1306        match self {
1307            Self::Web => "web",
1308            Self::Bundler => "bundler",
1309            Self::Nodejs => "nodejs",
1310            Self::NoModules => "no-modules",
1311        }
1312    }
1313}
1314
1315/// Arguments for the watch command
1316#[derive(Parser, Debug)]
1317pub struct WatchArgs {
1318    /// Package directory to watch (default: current directory)
1319    #[arg(default_value = ".")]
1320    pub path: PathBuf,
1321
1322    /// Also start the dev server
1323    #[arg(long)]
1324    pub serve: bool,
1325
1326    /// Server port (when --serve is used)
1327    #[arg(short, long, default_value = "8080")]
1328    pub port: u16,
1329
1330    /// WebSocket port for hot reload
1331    #[arg(long, default_value = "8081")]
1332    pub ws_port: u16,
1333
1334    /// Build target
1335    #[arg(short, long, default_value = "web")]
1336    pub target: WasmTarget,
1337
1338    /// Build in release mode
1339    #[arg(long)]
1340    pub release: bool,
1341
1342    /// Debounce delay in milliseconds
1343    #[arg(long, default_value = "500")]
1344    pub debounce: u64,
1345}
1346
1347/// Arguments for the playbook command
1348#[derive(Parser, Debug)]
1349#[allow(clippy::struct_excessive_bools)]
1350pub struct PlaybookArgs {
1351    /// Playbook YAML file(s) to run
1352    #[arg(required = true)]
1353    pub files: Vec<PathBuf>,
1354
1355    /// Validate playbook without running
1356    #[arg(long)]
1357    pub validate: bool,
1358
1359    /// Export state machine diagram
1360    #[arg(long, value_enum)]
1361    pub export: Option<DiagramFormat>,
1362
1363    /// Output file for diagram export
1364    #[arg(long)]
1365    pub export_output: Option<PathBuf>,
1366
1367    /// Run mutation testing (M1-M5)
1368    #[arg(long)]
1369    pub mutate: bool,
1370
1371    /// Mutation classes to run (e.g., M1,M2)
1372    #[arg(long, value_delimiter = ',')]
1373    pub mutation_classes: Option<Vec<String>>,
1374
1375    /// Fail fast on first error
1376    #[arg(long)]
1377    pub fail_fast: bool,
1378
1379    /// Continue on step failure
1380    #[arg(long)]
1381    pub continue_on_error: bool,
1382
1383    /// Output format for results
1384    #[arg(short, long, default_value = "text")]
1385    pub format: PlaybookOutputFormat,
1386
1387    /// Output directory for results
1388    #[arg(short, long, default_value = "target/probar/playbooks")]
1389    pub output: PathBuf,
1390}
1391
1392/// Diagram export format
1393#[derive(ValueEnum, Clone, Debug)]
1394pub enum DiagramFormat {
1395    /// DOT format (Graphviz)
1396    Dot,
1397    /// SVG format
1398    Svg,
1399}
1400
1401/// Output format for playbook results
1402#[derive(ValueEnum, Clone, Debug, Default)]
1403pub enum PlaybookOutputFormat {
1404    /// Human-readable text
1405    #[default]
1406    Text,
1407    /// JSON output
1408    Json,
1409    /// `JUnit` XML
1410    Junit,
1411}
1412
1413/// Arguments for the comply command
1414#[derive(Parser, Debug)]
1415#[allow(clippy::struct_excessive_bools)]
1416pub struct ComplyArgs {
1417    /// Comply subcommand (check, migrate, diff, enforce, report)
1418    #[command(subcommand)]
1419    pub subcommand: Option<ComplySubcommand>,
1420
1421    /// Directory to check (default: current directory)
1422    #[arg(default_value = ".")]
1423    pub path: PathBuf,
1424
1425    /// Specific checks to run (e.g., C001,C002)
1426    #[arg(long, value_delimiter = ',')]
1427    pub checks: Option<Vec<String>>,
1428
1429    /// Fail on first non-compliance
1430    #[arg(long)]
1431    pub fail_fast: bool,
1432
1433    /// Output format
1434    #[arg(long, default_value = "text")]
1435    pub format: ComplyOutputFormat,
1436
1437    /// Maximum WASM binary size in bytes (for C009)
1438    #[arg(long, default_value = "5242880")]
1439    pub max_wasm_size: usize,
1440
1441    /// Enable strict mode (all checks must pass)
1442    #[arg(long)]
1443    pub strict: bool,
1444
1445    /// Generate compliance report file
1446    #[arg(long)]
1447    pub report: Option<PathBuf>,
1448
1449    /// Show detailed check results
1450    #[arg(long)]
1451    pub detailed: bool,
1452}
1453
1454/// Comply subcommands (per PROBAR-SPEC-011 Section 3.1)
1455#[derive(Subcommand, Debug, Clone)]
1456pub enum ComplySubcommand {
1457    /// Check WASM testing compliance
1458    Check(ComplyCheckArgs),
1459
1460    /// Migrate to latest probador standards
1461    Migrate(ComplyMigrateArgs),
1462
1463    /// Show changelog between versions
1464    Diff(ComplyDiffArgs),
1465
1466    /// Install WASM quality hooks
1467    Enforce(ComplyEnforceArgs),
1468
1469    /// Generate compliance report
1470    Report(ComplyReportArgs),
1471}
1472
1473/// Arguments for comply check subcommand
1474#[derive(Parser, Debug, Clone)]
1475#[allow(clippy::struct_excessive_bools)]
1476pub struct ComplyCheckArgs {
1477    /// Directory to check (default: current directory)
1478    #[arg(default_value = ".")]
1479    pub path: PathBuf,
1480
1481    /// Exit with error if non-compliant
1482    #[arg(long)]
1483    pub strict: bool,
1484
1485    /// Output format
1486    #[arg(long, default_value = "text")]
1487    pub format: ComplyOutputFormat,
1488
1489    /// Specific checks to run (e.g., C001,C002)
1490    #[arg(long, value_delimiter = ',')]
1491    pub checks: Option<Vec<String>>,
1492
1493    /// Show detailed check results
1494    #[arg(long)]
1495    pub detailed: bool,
1496}
1497
1498/// Arguments for comply migrate subcommand
1499#[derive(Parser, Debug, Clone)]
1500pub struct ComplyMigrateArgs {
1501    /// Directory to migrate (default: current directory)
1502    #[arg(default_value = ".")]
1503    pub path: PathBuf,
1504
1505    /// Target version to migrate to
1506    #[arg(long)]
1507    pub version: Option<String>,
1508
1509    /// Preview changes without applying
1510    #[arg(long)]
1511    pub dry_run: bool,
1512
1513    /// Force migration even with uncommitted changes
1514    #[arg(long)]
1515    pub force: bool,
1516}
1517
1518/// Arguments for comply diff subcommand
1519#[derive(Parser, Debug, Clone)]
1520pub struct ComplyDiffArgs {
1521    /// From version
1522    #[arg(long)]
1523    pub from: Option<String>,
1524
1525    /// To version
1526    #[arg(long)]
1527    pub to: Option<String>,
1528
1529    /// Show only breaking changes
1530    #[arg(long)]
1531    pub breaking_only: bool,
1532}
1533
1534/// Arguments for comply enforce subcommand
1535#[derive(Parser, Debug, Clone)]
1536pub struct ComplyEnforceArgs {
1537    /// Directory to enforce (default: current directory)
1538    #[arg(default_value = ".")]
1539    pub path: PathBuf,
1540
1541    /// Skip confirmation
1542    #[arg(long)]
1543    pub yes: bool,
1544
1545    /// Remove hooks instead of installing
1546    #[arg(long)]
1547    pub disable: bool,
1548}
1549
1550/// Arguments for comply report subcommand
1551#[derive(Parser, Debug, Clone)]
1552pub struct ComplyReportArgs {
1553    /// Directory to report on (default: current directory)
1554    #[arg(default_value = ".")]
1555    pub path: PathBuf,
1556
1557    /// Output format
1558    #[arg(long, default_value = "text")]
1559    pub format: ComplyReportFormat,
1560
1561    /// Output file (default: stdout)
1562    #[arg(long)]
1563    pub output: Option<PathBuf>,
1564}
1565
1566/// Output format for comply report
1567#[derive(ValueEnum, Clone, Debug, Default)]
1568pub enum ComplyReportFormat {
1569    /// Human-readable text
1570    #[default]
1571    Text,
1572    /// JSON output
1573    Json,
1574    /// Markdown
1575    Markdown,
1576    /// HTML
1577    Html,
1578}
1579
1580/// Output format for comply command
1581#[derive(ValueEnum, Clone, Debug, Default)]
1582pub enum ComplyOutputFormat {
1583    /// Human-readable text
1584    #[default]
1585    Text,
1586    /// JSON output for CI integration
1587    Json,
1588    /// `JUnit` XML for CI systems
1589    Junit,
1590}
1591
1592/// Color argument for CLI
1593#[derive(ValueEnum, Clone, Debug, Default)]
1594pub enum ColorArg {
1595    /// Automatic color detection
1596    #[default]
1597    Auto,
1598    /// Always use colors
1599    Always,
1600    /// Never use colors
1601    Never,
1602}
1603
1604impl From<ColorArg> for crate::config::ColorChoice {
1605    fn from(arg: ColorArg) -> Self {
1606        match arg {
1607            ColorArg::Auto => Self::Auto,
1608            ColorArg::Always => Self::Always,
1609            ColorArg::Never => Self::Never,
1610        }
1611    }
1612}
1613
1614#[cfg(test)]
1615#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
1616mod tests {
1617    use super::*;
1618
1619    mod cli_tests {
1620        use super::*;
1621
1622        #[test]
1623        fn test_parse_test_command() {
1624            let cli = Cli::parse_from(["probar", "test"]);
1625            assert!(matches!(cli.command, Commands::Test(_)));
1626        }
1627
1628        #[test]
1629        fn test_parse_test_with_filter() {
1630            let cli = Cli::parse_from(["probar", "test", "--filter", "game::*"]);
1631            if let Commands::Test(args) = cli.command {
1632                assert_eq!(args.filter, Some("game::*".to_string()));
1633            } else {
1634                panic!("expected Test command");
1635            }
1636        }
1637
1638        #[test]
1639        fn test_parse_test_with_parallel() {
1640            let cli = Cli::parse_from(["probar", "test", "-j", "4"]);
1641            if let Commands::Test(args) = cli.command {
1642                assert_eq!(args.parallel, 4);
1643            } else {
1644                panic!("expected Test command");
1645            }
1646        }
1647
1648        #[test]
1649        fn test_parse_test_with_coverage() {
1650            let cli = Cli::parse_from(["probar", "test", "--coverage"]);
1651            if let Commands::Test(args) = cli.command {
1652                assert!(args.coverage);
1653            } else {
1654                panic!("expected Test command");
1655            }
1656        }
1657
1658        #[test]
1659        fn test_parse_test_with_fail_fast() {
1660            let cli = Cli::parse_from(["probar", "test", "--fail-fast"]);
1661            if let Commands::Test(args) = cli.command {
1662                assert!(args.fail_fast);
1663            } else {
1664                panic!("expected Test command");
1665            }
1666        }
1667
1668        #[test]
1669        fn test_parse_record_command() {
1670            let cli = Cli::parse_from(["probar", "record", "test_login"]);
1671            if let Commands::Record(args) = cli.command {
1672                assert_eq!(args.test, "test_login");
1673            } else {
1674                panic!("expected Record command");
1675            }
1676        }
1677
1678        #[test]
1679        fn test_parse_record_with_format() {
1680            let cli = Cli::parse_from(["probar", "record", "test_login", "--format", "png"]);
1681            if let Commands::Record(args) = cli.command {
1682                assert!(matches!(args.format, RecordFormat::Png));
1683            } else {
1684                panic!("expected Record command");
1685            }
1686        }
1687
1688        #[test]
1689        fn test_parse_report_command() {
1690            let cli = Cli::parse_from(["probar", "report"]);
1691            assert!(matches!(cli.command, Commands::Report(_)));
1692        }
1693
1694        #[test]
1695        fn test_parse_report_with_format() {
1696            let cli = Cli::parse_from(["probar", "report", "--format", "lcov"]);
1697            if let Commands::Report(args) = cli.command {
1698                assert!(matches!(args.format, ReportFormat::Lcov));
1699            } else {
1700                panic!("expected Report command");
1701            }
1702        }
1703
1704        #[test]
1705        fn test_parse_init_command() {
1706            let cli = Cli::parse_from(["probar", "init"]);
1707            assert!(matches!(cli.command, Commands::Init(_)));
1708        }
1709
1710        #[test]
1711        fn test_parse_config_command() {
1712            let cli = Cli::parse_from(["probar", "config", "--show"]);
1713            if let Commands::Config(args) = cli.command {
1714                assert!(args.show);
1715            } else {
1716                panic!("expected Config command");
1717            }
1718        }
1719
1720        #[test]
1721        fn test_global_verbose_flag() {
1722            let cli = Cli::parse_from(["probar", "-vvv", "test"]);
1723            assert_eq!(cli.verbose, 3);
1724        }
1725
1726        #[test]
1727        fn test_global_quiet_flag() {
1728            let cli = Cli::parse_from(["probar", "-q", "test"]);
1729            assert!(cli.quiet);
1730        }
1731
1732        #[test]
1733        fn test_global_color_flag() {
1734            let cli = Cli::parse_from(["probar", "--color", "never", "test"]);
1735            assert!(matches!(cli.color, ColorArg::Never));
1736        }
1737    }
1738
1739    mod format_tests {
1740        use super::*;
1741
1742        #[test]
1743        fn test_record_format_default() {
1744            let format = RecordFormat::default();
1745            assert!(matches!(format, RecordFormat::Gif));
1746        }
1747
1748        #[test]
1749        fn test_report_format_default() {
1750            let format = ReportFormat::default();
1751            assert!(matches!(format, ReportFormat::Html));
1752        }
1753
1754        #[test]
1755        fn test_color_arg_conversion() {
1756            use crate::config::ColorChoice;
1757
1758            let auto: ColorChoice = ColorArg::Auto.into();
1759            assert!(matches!(auto, ColorChoice::Auto));
1760
1761            let always: ColorChoice = ColorArg::Always.into();
1762            assert!(matches!(always, ColorChoice::Always));
1763
1764            let never: ColorChoice = ColorArg::Never.into();
1765            assert!(matches!(never, ColorChoice::Never));
1766        }
1767    }
1768
1769    mod record_format_tests {
1770        use super::*;
1771
1772        #[test]
1773        fn test_default() {
1774            let format = RecordFormat::default();
1775            assert!(matches!(format, RecordFormat::Gif));
1776        }
1777
1778        #[test]
1779        fn test_all_variants() {
1780            let _ = RecordFormat::Gif;
1781            let _ = RecordFormat::Png;
1782            let _ = RecordFormat::Svg;
1783            let _ = RecordFormat::Mp4;
1784        }
1785
1786        #[test]
1787        fn test_debug() {
1788            let debug = format!("{:?}", RecordFormat::Gif);
1789            assert!(debug.contains("Gif"));
1790        }
1791
1792        #[test]
1793        fn test_clone() {
1794            let format = RecordFormat::Mp4;
1795            let cloned = format;
1796            assert!(matches!(cloned, RecordFormat::Mp4));
1797        }
1798    }
1799
1800    mod report_format_tests {
1801        use super::*;
1802
1803        #[test]
1804        fn test_default() {
1805            let format = ReportFormat::default();
1806            assert!(matches!(format, ReportFormat::Html));
1807        }
1808
1809        #[test]
1810        fn test_all_variants() {
1811            let _ = ReportFormat::Html;
1812            let _ = ReportFormat::Junit;
1813            let _ = ReportFormat::Lcov;
1814            let _ = ReportFormat::Cobertura;
1815            let _ = ReportFormat::Json;
1816        }
1817
1818        #[test]
1819        fn test_debug() {
1820            let debug = format!("{:?}", ReportFormat::Junit);
1821            assert!(debug.contains("Junit"));
1822        }
1823    }
1824
1825    mod test_args_tests {
1826        use super::*;
1827
1828        #[test]
1829        fn test_defaults() {
1830            // Verify TestArgs can be created with defaults via clap
1831            let args = TestArgs {
1832                filter: None,
1833                parallel: 0,
1834                coverage: false,
1835                mutants: false,
1836                fail_fast: false,
1837                watch: false,
1838                timeout: 30000,
1839                output: PathBuf::from("target/probar"),
1840                skip_compile: false,
1841            };
1842            assert!(!args.coverage);
1843            assert_eq!(args.timeout, 30000);
1844        }
1845
1846        #[test]
1847        fn test_debug() {
1848            let args = TestArgs {
1849                filter: Some("test_*".to_string()),
1850                parallel: 4,
1851                coverage: true,
1852                mutants: false,
1853                fail_fast: true,
1854                watch: false,
1855                timeout: 5000,
1856                output: PathBuf::from("target"),
1857                skip_compile: false,
1858            };
1859            let debug = format!("{args:?}");
1860            assert!(debug.contains("TestArgs"));
1861        }
1862
1863        #[test]
1864        fn test_skip_compile_flag() {
1865            let args = TestArgs {
1866                filter: None,
1867                parallel: 0,
1868                coverage: false,
1869                mutants: false,
1870                fail_fast: false,
1871                watch: false,
1872                timeout: 30000,
1873                output: PathBuf::from("target/probar"),
1874                skip_compile: true,
1875            };
1876            assert!(args.skip_compile);
1877        }
1878    }
1879
1880    mod record_args_tests {
1881        use super::*;
1882
1883        #[test]
1884        fn test_creation() {
1885            let args = RecordArgs {
1886                test: "my_test".to_string(),
1887                format: RecordFormat::Gif,
1888                output: None,
1889                fps: 10,
1890                quality: 80,
1891            };
1892            assert_eq!(args.test, "my_test");
1893            assert_eq!(args.fps, 10);
1894        }
1895
1896        #[test]
1897        fn test_debug() {
1898            let args = RecordArgs {
1899                test: "test".to_string(),
1900                format: RecordFormat::Png,
1901                output: Some(PathBuf::from("out.png")),
1902                fps: 30,
1903                quality: 100,
1904            };
1905            let debug = format!("{args:?}");
1906            assert!(debug.contains("RecordArgs"));
1907        }
1908    }
1909
1910    mod report_args_tests {
1911        use super::*;
1912
1913        #[test]
1914        fn test_creation() {
1915            let args = ReportArgs {
1916                format: ReportFormat::Lcov,
1917                output: PathBuf::from("coverage"),
1918                open: true,
1919            };
1920            assert!(args.open);
1921        }
1922
1923        #[test]
1924        fn test_debug() {
1925            let args = ReportArgs {
1926                format: ReportFormat::Html,
1927                output: PathBuf::from("reports"),
1928                open: false,
1929            };
1930            let debug = format!("{args:?}");
1931            assert!(debug.contains("ReportArgs"));
1932        }
1933    }
1934
1935    mod init_args_tests {
1936        use super::*;
1937
1938        #[test]
1939        fn test_creation() {
1940            let args = InitArgs {
1941                path: PathBuf::from("."),
1942                force: false,
1943            };
1944            assert!(!args.force);
1945        }
1946    }
1947
1948    mod config_args_tests {
1949        use super::*;
1950
1951        #[test]
1952        fn test_creation() {
1953            let args = ConfigArgs {
1954                show: false,
1955                set: None,
1956                reset: false,
1957            };
1958            assert!(!args.show);
1959        }
1960    }
1961
1962    mod cli_additional_tests {
1963        use super::*;
1964
1965        #[test]
1966        fn test_cli_debug() {
1967            let cli = Cli {
1968                verbose: 0,
1969                quiet: false,
1970                color: ColorArg::Auto,
1971                command: Commands::Config(ConfigArgs {
1972                    show: true,
1973                    set: None,
1974                    reset: false,
1975                }),
1976            };
1977            let debug = format!("{cli:?}");
1978            assert!(debug.contains("Cli"));
1979        }
1980    }
1981
1982    mod coverage_tests {
1983        use super::*;
1984
1985        #[test]
1986        fn test_parse_coverage_command() {
1987            let cli = Cli::parse_from(["probar", "coverage"]);
1988            assert!(matches!(cli.command, Commands::Coverage(_)));
1989        }
1990
1991        #[test]
1992        fn test_parse_coverage_with_png() {
1993            let cli = Cli::parse_from(["probar", "coverage", "--png", "output.png"]);
1994            if let Commands::Coverage(args) = cli.command {
1995                assert_eq!(args.png, Some(PathBuf::from("output.png")));
1996            } else {
1997                panic!("expected Coverage command");
1998            }
1999        }
2000
2001        #[test]
2002        fn test_parse_coverage_with_palette() {
2003            let cli = Cli::parse_from(["probar", "coverage", "--palette", "magma"]);
2004            if let Commands::Coverage(args) = cli.command {
2005                assert!(matches!(args.palette, PaletteArg::Magma));
2006            } else {
2007                panic!("expected Coverage command");
2008            }
2009        }
2010
2011        #[test]
2012        fn test_parse_coverage_with_legend() {
2013            let cli = Cli::parse_from(["probar", "coverage", "--legend"]);
2014            if let Commands::Coverage(args) = cli.command {
2015                assert!(args.legend);
2016            } else {
2017                panic!("expected Coverage command");
2018            }
2019        }
2020
2021        #[test]
2022        fn test_parse_coverage_with_gaps() {
2023            let cli = Cli::parse_from(["probar", "coverage", "--gaps"]);
2024            if let Commands::Coverage(args) = cli.command {
2025                assert!(args.gaps);
2026            } else {
2027                panic!("expected Coverage command");
2028            }
2029        }
2030
2031        #[test]
2032        fn test_parse_coverage_with_title() {
2033            let cli = Cli::parse_from(["probar", "coverage", "--title", "My Coverage"]);
2034            if let Commands::Coverage(args) = cli.command {
2035                assert_eq!(args.title, Some("My Coverage".to_string()));
2036            } else {
2037                panic!("expected Coverage command");
2038            }
2039        }
2040
2041        #[test]
2042        fn test_parse_coverage_with_dimensions() {
2043            let cli = Cli::parse_from(["probar", "coverage", "--width", "1024", "--height", "768"]);
2044            if let Commands::Coverage(args) = cli.command {
2045                assert_eq!(args.width, 1024);
2046                assert_eq!(args.height, 768);
2047            } else {
2048                panic!("expected Coverage command");
2049            }
2050        }
2051
2052        #[test]
2053        fn test_parse_coverage_full_options() {
2054            let cli = Cli::parse_from([
2055                "probar",
2056                "coverage",
2057                "--png",
2058                "heatmap.png",
2059                "--palette",
2060                "heat",
2061                "--legend",
2062                "--gaps",
2063                "--title",
2064                "Test Coverage",
2065                "--width",
2066                "1920",
2067                "--height",
2068                "1080",
2069            ]);
2070            if let Commands::Coverage(args) = cli.command {
2071                assert_eq!(args.png, Some(PathBuf::from("heatmap.png")));
2072                assert!(matches!(args.palette, PaletteArg::Heat));
2073                assert!(args.legend);
2074                assert!(args.gaps);
2075                assert_eq!(args.title, Some("Test Coverage".to_string()));
2076                assert_eq!(args.width, 1920);
2077                assert_eq!(args.height, 1080);
2078            } else {
2079                panic!("expected Coverage command");
2080            }
2081        }
2082
2083        #[test]
2084        fn test_palette_default() {
2085            let palette = PaletteArg::default();
2086            assert!(matches!(palette, PaletteArg::Viridis));
2087        }
2088
2089        #[test]
2090        fn test_coverage_args_defaults() {
2091            let args = CoverageArgs {
2092                png: None,
2093                json: None,
2094                palette: PaletteArg::default(),
2095                legend: false,
2096                gaps: false,
2097                title: None,
2098                width: 800,
2099                height: 600,
2100                input: None,
2101            };
2102            assert_eq!(args.width, 800);
2103            assert_eq!(args.height, 600);
2104            assert!(matches!(args.palette, PaletteArg::Viridis));
2105        }
2106
2107        #[test]
2108        fn test_coverage_args_debug() {
2109            let args = CoverageArgs {
2110                png: Some(PathBuf::from("test.png")),
2111                json: None,
2112                palette: PaletteArg::Magma,
2113                legend: true,
2114                gaps: true,
2115                title: Some("Test".to_string()),
2116                width: 640,
2117                height: 480,
2118                input: None,
2119            };
2120            let debug = format!("{args:?}");
2121            assert!(debug.contains("CoverageArgs"));
2122        }
2123    }
2124
2125    mod playbook_tests {
2126        use super::*;
2127
2128        #[test]
2129        fn test_parse_playbook_command() {
2130            let cli = Cli::parse_from(["probar", "playbook", "test.yaml"]);
2131            assert!(matches!(cli.command, Commands::Playbook(_)));
2132        }
2133
2134        #[test]
2135        fn test_parse_playbook_multiple_files() {
2136            let cli = Cli::parse_from(["probar", "playbook", "a.yaml", "b.yaml", "c.yaml"]);
2137            if let Commands::Playbook(args) = cli.command {
2138                assert_eq!(args.files.len(), 3);
2139            } else {
2140                panic!("expected Playbook command");
2141            }
2142        }
2143
2144        #[test]
2145        fn test_parse_playbook_validate() {
2146            let cli = Cli::parse_from(["probar", "playbook", "test.yaml", "--validate"]);
2147            if let Commands::Playbook(args) = cli.command {
2148                assert!(args.validate);
2149            } else {
2150                panic!("expected Playbook command");
2151            }
2152        }
2153
2154        #[test]
2155        fn test_parse_playbook_export_dot() {
2156            let cli = Cli::parse_from(["probar", "playbook", "test.yaml", "--export", "dot"]);
2157            if let Commands::Playbook(args) = cli.command {
2158                assert!(matches!(args.export, Some(DiagramFormat::Dot)));
2159            } else {
2160                panic!("expected Playbook command");
2161            }
2162        }
2163
2164        #[test]
2165        fn test_parse_playbook_export_svg() {
2166            let cli = Cli::parse_from([
2167                "probar",
2168                "playbook",
2169                "test.yaml",
2170                "--export",
2171                "svg",
2172                "--export-output",
2173                "diagram.svg",
2174            ]);
2175            if let Commands::Playbook(args) = cli.command {
2176                assert!(matches!(args.export, Some(DiagramFormat::Svg)));
2177                assert_eq!(args.export_output, Some(PathBuf::from("diagram.svg")));
2178            } else {
2179                panic!("expected Playbook command");
2180            }
2181        }
2182
2183        #[test]
2184        fn test_parse_playbook_mutate() {
2185            let cli = Cli::parse_from(["probar", "playbook", "test.yaml", "--mutate"]);
2186            if let Commands::Playbook(args) = cli.command {
2187                assert!(args.mutate);
2188            } else {
2189                panic!("expected Playbook command");
2190            }
2191        }
2192
2193        #[test]
2194        fn test_parse_playbook_mutation_classes() {
2195            let cli = Cli::parse_from([
2196                "probar",
2197                "playbook",
2198                "test.yaml",
2199                "--mutate",
2200                "--mutation-classes",
2201                "M1,M2,M3",
2202            ]);
2203            if let Commands::Playbook(args) = cli.command {
2204                assert!(args.mutate);
2205                let classes = args.mutation_classes.expect("mutation classes");
2206                assert_eq!(classes.len(), 3);
2207                assert!(classes.contains(&"M1".to_string()));
2208                assert!(classes.contains(&"M2".to_string()));
2209                assert!(classes.contains(&"M3".to_string()));
2210            } else {
2211                panic!("expected Playbook command");
2212            }
2213        }
2214
2215        #[test]
2216        fn test_parse_playbook_fail_fast() {
2217            let cli = Cli::parse_from(["probar", "playbook", "test.yaml", "--fail-fast"]);
2218            if let Commands::Playbook(args) = cli.command {
2219                assert!(args.fail_fast);
2220            } else {
2221                panic!("expected Playbook command");
2222            }
2223        }
2224
2225        #[test]
2226        fn test_parse_playbook_continue_on_error() {
2227            let cli = Cli::parse_from(["probar", "playbook", "test.yaml", "--continue-on-error"]);
2228            if let Commands::Playbook(args) = cli.command {
2229                assert!(args.continue_on_error);
2230            } else {
2231                panic!("expected Playbook command");
2232            }
2233        }
2234
2235        #[test]
2236        fn test_parse_playbook_format_json() {
2237            let cli = Cli::parse_from(["probar", "playbook", "test.yaml", "--format", "json"]);
2238            if let Commands::Playbook(args) = cli.command {
2239                assert!(matches!(args.format, PlaybookOutputFormat::Json));
2240            } else {
2241                panic!("expected Playbook command");
2242            }
2243        }
2244
2245        #[test]
2246        fn test_parse_playbook_output_dir() {
2247            let cli =
2248                Cli::parse_from(["probar", "playbook", "test.yaml", "--output", "results/pb"]);
2249            if let Commands::Playbook(args) = cli.command {
2250                assert_eq!(args.output, PathBuf::from("results/pb"));
2251            } else {
2252                panic!("expected Playbook command");
2253            }
2254        }
2255
2256        #[test]
2257        fn test_playbook_args_defaults() {
2258            let args = PlaybookArgs {
2259                files: vec![PathBuf::from("test.yaml")],
2260                validate: false,
2261                export: None,
2262                export_output: None,
2263                mutate: false,
2264                mutation_classes: None,
2265                fail_fast: false,
2266                continue_on_error: false,
2267                format: PlaybookOutputFormat::default(),
2268                output: PathBuf::from("target/probar/playbooks"),
2269            };
2270            assert!(!args.validate);
2271            assert!(!args.mutate);
2272            assert!(matches!(args.format, PlaybookOutputFormat::Text));
2273        }
2274
2275        #[test]
2276        fn test_playbook_args_debug() {
2277            let args = PlaybookArgs {
2278                files: vec![PathBuf::from("login.yaml")],
2279                validate: true,
2280                export: Some(DiagramFormat::Svg),
2281                export_output: Some(PathBuf::from("out.svg")),
2282                mutate: true,
2283                mutation_classes: Some(vec!["M1".to_string(), "M2".to_string()]),
2284                fail_fast: true,
2285                continue_on_error: false,
2286                format: PlaybookOutputFormat::Json,
2287                output: PathBuf::from("output"),
2288            };
2289            let debug = format!("{args:?}");
2290            assert!(debug.contains("PlaybookArgs"));
2291        }
2292
2293        #[test]
2294        fn test_diagram_format_debug() {
2295            let dot_debug = format!("{:?}", DiagramFormat::Dot);
2296            assert!(dot_debug.contains("Dot"));
2297
2298            let svg_debug = format!("{:?}", DiagramFormat::Svg);
2299            assert!(svg_debug.contains("Svg"));
2300        }
2301
2302        #[test]
2303        fn test_playbook_output_format_default() {
2304            let format = PlaybookOutputFormat::default();
2305            assert!(matches!(format, PlaybookOutputFormat::Text));
2306        }
2307
2308        #[test]
2309        fn test_playbook_output_format_all_variants() {
2310            let _ = PlaybookOutputFormat::Text;
2311            let _ = PlaybookOutputFormat::Json;
2312            let _ = PlaybookOutputFormat::Junit;
2313        }
2314    }
2315
2316    mod av_sync_tests {
2317        use super::*;
2318
2319        #[test]
2320        fn test_parse_av_sync_check() {
2321            let cli = Cli::parse_from(["probar", "av-sync", "check", "video.mp4"]);
2322            assert!(matches!(cli.command, Commands::AvSync(_)));
2323        }
2324
2325        #[test]
2326        fn test_parse_av_sync_check_with_edl() {
2327            let cli = Cli::parse_from([
2328                "probar",
2329                "av-sync",
2330                "check",
2331                "video.mp4",
2332                "--edl",
2333                "video.edl.json",
2334            ]);
2335            if let Commands::AvSync(args) = cli.command {
2336                if let AvSyncSubcommand::Check(check_args) = args.subcommand {
2337                    assert_eq!(check_args.edl, Some(PathBuf::from("video.edl.json")));
2338                } else {
2339                    panic!("expected Check subcommand");
2340                }
2341            } else {
2342                panic!("expected AvSync command");
2343            }
2344        }
2345
2346        #[test]
2347        fn test_parse_av_sync_check_with_tolerance() {
2348            let cli = Cli::parse_from([
2349                "probar",
2350                "av-sync",
2351                "check",
2352                "video.mp4",
2353                "--tolerance-ms",
2354                "30",
2355            ]);
2356            if let Commands::AvSync(args) = cli.command {
2357                if let AvSyncSubcommand::Check(check_args) = args.subcommand {
2358                    assert!((check_args.tolerance_ms - 30.0).abs() < f64::EPSILON);
2359                } else {
2360                    panic!("expected Check subcommand");
2361                }
2362            } else {
2363                panic!("expected AvSync command");
2364            }
2365        }
2366
2367        #[test]
2368        fn test_parse_av_sync_report() {
2369            let cli = Cli::parse_from(["probar", "av-sync", "report", "/output/dir"]);
2370            if let Commands::AvSync(args) = cli.command {
2371                if let AvSyncSubcommand::Report(report_args) = args.subcommand {
2372                    assert_eq!(report_args.dir, PathBuf::from("/output/dir"));
2373                } else {
2374                    panic!("expected Report subcommand");
2375                }
2376            } else {
2377                panic!("expected AvSync command");
2378            }
2379        }
2380
2381        #[test]
2382        fn test_parse_av_sync_check_detailed() {
2383            let cli = Cli::parse_from(["probar", "av-sync", "check", "video.mp4", "--detailed"]);
2384            if let Commands::AvSync(args) = cli.command {
2385                if let AvSyncSubcommand::Check(check_args) = args.subcommand {
2386                    assert!(check_args.detailed);
2387                } else {
2388                    panic!("expected Check subcommand");
2389                }
2390            } else {
2391                panic!("expected AvSync command");
2392            }
2393        }
2394
2395        #[test]
2396        fn test_parse_av_sync_report_with_output() {
2397            let cli = Cli::parse_from([
2398                "probar",
2399                "av-sync",
2400                "report",
2401                "/output",
2402                "-o",
2403                "report.json",
2404            ]);
2405            if let Commands::AvSync(args) = cli.command {
2406                if let AvSyncSubcommand::Report(report_args) = args.subcommand {
2407                    assert_eq!(report_args.output, Some(PathBuf::from("report.json")));
2408                } else {
2409                    panic!("expected Report subcommand");
2410                }
2411            } else {
2412                panic!("expected AvSync command");
2413            }
2414        }
2415
2416        #[test]
2417        fn test_av_sync_output_format_default() {
2418            let format = AvSyncOutputFormat::default();
2419            assert!(matches!(format, AvSyncOutputFormat::Text));
2420        }
2421
2422        #[test]
2423        fn test_parse_av_sync_check_json_format() {
2424            let cli = Cli::parse_from([
2425                "probar",
2426                "av-sync",
2427                "check",
2428                "video.mp4",
2429                "--format",
2430                "json",
2431            ]);
2432            if let Commands::AvSync(args) = cli.command {
2433                if let AvSyncSubcommand::Check(check_args) = args.subcommand {
2434                    assert!(matches!(check_args.format, AvSyncOutputFormat::Json));
2435                } else {
2436                    panic!("expected Check subcommand");
2437                }
2438            } else {
2439                panic!("expected AvSync command");
2440            }
2441        }
2442    }
2443
2444    mod audio_tests {
2445        use super::*;
2446
2447        #[test]
2448        fn test_parse_audio_check() {
2449            let cli = Cli::parse_from(["probar", "audio", "check", "video.mp4"]);
2450            assert!(matches!(cli.command, Commands::Audio(_)));
2451        }
2452
2453        #[test]
2454        fn test_parse_audio_check_with_options() {
2455            let cli = Cli::parse_from([
2456                "probar",
2457                "audio",
2458                "check",
2459                "video.mp4",
2460                "--min-rms-dbfs",
2461                "-30",
2462                "--sample-rate",
2463                "44100",
2464            ]);
2465            if let Commands::Audio(args) = cli.command {
2466                if let AudioSubcommand::Check(check_args) = args.subcommand {
2467                    assert!((check_args.min_rms_dbfs - (-30.0)).abs() < f64::EPSILON);
2468                    assert_eq!(check_args.sample_rate, 44100);
2469                } else {
2470                    panic!("expected Check subcommand");
2471                }
2472            } else {
2473                panic!("expected Audio command");
2474            }
2475        }
2476
2477        #[test]
2478        fn test_output_format_default() {
2479            let format = OutputFormat::default();
2480            assert!(matches!(format, OutputFormat::Text));
2481        }
2482    }
2483
2484    mod video_tests {
2485        use super::*;
2486
2487        #[test]
2488        fn test_parse_video_check() {
2489            let cli = Cli::parse_from(["probar", "video", "check", "video.mp4"]);
2490            assert!(matches!(cli.command, Commands::Video(_)));
2491        }
2492
2493        #[test]
2494        fn test_parse_video_check_with_expectations() {
2495            let cli = Cli::parse_from([
2496                "probar",
2497                "video",
2498                "check",
2499                "video.mp4",
2500                "--width",
2501                "1920",
2502                "--height",
2503                "1080",
2504                "--fps",
2505                "24",
2506                "--codec",
2507                "h264",
2508                "--require-audio",
2509            ]);
2510            if let Commands::Video(args) = cli.command {
2511                if let VideoSubcommand::Check(check_args) = args.subcommand {
2512                    assert_eq!(check_args.width, Some(1920));
2513                    assert_eq!(check_args.height, Some(1080));
2514                    assert!((check_args.fps.unwrap() - 24.0).abs() < f64::EPSILON);
2515                    assert_eq!(check_args.codec.as_deref(), Some("h264"));
2516                    assert!(check_args.require_audio);
2517                } else {
2518                    panic!("expected Check subcommand");
2519                }
2520            } else {
2521                panic!("expected Video command");
2522            }
2523        }
2524    }
2525
2526    mod animation_tests {
2527        use super::*;
2528
2529        #[test]
2530        fn test_parse_animation_check() {
2531            let cli = Cli::parse_from([
2532                "probar",
2533                "animation",
2534                "check",
2535                "timeline.json",
2536                "observed.json",
2537            ]);
2538            assert!(matches!(cli.command, Commands::Animation(_)));
2539        }
2540
2541        #[test]
2542        fn test_parse_animation_check_with_tolerance() {
2543            let cli = Cli::parse_from([
2544                "probar",
2545                "animation",
2546                "check",
2547                "timeline.json",
2548                "observed.json",
2549                "--tolerance-ms",
2550                "30",
2551            ]);
2552            if let Commands::Animation(args) = cli.command {
2553                if let AnimationSubcommand::Check(check_args) = args.subcommand {
2554                    assert!((check_args.tolerance_ms - 30.0).abs() < f64::EPSILON);
2555                } else {
2556                    panic!("expected Check subcommand");
2557                }
2558            } else {
2559                panic!("expected Animation command");
2560            }
2561        }
2562    }
2563
2564    mod stress_args_tests {
2565        use super::*;
2566
2567        fn make_stress_args(
2568            atomics: bool,
2569            worker_msg: bool,
2570            render: bool,
2571            trace: bool,
2572            full: bool,
2573            mode: &str,
2574        ) -> StressArgs {
2575            StressArgs {
2576                mode: mode.to_string(),
2577                duration: 30,
2578                concurrency: 4,
2579                output: "text".to_string(),
2580                atomics,
2581                worker_msg,
2582                render,
2583                trace,
2584                full,
2585            }
2586        }
2587
2588        #[test]
2589        fn test_get_mode_atomics() {
2590            let args = make_stress_args(true, false, false, false, false, "default");
2591            assert_eq!(args.get_mode(), "atomics");
2592        }
2593
2594        #[test]
2595        fn test_get_mode_worker_msg() {
2596            let args = make_stress_args(false, true, false, false, false, "default");
2597            assert_eq!(args.get_mode(), "worker-msg");
2598        }
2599
2600        #[test]
2601        fn test_get_mode_render() {
2602            let args = make_stress_args(false, false, true, false, false, "default");
2603            assert_eq!(args.get_mode(), "render");
2604        }
2605
2606        #[test]
2607        fn test_get_mode_trace() {
2608            let args = make_stress_args(false, false, false, true, false, "default");
2609            assert_eq!(args.get_mode(), "trace");
2610        }
2611
2612        #[test]
2613        fn test_get_mode_full() {
2614            let args = make_stress_args(false, false, false, false, true, "default");
2615            assert_eq!(args.get_mode(), "full");
2616        }
2617
2618        #[test]
2619        fn test_get_mode_default() {
2620            let args = make_stress_args(false, false, false, false, false, "custom-mode");
2621            assert_eq!(args.get_mode(), "custom-mode");
2622        }
2623
2624        #[test]
2625        fn test_stress_args_debug() {
2626            let args = make_stress_args(true, false, false, false, false, "atomics");
2627            let debug = format!("{args:?}");
2628            assert!(debug.contains("StressArgs"));
2629        }
2630
2631        #[test]
2632        fn test_parse_stress_command() {
2633            let cli = Cli::parse_from(["probar", "stress"]);
2634            assert!(matches!(cli.command, Commands::Stress(_)));
2635        }
2636
2637        #[test]
2638        fn test_parse_stress_with_duration() {
2639            let cli = Cli::parse_from(["probar", "stress", "--duration", "60"]);
2640            if let Commands::Stress(args) = cli.command {
2641                assert_eq!(args.duration, 60);
2642            } else {
2643                panic!("expected Stress command");
2644            }
2645        }
2646
2647        #[test]
2648        fn test_parse_stress_with_concurrency() {
2649            let cli = Cli::parse_from(["probar", "stress", "--concurrency", "8"]);
2650            if let Commands::Stress(args) = cli.command {
2651                assert_eq!(args.concurrency, 8);
2652            } else {
2653                panic!("expected Stress command");
2654            }
2655        }
2656
2657        #[test]
2658        fn test_parse_stress_with_atomics_flag() {
2659            let cli = Cli::parse_from(["probar", "stress", "--atomics"]);
2660            if let Commands::Stress(args) = cli.command {
2661                assert!(args.atomics);
2662                assert_eq!(args.get_mode(), "atomics");
2663            } else {
2664                panic!("expected Stress command");
2665            }
2666        }
2667    }
2668}