Skip to main content

apr_cli/
lib.rs

1//! apr-cli library
2//!
3//! This library is the foundation for the apr CLI binary.
4//! Exports CLI structures for testing and reuse.
5
6use clap::{Parser, Subcommand};
7use std::path::{Path, PathBuf};
8
9mod commands;
10pub mod error;
11mod output;
12
13pub use error::CliError;
14
15// Public re-exports for integration tests
16pub mod qa_types {
17    pub use crate::commands::qa::{GateResult, QaReport, SystemInfo};
18}
19
20#[cfg(feature = "inference")]
21pub mod federation;
22
23// Commands are crate-private, used internally by execute_command
24use commands::{
25    bench, canary, canary::CanaryCommands, cbtop, chat, compare_hf, convert, debug, diff, eval,
26    explain, export, flow, hex, import, inspect, lint, merge, oracle, probar, profile, ptx_explain,
27    publish, pull, qa, rosetta, rosetta::RosettaCommands, run, serve, showcase, tensors, trace,
28    tree, tui, tune, validate,
29};
30
31/// apr - APR Model Operations Tool
32///
33/// Inspect, debug, and manage .apr model files.
34/// Toyota Way: Genchi Genbutsu - Go and see the actual data.
35#[derive(Parser, Debug)]
36#[command(name = "apr")]
37#[command(author, version = concat!(env!("CARGO_PKG_VERSION"), " (", env!("APR_GIT_SHA"), ")"), about, long_about = None)]
38#[command(propagate_version = true)]
39pub struct Cli {
40    #[command(subcommand)]
41    pub command: Box<Commands>,
42
43    /// Output as JSON
44    #[arg(long, global = true)]
45    pub json: bool,
46
47    /// Verbose output
48    #[arg(short, long, global = true)]
49    pub verbose: bool,
50
51    /// Quiet mode (errors only)
52    #[arg(short, long, global = true)]
53    pub quiet: bool,
54
55    /// Disable network access (Sovereign AI compliance, Section 9)
56    #[arg(long, global = true)]
57    pub offline: bool,
58
59    /// Skip tensor contract validation (PMAT-237: use with diagnostic tooling)
60    #[arg(long, global = true)]
61    pub skip_contract: bool,
62}
63
64#[derive(Subcommand, Debug)]
65pub enum Commands {
66    /// Run model directly (auto-download, cache, execute)
67    Run {
68        /// Model source: local path, hf://org/repo, or URL
69        #[arg(value_name = "SOURCE")]
70        source: String,
71
72        /// Text prompt (positional): `apr run model.gguf "What is 2+2?"`
73        #[arg(value_name = "PROMPT")]
74        positional_prompt: Option<String>,
75
76        /// Input file (audio, text, etc.)
77        #[arg(short, long)]
78        input: Option<PathBuf>,
79
80        /// Text prompt for generation (for LLM models)
81        #[arg(short, long)]
82        prompt: Option<String>,
83
84        /// Maximum tokens to generate (default: 32)
85        #[arg(short = 'n', long, default_value = "32")]
86        max_tokens: usize,
87
88        /// Enable streaming output
89        #[arg(long)]
90        stream: bool,
91
92        /// Language code (for ASR models)
93        #[arg(short, long)]
94        language: Option<String>,
95
96        /// Task (transcribe, translate)
97        #[arg(short, long)]
98        task: Option<String>,
99
100        /// Output format (text, json, srt, vtt)
101        #[arg(short = 'f', long, default_value = "text")]
102        format: String,
103
104        /// Disable GPU acceleration
105        #[arg(long, conflicts_with = "gpu")]
106        no_gpu: bool,
107
108        /// Force GPU acceleration
109        #[arg(long, conflicts_with = "no_gpu")]
110        gpu: bool,
111
112        /// Offline mode: block all network access (Sovereign AI compliance)
113        #[arg(long)]
114        offline: bool,
115
116        /// Benchmark mode: output performance metrics (tok/s, latency)
117        #[arg(long)]
118        benchmark: bool,
119
120        /// Enable inference tracing (APR-TRACE-001)
121        #[arg(long)]
122        trace: bool,
123
124        /// Trace specific steps only (comma-separated)
125        #[arg(long, value_delimiter = ',')]
126        trace_steps: Option<Vec<String>>,
127
128        /// Verbose tracing (show tensor values)
129        #[arg(long)]
130        trace_verbose: bool,
131
132        /// Save trace output to JSON file
133        #[arg(long, value_name = "FILE")]
134        trace_output: Option<PathBuf>,
135
136        /// Trace detail level (none, basic, layer, payload)
137        #[arg(long, value_name = "LEVEL", default_value = "basic")]
138        trace_level: String,
139
140        /// Shorthand for --trace --trace-level payload (tensor value inspection)
141        #[arg(long)]
142        trace_payload: bool,
143
144        /// Enable inline Roofline profiling (PMAT-SHOWCASE-METHODOLOGY-001)
145        #[arg(long)]
146        profile: bool,
147
148        /// Apply chat template for Instruct models (GAP-UX-001)
149        ///
150        /// Wraps prompt in ChatML format for Qwen2, LLaMA, Mistral Instruct models.
151        /// Format: <|im_start|>user\n{prompt}<|im_end|>\n<|im_start|>assistant\n
152        #[arg(long)]
153        chat: bool,
154
155        /// Show verbose output (model loading, backend info)
156        #[arg(short, long)]
157        verbose: bool,
158    },
159
160    /// Start inference server (REST API, streaming, metrics)
161    Serve {
162        /// Path to model file
163        #[arg(value_name = "FILE")]
164        file: PathBuf,
165
166        /// Port to listen on
167        #[arg(short, long, default_value = "8080")]
168        port: u16,
169
170        /// Host to bind to
171        #[arg(long, default_value = "127.0.0.1")]
172        host: String,
173
174        /// Disable CORS
175        #[arg(long)]
176        no_cors: bool,
177
178        /// Disable Prometheus metrics endpoint
179        #[arg(long)]
180        no_metrics: bool,
181
182        /// Disable GPU acceleration
183        #[arg(long)]
184        no_gpu: bool,
185
186        /// Force GPU acceleration (requires CUDA)
187        #[arg(long)]
188        gpu: bool,
189
190        /// Enable batched GPU inference for 2X+ throughput
191        #[arg(long)]
192        batch: bool,
193
194        /// Enable inference tracing (PMAT-SHOWCASE-METHODOLOGY-001)
195        #[arg(long)]
196        trace: bool,
197
198        /// Trace detail level (none, basic, layer)
199        #[arg(long, value_name = "LEVEL", default_value = "basic")]
200        trace_level: String,
201
202        /// Enable inline Roofline profiling (adds X-Profile headers)
203        #[arg(long)]
204        profile: bool,
205    },
206
207    /// Inspect model metadata, vocab, and structure
208    Inspect {
209        /// Path to .apr model file
210        #[arg(value_name = "FILE")]
211        file: PathBuf,
212
213        /// Show vocabulary details
214        #[arg(long)]
215        vocab: bool,
216
217        /// Show filter/security details
218        #[arg(long)]
219        filters: bool,
220
221        /// Show weight statistics
222        #[arg(long)]
223        weights: bool,
224
225        /// Output as JSON
226        #[arg(long)]
227        json: bool,
228    },
229
230    /// Simple debugging output ("drama" mode available)
231    Debug {
232        /// Path to .apr model file
233        #[arg(value_name = "FILE")]
234        file: PathBuf,
235
236        /// Theatrical "drama" mode output
237        #[arg(long)]
238        drama: bool,
239
240        /// Show hex dump
241        #[arg(long)]
242        hex: bool,
243
244        /// Extract ASCII strings
245        #[arg(long)]
246        strings: bool,
247
248        /// Limit output lines
249        #[arg(long, default_value = "256")]
250        limit: usize,
251    },
252
253    /// Validate model integrity and quality
254    Validate {
255        /// Path to .apr model file
256        #[arg(value_name = "FILE")]
257        file: PathBuf,
258
259        /// Show 100-point quality assessment
260        #[arg(long)]
261        quality: bool,
262
263        /// Strict validation (fail on warnings)
264        #[arg(long)]
265        strict: bool,
266
267        /// Minimum score to pass (0-100)
268        #[arg(long)]
269        min_score: Option<u8>,
270    },
271
272    /// Compare two models
273    Diff {
274        /// First model file
275        #[arg(value_name = "FILE1")]
276        file1: PathBuf,
277
278        /// Second model file
279        #[arg(value_name = "FILE2")]
280        file2: PathBuf,
281
282        /// Show weight-level differences
283        #[arg(long)]
284        weights: bool,
285
286        /// Compare actual tensor values with statistical analysis
287        #[arg(long)]
288        values: bool,
289
290        /// Filter tensors by name pattern (for --values)
291        #[arg(long)]
292        filter: Option<String>,
293
294        /// Maximum number of tensors to compare (for --values)
295        #[arg(long, default_value = "10")]
296        limit: usize,
297
298        /// Account for transpose when comparing (GGUF col-major vs APR row-major)
299        #[arg(long)]
300        transpose_aware: bool,
301
302        /// Output as JSON
303        #[arg(long)]
304        json: bool,
305    },
306
307    /// List tensor names and shapes
308    Tensors {
309        /// Path to .apr model file
310        #[arg(value_name = "FILE")]
311        file: PathBuf,
312
313        /// Show tensor statistics (mean, std, min, max)
314        #[arg(long)]
315        stats: bool,
316
317        /// Filter tensors by name pattern
318        #[arg(long)]
319        filter: Option<String>,
320
321        /// Limit number of tensors shown (0 = unlimited)
322        #[arg(long, default_value = "0")]
323        limit: usize,
324
325        /// Output as JSON
326        #[arg(long)]
327        json: bool,
328    },
329
330    /// Layer-by-layer trace analysis
331    Trace {
332        /// Path to .apr model file
333        #[arg(value_name = "FILE")]
334        file: PathBuf,
335
336        /// Filter layers by name pattern
337        #[arg(long)]
338        layer: Option<String>,
339
340        /// Compare with reference model
341        #[arg(long)]
342        reference: Option<PathBuf>,
343
344        /// Output as JSON
345        #[arg(long)]
346        json: bool,
347
348        /// Verbose output with per-layer stats
349        #[arg(short, long)]
350        verbose: bool,
351
352        /// Trace payload through model
353        #[arg(long)]
354        payload: bool,
355
356        /// Diff mode
357        #[arg(long)]
358        diff: bool,
359
360        /// Interactive mode
361        #[arg(long)]
362        interactive: bool,
363    },
364
365    /// Check for best practices and conventions
366    Lint {
367        /// Path to .apr model file
368        #[arg(value_name = "FILE")]
369        file: PathBuf,
370    },
371
372    /// Explain errors, architecture, and tensors
373    Explain {
374        /// Explain a specific error code
375        #[arg(value_name = "CODE")]
376        code: Option<String>,
377
378        /// Path to .apr model file (optional context)
379        #[arg(short, long)]
380        file: Option<PathBuf>,
381
382        /// Explain a specific tensor
383        #[arg(long)]
384        tensor: Option<String>,
385    },
386
387    /// Manage canary tests for regression
388    Canary {
389        #[command(subcommand)]
390        command: CanaryCommands,
391    },
392
393    /// Export model to other formats
394    Export {
395        /// Path to .apr model file
396        #[arg(value_name = "FILE")]
397        file: PathBuf,
398
399        /// Output format (onnx, safetensors, gguf)
400        #[arg(long, default_value = "safetensors")]
401        format: String,
402
403        /// Output file path
404        #[arg(short, long)]
405        output: PathBuf,
406
407        /// Apply quantization during export (int8, int4, fp16)
408        #[arg(long)]
409        quantize: Option<String>,
410    },
411
412    /// Import from external formats (hf://org/repo, local files, URLs)
413    Import {
414        /// Source: hf://org/repo, local file, or URL
415        #[arg(value_name = "SOURCE")]
416        source: String,
417
418        /// Output .apr file path (default: derived from source name)
419        #[arg(short, long)]
420        output: Option<PathBuf>,
421
422        /// Model architecture (whisper, llama, bert, auto)
423        #[arg(long, default_value = "auto")]
424        arch: String,
425
426        /// Quantization (int8, int4, fp16)
427        #[arg(long)]
428        quantize: Option<String>,
429
430        /// Strict mode: reject unverified architectures and fail on validation errors
431        #[arg(long)]
432        strict: bool,
433
434        /// Preserve Q4K quantization for fused kernel inference (GGUF only)
435        /// Uses realizar's Q4K converter instead of dequantizing to F32
436        #[arg(long)]
437        preserve_q4k: bool,
438
439        /// PMAT-232: External tokenizer.json for weights-only GGUF files.
440        /// Required if the GGUF has no embedded tokenizer vocabulary.
441        #[arg(long)]
442        tokenizer: Option<PathBuf>,
443
444        /// F-GT-001: Enforce provenance chain. Rejects pre-baked GGUF imports
445        /// (only SafeTensors sources allowed). Ensures single-provenance testing.
446        #[arg(long)]
447        enforce_provenance: bool,
448    },
449
450    /// Download and cache model from HuggingFace (Ollama-like UX)
451    Pull {
452        /// Model reference (alias, hf:// URI, or org/repo)
453        #[arg(value_name = "MODEL")]
454        model_ref: String,
455
456        /// Force re-download even if cached
457        #[arg(long)]
458        force: bool,
459    },
460
461    /// List cached models
462    #[command(name = "list", alias = "ls")]
463    List,
464
465    /// Remove model from cache
466    #[command(name = "rm", alias = "remove")]
467    Rm {
468        /// Model reference to remove
469        #[arg(value_name = "MODEL")]
470        model_ref: String,
471    },
472
473    /// Convert/optimize model
474    Convert {
475        /// Path to .apr model file
476        #[arg(value_name = "FILE")]
477        file: PathBuf,
478
479        /// Quantize to format (int8, int4, fp16, q4k)
480        #[arg(long)]
481        quantize: Option<String>,
482
483        /// Compress output (none, zstd, zstd-max, lz4)
484        #[arg(long)]
485        compress: Option<String>,
486
487        /// Output file path
488        #[arg(short, long)]
489        output: PathBuf,
490
491        /// Force overwrite existing files
492        #[arg(short, long)]
493        force: bool,
494    },
495
496    /// Merge multiple models
497    Merge {
498        /// Model files to merge
499        #[arg(value_name = "FILES", num_args = 2..)]
500        files: Vec<PathBuf>,
501
502        /// Merge strategy (average, weighted, ties)
503        #[arg(long, default_value = "average")]
504        strategy: String,
505
506        /// Output file path
507        #[arg(short, long)]
508        output: PathBuf,
509
510        /// Weights for weighted merge (comma-separated, e.g., "0.7,0.3")
511        #[arg(long, value_delimiter = ',')]
512        weights: Option<Vec<f32>>,
513    },
514
515    /// Interactive terminal UI
516    Tui {
517        /// Path to .apr model file
518        #[arg(value_name = "FILE")]
519        file: Option<PathBuf>,
520    },
521
522    /// ComputeBrick pipeline monitor (cbtop)
523    Cbtop {
524        /// Model name (e.g., qwen2.5-coder-1.5b)
525        #[arg(long)]
526        model: Option<String>,
527
528        /// Attach to running realizar process
529        #[arg(long)]
530        attach: Option<String>,
531
532        /// Path to GGUF model file for real profiling
533        #[arg(long, value_name = "MODEL")]
534        model_path: Option<PathBuf>,
535
536        /// Run in headless mode (no TUI, for CI/automation)
537        #[arg(long)]
538        headless: bool,
539
540        /// Output JSON format (requires --headless)
541        #[arg(long)]
542        json: bool,
543
544        /// Output file path (requires --headless)
545        #[arg(long, value_name = "FILE")]
546        output: Option<PathBuf>,
547
548        /// CI mode: exit with code 1 if thresholds not met
549        #[arg(long)]
550        ci: bool,
551
552        /// Minimum throughput threshold in tok/s (for --ci)
553        #[arg(long, value_name = "TOK_S")]
554        throughput: Option<f64>,
555
556        /// Minimum brick score threshold 0-100 (for --ci)
557        #[arg(long, value_name = "SCORE")]
558        brick_score: Option<u32>,
559
560        /// Number of warmup iterations before measurement
561        #[arg(long, default_value = "10")]
562        warmup: usize,
563
564        /// Number of measurement iterations
565        #[arg(long, default_value = "100")]
566        iterations: usize,
567
568        /// PAR-100: Enable speculative decoding benchmark
569        #[arg(long)]
570        speculative: bool,
571
572        /// PAR-100: Number of tokens to draft speculatively (default: 4)
573        #[arg(long, default_value = "4")]
574        speculation_k: usize,
575
576        /// PAR-099: Path to draft model for speculative decoding
577        #[arg(long, value_name = "DRAFT_MODEL")]
578        draft_model: Option<PathBuf>,
579
580        /// PAR-102: Number of concurrent requests
581        #[arg(long, default_value = "1")]
582        concurrent: usize,
583
584        /// Use simulated data (for CI testing only)
585        #[arg(long)]
586        simulated: bool,
587    },
588
589    /// Export for probar visual testing
590    Probar {
591        /// Path to .apr model file
592        #[arg(value_name = "FILE")]
593        file: PathBuf,
594
595        /// Output directory for test artifacts
596        #[arg(short, long, default_value = "./probar-export")]
597        output: PathBuf,
598
599        /// Export format: json, png, or both
600        #[arg(long, default_value = "both")]
601        format: String,
602
603        /// Golden reference directory for comparison
604        #[arg(long)]
605        golden: Option<PathBuf>,
606
607        /// Filter layers by name pattern
608        #[arg(long)]
609        layer: Option<String>,
610    },
611
612    /// Compare APR model against HuggingFace source
613    #[command(name = "compare-hf")]
614    CompareHf {
615        /// Path to .apr model file
616        #[arg(value_name = "FILE")]
617        file: PathBuf,
618
619        /// HuggingFace repo ID (e.g., openai/whisper-tiny)
620        #[arg(long)]
621        hf: String,
622
623        /// Filter tensors by name pattern
624        #[arg(long)]
625        tensor: Option<String>,
626
627        /// Comparison threshold (default: 1e-5)
628        #[arg(long, default_value = "1e-5")]
629        threshold: f64,
630
631        /// Output as JSON
632        #[arg(long)]
633        json: bool,
634    },
635
636    /// Format-aware binary forensics (10X better than xxd)
637    Hex {
638        /// Path to model file (APR, GGUF, or SafeTensors)
639        #[arg(value_name = "FILE")]
640        file: PathBuf,
641
642        /// Filter tensors by name pattern
643        #[arg(long)]
644        tensor: Option<String>,
645
646        /// Limit bytes/values to display
647        #[arg(long, default_value = "64")]
648        limit: usize,
649
650        /// Show tensor statistics
651        #[arg(long)]
652        stats: bool,
653
654        /// List tensor names only
655        #[arg(long)]
656        list: bool,
657
658        /// Output as JSON
659        #[arg(long)]
660        json: bool,
661
662        /// Annotated file header (magic, version, tensor count, metadata)
663        #[arg(long)]
664        header: bool,
665
666        /// Q4K/Q6K/Q8_0 super-block structure with field annotations
667        #[arg(long)]
668        blocks: bool,
669
670        /// Value histogram + entropy + kurtosis analysis
671        #[arg(long)]
672        distribution: bool,
673
674        /// Layout contract verification overlay per tensor
675        #[arg(long)]
676        contract: bool,
677
678        /// Per-region byte entropy analysis
679        #[arg(long)]
680        entropy: bool,
681
682        /// Raw bytes (like xxd but format-aware, with ASCII column)
683        #[arg(long)]
684        raw: bool,
685
686        /// Start at byte offset (supports 0x prefix for hex)
687        #[arg(long, default_value = "0")]
688        offset: String,
689
690        /// Bytes per row for raw output (default: 16)
691        #[arg(long, default_value = "16")]
692        width: usize,
693    },
694
695    /// Model architecture tree view
696    Tree {
697        /// Path to .apr model file
698        #[arg(value_name = "FILE")]
699        file: PathBuf,
700
701        /// Filter by component pattern
702        #[arg(long)]
703        filter: Option<String>,
704
705        /// Output format: ascii, dot, mermaid, json
706        #[arg(long, default_value = "ascii")]
707        format: String,
708
709        /// Show tensor sizes
710        #[arg(long)]
711        sizes: bool,
712
713        /// Maximum tree depth
714        #[arg(long)]
715        depth: Option<usize>,
716    },
717
718    /// Data flow visualization
719    Flow {
720        /// Path to .apr model file
721        #[arg(value_name = "FILE")]
722        file: PathBuf,
723
724        /// Filter by layer pattern
725        #[arg(long)]
726        layer: Option<String>,
727
728        /// Component to visualize: full, encoder, decoder, etc.
729        #[arg(long, default_value = "full")]
730        component: String,
731
732        /// Verbose output with statistics
733        #[arg(short, long)]
734        verbose: bool,
735    },
736
737    /// Interactive chat with language model
738    Chat {
739        /// Path to .apr model file
740        #[arg(value_name = "FILE")]
741        file: PathBuf,
742
743        /// Sampling temperature (0 = greedy, higher = more random)
744        #[arg(long, default_value = "0.7")]
745        temperature: f32,
746
747        /// Nucleus sampling threshold
748        #[arg(long, default_value = "0.9")]
749        top_p: f32,
750
751        /// Maximum tokens to generate per response
752        #[arg(long, default_value = "512")]
753        max_tokens: usize,
754
755        /// System prompt to set model behavior
756        #[arg(long)]
757        system: Option<String>,
758
759        /// Show inspection info (top-k probs, tokens/sec)
760        #[arg(long)]
761        inspect: bool,
762
763        /// Disable GPU acceleration (use CPU)
764        #[arg(long)]
765        no_gpu: bool,
766
767        /// Force GPU acceleration (requires CUDA)
768        #[arg(long)]
769        gpu: bool,
770
771        /// Enable inference tracing (APR-TRACE-001)
772        #[arg(long)]
773        trace: bool,
774
775        /// Trace specific steps only (comma-separated)
776        #[arg(long, value_delimiter = ',')]
777        trace_steps: Option<Vec<String>>,
778
779        /// Verbose tracing
780        #[arg(long)]
781        trace_verbose: bool,
782
783        /// Save trace output to JSON file
784        #[arg(long, value_name = "FILE")]
785        trace_output: Option<PathBuf>,
786
787        /// Trace detail level (none, basic, layer, payload)
788        #[arg(long, value_name = "LEVEL", default_value = "basic")]
789        trace_level: String,
790
791        /// Enable inline Roofline profiling (PMAT-SHOWCASE-METHODOLOGY-001)
792        #[arg(long)]
793        profile: bool,
794    },
795
796    /// Benchmark throughput (spec H12: >= 10 tok/s)
797    Bench {
798        /// Path to model file
799        #[arg(value_name = "FILE")]
800        file: PathBuf,
801
802        /// Number of warmup iterations
803        #[arg(long, default_value = "3")]
804        warmup: usize,
805
806        /// Number of measurement iterations
807        #[arg(long, default_value = "5")]
808        iterations: usize,
809
810        /// Max tokens to generate per iteration
811        #[arg(long, default_value = "32")]
812        max_tokens: usize,
813
814        /// Test prompt
815        #[arg(long)]
816        prompt: Option<String>,
817
818        /// Use realizar for fast inference (vs aprender baseline)
819        #[arg(long)]
820        fast: bool,
821
822        /// Benchmark specific brick
823        #[arg(long)]
824        brick: Option<String>,
825    },
826
827    /// Evaluate model perplexity (spec H13: PPL <= 20)
828    Eval {
829        /// Path to model file
830        #[arg(value_name = "FILE")]
831        file: PathBuf,
832
833        /// Dataset: wikitext-2, lambada, or custom
834        #[arg(long, default_value = "wikitext-2")]
835        dataset: String,
836
837        /// Custom text (when dataset=custom)
838        #[arg(long)]
839        text: Option<String>,
840
841        /// Maximum tokens to evaluate
842        #[arg(long, default_value = "512")]
843        max_tokens: usize,
844
845        /// Perplexity threshold for pass/fail
846        #[arg(long, default_value = "20.0")]
847        threshold: f32,
848    },
849
850    /// Deep profiling with Roofline analysis
851    Profile {
852        /// Path to model file
853        #[arg(value_name = "FILE")]
854        file: PathBuf,
855
856        /// Layer-by-layer granular analysis
857        #[arg(long)]
858        granular: bool,
859
860        /// Output format (human, json, flamegraph)
861        #[arg(long, default_value = "human")]
862        format: String,
863
864        /// Focus on specific operation
865        #[arg(long)]
866        focus: Option<String>,
867
868        /// Detect naive implementations
869        #[arg(long)]
870        detect_naive: bool,
871
872        /// GFLOPS threshold for naive detection
873        #[arg(long, default_value = "10.0")]
874        threshold: f64,
875
876        /// Compare against HuggingFace baseline
877        #[arg(long)]
878        compare_hf: Option<String>,
879
880        /// Measure energy consumption (requires RAPL)
881        #[arg(long)]
882        energy: bool,
883
884        /// Compute performance grade (vs Ollama baseline)
885        #[arg(long)]
886        perf_grade: bool,
887
888        /// Show call graph
889        #[arg(long)]
890        callgraph: bool,
891
892        /// Exit non-zero if naive implementation detected
893        #[arg(long)]
894        fail_on_naive: bool,
895
896        /// Output file path for flamegraph SVG (GH-174, PMAT-182)
897        #[arg(long, short = 'o')]
898        output: Option<PathBuf>,
899
900        // PMAT-192: CI Assertion Mode (GH-180)
901        /// Enable CI mode with assertion checks (exits 1 on failure)
902        #[arg(long)]
903        ci: bool,
904
905        /// Minimum throughput in tok/s (CI assertion, exits 1 if below)
906        #[arg(long)]
907        assert_throughput: Option<f64>,
908
909        /// Maximum p99 latency in ms (CI assertion, exits 1 if above)
910        #[arg(long)]
911        assert_p99: Option<f64>,
912
913        /// Maximum p50 latency in ms (CI assertion, exits 1 if above)
914        #[arg(long)]
915        assert_p50: Option<f64>,
916
917        /// Warmup passes before measurement (default: 3)
918        #[arg(long, default_value = "3")]
919        warmup: usize,
920
921        /// Measurement passes (default: 10)
922        #[arg(long, default_value = "10")]
923        measure: usize,
924
925        /// Number of tokens to generate per measurement pass (default: 32)
926        #[arg(long, default_value = "32")]
927        tokens: usize,
928
929        /// Compare against Ollama baseline (runs ollama for comparison)
930        #[arg(long)]
931        ollama: bool,
932
933        /// Disable GPU (force CPU-only profiling)
934        #[arg(long)]
935        no_gpu: bool,
936
937        /// Compare against another model format (F-PROFILE-011)
938        #[arg(long, value_name = "FILE")]
939        compare: Option<PathBuf>,
940    },
941
942    /// Falsifiable QA checklist for model releases
943    Qa {
944        /// Path to model file
945        #[arg(value_name = "FILE")]
946        file: PathBuf,
947
948        /// Minimum throughput threshold in tok/s
949        #[arg(long, value_name = "TPS")]
950        assert_tps: Option<f64>,
951
952        /// Minimum speedup vs Ollama
953        #[arg(long, value_name = "SPEEDUP")]
954        assert_speedup: Option<f64>,
955
956        /// Minimum GPU vs CPU speedup (F-PERF-042)
957        #[arg(long, value_name = "SPEEDUP")]
958        assert_gpu_speedup: Option<f64>,
959
960        /// Skip golden output test
961        #[arg(long)]
962        skip_golden: bool,
963
964        /// Skip throughput benchmark
965        #[arg(long)]
966        skip_throughput: bool,
967
968        /// Skip Ollama parity comparison
969        #[arg(long)]
970        skip_ollama: bool,
971
972        /// Skip GPU vs CPU speedup test (F-PERF-042)
973        #[arg(long)]
974        skip_gpu_speedup: bool,
975
976        /// Skip tensor contract validation (PMAT-235)
977        #[arg(long)]
978        skip_contract: bool,
979
980        /// Skip cross-format parity test (F-QUAL-032)
981        #[arg(long)]
982        skip_format_parity: bool,
983
984        /// Skip PTX parity validation (GH-219)
985        #[arg(long)]
986        skip_ptx_parity: bool,
987
988        /// SafeTensors model path for cross-format parity test (F-QUAL-032)
989        #[arg(long, value_name = "PATH")]
990        safetensors_path: Option<PathBuf>,
991
992        /// Number of benchmark iterations
993        #[arg(long, default_value = "10")]
994        iterations: usize,
995
996        /// Number of warmup iterations
997        #[arg(long, default_value = "3")]
998        warmup: usize,
999
1000        /// Maximum tokens to generate
1001        #[arg(long, default_value = "32")]
1002        max_tokens: usize,
1003
1004        /// Output as JSON (for CI integration)
1005        #[arg(long)]
1006        json: bool,
1007
1008        /// Verbose output
1009        #[arg(short, long)]
1010        verbose: bool,
1011
1012        /// Minimum number of gates that must execute (fail if fewer)
1013        #[arg(long, value_name = "N")]
1014        min_executed: Option<usize>,
1015
1016        /// Previous QA report for regression detection
1017        #[arg(long, value_name = "FILE")]
1018        previous_report: Option<PathBuf>,
1019
1020        /// Maximum allowed performance regression ratio (default: 0.10 = 10%)
1021        #[arg(long, value_name = "RATIO")]
1022        regression_threshold: Option<f64>,
1023
1024        /// Skip GPU state isolation test
1025        #[arg(long)]
1026        skip_gpu_state: bool,
1027
1028        /// Skip metadata plausibility validation (Bug 210, GH-222)
1029        #[arg(long)]
1030        skip_metadata: bool,
1031    },
1032
1033    /// GPU/CPU parity check (PMAT-232: genchi genbutsu — see where GPU diverges)
1034    Parity {
1035        /// Path to GGUF model file
1036        #[arg(value_name = "FILE")]
1037        file: PathBuf,
1038
1039        /// Prompt text (default: "What is 2+2?")
1040        #[arg(short, long, default_value = "What is 2+2?")]
1041        prompt: String,
1042
1043        /// Assert parity (exit non-zero on divergence)
1044        #[arg(long)]
1045        assert: bool,
1046    },
1047
1048    /// Model-to-PTX source mapping (Mieruka: make GPU kernel dispatch visible)
1049    #[command(name = "ptx-map")]
1050    PtxMap {
1051        /// Path to GGUF model file
1052        #[arg(value_name = "FILE")]
1053        file: PathBuf,
1054
1055        /// Filter to specific kernel (e.g., --kernel Q4KGemv)
1056        #[arg(long)]
1057        kernel: Option<String>,
1058
1059        /// Reverse lookup: kernel name -> which layers/steps use it
1060        #[arg(long)]
1061        reverse: Option<String>,
1062
1063        /// Output as JSON
1064        #[arg(long)]
1065        json: bool,
1066
1067        /// Full PTX snippets and detailed analysis
1068        #[arg(short, long)]
1069        verbose: bool,
1070
1071        /// Show batched prefill kernel variants instead of decode
1072        #[arg(long)]
1073        prefill: bool,
1074    },
1075
1076    /// PTX analysis and bug detection (trueno-explain: register pressure, roofline, 15+ bug detectors)
1077    #[command(name = "ptx")]
1078    Ptx {
1079        /// Path to a PTX source file
1080        #[arg(value_name = "FILE")]
1081        file: Option<PathBuf>,
1082
1083        /// Analyze a named kernel from trueno-gpu
1084        #[arg(long, short)]
1085        kernel: Option<String>,
1086
1087        /// Strict mode (no performance whitelist)
1088        #[arg(long)]
1089        strict: bool,
1090
1091        /// Show only bug analysis (skip register/memory/roofline)
1092        #[arg(long)]
1093        bugs: bool,
1094
1095        /// Output as JSON
1096        #[arg(long)]
1097        json: bool,
1098
1099        /// Verbose output (include PTX source listing)
1100        #[arg(short, long)]
1101        verbose: bool,
1102    },
1103
1104    /// ML tuning: LoRA/QLoRA configuration and memory planning (GH-176)
1105    Tune {
1106        /// Path to model file (optional if using --model)
1107        #[arg(value_name = "FILE")]
1108        file: Option<PathBuf>,
1109
1110        /// Tuning method: auto, full, lora, qlora
1111        #[arg(long, short = 'm', default_value = "auto")]
1112        method: String,
1113
1114        /// LoRA rank (default: auto-selected)
1115        #[arg(long, short = 'r')]
1116        rank: Option<u32>,
1117
1118        /// Available VRAM in GB
1119        #[arg(long, default_value = "16.0")]
1120        vram: f64,
1121
1122        /// Only plan configuration, don't train
1123        #[arg(long)]
1124        plan: bool,
1125
1126        /// Model size for planning (e.g., "7B", "1.5B")
1127        #[arg(long, value_name = "SIZE")]
1128        model: Option<String>,
1129
1130        /// Freeze base model weights
1131        #[arg(long)]
1132        freeze_base: bool,
1133
1134        /// Training data file (JSONL format)
1135        #[arg(long, value_name = "FILE")]
1136        train_data: Option<PathBuf>,
1137
1138        /// Output as JSON (for CI integration)
1139        #[arg(long)]
1140        json: bool,
1141    },
1142
1143    /// Qwen2.5-Coder showcase demo
1144    Showcase {
1145        /// Run all steps with auto-verification
1146        #[arg(long)]
1147        auto_verify: bool,
1148
1149        /// Run specific step
1150        #[arg(long)]
1151        step: Option<String>,
1152
1153        /// Model tier: tiny (0.5B), small (1.5B), medium (7B), large (32B)
1154        #[arg(long, default_value = "small")]
1155        tier: String,
1156
1157        /// Model directory
1158        #[arg(long, default_value = "./models")]
1159        model_dir: PathBuf,
1160
1161        /// Baselines to compare: llama-cpp,ollama
1162        #[arg(long, default_value = "llama-cpp,ollama")]
1163        baseline: String,
1164
1165        /// Enable ZRAM compression
1166        #[arg(long)]
1167        zram: bool,
1168
1169        /// Number of benchmark runs (spec: minimum 30)
1170        #[arg(long, default_value = "30")]
1171        runs: usize,
1172
1173        /// Force GPU acceleration
1174        #[arg(long)]
1175        gpu: bool,
1176
1177        /// Output results as JSON
1178        #[arg(long)]
1179        json: bool,
1180
1181        /// Verbose output
1182        #[arg(short, long)]
1183        verbose: bool,
1184
1185        /// Quiet mode (errors only)
1186        #[arg(short, long)]
1187        quiet: bool,
1188    },
1189
1190    /// Model self-test: 10-stage pipeline integrity check (APR-TRACE-001)
1191    Check {
1192        /// Path to model file
1193        #[arg(value_name = "FILE")]
1194        file: PathBuf,
1195
1196        /// Disable GPU acceleration
1197        #[arg(long)]
1198        no_gpu: bool,
1199    },
1200
1201    /// Rosetta Stone - Universal model format converter (PMAT-ROSETTA-001)
1202    Rosetta {
1203        #[command(subcommand)]
1204        action: RosettaCommands,
1205    },
1206
1207    /// Publish model to HuggingFace Hub (APR-PUB-001)
1208    Publish {
1209        /// Directory containing model files to publish
1210        #[arg(value_name = "DIRECTORY")]
1211        directory: PathBuf,
1212
1213        /// HuggingFace repository ID (e.g., paiml/whisper-apr-tiny)
1214        #[arg(value_name = "REPO_ID")]
1215        repo_id: String,
1216
1217        /// Model display name
1218        #[arg(long)]
1219        model_name: Option<String>,
1220
1221        /// License (SPDX identifier, default: mit)
1222        #[arg(long, default_value = "mit")]
1223        license: String,
1224
1225        /// Pipeline tag (e.g., automatic-speech-recognition, text-generation)
1226        #[arg(long, default_value = "text-generation")]
1227        pipeline_tag: String,
1228
1229        /// Library name (e.g., whisper-apr, aprender)
1230        #[arg(long)]
1231        library_name: Option<String>,
1232
1233        /// Additional tags (comma-separated)
1234        #[arg(long, value_delimiter = ',')]
1235        tags: Option<Vec<String>>,
1236
1237        /// Commit message
1238        #[arg(long)]
1239        message: Option<String>,
1240
1241        /// Dry run (preview without uploading)
1242        #[arg(long)]
1243        dry_run: bool,
1244    },
1245
1246    /// Model Oracle: identify family, size, constraints, and contract compliance
1247    ///
1248    /// Three modes:
1249    ///   apr oracle <FILE>         - Analyze local model file
1250    ///   apr oracle hf://org/repo  - Query HuggingFace API
1251    ///   apr oracle --family qwen2 - Describe contract from YAML
1252    Oracle {
1253        /// Model file path or hf:// URI
1254        #[arg(value_name = "SOURCE")]
1255        source: Option<String>,
1256
1257        /// Show contract for a model family (e.g., qwen2, llama, whisper, bert)
1258        #[arg(long)]
1259        family: Option<String>,
1260
1261        /// Filter to a specific size variant (e.g., 0.5b, 7b)
1262        #[arg(long)]
1263        size: Option<String>,
1264
1265        /// Run full contract compliance check
1266        #[arg(long)]
1267        compliance: bool,
1268
1269        /// List all tensor shapes
1270        #[arg(long)]
1271        tensors: bool,
1272
1273        /// Show statistical analysis (GQA, memory, FFN, FLOPS)
1274        #[arg(long)]
1275        stats: bool,
1276
1277        /// Show architecture explanations with literature references
1278        #[arg(long)]
1279        explain: bool,
1280
1281        /// Show kernel compatibility report (quantization, TPS estimates)
1282        #[arg(long)]
1283        kernels: bool,
1284
1285        /// Cross-validate contract against HuggingFace config.json
1286        #[arg(long)]
1287        validate: bool,
1288
1289        /// Enable all analysis sections (stats + explain + kernels + validate)
1290        #[arg(long)]
1291        full: bool,
1292    },
1293}
1294
1295/// PMAT-237: Extract model file paths from a command variant.
1296///
1297/// Returns paths for action commands (run, serve, bench, etc.) that should be
1298/// validated against the tensor contract. Returns empty vec for diagnostic
1299/// commands (qa, validate, inspect, debug, etc.) that must work on corrupt models.
1300fn extract_model_paths(command: &Commands) -> Vec<PathBuf> {
1301    match command {
1302        // === ACTION COMMANDS (gated) ===
1303        Commands::Run { source, .. } => {
1304            // Only validate local files, not hf:// or URLs
1305            let path = PathBuf::from(source);
1306            if path.exists() {
1307                vec![path]
1308            } else {
1309                vec![]
1310            }
1311        }
1312        Commands::Serve { file, .. }
1313        | Commands::Trace { file, .. }
1314        | Commands::Export { file, .. }
1315        | Commands::Convert { file, .. }
1316        | Commands::Probar { file, .. }
1317        | Commands::CompareHf { file, .. }
1318        | Commands::Chat { file, .. }
1319        | Commands::Bench { file, .. }
1320        | Commands::Eval { file, .. }
1321        | Commands::Profile { file, .. }
1322        | Commands::Check { file, .. } => vec![file.clone()],
1323
1324        Commands::Merge { files, .. } => files.clone(),
1325
1326        Commands::Cbtop { model_path, .. } => model_path.iter().cloned().collect(),
1327        Commands::Tui { file, .. } => file.iter().cloned().collect(),
1328        Commands::Import { source, .. } => {
1329            let path = PathBuf::from(source);
1330            if path.exists() {
1331                vec![path]
1332            } else {
1333                vec![]
1334            }
1335        }
1336
1337        // Rosetta action subcommands
1338        Commands::Rosetta { action } => match action {
1339            RosettaCommands::Convert { source, .. }
1340            | RosettaCommands::Chain { source, .. }
1341            | RosettaCommands::Verify { source, .. } => vec![source.clone()],
1342            RosettaCommands::CompareInference {
1343                model_a, model_b, ..
1344            } => {
1345                vec![model_a.clone(), model_b.clone()]
1346            }
1347            // Diagnostic rosetta commands — exempt
1348            _ => vec![],
1349        },
1350
1351        // === DIAGNOSTIC COMMANDS (exempt) ===
1352        // qa, validate, inspect, debug, tensors, hex, diff, lint, tree, flow,
1353        // explain, list, rm, pull, showcase, tune, canary, publish
1354        _ => vec![],
1355    }
1356}
1357
1358/// PMAT-237: Validate model files against tensor contract before dispatch.
1359///
1360/// Uses `RosettaStone::validate()` to check for NaN, Inf, all-zeros, density,
1361/// and other contract violations. Returns `CliError::ValidationFailed` (exit 5)
1362/// if any violations are found.
1363///
1364/// GH-213: For sharded SafeTensors models (index.json), validates shard integrity
1365/// via `.apr-manifest.json` checksums instead of RosettaStone (which can't parse
1366/// index files). This catches truncated downloads before inference.
1367fn validate_model_contract(paths: &[PathBuf]) -> Result<(), CliError> {
1368    let rosetta = aprender::format::rosetta::RosettaStone::new();
1369    for path in paths {
1370        if !path.exists() {
1371            continue; // Let the subcommand handle FileNotFound
1372        }
1373        if path.to_string_lossy().ends_with(".safetensors.index.json") {
1374            validate_shard_index(path)?;
1375            continue;
1376        }
1377        validate_single_model(&rosetta, path)?;
1378    }
1379    Ok(())
1380}
1381
1382/// GH-213: For sharded index.json, validate shard integrity via manifest.
1383fn validate_shard_index(path: &Path) -> Result<(), CliError> {
1384    let Some(parent) = path.parent() else {
1385        return Ok(());
1386    };
1387    let manifest_path = parent.join(".apr-manifest.json");
1388    if manifest_path.exists() {
1389        validate_shard_manifest(&manifest_path, parent)?;
1390    }
1391    Ok(())
1392}
1393
1394/// Validate a single model file against the tensor layout contract.
1395fn validate_single_model(
1396    rosetta: &aprender::format::rosetta::RosettaStone,
1397    path: &Path,
1398) -> Result<(), CliError> {
1399    let report = rosetta.validate(path).map_err(|e| {
1400        CliError::ValidationFailed(format!(
1401            "Contract validation failed for {}: {e}",
1402            path.display()
1403        ))
1404    })?;
1405    if !report.is_valid {
1406        let violation_count: usize = report.tensors.iter().map(|t| t.failures.len()).sum();
1407        return Err(CliError::ValidationFailed(format!(
1408            "PMAT-237 CONTRACT VIOLATION: {} has {} violations in {} tensors. \
1409             Use 'apr qa {}' for details. Use --skip-contract to bypass.",
1410            path.display(),
1411            violation_count,
1412            report.failed_tensor_count,
1413            path.display(),
1414        )));
1415    }
1416    Ok(())
1417}
1418
1419/// GH-213: Validate sharded model integrity by checking file sizes against manifest.
1420///
1421/// This is an O(1)-per-file check (stat syscall only, no hashing) that catches
1422/// truncated downloads before they cause cryptic "tensor not found" errors.
1423fn validate_shard_manifest(
1424    manifest_path: &std::path::Path,
1425    cache_dir: &std::path::Path,
1426) -> Result<(), CliError> {
1427    let manifest_str = std::fs::read_to_string(manifest_path).map_err(|e| {
1428        CliError::ValidationFailed(format!(
1429            "Failed to read manifest {}: {e}",
1430            manifest_path.display()
1431        ))
1432    })?;
1433    let manifest: commands::pull::ShardManifest =
1434        serde_json::from_str(&manifest_str).map_err(|e| {
1435            CliError::ValidationFailed(format!(
1436                "Failed to parse manifest {}: {e}",
1437                manifest_path.display()
1438            ))
1439        })?;
1440
1441    for (filename, checksum) in &manifest.files {
1442        let file_path = cache_dir.join(filename);
1443        if !file_path.exists() {
1444            return Err(CliError::ValidationFailed(format!(
1445                "Shard '{}' is missing. Re-run 'apr pull --force' to re-download.",
1446                filename
1447            )));
1448        }
1449        let actual_size = std::fs::metadata(&file_path)
1450            .map(|m| m.len())
1451            .map_err(|e| {
1452                CliError::ValidationFailed(format!("Failed to stat shard '{}': {e}", filename))
1453            })?;
1454        if actual_size != checksum.size {
1455            return Err(CliError::ValidationFailed(format!(
1456                "Shard '{}' size mismatch: expected {} bytes, got {} bytes \
1457                 (file may be truncated). Re-run 'apr pull --force' to re-download.",
1458                filename, checksum.size, actual_size
1459            )));
1460        }
1461    }
1462    Ok(())
1463}
1464
1465/// Dispatch `apr cbtop` — extracted to reduce cognitive complexity of `execute_command`
1466#[allow(clippy::too_many_arguments)]
1467fn dispatch_cbtop(
1468    model: Option<&str>,
1469    attach: Option<&str>,
1470    model_path: Option<&Path>,
1471    headless: bool,
1472    json: bool,
1473    output: Option<&Path>,
1474    ci: bool,
1475    throughput: Option<f64>,
1476    brick_score: Option<u32>,
1477    warmup: usize,
1478    iterations: usize,
1479    speculative: bool,
1480    speculation_k: usize,
1481    draft_model: Option<&Path>,
1482    concurrent: usize,
1483    simulated: bool,
1484) -> Result<(), CliError> {
1485    let (resolved_model, resolved_model_path) = if let Some(m) = model {
1486        let path = std::path::Path::new(m);
1487        let is_gguf = path
1488            .extension()
1489            .is_some_and(|ext| ext.eq_ignore_ascii_case("gguf"));
1490        if is_gguf || path.exists() {
1491            (
1492                Some(
1493                    path.file_stem()
1494                        .and_then(|s| s.to_str())
1495                        .unwrap_or(m)
1496                        .to_string(),
1497                ),
1498                Some(PathBuf::from(m)),
1499            )
1500        } else {
1501            (Some(m.to_string()), model_path.map(PathBuf::from))
1502        }
1503    } else {
1504        (None, model_path.map(PathBuf::from))
1505    };
1506
1507    cbtop::run(cbtop::CbtopConfig {
1508        model: resolved_model,
1509        attach: attach.map(String::from),
1510        model_path: resolved_model_path,
1511        headless,
1512        json,
1513        output: output.map(PathBuf::from),
1514        ci,
1515        throughput_threshold: throughput,
1516        brick_score_threshold: brick_score,
1517        warmup,
1518        iterations,
1519        speculative,
1520        speculation_k,
1521        draft_model_path: draft_model.map(PathBuf::from),
1522        concurrent,
1523        simulated,
1524    })
1525}
1526
1527/// Dispatch `apr showcase` — extracted to reduce cognitive complexity of `execute_command`
1528#[allow(clippy::too_many_arguments)]
1529fn dispatch_showcase(
1530    auto_verify: bool,
1531    step: Option<&str>,
1532    tier: &str,
1533    model_dir: &Path,
1534    baseline: &str,
1535    zram: bool,
1536    runs: usize,
1537    gpu: bool,
1538    json: bool,
1539    verbose: bool,
1540    quiet: bool,
1541) -> Result<(), CliError> {
1542    let step = step.and_then(|s| match s {
1543        "import" => Some(showcase::ShowcaseStep::Import),
1544        "gguf" => Some(showcase::ShowcaseStep::GgufInference),
1545        "convert" => Some(showcase::ShowcaseStep::Convert),
1546        "apr" => Some(showcase::ShowcaseStep::AprInference),
1547        "bench" => Some(showcase::ShowcaseStep::Benchmark),
1548        "chat" => Some(showcase::ShowcaseStep::Chat),
1549        "visualize" => Some(showcase::ShowcaseStep::Visualize),
1550        "zram" => Some(showcase::ShowcaseStep::ZramDemo),
1551        "cuda" => Some(showcase::ShowcaseStep::CudaDemo),
1552        "brick" => Some(showcase::ShowcaseStep::BrickDemo),
1553        "all" => Some(showcase::ShowcaseStep::All),
1554        _ => None,
1555    });
1556
1557    let tier = match tier {
1558        "tiny" => showcase::ModelTier::Tiny,
1559        "small" => showcase::ModelTier::Small,
1560        "medium" => showcase::ModelTier::Medium,
1561        "large" => showcase::ModelTier::Large,
1562        _ => showcase::ModelTier::Small,
1563    };
1564
1565    let baselines: Vec<showcase::Baseline> = baseline
1566        .split(',')
1567        .filter_map(|b| match b.trim() {
1568            "llama-cpp" => Some(showcase::Baseline::LlamaCpp),
1569            "ollama" => Some(showcase::Baseline::Ollama),
1570            _ => None,
1571        })
1572        .collect();
1573
1574    let export_format = if json {
1575        showcase::ExportFormat::Json
1576    } else {
1577        showcase::ExportFormat::None
1578    };
1579
1580    let config = showcase::ShowcaseConfig {
1581        tier,
1582        model: tier.model_path().to_string(),
1583        quant: "Q4_K_M".to_string(),
1584        model_dir: model_dir.to_path_buf(),
1585        auto_verify,
1586        step,
1587        baselines,
1588        zram,
1589        bench_runs: runs,
1590        export_format,
1591        export_path: None,
1592        gpu,
1593        verbose,
1594        quiet,
1595    };
1596
1597    showcase::run(&config)
1598}
1599
1600/// Dispatch `apr profile` — extracted to reduce cognitive complexity of `execute_command`
1601#[allow(clippy::too_many_arguments)]
1602fn dispatch_profile(
1603    file: &Path,
1604    granular: bool,
1605    format: &str,
1606    focus: Option<&str>,
1607    detect_naive: bool,
1608    threshold: f64,
1609    compare_hf: Option<&str>,
1610    energy: bool,
1611    perf_grade: bool,
1612    callgraph: bool,
1613    fail_on_naive: bool,
1614    output: Option<&Path>,
1615    ci: bool,
1616    assert_throughput: Option<f64>,
1617    assert_p99: Option<f64>,
1618    assert_p50: Option<f64>,
1619    warmup: usize,
1620    measure: usize,
1621    tokens: usize,
1622    ollama: bool,
1623    no_gpu: bool,
1624    compare: Option<&Path>,
1625) -> Result<(), CliError> {
1626    let output_format = format.parse().unwrap_or(profile::OutputFormat::Human);
1627
1628    // PMAT-192: CI mode takes precedence
1629    if ci || assert_throughput.is_some() || assert_p99.is_some() || assert_p50.is_some() {
1630        let assertions = profile::CiAssertions {
1631            min_throughput: assert_throughput,
1632            max_p99_ms: assert_p99,
1633            max_p50_ms: assert_p50,
1634            max_memory_mb: None,
1635        };
1636        match profile::run_ci(file, output_format, &assertions, warmup, measure) {
1637            Ok(true) => Ok(()),
1638            Ok(false) => {
1639                std::process::exit(1);
1640            }
1641            Err(e) => Err(e),
1642        }
1643    } else if let Some(compare_path) = compare {
1644        // F-PROFILE-011: Cross-format performance comparison
1645        profile::run_cross_format_comparison(file, compare_path, warmup, measure, tokens, no_gpu)
1646    } else {
1647        let profile_focus = focus
1648            .and_then(|f| f.parse().ok())
1649            .unwrap_or(profile::ProfileFocus::All);
1650        profile::run(
1651            file,
1652            granular,
1653            output_format,
1654            profile_focus,
1655            detect_naive,
1656            threshold,
1657            compare_hf,
1658            energy,
1659            perf_grade,
1660            callgraph,
1661            fail_on_naive,
1662            output,
1663            tokens,
1664            ollama,
1665            no_gpu,
1666        )
1667    }
1668}
1669
1670/// Dispatch `apr run` — extracted to reduce cognitive complexity of `execute_command`
1671#[allow(clippy::too_many_arguments)]
1672fn dispatch_run(
1673    source: &str,
1674    positional_prompt: Option<&String>,
1675    input: Option<&Path>,
1676    prompt: Option<&String>,
1677    max_tokens: usize,
1678    stream: bool,
1679    language: Option<&str>,
1680    task: Option<&str>,
1681    format: &str,
1682    no_gpu: bool,
1683    offline: bool,
1684    benchmark: bool,
1685    verbose: bool,
1686    trace: bool,
1687    trace_payload: bool,
1688    trace_steps: Option<&[String]>,
1689    trace_verbose: bool,
1690    trace_output: Option<PathBuf>,
1691    trace_level: &str,
1692    profile: bool,
1693    chat: bool,
1694) -> Result<(), CliError> {
1695    let effective_trace = trace || trace_payload;
1696    let effective_trace_level = if trace_payload {
1697        "payload"
1698    } else {
1699        trace_level
1700    };
1701    let merged_prompt = prompt.or(positional_prompt).cloned();
1702    let effective_prompt = if chat {
1703        merged_prompt
1704            .as_ref()
1705            .map(|p| format!("<|im_start|>user\n{p}<|im_end|>\n<|im_start|>assistant\n"))
1706    } else {
1707        merged_prompt
1708    };
1709
1710    run::run(
1711        source,
1712        input,
1713        effective_prompt.as_deref(),
1714        max_tokens,
1715        stream,
1716        language,
1717        task,
1718        format,
1719        no_gpu,
1720        offline,
1721        benchmark,
1722        verbose,
1723        effective_trace,
1724        trace_steps,
1725        trace_verbose,
1726        trace_output,
1727        effective_trace_level,
1728        profile,
1729    )
1730}
1731
1732/// Build server config and launch serve.
1733#[allow(clippy::too_many_arguments)]
1734fn dispatch_serve(
1735    file: &Path,
1736    port: u16,
1737    host: &str,
1738    no_cors: bool,
1739    no_metrics: bool,
1740    no_gpu: bool,
1741    gpu: bool,
1742    batch: bool,
1743    trace: bool,
1744    trace_level: &str,
1745    profile: bool,
1746    verbose: bool,
1747) -> Result<(), CliError> {
1748    let config = serve::ServerConfig {
1749        port,
1750        host: host.to_owned(),
1751        cors: !no_cors,
1752        metrics: !no_metrics,
1753        no_gpu,
1754        gpu,
1755        batch,
1756        trace,
1757        trace_level: trace_level.to_owned(),
1758        profile,
1759        verbose,
1760        ..Default::default()
1761    };
1762    serve::run(file, &config)
1763}
1764
1765/// Parse hex offset and run hex inspection.
1766#[allow(clippy::too_many_arguments)]
1767fn dispatch_hex(
1768    file: &Path,
1769    tensor: Option<&str>,
1770    limit: usize,
1771    stats: bool,
1772    list: bool,
1773    json: bool,
1774    header: bool,
1775    blocks: bool,
1776    distribution: bool,
1777    contract: bool,
1778    entropy: bool,
1779    raw: bool,
1780    offset: &str,
1781    width: usize,
1782) -> Result<(), CliError> {
1783    let parsed_offset = hex::parse_hex_offset(offset).map_err(CliError::InvalidFormat)?;
1784    hex::run(&hex::HexOptions {
1785        file: file.to_path_buf(),
1786        tensor: tensor.map(String::from),
1787        limit,
1788        stats,
1789        list,
1790        json,
1791        header,
1792        blocks,
1793        distribution,
1794        contract,
1795        entropy,
1796        raw,
1797        offset: parsed_offset,
1798        width,
1799    })
1800}
1801
1802/// Dispatch a rosetta subcommand.
1803fn dispatch_rosetta(action: &RosettaCommands, global_json: bool) -> Result<(), CliError> {
1804    match action {
1805        RosettaCommands::Inspect {
1806            file,
1807            hexdump,
1808            json,
1809        } => rosetta::run_inspect(file, *hexdump, *json || global_json),
1810        RosettaCommands::Convert {
1811            source,
1812            target,
1813            quantize,
1814            verify,
1815            json,
1816            tokenizer,
1817        } => rosetta::run_convert(
1818            source,
1819            target,
1820            quantize.as_deref(),
1821            *verify,
1822            *json || global_json,
1823            tokenizer.as_deref(),
1824        ),
1825        RosettaCommands::Chain {
1826            source,
1827            formats,
1828            work_dir,
1829            json,
1830        } => rosetta::run_chain(source, formats, work_dir, *json || global_json),
1831        RosettaCommands::Verify {
1832            source,
1833            intermediate,
1834            tolerance,
1835            json,
1836        } => rosetta::run_verify(source, intermediate, *tolerance, *json || global_json),
1837        RosettaCommands::CompareInference {
1838            model_a,
1839            model_b,
1840            prompt,
1841            max_tokens,
1842            temperature,
1843            tolerance,
1844            json,
1845        } => rosetta::run_compare_inference(
1846            model_a,
1847            model_b,
1848            prompt,
1849            *max_tokens,
1850            *temperature,
1851            *tolerance,
1852            *json || global_json,
1853        ),
1854        RosettaCommands::DiffTensors {
1855            model_a,
1856            model_b,
1857            mismatches_only,
1858            show_values,
1859            filter,
1860            json,
1861        } => rosetta::run_diff_tensors(
1862            model_a,
1863            model_b,
1864            *mismatches_only,
1865            *show_values,
1866            filter.as_deref(),
1867            *json || global_json,
1868        ),
1869        RosettaCommands::Fingerprint {
1870            model,
1871            model_b,
1872            output,
1873            filter,
1874            verbose,
1875            json,
1876        } => rosetta::run_fingerprint(
1877            model,
1878            model_b.as_ref().map(std::path::PathBuf::as_path),
1879            output.as_ref().map(std::path::PathBuf::as_path),
1880            filter.as_deref(),
1881            *verbose,
1882            *json || global_json,
1883        ),
1884        RosettaCommands::ValidateStats {
1885            model,
1886            reference,
1887            fingerprints,
1888            threshold,
1889            strict,
1890            json,
1891        } => rosetta::run_validate_stats(
1892            model,
1893            reference.as_ref().map(std::path::PathBuf::as_path),
1894            fingerprints.as_ref().map(std::path::PathBuf::as_path),
1895            *threshold,
1896            *strict,
1897            *json || global_json,
1898        ),
1899    }
1900}
1901
1902/// Execute the CLI command and return the result.
1903pub fn execute_command(cli: &Cli) -> Result<(), CliError> {
1904    // PMAT-237: Contract gate — refuse to operate on corrupt models
1905    if !cli.skip_contract {
1906        let paths = extract_model_paths(&cli.command);
1907        validate_model_contract(&paths)?;
1908    }
1909
1910    dispatch_core_command(cli).unwrap_or_else(|| dispatch_extended_command(cli))
1911}
1912
1913/// Dispatch core commands (run, serve, inspection, format operations).
1914#[allow(clippy::too_many_lines)]
1915fn dispatch_core_command(cli: &Cli) -> Option<Result<(), CliError>> {
1916    Some(match cli.command.as_ref() {
1917        Commands::Check { file, no_gpu } => commands::check::run(file, *no_gpu),
1918        Commands::Run {
1919            source,
1920            positional_prompt,
1921            input,
1922            prompt,
1923            max_tokens,
1924            stream,
1925            language,
1926            task,
1927            format,
1928            no_gpu,
1929            gpu: _,
1930            offline,
1931            benchmark,
1932            trace,
1933            trace_steps,
1934            trace_verbose,
1935            trace_output,
1936            trace_level,
1937            trace_payload,
1938            profile,
1939            chat,
1940            verbose,
1941        } => dispatch_run(
1942            source,
1943            positional_prompt.as_ref(),
1944            input.as_deref(),
1945            prompt.as_ref(),
1946            *max_tokens,
1947            *stream,
1948            language.as_deref(),
1949            task.as_deref(),
1950            format,
1951            *no_gpu,
1952            *offline,
1953            *benchmark,
1954            *verbose || cli.verbose,
1955            *trace,
1956            *trace_payload,
1957            trace_steps.as_deref(),
1958            *trace_verbose,
1959            trace_output.clone(),
1960            trace_level.as_str(),
1961            *profile,
1962            *chat,
1963        ),
1964
1965        Commands::Serve {
1966            file,
1967            port,
1968            host,
1969            no_cors,
1970            no_metrics,
1971            no_gpu,
1972            gpu,
1973            batch,
1974            trace,
1975            trace_level,
1976            profile,
1977        } => dispatch_serve(
1978            file,
1979            *port,
1980            host,
1981            *no_cors,
1982            *no_metrics,
1983            *no_gpu,
1984            *gpu,
1985            *batch,
1986            *trace,
1987            trace_level,
1988            *profile,
1989            cli.verbose,
1990        ),
1991
1992        Commands::Inspect {
1993            file,
1994            vocab,
1995            filters,
1996            weights,
1997            json,
1998        } => inspect::run(file, *vocab, *filters, *weights, *json || cli.json),
1999
2000        Commands::Debug {
2001            file,
2002            drama,
2003            hex,
2004            strings,
2005            limit,
2006        } => debug::run(file, *drama, *hex, *strings, *limit),
2007
2008        Commands::Validate {
2009            file,
2010            quality,
2011            strict,
2012            min_score,
2013        } => validate::run(file, *quality, *strict, *min_score),
2014
2015        Commands::Diff {
2016            file1,
2017            file2,
2018            weights,
2019            values,
2020            filter,
2021            limit,
2022            transpose_aware,
2023            json,
2024        } => diff::run(
2025            file1,
2026            file2,
2027            *weights,
2028            *values,
2029            filter.as_deref(),
2030            *limit,
2031            *transpose_aware,
2032            *json || cli.json,
2033        ),
2034
2035        Commands::Tensors {
2036            file,
2037            stats,
2038            filter,
2039            limit,
2040            json,
2041        } => tensors::run(file, *stats, filter.as_deref(), *json || cli.json, *limit),
2042
2043        Commands::Trace {
2044            file,
2045            layer,
2046            reference,
2047            json,
2048            verbose,
2049            payload,
2050            diff,
2051            interactive,
2052        } => trace::run(
2053            file,
2054            layer.as_deref(),
2055            reference.as_deref(),
2056            *json || cli.json,
2057            *verbose || cli.verbose,
2058            *payload,
2059            *diff,
2060            *interactive,
2061        ),
2062
2063        Commands::Lint { file } => lint::run(file),
2064        Commands::Explain { code, file, tensor } => {
2065            explain::run(code.clone(), file.clone(), tensor.clone())
2066        }
2067        Commands::Canary { command } => canary::run(command.clone()),
2068        Commands::Export {
2069            file,
2070            format,
2071            output,
2072            quantize,
2073        } => export::run(file, format, output, quantize.as_deref()),
2074        Commands::Import {
2075            source,
2076            output,
2077            arch,
2078            quantize,
2079            strict,
2080            preserve_q4k,
2081            tokenizer,
2082            enforce_provenance,
2083        } => import::run(
2084            source,
2085            output.as_deref(),
2086            Some(arch.as_str()),
2087            quantize.as_deref(),
2088            *strict,
2089            *preserve_q4k,
2090            tokenizer.as_ref(),
2091            *enforce_provenance,
2092        ),
2093        Commands::Pull { model_ref, force } => pull::run(model_ref, *force),
2094        Commands::List => pull::list(),
2095        Commands::Rm { model_ref } => pull::remove(model_ref),
2096        Commands::Convert {
2097            file,
2098            quantize,
2099            compress,
2100            output,
2101            force,
2102        } => convert::run(
2103            file,
2104            quantize.as_deref(),
2105            compress.as_deref(),
2106            output,
2107            *force,
2108        ),
2109        Commands::Merge {
2110            files,
2111            strategy,
2112            output,
2113            weights,
2114        } => merge::run(files, strategy, output, weights.clone()),
2115        Commands::Tui { file } => tui::run(file.clone()),
2116        _ => return None,
2117    })
2118}
2119
2120/// Dispatch extended commands (analysis, profiling, QA, benchmarks).
2121#[allow(clippy::too_many_lines)]
2122fn dispatch_extended_command(cli: &Cli) -> Result<(), CliError> {
2123    match cli.command.as_ref() {
2124        Commands::Cbtop {
2125            model,
2126            attach,
2127            model_path,
2128            headless,
2129            json,
2130            output,
2131            ci,
2132            throughput,
2133            brick_score,
2134            warmup,
2135            iterations,
2136            speculative,
2137            speculation_k,
2138            draft_model,
2139            concurrent,
2140            simulated,
2141        } => dispatch_cbtop(
2142            model.as_deref(),
2143            attach.as_deref(),
2144            model_path.as_deref(),
2145            *headless,
2146            *json,
2147            output.as_deref(),
2148            *ci,
2149            *throughput,
2150            *brick_score,
2151            *warmup,
2152            *iterations,
2153            *speculative,
2154            *speculation_k,
2155            draft_model.as_deref(),
2156            *concurrent,
2157            *simulated,
2158        ),
2159
2160        Commands::Probar {
2161            file,
2162            output,
2163            format,
2164            golden,
2165            layer,
2166        } => probar::run(
2167            file,
2168            output,
2169            format.parse().unwrap_or(probar::ExportFormat::Both),
2170            golden.as_deref(),
2171            layer.as_deref(),
2172        ),
2173
2174        Commands::CompareHf {
2175            file,
2176            hf,
2177            tensor,
2178            threshold,
2179            json,
2180        } => compare_hf::run(file, hf, tensor.as_deref(), *threshold, *json || cli.json),
2181
2182        Commands::Hex {
2183            file,
2184            tensor,
2185            limit,
2186            stats,
2187            list,
2188            json,
2189            header,
2190            blocks,
2191            distribution,
2192            contract,
2193            entropy,
2194            raw,
2195            offset,
2196            width,
2197        } => dispatch_hex(
2198            file,
2199            tensor.as_deref(),
2200            *limit,
2201            *stats,
2202            *list,
2203            *json || cli.json,
2204            *header,
2205            *blocks,
2206            *distribution,
2207            *contract,
2208            *entropy,
2209            *raw,
2210            offset,
2211            *width,
2212        ),
2213
2214        Commands::Tree {
2215            file,
2216            filter,
2217            format,
2218            sizes,
2219            depth,
2220        } => tree::run(
2221            file,
2222            filter.as_deref(),
2223            format.parse().unwrap_or(tree::TreeFormat::Ascii),
2224            *sizes,
2225            *depth,
2226        ),
2227
2228        Commands::Flow {
2229            file,
2230            layer,
2231            component,
2232            verbose,
2233        } => flow::run(
2234            file,
2235            layer.as_deref(),
2236            component.parse().unwrap_or(flow::FlowComponent::Full),
2237            *verbose || cli.verbose,
2238        ),
2239
2240        Commands::Chat {
2241            file,
2242            temperature,
2243            top_p,
2244            max_tokens,
2245            system,
2246            inspect,
2247            no_gpu,
2248            gpu: _,
2249            trace,
2250            trace_steps,
2251            trace_verbose,
2252            trace_output,
2253            trace_level,
2254            profile,
2255        } => chat::run(
2256            file,
2257            *temperature,
2258            *top_p,
2259            *max_tokens,
2260            system.as_deref(),
2261            *inspect,
2262            *no_gpu,
2263            *trace,
2264            trace_steps.as_deref(),
2265            *trace_verbose,
2266            trace_output.clone(),
2267            trace_level.as_str(),
2268            *profile,
2269        ),
2270
2271        Commands::Bench {
2272            file,
2273            warmup,
2274            iterations,
2275            max_tokens,
2276            prompt,
2277            fast,
2278            brick,
2279        } => bench::run(
2280            file,
2281            *warmup,
2282            *iterations,
2283            *max_tokens,
2284            prompt.as_deref(),
2285            *fast,
2286            brick.as_deref(),
2287        ),
2288
2289        Commands::Eval {
2290            file,
2291            dataset,
2292            text,
2293            max_tokens,
2294            threshold,
2295        } => eval::run(
2296            file,
2297            dataset,
2298            text.as_deref(),
2299            Some(*max_tokens),
2300            Some(*threshold),
2301        ),
2302
2303        Commands::Profile {
2304            file,
2305            granular,
2306            format,
2307            focus,
2308            detect_naive,
2309            threshold,
2310            compare_hf,
2311            energy,
2312            perf_grade,
2313            callgraph,
2314            fail_on_naive,
2315            output,
2316            ci,
2317            assert_throughput,
2318            assert_p99,
2319            assert_p50,
2320            warmup,
2321            measure,
2322            tokens,
2323            ollama,
2324            no_gpu,
2325            compare,
2326        } => dispatch_profile(
2327            file,
2328            *granular,
2329            format,
2330            focus.as_deref(),
2331            *detect_naive,
2332            *threshold,
2333            compare_hf.as_deref(),
2334            *energy,
2335            *perf_grade,
2336            *callgraph,
2337            *fail_on_naive,
2338            output.as_deref(),
2339            *ci,
2340            *assert_throughput,
2341            *assert_p99,
2342            *assert_p50,
2343            *warmup,
2344            *measure,
2345            *tokens,
2346            *ollama,
2347            *no_gpu,
2348            compare.as_deref(),
2349        ),
2350
2351        Commands::Qa {
2352            file,
2353            assert_tps,
2354            assert_speedup,
2355            assert_gpu_speedup,
2356            skip_golden,
2357            skip_throughput,
2358            skip_ollama,
2359            skip_gpu_speedup,
2360            skip_contract,
2361            skip_format_parity,
2362            skip_ptx_parity,
2363            safetensors_path,
2364            iterations,
2365            warmup,
2366            max_tokens,
2367            json,
2368            verbose,
2369            min_executed,
2370            previous_report,
2371            regression_threshold,
2372            skip_gpu_state,
2373            skip_metadata,
2374        } => qa::run(
2375            file,
2376            *assert_tps,
2377            *assert_speedup,
2378            *assert_gpu_speedup,
2379            *skip_golden,
2380            *skip_throughput,
2381            *skip_ollama,
2382            *skip_gpu_speedup,
2383            *skip_contract,
2384            *skip_format_parity,
2385            *skip_ptx_parity,
2386            safetensors_path.clone(),
2387            *iterations,
2388            *warmup,
2389            *max_tokens,
2390            *json || cli.json,
2391            *verbose || cli.verbose,
2392            *min_executed,
2393            previous_report.clone(),
2394            *regression_threshold,
2395            *skip_gpu_state,
2396            *skip_metadata,
2397        ),
2398
2399        Commands::Parity {
2400            file,
2401            prompt,
2402            assert,
2403        } => commands::parity::run(file, prompt, *assert, cli.verbose),
2404
2405        Commands::PtxMap {
2406            file,
2407            kernel,
2408            reverse,
2409            json,
2410            verbose,
2411            prefill,
2412        } => commands::ptx_map::run(
2413            file,
2414            kernel.as_deref(),
2415            reverse.as_deref(),
2416            *json || cli.json,
2417            *verbose || cli.verbose,
2418            *prefill,
2419        ),
2420
2421        Commands::Ptx {
2422            file,
2423            kernel,
2424            strict,
2425            bugs,
2426            json,
2427            verbose,
2428        } => ptx_explain::run(
2429            file.as_deref(),
2430            kernel.as_deref(),
2431            *strict,
2432            *bugs,
2433            *json || cli.json,
2434            *verbose || cli.verbose,
2435        ),
2436
2437        Commands::Tune {
2438            file,
2439            method,
2440            rank,
2441            vram,
2442            plan,
2443            model,
2444            freeze_base,
2445            train_data,
2446            json,
2447        } => tune::run(
2448            file.as_deref(),
2449            method.parse().unwrap_or(tune::TuneMethod::Auto),
2450            *rank,
2451            *vram,
2452            *plan,
2453            model.as_deref(),
2454            *freeze_base,
2455            train_data.as_deref(),
2456            *json || cli.json,
2457        ),
2458
2459        Commands::Showcase {
2460            auto_verify,
2461            step,
2462            tier,
2463            model_dir,
2464            baseline,
2465            zram,
2466            runs,
2467            gpu,
2468            json,
2469            verbose,
2470            quiet,
2471        } => dispatch_showcase(
2472            *auto_verify,
2473            step.as_deref(),
2474            tier,
2475            model_dir,
2476            baseline,
2477            *zram,
2478            *runs,
2479            *gpu,
2480            *json,
2481            *verbose,
2482            *quiet,
2483        ),
2484
2485        Commands::Rosetta { action } => dispatch_rosetta(action, cli.json),
2486
2487        Commands::Publish {
2488            directory,
2489            repo_id,
2490            model_name,
2491            license,
2492            pipeline_tag,
2493            library_name,
2494            tags,
2495            message,
2496            dry_run,
2497        } => publish::execute(
2498            directory,
2499            repo_id,
2500            model_name.as_deref(),
2501            license,
2502            pipeline_tag,
2503            library_name.as_deref(),
2504            tags.as_ref().map_or(&[], std::vec::Vec::as_slice),
2505            message.as_deref(),
2506            *dry_run,
2507            cli.verbose,
2508        ),
2509
2510        Commands::Oracle {
2511            source,
2512            family,
2513            size,
2514            compliance,
2515            tensors,
2516            stats,
2517            explain,
2518            kernels,
2519            validate,
2520            full,
2521        } => oracle::run(
2522            source.as_ref(),
2523            family.as_ref(),
2524            size.as_ref(),
2525            *compliance,
2526            *tensors,
2527            cli.json,
2528            cli.verbose,
2529            cli.offline,
2530            oracle::OracleFlags {
2531                stats: *stats,
2532                explain: *explain,
2533                kernels: *kernels,
2534                validate: *validate,
2535                full: *full,
2536            },
2537        ),
2538
2539        // All other commands handled by dispatch_core_command
2540        _ => unreachable!("dispatch_core_command handles all remaining variants"),
2541    }
2542}
2543
2544// ============================================================================
2545// Tests
2546// ============================================================================
2547
2548#[cfg(test)]
2549mod tests {
2550    use super::*;
2551
2552    /// Parse CLI args on a thread with 16 MB stack.
2553    /// Clap's parser for 34 subcommands exceeds the default test-thread
2554    /// stack in debug builds.
2555    fn parse_cli(args: Vec<&'static str>) -> Result<Cli, clap::error::Error> {
2556        std::thread::Builder::new()
2557            .stack_size(16 * 1024 * 1024)
2558            .spawn(move || Cli::try_parse_from(args))
2559            .expect("spawn thread")
2560            .join()
2561            .expect("join thread")
2562    }
2563
2564    /// Test CLI parsing with clap's debug_assert
2565    #[test]
2566    fn test_cli_parsing_valid() {
2567        use clap::CommandFactory;
2568        std::thread::Builder::new()
2569            .stack_size(16 * 1024 * 1024)
2570            .spawn(|| Cli::command().debug_assert())
2571            .expect("spawn")
2572            .join()
2573            .expect("join");
2574    }
2575
2576    /// Test parsing 'apr inspect' command
2577    #[test]
2578    fn test_parse_inspect_command() {
2579        let args = vec!["apr", "inspect", "model.apr"];
2580        let cli = parse_cli(args).expect("Failed to parse");
2581        match *cli.command {
2582            Commands::Inspect { file, .. } => {
2583                assert_eq!(file, PathBuf::from("model.apr"));
2584            }
2585            _ => panic!("Expected Inspect command"),
2586        }
2587    }
2588
2589    /// Test parsing 'apr inspect' with flags
2590    #[test]
2591    fn test_parse_inspect_with_flags() {
2592        let args = vec!["apr", "inspect", "model.apr", "--vocab", "--json"];
2593        let cli = parse_cli(args).expect("Failed to parse");
2594        match *cli.command {
2595            Commands::Inspect {
2596                file, vocab, json, ..
2597            } => {
2598                assert_eq!(file, PathBuf::from("model.apr"));
2599                assert!(vocab);
2600                assert!(json);
2601            }
2602            _ => panic!("Expected Inspect command"),
2603        }
2604    }
2605
2606    /// Test parsing 'apr serve' command
2607    #[test]
2608    fn test_parse_serve_command() {
2609        let args = vec!["apr", "serve", "model.apr", "--port", "3000"];
2610        let cli = parse_cli(args).expect("Failed to parse");
2611        match *cli.command {
2612            Commands::Serve { file, port, .. } => {
2613                assert_eq!(file, PathBuf::from("model.apr"));
2614                assert_eq!(port, 3000);
2615            }
2616            _ => panic!("Expected Serve command"),
2617        }
2618    }
2619
2620    /// Test parsing 'apr run' command
2621    #[test]
2622    fn test_parse_run_command() {
2623        let args = vec![
2624            "apr",
2625            "run",
2626            "hf://openai/whisper-tiny",
2627            "--prompt",
2628            "Hello",
2629            "--max-tokens",
2630            "64",
2631        ];
2632        let cli = parse_cli(args).expect("Failed to parse");
2633        match *cli.command {
2634            Commands::Run {
2635                source,
2636                prompt,
2637                max_tokens,
2638                ..
2639            } => {
2640                assert_eq!(source, "hf://openai/whisper-tiny");
2641                assert_eq!(prompt, Some("Hello".to_string()));
2642                assert_eq!(max_tokens, 64);
2643            }
2644            _ => panic!("Expected Run command"),
2645        }
2646    }
2647
2648    /// Test parsing 'apr chat' command
2649    #[test]
2650    fn test_parse_chat_command() {
2651        let args = vec![
2652            "apr",
2653            "chat",
2654            "model.gguf",
2655            "--temperature",
2656            "0.5",
2657            "--top-p",
2658            "0.95",
2659        ];
2660        let cli = parse_cli(args).expect("Failed to parse");
2661        match *cli.command {
2662            Commands::Chat {
2663                file,
2664                temperature,
2665                top_p,
2666                ..
2667            } => {
2668                assert_eq!(file, PathBuf::from("model.gguf"));
2669                assert!((temperature - 0.5).abs() < f32::EPSILON);
2670                assert!((top_p - 0.95).abs() < f32::EPSILON);
2671            }
2672            _ => panic!("Expected Chat command"),
2673        }
2674    }
2675
2676    /// Test parsing 'apr validate' command with quality flag
2677    #[test]
2678    fn test_parse_validate_with_quality() {
2679        let args = vec!["apr", "validate", "model.apr", "--quality", "--strict"];
2680        let cli = parse_cli(args).expect("Failed to parse");
2681        match *cli.command {
2682            Commands::Validate {
2683                file,
2684                quality,
2685                strict,
2686                ..
2687            } => {
2688                assert_eq!(file, PathBuf::from("model.apr"));
2689                assert!(quality);
2690                assert!(strict);
2691            }
2692            _ => panic!("Expected Validate command"),
2693        }
2694    }
2695
2696    /// Test parsing 'apr diff' command
2697    #[test]
2698    fn test_parse_diff_command() {
2699        let args = vec!["apr", "diff", "model1.apr", "model2.apr", "--weights"];
2700        let cli = parse_cli(args).expect("Failed to parse");
2701        match *cli.command {
2702            Commands::Diff {
2703                file1,
2704                file2,
2705                weights,
2706                ..
2707            } => {
2708                assert_eq!(file1, PathBuf::from("model1.apr"));
2709                assert_eq!(file2, PathBuf::from("model2.apr"));
2710                assert!(weights);
2711            }
2712            _ => panic!("Expected Diff command"),
2713        }
2714    }
2715
2716    /// Test parsing 'apr bench' command
2717    #[test]
2718    fn test_parse_bench_command() {
2719        let args = vec![
2720            "apr",
2721            "bench",
2722            "model.gguf",
2723            "--warmup",
2724            "5",
2725            "--iterations",
2726            "10",
2727        ];
2728        let cli = parse_cli(args).expect("Failed to parse");
2729        match *cli.command {
2730            Commands::Bench {
2731                file,
2732                warmup,
2733                iterations,
2734                ..
2735            } => {
2736                assert_eq!(file, PathBuf::from("model.gguf"));
2737                assert_eq!(warmup, 5);
2738                assert_eq!(iterations, 10);
2739            }
2740            _ => panic!("Expected Bench command"),
2741        }
2742    }
2743
2744    /// Test parsing 'apr cbtop' command with CI flags
2745    #[test]
2746    fn test_parse_cbtop_ci_mode() {
2747        let args = vec![
2748            "apr",
2749            "cbtop",
2750            "--headless",
2751            "--ci",
2752            "--throughput",
2753            "100.0",
2754            "--brick-score",
2755            "90",
2756        ];
2757        let cli = parse_cli(args).expect("Failed to parse");
2758        match *cli.command {
2759            Commands::Cbtop {
2760                headless,
2761                ci,
2762                throughput,
2763                brick_score,
2764                ..
2765            } => {
2766                assert!(headless);
2767                assert!(ci);
2768                assert_eq!(throughput, Some(100.0));
2769                assert_eq!(brick_score, Some(90));
2770            }
2771            _ => panic!("Expected Cbtop command"),
2772        }
2773    }
2774
2775    /// Test parsing 'apr qa' command
2776    #[test]
2777    fn test_parse_qa_command() {
2778        let args = vec![
2779            "apr",
2780            "qa",
2781            "model.gguf",
2782            "--assert-tps",
2783            "50.0",
2784            "--skip-ollama",
2785        ];
2786        let cli = parse_cli(args).expect("Failed to parse");
2787        match *cli.command {
2788            Commands::Qa {
2789                file,
2790                assert_tps,
2791                skip_ollama,
2792                ..
2793            } => {
2794                assert_eq!(file, PathBuf::from("model.gguf"));
2795                assert_eq!(assert_tps, Some(50.0));
2796                assert!(skip_ollama);
2797            }
2798            _ => panic!("Expected Qa command"),
2799        }
2800    }
2801
2802    /// Test global --verbose flag
2803    #[test]
2804    fn test_global_verbose_flag() {
2805        let args = vec!["apr", "--verbose", "inspect", "model.apr"];
2806        let cli = parse_cli(args).expect("Failed to parse");
2807        assert!(cli.verbose);
2808    }
2809
2810    /// Test global --json flag
2811    #[test]
2812    fn test_global_json_flag() {
2813        let args = vec!["apr", "--json", "inspect", "model.apr"];
2814        let cli = parse_cli(args).expect("Failed to parse");
2815        assert!(cli.json);
2816    }
2817
2818    /// Test parsing 'apr list' command (alias 'ls')
2819    #[test]
2820    fn test_parse_list_command() {
2821        let args = vec!["apr", "list"];
2822        let cli = parse_cli(args).expect("Failed to parse");
2823        assert!(matches!(*cli.command, Commands::List));
2824    }
2825
2826    /// Test parsing 'apr ls' alias
2827    #[test]
2828    fn test_parse_ls_alias() {
2829        let args = vec!["apr", "ls"];
2830        let cli = parse_cli(args).expect("Failed to parse");
2831        assert!(matches!(*cli.command, Commands::List));
2832    }
2833
2834    /// Test parsing 'apr rm' command (alias 'remove')
2835    #[test]
2836    fn test_parse_rm_command() {
2837        let args = vec!["apr", "rm", "model-name"];
2838        let cli = parse_cli(args).expect("Failed to parse");
2839        match *cli.command {
2840            Commands::Rm { model_ref } => {
2841                assert_eq!(model_ref, "model-name");
2842            }
2843            _ => panic!("Expected Rm command"),
2844        }
2845    }
2846
2847    /// Test invalid command fails parsing
2848    #[test]
2849    fn test_invalid_command() {
2850        let args = vec!["apr", "invalid-command"];
2851        let result = parse_cli(args);
2852        assert!(result.is_err());
2853    }
2854
2855    /// Test missing required argument fails
2856    #[test]
2857    fn test_missing_required_arg() {
2858        let args = vec!["apr", "inspect"]; // Missing FILE
2859        let result = parse_cli(args);
2860        assert!(result.is_err());
2861    }
2862
2863    /// Test parsing 'apr merge' with multiple files and weights
2864    #[test]
2865    fn test_parse_merge_command() {
2866        let args = vec![
2867            "apr",
2868            "merge",
2869            "model1.apr",
2870            "model2.apr",
2871            "--strategy",
2872            "weighted",
2873            "--weights",
2874            "0.7,0.3",
2875            "-o",
2876            "merged.apr",
2877        ];
2878        let cli = parse_cli(args).expect("Failed to parse");
2879        match *cli.command {
2880            Commands::Merge {
2881                files,
2882                strategy,
2883                output,
2884                weights,
2885            } => {
2886                assert_eq!(files.len(), 2);
2887                assert_eq!(strategy, "weighted");
2888                assert_eq!(output, PathBuf::from("merged.apr"));
2889                assert_eq!(weights, Some(vec![0.7, 0.3]));
2890            }
2891            _ => panic!("Expected Merge command"),
2892        }
2893    }
2894
2895    /// Test parsing 'apr showcase' command
2896    #[test]
2897    fn test_parse_showcase_command() {
2898        let args = vec![
2899            "apr",
2900            "showcase",
2901            "--tier",
2902            "medium",
2903            "--gpu",
2904            "--auto-verify",
2905        ];
2906        let cli = parse_cli(args).expect("Failed to parse");
2907        match *cli.command {
2908            Commands::Showcase {
2909                tier,
2910                gpu,
2911                auto_verify,
2912                ..
2913            } => {
2914                assert_eq!(tier, "medium");
2915                assert!(gpu);
2916                assert!(auto_verify);
2917            }
2918            _ => panic!("Expected Showcase command"),
2919        }
2920    }
2921
2922    /// Test parsing 'apr profile' with all options
2923    #[test]
2924    fn test_parse_profile_command() {
2925        let args = vec![
2926            "apr",
2927            "profile",
2928            "model.apr",
2929            "--granular",
2930            "--detect-naive",
2931            "--fail-on-naive",
2932        ];
2933        let cli = parse_cli(args).expect("Failed to parse");
2934        match *cli.command {
2935            Commands::Profile {
2936                file,
2937                granular,
2938                detect_naive,
2939                fail_on_naive,
2940                ..
2941            } => {
2942                assert_eq!(file, PathBuf::from("model.apr"));
2943                assert!(granular);
2944                assert!(detect_naive);
2945                assert!(fail_on_naive);
2946            }
2947            _ => panic!("Expected Profile command"),
2948        }
2949    }
2950
2951    /// Test parsing 'apr profile' with CI assertions (PMAT-192, GH-180)
2952    #[test]
2953    fn test_parse_profile_ci_mode() {
2954        let args = vec![
2955            "apr",
2956            "profile",
2957            "model.gguf",
2958            "--ci",
2959            "--assert-throughput",
2960            "100",
2961            "--assert-p99",
2962            "50",
2963            "--format",
2964            "json",
2965        ];
2966        let cli = parse_cli(args).expect("Failed to parse");
2967        match *cli.command {
2968            Commands::Profile {
2969                file,
2970                ci,
2971                assert_throughput,
2972                assert_p99,
2973                format,
2974                ..
2975            } => {
2976                assert_eq!(file, PathBuf::from("model.gguf"));
2977                assert!(ci);
2978                assert_eq!(assert_throughput, Some(100.0));
2979                assert_eq!(assert_p99, Some(50.0));
2980                assert_eq!(format, "json");
2981            }
2982            _ => panic!("Expected Profile command"),
2983        }
2984    }
2985
2986    /// Test parsing 'apr rosetta inspect' command
2987    #[test]
2988    fn test_parse_rosetta_inspect() {
2989        let args = vec!["apr", "rosetta", "inspect", "model.gguf", "--json"];
2990        let cli = parse_cli(args).expect("Failed to parse");
2991        match *cli.command {
2992            Commands::Rosetta { action } => match action {
2993                RosettaCommands::Inspect { file, json, .. } => {
2994                    assert_eq!(file, PathBuf::from("model.gguf"));
2995                    assert!(json);
2996                }
2997                _ => panic!("Expected Inspect subcommand"),
2998            },
2999            _ => panic!("Expected Rosetta command"),
3000        }
3001    }
3002
3003    /// Test parsing 'apr rosetta convert' command
3004    #[test]
3005    fn test_parse_rosetta_convert() {
3006        let args = vec![
3007            "apr",
3008            "rosetta",
3009            "convert",
3010            "model.gguf",
3011            "model.safetensors",
3012            "--verify",
3013        ];
3014        let cli = parse_cli(args).expect("Failed to parse");
3015        match *cli.command {
3016            Commands::Rosetta { action } => match action {
3017                RosettaCommands::Convert {
3018                    source,
3019                    target,
3020                    verify,
3021                    ..
3022                } => {
3023                    assert_eq!(source, PathBuf::from("model.gguf"));
3024                    assert_eq!(target, PathBuf::from("model.safetensors"));
3025                    assert!(verify);
3026                }
3027                _ => panic!("Expected Convert subcommand"),
3028            },
3029            _ => panic!("Expected Rosetta command"),
3030        }
3031    }
3032
3033    /// Test parsing 'apr rosetta chain' command
3034    #[test]
3035    fn test_parse_rosetta_chain() {
3036        let args = vec![
3037            "apr",
3038            "rosetta",
3039            "chain",
3040            "model.gguf",
3041            "safetensors",
3042            "apr",
3043            "--work-dir",
3044            "/tmp/rosetta",
3045        ];
3046        let cli = parse_cli(args).expect("Failed to parse");
3047        match *cli.command {
3048            Commands::Rosetta { action } => match action {
3049                RosettaCommands::Chain {
3050                    source,
3051                    formats,
3052                    work_dir,
3053                    ..
3054                } => {
3055                    assert_eq!(source, PathBuf::from("model.gguf"));
3056                    assert_eq!(formats, vec!["safetensors", "apr"]);
3057                    assert_eq!(work_dir, PathBuf::from("/tmp/rosetta"));
3058                }
3059                _ => panic!("Expected Chain subcommand"),
3060            },
3061            _ => panic!("Expected Rosetta command"),
3062        }
3063    }
3064
3065    /// Test parsing 'apr rosetta verify' command
3066    #[test]
3067    fn test_parse_rosetta_verify() {
3068        let args = vec![
3069            "apr",
3070            "rosetta",
3071            "verify",
3072            "model.apr",
3073            "--intermediate",
3074            "gguf",
3075            "--tolerance",
3076            "1e-4",
3077        ];
3078        let cli = parse_cli(args).expect("Failed to parse");
3079        match *cli.command {
3080            Commands::Rosetta { action } => match action {
3081                RosettaCommands::Verify {
3082                    source,
3083                    intermediate,
3084                    tolerance,
3085                    ..
3086                } => {
3087                    assert_eq!(source, PathBuf::from("model.apr"));
3088                    assert_eq!(intermediate, "gguf");
3089                    assert!((tolerance - 1e-4).abs() < f32::EPSILON);
3090                }
3091                _ => panic!("Expected Verify subcommand"),
3092            },
3093            _ => panic!("Expected Rosetta command"),
3094        }
3095    }
3096
3097    // =========================================================================
3098    // PMAT-237: Contract gate tests
3099    // =========================================================================
3100
3101    /// Test that --skip-contract global flag is parsed
3102    #[test]
3103    fn test_parse_skip_contract_flag() {
3104        let args = vec!["apr", "--skip-contract", "inspect", "model.apr"];
3105        let cli = parse_cli(args).expect("Failed to parse");
3106        assert!(cli.skip_contract);
3107    }
3108
3109    /// Test that --skip-contract defaults to false
3110    #[test]
3111    fn test_skip_contract_default_false() {
3112        let args = vec!["apr", "inspect", "model.apr"];
3113        let cli = parse_cli(args).expect("Failed to parse");
3114        assert!(!cli.skip_contract);
3115    }
3116
3117    /// Test extract_model_paths: diagnostic commands return empty vec
3118    #[test]
3119    fn test_extract_paths_diagnostic_exempt() {
3120        // Diagnostic commands should return no paths (exempt from validation)
3121        let diagnostic_commands = vec![
3122            Commands::Inspect {
3123                file: PathBuf::from("m.apr"),
3124                vocab: false,
3125                filters: false,
3126                weights: false,
3127                json: false,
3128            },
3129            Commands::Debug {
3130                file: PathBuf::from("m.apr"),
3131                drama: false,
3132                hex: false,
3133                strings: false,
3134                limit: 256,
3135            },
3136            Commands::Validate {
3137                file: PathBuf::from("m.apr"),
3138                quality: false,
3139                strict: false,
3140                min_score: None,
3141            },
3142            Commands::Tensors {
3143                file: PathBuf::from("m.apr"),
3144                stats: false,
3145                filter: None,
3146                limit: 0,
3147                json: false,
3148            },
3149            Commands::Lint {
3150                file: PathBuf::from("m.apr"),
3151            },
3152            Commands::Qa {
3153                file: PathBuf::from("m.apr"),
3154                assert_tps: None,
3155                assert_speedup: None,
3156                assert_gpu_speedup: None,
3157                skip_golden: false,
3158                skip_throughput: false,
3159                skip_ollama: false,
3160                skip_gpu_speedup: false,
3161                skip_contract: false,
3162                skip_format_parity: false,
3163                skip_ptx_parity: false,
3164                safetensors_path: None,
3165                iterations: 10,
3166                warmup: 3,
3167                max_tokens: 32,
3168                json: false,
3169                verbose: false,
3170                min_executed: None,
3171                previous_report: None,
3172                regression_threshold: None,
3173                skip_gpu_state: false,
3174                skip_metadata: false,
3175            },
3176            Commands::Hex {
3177                file: PathBuf::from("m.apr"),
3178                tensor: None,
3179                limit: 64,
3180                stats: false,
3181                list: false,
3182                json: false,
3183                header: false,
3184                blocks: false,
3185                distribution: false,
3186                contract: false,
3187                entropy: false,
3188                raw: false,
3189                offset: "0".to_string(),
3190                width: 16,
3191            },
3192            Commands::Tree {
3193                file: PathBuf::from("m.apr"),
3194                filter: None,
3195                format: "ascii".to_string(),
3196                sizes: false,
3197                depth: None,
3198            },
3199            Commands::Flow {
3200                file: PathBuf::from("m.apr"),
3201                layer: None,
3202                component: "full".to_string(),
3203                verbose: false,
3204            },
3205            Commands::Explain {
3206                code: None,
3207                file: None,
3208                tensor: None,
3209            },
3210            Commands::List,
3211        ];
3212        for cmd in &diagnostic_commands {
3213            let paths = extract_model_paths(cmd);
3214            assert!(
3215                paths.is_empty(),
3216                "Diagnostic command should be exempt: {cmd:?}"
3217            );
3218        }
3219    }
3220
3221    /// Test extract_model_paths: action commands return file paths
3222    #[test]
3223    fn test_extract_paths_action_commands() {
3224        let serve_cmd = Commands::Serve {
3225            file: PathBuf::from("model.gguf"),
3226            port: 8080,
3227            host: "127.0.0.1".to_string(),
3228            no_cors: false,
3229            no_metrics: false,
3230            no_gpu: false,
3231            gpu: false,
3232            batch: false,
3233            trace: false,
3234            trace_level: "basic".to_string(),
3235            profile: false,
3236        };
3237        let paths = extract_model_paths(&serve_cmd);
3238        assert_eq!(paths, vec![PathBuf::from("model.gguf")]);
3239
3240        let bench_cmd = Commands::Bench {
3241            file: PathBuf::from("model.apr"),
3242            warmup: 3,
3243            iterations: 5,
3244            max_tokens: 32,
3245            prompt: None,
3246            fast: false,
3247            brick: None,
3248        };
3249        let paths = extract_model_paths(&bench_cmd);
3250        assert_eq!(paths, vec![PathBuf::from("model.apr")]);
3251    }
3252
3253    /// Test extract_model_paths: Run with hf:// URL returns empty
3254    #[test]
3255    fn test_extract_paths_run_hf_url() {
3256        let cmd = Commands::Run {
3257            source: "hf://org/repo".to_string(),
3258            positional_prompt: None,
3259            input: None,
3260            prompt: None,
3261            max_tokens: 32,
3262            stream: false,
3263            language: None,
3264            task: None,
3265            format: "text".to_string(),
3266            no_gpu: false,
3267            gpu: false,
3268            offline: false,
3269            benchmark: false,
3270            trace: false,
3271            trace_steps: None,
3272            trace_verbose: false,
3273            trace_output: None,
3274            trace_level: "basic".to_string(),
3275            trace_payload: false,
3276            profile: false,
3277            chat: false,
3278            verbose: false,
3279        };
3280        let paths = extract_model_paths(&cmd);
3281        assert!(
3282            paths.is_empty(),
3283            "hf:// URLs should not be validated locally"
3284        );
3285    }
3286
3287    /// Test extract_model_paths: Merge returns multiple files
3288    #[test]
3289    fn test_extract_paths_merge_multiple() {
3290        let cmd = Commands::Merge {
3291            files: vec![
3292                PathBuf::from("a.apr"),
3293                PathBuf::from("b.apr"),
3294                PathBuf::from("c.apr"),
3295            ],
3296            strategy: "average".to_string(),
3297            output: PathBuf::from("merged.apr"),
3298            weights: None,
3299        };
3300        let paths = extract_model_paths(&cmd);
3301        assert_eq!(paths.len(), 3);
3302    }
3303
3304    /// Test validate_model_contract: non-existent path is skipped (Ok)
3305    #[test]
3306    fn test_validate_contract_nonexistent_skipped() {
3307        let paths = vec![PathBuf::from("nonexistent_model_xyz.apr")];
3308        let result = validate_model_contract(&paths);
3309        assert!(result.is_ok(), "Non-existent paths should be skipped");
3310    }
3311
3312    /// Test validate_model_contract: empty paths is Ok
3313    #[test]
3314    fn test_validate_contract_empty_paths() {
3315        let result = validate_model_contract(&[]);
3316        assert!(result.is_ok());
3317    }
3318
3319    // =========================================================================
3320    // Parse tests for all remaining command variants
3321    // =========================================================================
3322
3323    /// Test parsing 'apr publish' command with all options
3324    #[test]
3325    fn test_parse_publish_command() {
3326        let args = vec![
3327            "apr",
3328            "publish",
3329            "/tmp/models",
3330            "paiml/whisper-apr-tiny",
3331            "--model-name",
3332            "Whisper Tiny",
3333            "--license",
3334            "apache-2.0",
3335            "--pipeline-tag",
3336            "automatic-speech-recognition",
3337            "--library-name",
3338            "whisper-apr",
3339            "--tags",
3340            "whisper,tiny,asr",
3341            "--message",
3342            "Initial release",
3343            "--dry-run",
3344        ];
3345        let cli = parse_cli(args).expect("Failed to parse");
3346        match *cli.command {
3347            Commands::Publish {
3348                directory,
3349                repo_id,
3350                model_name,
3351                license,
3352                pipeline_tag,
3353                library_name,
3354                tags,
3355                message,
3356                dry_run,
3357            } => {
3358                assert_eq!(directory, PathBuf::from("/tmp/models"));
3359                assert_eq!(repo_id, "paiml/whisper-apr-tiny");
3360                assert_eq!(model_name, Some("Whisper Tiny".to_string()));
3361                assert_eq!(license, "apache-2.0");
3362                assert_eq!(pipeline_tag, "automatic-speech-recognition");
3363                assert_eq!(library_name, Some("whisper-apr".to_string()));
3364                assert_eq!(
3365                    tags,
3366                    Some(vec![
3367                        "whisper".to_string(),
3368                        "tiny".to_string(),
3369                        "asr".to_string()
3370                    ])
3371                );
3372                assert_eq!(message, Some("Initial release".to_string()));
3373                assert!(dry_run);
3374            }
3375            _ => panic!("Expected Publish command"),
3376        }
3377    }
3378
3379    /// Test parsing 'apr publish' with defaults
3380    #[test]
3381    fn test_parse_publish_defaults() {
3382        let args = vec!["apr", "publish", "./models", "org/repo"];
3383        let cli = parse_cli(args).expect("Failed to parse");
3384        match *cli.command {
3385            Commands::Publish {
3386                license,
3387                pipeline_tag,
3388                dry_run,
3389                model_name,
3390                library_name,
3391                tags,
3392                message,
3393                ..
3394            } => {
3395                assert_eq!(license, "mit");
3396                assert_eq!(pipeline_tag, "text-generation");
3397                assert!(!dry_run);
3398                assert!(model_name.is_none());
3399                assert!(library_name.is_none());
3400                assert!(tags.is_none());
3401                assert!(message.is_none());
3402            }
3403            _ => panic!("Expected Publish command"),
3404        }
3405    }
3406
3407    /// Test parsing 'apr eval' command with all options
3408    #[test]
3409    fn test_parse_eval_command() {
3410        let args = vec![
3411            "apr",
3412            "eval",
3413            "model.gguf",
3414            "--dataset",
3415            "lambada",
3416            "--text",
3417            "The quick brown fox",
3418            "--max-tokens",
3419            "256",
3420            "--threshold",
3421            "15.5",
3422        ];
3423        let cli = parse_cli(args).expect("Failed to parse");
3424        match *cli.command {
3425            Commands::Eval {
3426                file,
3427                dataset,
3428                text,
3429                max_tokens,
3430                threshold,
3431            } => {
3432                assert_eq!(file, PathBuf::from("model.gguf"));
3433                assert_eq!(dataset, "lambada");
3434                assert_eq!(text, Some("The quick brown fox".to_string()));
3435                assert_eq!(max_tokens, 256);
3436                assert!((threshold - 15.5).abs() < f32::EPSILON);
3437            }
3438            _ => panic!("Expected Eval command"),
3439        }
3440    }
3441
3442    /// Test parsing 'apr eval' with defaults
3443    #[test]
3444    fn test_parse_eval_defaults() {
3445        let args = vec!["apr", "eval", "model.apr"];
3446        let cli = parse_cli(args).expect("Failed to parse");
3447        match *cli.command {
3448            Commands::Eval {
3449                dataset,
3450                text,
3451                max_tokens,
3452                threshold,
3453                ..
3454            } => {
3455                assert_eq!(dataset, "wikitext-2");
3456                assert!(text.is_none());
3457                assert_eq!(max_tokens, 512);
3458                assert!((threshold - 20.0).abs() < f32::EPSILON);
3459            }
3460            _ => panic!("Expected Eval command"),
3461        }
3462    }
3463
3464    /// Test parsing 'apr flow' command with options
3465    #[test]
3466    fn test_parse_flow_command() {
3467        let args = vec![
3468            "apr",
3469            "flow",
3470            "model.apr",
3471            "--layer",
3472            "encoder.0",
3473            "--component",
3474            "encoder",
3475            "-v",
3476        ];
3477        let cli = parse_cli(args).expect("Failed to parse");
3478        match *cli.command {
3479            Commands::Flow {
3480                file,
3481                layer,
3482                component,
3483                verbose,
3484            } => {
3485                assert_eq!(file, PathBuf::from("model.apr"));
3486                assert_eq!(layer, Some("encoder.0".to_string()));
3487                assert_eq!(component, "encoder");
3488                assert!(verbose);
3489            }
3490            _ => panic!("Expected Flow command"),
3491        }
3492    }
3493
3494    /// Test parsing 'apr flow' with defaults
3495    #[test]
3496    fn test_parse_flow_defaults() {
3497        let args = vec!["apr", "flow", "model.apr"];
3498        let cli = parse_cli(args).expect("Failed to parse");
3499        match *cli.command {
3500            Commands::Flow {
3501                component,
3502                verbose,
3503                layer,
3504                ..
3505            } => {
3506                assert_eq!(component, "full");
3507                assert!(!verbose);
3508                assert!(layer.is_none());
3509            }
3510            _ => panic!("Expected Flow command"),
3511        }
3512    }
3513
3514    /// Test parsing 'apr hex' command with all options
3515    #[test]
3516    fn test_parse_hex_command() {
3517        let args = vec![
3518            "apr",
3519            "hex",
3520            "model.apr",
3521            "--tensor",
3522            "embed.weight",
3523            "--limit",
3524            "128",
3525            "--stats",
3526            "--list",
3527            "--json",
3528        ];
3529        let cli = parse_cli(args).expect("Failed to parse");
3530        match *cli.command {
3531            Commands::Hex {
3532                file,
3533                tensor,
3534                limit,
3535                stats,
3536                list,
3537                json,
3538                ..
3539            } => {
3540                assert_eq!(file, PathBuf::from("model.apr"));
3541                assert_eq!(tensor, Some("embed.weight".to_string()));
3542                assert_eq!(limit, 128);
3543                assert!(stats);
3544                assert!(list);
3545                assert!(json);
3546            }
3547            _ => panic!("Expected Hex command"),
3548        }
3549    }
3550
3551    /// Test parsing 'apr hex' with defaults
3552    #[test]
3553    fn test_parse_hex_defaults() {
3554        let args = vec!["apr", "hex", "model.apr"];
3555        let cli = parse_cli(args).expect("Failed to parse");
3556        match *cli.command {
3557            Commands::Hex {
3558                limit,
3559                stats,
3560                list,
3561                json,
3562                tensor,
3563                ..
3564            } => {
3565                assert_eq!(limit, 64);
3566                assert!(!stats);
3567                assert!(!list);
3568                assert!(!json);
3569                assert!(tensor.is_none());
3570            }
3571            _ => panic!("Expected Hex command"),
3572        }
3573    }
3574
3575    /// Test parsing 'apr tree' command with options
3576    #[test]
3577    fn test_parse_tree_command() {
3578        let args = vec![
3579            "apr",
3580            "tree",
3581            "model.apr",
3582            "--filter",
3583            "encoder",
3584            "--format",
3585            "mermaid",
3586            "--sizes",
3587            "--depth",
3588            "3",
3589        ];
3590        let cli = parse_cli(args).expect("Failed to parse");
3591        match *cli.command {
3592            Commands::Tree {
3593                file,
3594                filter,
3595                format,
3596                sizes,
3597                depth,
3598            } => {
3599                assert_eq!(file, PathBuf::from("model.apr"));
3600                assert_eq!(filter, Some("encoder".to_string()));
3601                assert_eq!(format, "mermaid");
3602                assert!(sizes);
3603                assert_eq!(depth, Some(3));
3604            }
3605            _ => panic!("Expected Tree command"),
3606        }
3607    }
3608
3609    /// Test parsing 'apr tree' with defaults
3610    #[test]
3611    fn test_parse_tree_defaults() {
3612        let args = vec!["apr", "tree", "model.apr"];
3613        let cli = parse_cli(args).expect("Failed to parse");
3614        match *cli.command {
3615            Commands::Tree {
3616                format,
3617                sizes,
3618                depth,
3619                filter,
3620                ..
3621            } => {
3622                assert_eq!(format, "ascii");
3623                assert!(!sizes);
3624                assert!(depth.is_none());
3625                assert!(filter.is_none());
3626            }
3627            _ => panic!("Expected Tree command"),
3628        }
3629    }
3630
3631    /// Test parsing 'apr probar' command with options
3632    #[test]
3633    fn test_parse_probar_command() {
3634        let args = vec![
3635            "apr",
3636            "probar",
3637            "model.apr",
3638            "--output",
3639            "/tmp/probar",
3640            "--format",
3641            "json",
3642            "--golden",
3643            "/refs/golden",
3644            "--layer",
3645            "layer.0",
3646        ];
3647        let cli = parse_cli(args).expect("Failed to parse");
3648        match *cli.command {
3649            Commands::Probar {
3650                file,
3651                output,
3652                format,
3653                golden,
3654                layer,
3655            } => {
3656                assert_eq!(file, PathBuf::from("model.apr"));
3657                assert_eq!(output, PathBuf::from("/tmp/probar"));
3658                assert_eq!(format, "json");
3659                assert_eq!(golden, Some(PathBuf::from("/refs/golden")));
3660                assert_eq!(layer, Some("layer.0".to_string()));
3661            }
3662            _ => panic!("Expected Probar command"),
3663        }
3664    }
3665
3666    /// Test parsing 'apr probar' with defaults
3667    #[test]
3668    fn test_parse_probar_defaults() {
3669        let args = vec!["apr", "probar", "model.apr"];
3670        let cli = parse_cli(args).expect("Failed to parse");
3671        match *cli.command {
3672            Commands::Probar {
3673                output,
3674                format,
3675                golden,
3676                layer,
3677                ..
3678            } => {
3679                assert_eq!(output, PathBuf::from("./probar-export"));
3680                assert_eq!(format, "both");
3681                assert!(golden.is_none());
3682                assert!(layer.is_none());
3683            }
3684            _ => panic!("Expected Probar command"),
3685        }
3686    }
3687
3688    /// Test parsing 'apr debug' command with all flags
3689    #[test]
3690    fn test_parse_debug_command() {
3691        let args = vec![
3692            "apr",
3693            "debug",
3694            "model.apr",
3695            "--drama",
3696            "--hex",
3697            "--strings",
3698            "--limit",
3699            "512",
3700        ];
3701        let cli = parse_cli(args).expect("Failed to parse");
3702        match *cli.command {
3703            Commands::Debug {
3704                file,
3705                drama,
3706                hex,
3707                strings,
3708                limit,
3709            } => {
3710                assert_eq!(file, PathBuf::from("model.apr"));
3711                assert!(drama);
3712                assert!(hex);
3713                assert!(strings);
3714                assert_eq!(limit, 512);
3715            }
3716            _ => panic!("Expected Debug command"),
3717        }
3718    }
3719
3720    /// Test parsing 'apr debug' with defaults
3721    #[test]
3722    fn test_parse_debug_defaults() {
3723        let args = vec!["apr", "debug", "model.apr"];
3724        let cli = parse_cli(args).expect("Failed to parse");
3725        match *cli.command {
3726            Commands::Debug {
3727                drama,
3728                hex,
3729                strings,
3730                limit,
3731                ..
3732            } => {
3733                assert!(!drama);
3734                assert!(!hex);
3735                assert!(!strings);
3736                assert_eq!(limit, 256);
3737            }
3738            _ => panic!("Expected Debug command"),
3739        }
3740    }
3741
3742    /// Test parsing 'apr tui' command with file
3743    #[test]
3744    fn test_parse_tui_command_with_file() {
3745        let args = vec!["apr", "tui", "model.apr"];
3746        let cli = parse_cli(args).expect("Failed to parse");
3747        match *cli.command {
3748            Commands::Tui { file } => {
3749                assert_eq!(file, Some(PathBuf::from("model.apr")));
3750            }
3751            _ => panic!("Expected Tui command"),
3752        }
3753    }
3754
3755    /// Test parsing 'apr tui' without file (optional)
3756    #[test]
3757    fn test_parse_tui_command_no_file() {
3758        let args = vec!["apr", "tui"];
3759        let cli = parse_cli(args).expect("Failed to parse");
3760        match *cli.command {
3761            Commands::Tui { file } => {
3762                assert!(file.is_none());
3763            }
3764            _ => panic!("Expected Tui command"),
3765        }
3766    }
3767
3768    /// Test parsing 'apr import' command with all options
3769    #[test]
3770    fn test_parse_import_command() {
3771        let args = vec![
3772            "apr",
3773            "import",
3774            "hf://openai/whisper-tiny",
3775            "--output",
3776            "whisper.apr",
3777            "--arch",
3778            "whisper",
3779            "--quantize",
3780            "int8",
3781            "--strict",
3782            "--preserve-q4k",
3783            "--tokenizer",
3784            "/path/to/tokenizer.json",
3785        ];
3786        let cli = parse_cli(args).expect("Failed to parse");
3787        match *cli.command {
3788            Commands::Import {
3789                source,
3790                output,
3791                arch,
3792                quantize,
3793                strict,
3794                preserve_q4k,
3795                tokenizer,
3796                enforce_provenance,
3797            } => {
3798                assert_eq!(source, "hf://openai/whisper-tiny");
3799                assert_eq!(output, Some(PathBuf::from("whisper.apr")));
3800                assert_eq!(arch, "whisper");
3801                assert_eq!(quantize, Some("int8".to_string()));
3802                assert!(strict);
3803                assert!(preserve_q4k);
3804                assert_eq!(tokenizer, Some(PathBuf::from("/path/to/tokenizer.json")));
3805                assert!(!enforce_provenance);
3806            }
3807            _ => panic!("Expected Import command"),
3808        }
3809    }
3810
3811    /// Test parsing 'apr import' with defaults
3812    #[test]
3813    fn test_parse_import_defaults() {
3814        let args = vec!["apr", "import", "model.gguf"];
3815        let cli = parse_cli(args).expect("Failed to parse");
3816        match *cli.command {
3817            Commands::Import {
3818                arch,
3819                quantize,
3820                strict,
3821                preserve_q4k,
3822                output,
3823                tokenizer,
3824                ..
3825            } => {
3826                assert_eq!(arch, "auto");
3827                assert!(quantize.is_none());
3828                assert!(!strict);
3829                assert!(!preserve_q4k);
3830                assert!(output.is_none());
3831                assert!(tokenizer.is_none());
3832            }
3833            _ => panic!("Expected Import command"),
3834        }
3835    }
3836
3837    /// Test parsing 'apr export' command
3838    #[test]
3839    fn test_parse_export_command() {
3840        let args = vec![
3841            "apr",
3842            "export",
3843            "model.apr",
3844            "--format",
3845            "gguf",
3846            "-o",
3847            "model.gguf",
3848            "--quantize",
3849            "int4",
3850        ];
3851        let cli = parse_cli(args).expect("Failed to parse");
3852        match *cli.command {
3853            Commands::Export {
3854                file,
3855                format,
3856                output,
3857                quantize,
3858            } => {
3859                assert_eq!(file, PathBuf::from("model.apr"));
3860                assert_eq!(format, "gguf");
3861                assert_eq!(output, PathBuf::from("model.gguf"));
3862                assert_eq!(quantize, Some("int4".to_string()));
3863            }
3864            _ => panic!("Expected Export command"),
3865        }
3866    }
3867
3868    /// Test parsing 'apr export' with defaults
3869    #[test]
3870    fn test_parse_export_defaults() {
3871        let args = vec!["apr", "export", "model.apr", "-o", "out.safetensors"];
3872        let cli = parse_cli(args).expect("Failed to parse");
3873        match *cli.command {
3874            Commands::Export {
3875                format, quantize, ..
3876            } => {
3877                assert_eq!(format, "safetensors");
3878                assert!(quantize.is_none());
3879            }
3880            _ => panic!("Expected Export command"),
3881        }
3882    }
3883
3884    /// Test parsing 'apr convert' command with all options
3885    #[test]
3886    fn test_parse_convert_command() {
3887        let args = vec![
3888            "apr",
3889            "convert",
3890            "model.apr",
3891            "--quantize",
3892            "q4k",
3893            "--compress",
3894            "zstd",
3895            "-o",
3896            "model-q4k.apr",
3897            "--force",
3898        ];
3899        let cli = parse_cli(args).expect("Failed to parse");
3900        match *cli.command {
3901            Commands::Convert {
3902                file,
3903                quantize,
3904                compress,
3905                output,
3906                force,
3907            } => {
3908                assert_eq!(file, PathBuf::from("model.apr"));
3909                assert_eq!(quantize, Some("q4k".to_string()));
3910                assert_eq!(compress, Some("zstd".to_string()));
3911                assert_eq!(output, PathBuf::from("model-q4k.apr"));
3912                assert!(force);
3913            }
3914            _ => panic!("Expected Convert command"),
3915        }
3916    }
3917
3918    /// Test parsing 'apr convert' with defaults
3919    #[test]
3920    fn test_parse_convert_defaults() {
3921        let args = vec!["apr", "convert", "model.apr", "-o", "out.apr"];
3922        let cli = parse_cli(args).expect("Failed to parse");
3923        match *cli.command {
3924            Commands::Convert {
3925                quantize,
3926                compress,
3927                force,
3928                ..
3929            } => {
3930                assert!(quantize.is_none());
3931                assert!(compress.is_none());
3932                assert!(!force);
3933            }
3934            _ => panic!("Expected Convert command"),
3935        }
3936    }
3937
3938    /// Test parsing 'apr oracle' command with source
3939    #[test]
3940    fn test_parse_oracle_command_with_source() {
3941        let args = vec![
3942            "apr",
3943            "oracle",
3944            "model.gguf",
3945            "--compliance",
3946            "--tensors",
3947            "--stats",
3948            "--explain",
3949            "--kernels",
3950            "--validate",
3951            "--full",
3952        ];
3953        let cli = parse_cli(args).expect("Failed to parse");
3954        match *cli.command {
3955            Commands::Oracle {
3956                source,
3957                compliance,
3958                tensors,
3959                stats,
3960                explain,
3961                kernels,
3962                validate,
3963                full,
3964                family,
3965                size,
3966            } => {
3967                assert_eq!(source, Some("model.gguf".to_string()));
3968                assert!(compliance);
3969                assert!(tensors);
3970                assert!(stats);
3971                assert!(explain);
3972                assert!(kernels);
3973                assert!(validate);
3974                assert!(full);
3975                assert!(family.is_none());
3976                assert!(size.is_none());
3977            }
3978            _ => panic!("Expected Oracle command"),
3979        }
3980    }
3981
3982    /// Test parsing 'apr oracle' with --family flag
3983    #[test]
3984    fn test_parse_oracle_family_mode() {
3985        let args = vec!["apr", "oracle", "--family", "qwen2", "--size", "7b"];
3986        let cli = parse_cli(args).expect("Failed to parse");
3987        match *cli.command {
3988            Commands::Oracle {
3989                source,
3990                family,
3991                size,
3992                ..
3993            } => {
3994                assert!(source.is_none());
3995                assert_eq!(family, Some("qwen2".to_string()));
3996                assert_eq!(size, Some("7b".to_string()));
3997            }
3998            _ => panic!("Expected Oracle command"),
3999        }
4000    }
4001
4002    /// Test parsing 'apr oracle' with hf:// URI
4003    #[test]
4004    fn test_parse_oracle_hf_uri() {
4005        let args = vec!["apr", "oracle", "hf://Qwen/Qwen2.5-Coder-1.5B"];
4006        let cli = parse_cli(args).expect("Failed to parse");
4007        match *cli.command {
4008            Commands::Oracle { source, .. } => {
4009                assert_eq!(source, Some("hf://Qwen/Qwen2.5-Coder-1.5B".to_string()));
4010            }
4011            _ => panic!("Expected Oracle command"),
4012        }
4013    }
4014
4015    /// Test parsing 'apr canary create' subcommand
4016    #[test]
4017    fn test_parse_canary_create() {
4018        let args = vec![
4019            "apr",
4020            "canary",
4021            "create",
4022            "model.apr",
4023            "--input",
4024            "audio.wav",
4025            "--output",
4026            "canary.json",
4027        ];
4028        let cli = parse_cli(args).expect("Failed to parse");
4029        match *cli.command {
4030            Commands::Canary { command } => match command {
4031                CanaryCommands::Create {
4032                    file,
4033                    input,
4034                    output,
4035                } => {
4036                    assert_eq!(file, PathBuf::from("model.apr"));
4037                    assert_eq!(input, PathBuf::from("audio.wav"));
4038                    assert_eq!(output, PathBuf::from("canary.json"));
4039                }
4040                _ => panic!("Expected Create subcommand"),
4041            },
4042            _ => panic!("Expected Canary command"),
4043        }
4044    }
4045
4046    /// Test parsing 'apr canary check' subcommand
4047    #[test]
4048    fn test_parse_canary_check() {
4049        let args = vec![
4050            "apr",
4051            "canary",
4052            "check",
4053            "model.apr",
4054            "--canary",
4055            "canary.json",
4056        ];
4057        let cli = parse_cli(args).expect("Failed to parse");
4058        match *cli.command {
4059            Commands::Canary { command } => match command {
4060                CanaryCommands::Check { file, canary } => {
4061                    assert_eq!(file, PathBuf::from("model.apr"));
4062                    assert_eq!(canary, PathBuf::from("canary.json"));
4063                }
4064                _ => panic!("Expected Check subcommand"),
4065            },
4066            _ => panic!("Expected Canary command"),
4067        }
4068    }
4069
4070    /// Test parsing 'apr compare-hf' command
4071    #[test]
4072    fn test_parse_compare_hf_command() {
4073        let args = vec![
4074            "apr",
4075            "compare-hf",
4076            "model.apr",
4077            "--hf",
4078            "openai/whisper-tiny",
4079            "--tensor",
4080            "encoder.0",
4081            "--threshold",
4082            "1e-3",
4083            "--json",
4084        ];
4085        let cli = parse_cli(args).expect("Failed to parse");
4086        match *cli.command {
4087            Commands::CompareHf {
4088                file,
4089                hf,
4090                tensor,
4091                threshold,
4092                json,
4093            } => {
4094                assert_eq!(file, PathBuf::from("model.apr"));
4095                assert_eq!(hf, "openai/whisper-tiny");
4096                assert_eq!(tensor, Some("encoder.0".to_string()));
4097                assert!((threshold - 1e-3).abs() < f64::EPSILON);
4098                assert!(json);
4099            }
4100            _ => panic!("Expected CompareHf command"),
4101        }
4102    }
4103
4104    /// Test parsing 'apr compare-hf' with defaults
4105    #[test]
4106    fn test_parse_compare_hf_defaults() {
4107        let args = vec![
4108            "apr",
4109            "compare-hf",
4110            "model.apr",
4111            "--hf",
4112            "openai/whisper-tiny",
4113        ];
4114        let cli = parse_cli(args).expect("Failed to parse");
4115        match *cli.command {
4116            Commands::CompareHf {
4117                tensor,
4118                threshold,
4119                json,
4120                ..
4121            } => {
4122                assert!(tensor.is_none());
4123                assert!((threshold - 1e-5).abs() < f64::EPSILON);
4124                assert!(!json);
4125            }
4126            _ => panic!("Expected CompareHf command"),
4127        }
4128    }
4129
4130    /// Test parsing 'apr pull' command
4131    #[test]
4132    fn test_parse_pull_command() {
4133        let args = vec!["apr", "pull", "hf://Qwen/Qwen2.5-Coder-1.5B", "--force"];
4134        let cli = parse_cli(args).expect("Failed to parse");
4135        match *cli.command {
4136            Commands::Pull { model_ref, force } => {
4137                assert_eq!(model_ref, "hf://Qwen/Qwen2.5-Coder-1.5B");
4138                assert!(force);
4139            }
4140            _ => panic!("Expected Pull command"),
4141        }
4142    }
4143
4144    /// Test parsing 'apr pull' without force
4145    #[test]
4146    fn test_parse_pull_defaults() {
4147        let args = vec!["apr", "pull", "qwen2.5-coder"];
4148        let cli = parse_cli(args).expect("Failed to parse");
4149        match *cli.command {
4150            Commands::Pull { model_ref, force } => {
4151                assert_eq!(model_ref, "qwen2.5-coder");
4152                assert!(!force);
4153            }
4154            _ => panic!("Expected Pull command"),
4155        }
4156    }
4157
4158    /// Test parsing 'apr tune' command with all options
4159    #[test]
4160    fn test_parse_tune_command() {
4161        let args = vec![
4162            "apr",
4163            "tune",
4164            "model.apr",
4165            "--method",
4166            "lora",
4167            "--rank",
4168            "16",
4169            "--vram",
4170            "24.0",
4171            "--plan",
4172            "--model",
4173            "7B",
4174            "--freeze-base",
4175            "--train-data",
4176            "data.jsonl",
4177            "--json",
4178        ];
4179        let cli = parse_cli(args).expect("Failed to parse");
4180        match *cli.command {
4181            Commands::Tune {
4182                file,
4183                method,
4184                rank,
4185                vram,
4186                plan,
4187                model,
4188                freeze_base,
4189                train_data,
4190                json,
4191            } => {
4192                assert_eq!(file, Some(PathBuf::from("model.apr")));
4193                assert_eq!(method, "lora");
4194                assert_eq!(rank, Some(16));
4195                assert!((vram - 24.0).abs() < f64::EPSILON);
4196                assert!(plan);
4197                assert_eq!(model, Some("7B".to_string()));
4198                assert!(freeze_base);
4199                assert_eq!(train_data, Some(PathBuf::from("data.jsonl")));
4200                assert!(json);
4201            }
4202            _ => panic!("Expected Tune command"),
4203        }
4204    }
4205
4206    /// Test parsing 'apr tune' with defaults (no file)
4207    #[test]
4208    fn test_parse_tune_defaults() {
4209        let args = vec!["apr", "tune"];
4210        let cli = parse_cli(args).expect("Failed to parse");
4211        match *cli.command {
4212            Commands::Tune {
4213                file,
4214                method,
4215                rank,
4216                vram,
4217                plan,
4218                model,
4219                freeze_base,
4220                train_data,
4221                json,
4222            } => {
4223                assert!(file.is_none());
4224                assert_eq!(method, "auto");
4225                assert!(rank.is_none());
4226                assert!((vram - 16.0).abs() < f64::EPSILON);
4227                assert!(!plan);
4228                assert!(model.is_none());
4229                assert!(!freeze_base);
4230                assert!(train_data.is_none());
4231                assert!(!json);
4232            }
4233            _ => panic!("Expected Tune command"),
4234        }
4235    }
4236
4237    /// Test parsing 'apr check' command
4238    #[test]
4239    fn test_parse_check_command() {
4240        let args = vec!["apr", "check", "model.apr", "--no-gpu"];
4241        let cli = parse_cli(args).expect("Failed to parse");
4242        match *cli.command {
4243            Commands::Check { file, no_gpu } => {
4244                assert_eq!(file, PathBuf::from("model.apr"));
4245                assert!(no_gpu);
4246            }
4247            _ => panic!("Expected Check command"),
4248        }
4249    }
4250
4251    /// Test parsing 'apr check' with defaults
4252    #[test]
4253    fn test_parse_check_defaults() {
4254        let args = vec!["apr", "check", "model.gguf"];
4255        let cli = parse_cli(args).expect("Failed to parse");
4256        match *cli.command {
4257            Commands::Check { no_gpu, .. } => {
4258                assert!(!no_gpu);
4259            }
4260            _ => panic!("Expected Check command"),
4261        }
4262    }
4263
4264    /// Test parsing 'apr lint' command
4265    #[test]
4266    fn test_parse_lint_command() {
4267        let args = vec!["apr", "lint", "model.apr"];
4268        let cli = parse_cli(args).expect("Failed to parse");
4269        match *cli.command {
4270            Commands::Lint { file } => {
4271                assert_eq!(file, PathBuf::from("model.apr"));
4272            }
4273            _ => panic!("Expected Lint command"),
4274        }
4275    }
4276
4277    /// Test parsing 'apr tensors' command with all options
4278    #[test]
4279    fn test_parse_tensors_command() {
4280        let args = vec![
4281            "apr",
4282            "tensors",
4283            "model.apr",
4284            "--stats",
4285            "--filter",
4286            "encoder",
4287            "--limit",
4288            "20",
4289            "--json",
4290        ];
4291        let cli = parse_cli(args).expect("Failed to parse");
4292        match *cli.command {
4293            Commands::Tensors {
4294                file,
4295                stats,
4296                filter,
4297                limit,
4298                json,
4299            } => {
4300                assert_eq!(file, PathBuf::from("model.apr"));
4301                assert!(stats);
4302                assert_eq!(filter, Some("encoder".to_string()));
4303                assert_eq!(limit, 20);
4304                assert!(json);
4305            }
4306            _ => panic!("Expected Tensors command"),
4307        }
4308    }
4309
4310    /// Test parsing 'apr explain' command with code
4311    #[test]
4312    fn test_parse_explain_with_code() {
4313        let args = vec!["apr", "explain", "E001"];
4314        let cli = parse_cli(args).expect("Failed to parse");
4315        match *cli.command {
4316            Commands::Explain {
4317                code, file, tensor, ..
4318            } => {
4319                assert_eq!(code, Some("E001".to_string()));
4320                assert!(file.is_none());
4321                assert!(tensor.is_none());
4322            }
4323            _ => panic!("Expected Explain command"),
4324        }
4325    }
4326
4327    /// Test parsing 'apr explain' with tensor and file
4328    #[test]
4329    fn test_parse_explain_with_tensor_and_file() {
4330        let args = vec![
4331            "apr",
4332            "explain",
4333            "--file",
4334            "model.apr",
4335            "--tensor",
4336            "embed.weight",
4337        ];
4338        let cli = parse_cli(args).expect("Failed to parse");
4339        match *cli.command {
4340            Commands::Explain { code, file, tensor } => {
4341                assert!(code.is_none());
4342                assert_eq!(file, Some(PathBuf::from("model.apr")));
4343                assert_eq!(tensor, Some("embed.weight".to_string()));
4344            }
4345            _ => panic!("Expected Explain command"),
4346        }
4347    }
4348
4349    /// Test parsing 'apr trace' command with all options
4350    #[test]
4351    fn test_parse_trace_command() {
4352        let args = vec![
4353            "apr",
4354            "trace",
4355            "model.apr",
4356            "--layer",
4357            "layer.0",
4358            "--reference",
4359            "ref.apr",
4360            "--json",
4361            "-v",
4362            "--payload",
4363            "--diff",
4364            "--interactive",
4365        ];
4366        let cli = parse_cli(args).expect("Failed to parse");
4367        match *cli.command {
4368            Commands::Trace {
4369                file,
4370                layer,
4371                reference,
4372                json,
4373                verbose,
4374                payload,
4375                diff,
4376                interactive,
4377            } => {
4378                assert_eq!(file, PathBuf::from("model.apr"));
4379                assert_eq!(layer, Some("layer.0".to_string()));
4380                assert_eq!(reference, Some(PathBuf::from("ref.apr")));
4381                assert!(json);
4382                assert!(verbose);
4383                assert!(payload);
4384                assert!(diff);
4385                assert!(interactive);
4386            }
4387            _ => panic!("Expected Trace command"),
4388        }
4389    }
4390
4391    /// Test parsing 'apr validate' with min-score
4392    #[test]
4393    fn test_parse_validate_with_min_score() {
4394        let args = vec!["apr", "validate", "model.apr", "--min-score", "80"];
4395        let cli = parse_cli(args).expect("Failed to parse");
4396        match *cli.command {
4397            Commands::Validate {
4398                min_score, strict, ..
4399            } => {
4400                assert_eq!(min_score, Some(80));
4401                assert!(!strict);
4402            }
4403            _ => panic!("Expected Validate command"),
4404        }
4405    }
4406
4407    /// Test parsing 'apr diff' with all options
4408    #[test]
4409    fn test_parse_diff_with_all_options() {
4410        let args = vec![
4411            "apr",
4412            "diff",
4413            "a.apr",
4414            "b.apr",
4415            "--values",
4416            "--filter",
4417            "embed",
4418            "--limit",
4419            "5",
4420            "--transpose-aware",
4421            "--json",
4422        ];
4423        let cli = parse_cli(args).expect("Failed to parse");
4424        match *cli.command {
4425            Commands::Diff {
4426                file1,
4427                file2,
4428                values,
4429                filter,
4430                limit,
4431                transpose_aware,
4432                json,
4433                ..
4434            } => {
4435                assert_eq!(file1, PathBuf::from("a.apr"));
4436                assert_eq!(file2, PathBuf::from("b.apr"));
4437                assert!(values);
4438                assert_eq!(filter, Some("embed".to_string()));
4439                assert_eq!(limit, 5);
4440                assert!(transpose_aware);
4441                assert!(json);
4442            }
4443            _ => panic!("Expected Diff command"),
4444        }
4445    }
4446
4447    /// Test parsing 'apr run' with --chat flag
4448    #[test]
4449    fn test_parse_run_with_chat_flag() {
4450        let args = vec![
4451            "apr",
4452            "run",
4453            "model.gguf",
4454            "--prompt",
4455            "Hello world",
4456            "--chat",
4457        ];
4458        let cli = parse_cli(args).expect("Failed to parse");
4459        match *cli.command {
4460            Commands::Run {
4461                chat,
4462                prompt,
4463                source,
4464                ..
4465            } => {
4466                assert!(chat);
4467                assert_eq!(prompt, Some("Hello world".to_string()));
4468                assert_eq!(source, "model.gguf");
4469            }
4470            _ => panic!("Expected Run command"),
4471        }
4472    }
4473
4474    /// Test parsing 'apr run' with --trace-payload shorthand
4475    #[test]
4476    fn test_parse_run_with_trace_payload() {
4477        let args = vec![
4478            "apr",
4479            "run",
4480            "model.gguf",
4481            "--prompt",
4482            "test",
4483            "--trace-payload",
4484        ];
4485        let cli = parse_cli(args).expect("Failed to parse");
4486        match *cli.command {
4487            Commands::Run {
4488                trace_payload,
4489                trace,
4490                trace_level,
4491                ..
4492            } => {
4493                assert!(trace_payload);
4494                // trace itself should default to false (trace_payload is separate flag)
4495                assert!(!trace);
4496                assert_eq!(trace_level, "basic");
4497            }
4498            _ => panic!("Expected Run command"),
4499        }
4500    }
4501
4502    /// GH-217: Positional prompt is parsed as second argument
4503    #[test]
4504    fn test_parse_run_positional_prompt() {
4505        let args = vec!["apr", "run", "model.gguf", "What is 2+2?"];
4506        let cli = parse_cli(args).expect("Failed to parse");
4507        match *cli.command {
4508            Commands::Run {
4509                source,
4510                positional_prompt,
4511                prompt,
4512                ..
4513            } => {
4514                assert_eq!(source, "model.gguf");
4515                assert_eq!(positional_prompt, Some("What is 2+2?".to_string()));
4516                assert_eq!(prompt, None);
4517            }
4518            _ => panic!("Expected Run command"),
4519        }
4520    }
4521
4522    /// GH-217: --prompt flag still works and takes precedence
4523    #[test]
4524    fn test_parse_run_flag_prompt_overrides_positional() {
4525        let args = vec![
4526            "apr",
4527            "run",
4528            "model.gguf",
4529            "positional text",
4530            "--prompt",
4531            "flag text",
4532        ];
4533        let cli = parse_cli(args).expect("Failed to parse");
4534        match *cli.command {
4535            Commands::Run {
4536                positional_prompt,
4537                prompt,
4538                ..
4539            } => {
4540                assert_eq!(positional_prompt, Some("positional text".to_string()));
4541                assert_eq!(prompt, Some("flag text".to_string()));
4542            }
4543            _ => panic!("Expected Run command"),
4544        }
4545    }
4546
4547    /// GH-217: Positional prompt with -n short flag for max_tokens
4548    #[test]
4549    fn test_parse_run_positional_prompt_with_n_flag() {
4550        let args = vec!["apr", "run", "model.gguf", "What is 2+2?", "-n", "64"];
4551        let cli = parse_cli(args).expect("Failed to parse");
4552        match *cli.command {
4553            Commands::Run {
4554                source,
4555                positional_prompt,
4556                max_tokens,
4557                ..
4558            } => {
4559                assert_eq!(source, "model.gguf");
4560                assert_eq!(positional_prompt, Some("What is 2+2?".to_string()));
4561                assert_eq!(max_tokens, 64);
4562            }
4563            _ => panic!("Expected Run command"),
4564        }
4565    }
4566
4567    /// GH-217: No prompt provided (neither positional nor flag)
4568    #[test]
4569    fn test_parse_run_no_prompt() {
4570        let args = vec!["apr", "run", "model.gguf"];
4571        let cli = parse_cli(args).expect("Failed to parse");
4572        match *cli.command {
4573            Commands::Run {
4574                positional_prompt,
4575                prompt,
4576                ..
4577            } => {
4578                assert_eq!(positional_prompt, None);
4579                assert_eq!(prompt, None);
4580            }
4581            _ => panic!("Expected Run command"),
4582        }
4583    }
4584
4585    /// Test parsing 'apr run' with local verbose flag
4586    #[test]
4587    fn test_parse_run_with_local_verbose() {
4588        let args = vec!["apr", "run", "model.gguf", "--prompt", "hi", "-v"];
4589        let cli = parse_cli(args).expect("Failed to parse");
4590        match *cli.command {
4591            Commands::Run { verbose, .. } => {
4592                assert!(verbose);
4593            }
4594            _ => panic!("Expected Run command"),
4595        }
4596    }
4597
4598    /// Test parsing 'apr run' with all trace options
4599    #[test]
4600    fn test_parse_run_with_full_trace() {
4601        let args = vec![
4602            "apr",
4603            "run",
4604            "model.gguf",
4605            "--prompt",
4606            "test",
4607            "--trace",
4608            "--trace-steps",
4609            "Tokenize,Embed,Attention",
4610            "--trace-verbose",
4611            "--trace-output",
4612            "/tmp/trace.json",
4613            "--trace-level",
4614            "layer",
4615        ];
4616        let cli = parse_cli(args).expect("Failed to parse");
4617        match *cli.command {
4618            Commands::Run {
4619                trace,
4620                trace_steps,
4621                trace_verbose,
4622                trace_output,
4623                trace_level,
4624                ..
4625            } => {
4626                assert!(trace);
4627                assert_eq!(
4628                    trace_steps,
4629                    Some(vec![
4630                        "Tokenize".to_string(),
4631                        "Embed".to_string(),
4632                        "Attention".to_string()
4633                    ])
4634                );
4635                assert!(trace_verbose);
4636                assert_eq!(trace_output, Some(PathBuf::from("/tmp/trace.json")));
4637                assert_eq!(trace_level, "layer");
4638            }
4639            _ => panic!("Expected Run command"),
4640        }
4641    }
4642
4643    /// Test parsing 'apr run' with --benchmark and --profile flags
4644    #[test]
4645    fn test_parse_run_benchmark_and_profile() {
4646        let args = vec![
4647            "apr",
4648            "run",
4649            "model.gguf",
4650            "--prompt",
4651            "test",
4652            "--benchmark",
4653            "--profile",
4654        ];
4655        let cli = parse_cli(args).expect("Failed to parse");
4656        match *cli.command {
4657            Commands::Run {
4658                benchmark, profile, ..
4659            } => {
4660                assert!(benchmark);
4661                assert!(profile);
4662            }
4663            _ => panic!("Expected Run command"),
4664        }
4665    }
4666
4667    /// Test parsing 'apr run' with --no-gpu flag
4668    #[test]
4669    fn test_parse_run_no_gpu() {
4670        let args = vec!["apr", "run", "model.gguf", "--prompt", "test", "--no-gpu"];
4671        let cli = parse_cli(args).expect("Failed to parse");
4672        match *cli.command {
4673            Commands::Run { no_gpu, .. } => {
4674                assert!(no_gpu);
4675            }
4676            _ => panic!("Expected Run command"),
4677        }
4678    }
4679
4680    /// Test parsing 'apr run' with --offline flag
4681    #[test]
4682    fn test_parse_run_offline() {
4683        let args = vec!["apr", "run", "model.gguf", "--prompt", "test", "--offline"];
4684        let cli = parse_cli(args).expect("Failed to parse");
4685        match *cli.command {
4686            Commands::Run { offline, .. } => {
4687                assert!(offline);
4688            }
4689            _ => panic!("Expected Run command"),
4690        }
4691    }
4692
4693    /// Test parsing 'apr run' with --stream and --format options
4694    #[test]
4695    fn test_parse_run_stream_and_format() {
4696        let args = vec![
4697            "apr",
4698            "run",
4699            "model.gguf",
4700            "--prompt",
4701            "test",
4702            "--stream",
4703            "-f",
4704            "json",
4705        ];
4706        let cli = parse_cli(args).expect("Failed to parse");
4707        match *cli.command {
4708            Commands::Run { stream, format, .. } => {
4709                assert!(stream);
4710                assert_eq!(format, "json");
4711            }
4712            _ => panic!("Expected Run command"),
4713        }
4714    }
4715
4716    /// Test parsing 'apr run' with --input and --language for ASR
4717    #[test]
4718    fn test_parse_run_asr_options() {
4719        let args = vec![
4720            "apr",
4721            "run",
4722            "hf://openai/whisper-tiny",
4723            "--input",
4724            "audio.wav",
4725            "--language",
4726            "en",
4727            "--task",
4728            "transcribe",
4729        ];
4730        let cli = parse_cli(args).expect("Failed to parse");
4731        match *cli.command {
4732            Commands::Run {
4733                input,
4734                language,
4735                task,
4736                ..
4737            } => {
4738                assert_eq!(input, Some(PathBuf::from("audio.wav")));
4739                assert_eq!(language, Some("en".to_string()));
4740                assert_eq!(task, Some("transcribe".to_string()));
4741            }
4742            _ => panic!("Expected Run command"),
4743        }
4744    }
4745
4746    /// Test parsing 'apr remove' alias for 'rm'
4747    #[test]
4748    fn test_parse_remove_alias() {
4749        let args = vec!["apr", "remove", "my-model"];
4750        let cli = parse_cli(args).expect("Failed to parse");
4751        match *cli.command {
4752            Commands::Rm { model_ref } => {
4753                assert_eq!(model_ref, "my-model");
4754            }
4755            _ => panic!("Expected Rm command"),
4756        }
4757    }
4758
4759    /// Test parsing 'apr qa' with all skip flags
4760    #[test]
4761    fn test_parse_qa_all_skip_flags() {
4762        let args = vec![
4763            "apr",
4764            "qa",
4765            "model.gguf",
4766            "--skip-golden",
4767            "--skip-throughput",
4768            "--skip-ollama",
4769            "--skip-gpu-speedup",
4770            "--skip-contract",
4771            "--skip-format-parity",
4772            "--safetensors-path",
4773            "model.safetensors",
4774            "--iterations",
4775            "20",
4776            "--warmup",
4777            "5",
4778            "--max-tokens",
4779            "64",
4780            "--json",
4781            "-v",
4782        ];
4783        let cli = parse_cli(args).expect("Failed to parse");
4784        match *cli.command {
4785            Commands::Qa {
4786                skip_golden,
4787                skip_throughput,
4788                skip_ollama,
4789                skip_gpu_speedup,
4790                skip_contract,
4791                skip_format_parity,
4792                safetensors_path,
4793                iterations,
4794                warmup,
4795                max_tokens,
4796                json,
4797                verbose,
4798                ..
4799            } => {
4800                assert!(skip_golden);
4801                assert!(skip_throughput);
4802                assert!(skip_ollama);
4803                assert!(skip_gpu_speedup);
4804                assert!(skip_contract);
4805                assert!(skip_format_parity);
4806                assert_eq!(safetensors_path, Some(PathBuf::from("model.safetensors")));
4807                assert_eq!(iterations, 20);
4808                assert_eq!(warmup, 5);
4809                assert_eq!(max_tokens, 64);
4810                assert!(json);
4811                assert!(verbose);
4812            }
4813            _ => panic!("Expected Qa command"),
4814        }
4815    }
4816
4817    /// Test parsing 'apr serve' with all options
4818    #[test]
4819    fn test_parse_serve_all_options() {
4820        let args = vec![
4821            "apr",
4822            "serve",
4823            "model.apr",
4824            "--port",
4825            "9090",
4826            "--host",
4827            "0.0.0.0",
4828            "--no-cors",
4829            "--no-metrics",
4830            "--no-gpu",
4831            "--batch",
4832            "--trace",
4833            "--trace-level",
4834            "layer",
4835            "--profile",
4836        ];
4837        let cli = parse_cli(args).expect("Failed to parse");
4838        match *cli.command {
4839            Commands::Serve {
4840                port,
4841                host,
4842                no_cors,
4843                no_metrics,
4844                no_gpu,
4845                batch,
4846                trace,
4847                trace_level,
4848                profile,
4849                ..
4850            } => {
4851                assert_eq!(port, 9090);
4852                assert_eq!(host, "0.0.0.0");
4853                assert!(no_cors);
4854                assert!(no_metrics);
4855                assert!(no_gpu);
4856                assert!(batch);
4857                assert!(trace);
4858                assert_eq!(trace_level, "layer");
4859                assert!(profile);
4860            }
4861            _ => panic!("Expected Serve command"),
4862        }
4863    }
4864
4865    /// Test parsing 'apr bench' with all options
4866    #[test]
4867    fn test_parse_bench_all_options() {
4868        let args = vec![
4869            "apr",
4870            "bench",
4871            "model.gguf",
4872            "--warmup",
4873            "10",
4874            "--iterations",
4875            "20",
4876            "--max-tokens",
4877            "64",
4878            "--prompt",
4879            "The quick brown fox",
4880            "--fast",
4881            "--brick",
4882            "attention",
4883        ];
4884        let cli = parse_cli(args).expect("Failed to parse");
4885        match *cli.command {
4886            Commands::Bench {
4887                warmup,
4888                iterations,
4889                max_tokens,
4890                prompt,
4891                fast,
4892                brick,
4893                ..
4894            } => {
4895                assert_eq!(warmup, 10);
4896                assert_eq!(iterations, 20);
4897                assert_eq!(max_tokens, 64);
4898                assert_eq!(prompt, Some("The quick brown fox".to_string()));
4899                assert!(fast);
4900                assert_eq!(brick, Some("attention".to_string()));
4901            }
4902            _ => panic!("Expected Bench command"),
4903        }
4904    }
4905
4906    /// Test parsing 'apr cbtop' with speculative decoding flags
4907    #[test]
4908    fn test_parse_cbtop_speculative() {
4909        let args = vec![
4910            "apr",
4911            "cbtop",
4912            "--model-path",
4913            "model.gguf",
4914            "--speculative",
4915            "--speculation-k",
4916            "8",
4917            "--draft-model",
4918            "draft.gguf",
4919            "--concurrent",
4920            "4",
4921            "--simulated",
4922        ];
4923        let cli = parse_cli(args).expect("Failed to parse");
4924        match *cli.command {
4925            Commands::Cbtop {
4926                model_path,
4927                speculative,
4928                speculation_k,
4929                draft_model,
4930                concurrent,
4931                simulated,
4932                ..
4933            } => {
4934                assert_eq!(model_path, Some(PathBuf::from("model.gguf")));
4935                assert!(speculative);
4936                assert_eq!(speculation_k, 8);
4937                assert_eq!(draft_model, Some(PathBuf::from("draft.gguf")));
4938                assert_eq!(concurrent, 4);
4939                assert!(simulated);
4940            }
4941            _ => panic!("Expected Cbtop command"),
4942        }
4943    }
4944
4945    /// Test parsing 'apr profile' with energy and perf-grade flags
4946    #[test]
4947    fn test_parse_profile_energy_perf() {
4948        let args = vec![
4949            "apr",
4950            "profile",
4951            "model.apr",
4952            "--energy",
4953            "--perf-grade",
4954            "--callgraph",
4955            "--compare-hf",
4956            "openai/whisper-tiny",
4957            "--output",
4958            "/tmp/flame.svg",
4959        ];
4960        let cli = parse_cli(args).expect("Failed to parse");
4961        match *cli.command {
4962            Commands::Profile {
4963                energy,
4964                perf_grade,
4965                callgraph,
4966                compare_hf,
4967                output,
4968                ..
4969            } => {
4970                assert!(energy);
4971                assert!(perf_grade);
4972                assert!(callgraph);
4973                assert_eq!(compare_hf, Some("openai/whisper-tiny".to_string()));
4974                assert_eq!(output, Some(PathBuf::from("/tmp/flame.svg")));
4975            }
4976            _ => panic!("Expected Profile command"),
4977        }
4978    }
4979
4980    /// Test parsing 'apr chat' with all trace options
4981    #[test]
4982    fn test_parse_chat_with_trace() {
4983        let args = vec![
4984            "apr",
4985            "chat",
4986            "model.gguf",
4987            "--system",
4988            "You are a helpful assistant.",
4989            "--inspect",
4990            "--trace",
4991            "--trace-steps",
4992            "Tokenize,Decode",
4993            "--trace-verbose",
4994            "--trace-output",
4995            "/tmp/chat-trace.json",
4996            "--trace-level",
4997            "payload",
4998            "--profile",
4999        ];
5000        let cli = parse_cli(args).expect("Failed to parse");
5001        match *cli.command {
5002            Commands::Chat {
5003                system,
5004                inspect,
5005                trace,
5006                trace_steps,
5007                trace_verbose,
5008                trace_output,
5009                trace_level,
5010                profile,
5011                ..
5012            } => {
5013                assert_eq!(system, Some("You are a helpful assistant.".to_string()));
5014                assert!(inspect);
5015                assert!(trace);
5016                assert_eq!(
5017                    trace_steps,
5018                    Some(vec!["Tokenize".to_string(), "Decode".to_string()])
5019                );
5020                assert!(trace_verbose);
5021                assert_eq!(trace_output, Some(PathBuf::from("/tmp/chat-trace.json")));
5022                assert_eq!(trace_level, "payload");
5023                assert!(profile);
5024            }
5025            _ => panic!("Expected Chat command"),
5026        }
5027    }
5028
5029    /// Test parsing 'apr showcase' with step and all options
5030    #[test]
5031    fn test_parse_showcase_with_step() {
5032        let args = vec![
5033            "apr", "showcase", "--step", "bench", "--tier", "tiny", "--zram", "--runs", "50",
5034            "--json", "-v", "-q",
5035        ];
5036        let cli = parse_cli(args).expect("Failed to parse");
5037        match *cli.command {
5038            Commands::Showcase {
5039                step,
5040                tier,
5041                zram,
5042                runs,
5043                json,
5044                verbose,
5045                quiet,
5046                ..
5047            } => {
5048                assert_eq!(step, Some("bench".to_string()));
5049                assert_eq!(tier, "tiny");
5050                assert!(zram);
5051                assert_eq!(runs, 50);
5052                assert!(json);
5053                assert!(verbose);
5054                assert!(quiet);
5055            }
5056            _ => panic!("Expected Showcase command"),
5057        }
5058    }
5059
5060    /// Test parsing rosetta compare-inference subcommand
5061    #[test]
5062    fn test_parse_rosetta_compare_inference() {
5063        let args = vec![
5064            "apr",
5065            "rosetta",
5066            "compare-inference",
5067            "model_a.gguf",
5068            "model_b.apr",
5069            "--prompt",
5070            "What is 2+2?",
5071            "--max-tokens",
5072            "10",
5073            "--temperature",
5074            "0.5",
5075            "--tolerance",
5076            "0.05",
5077            "--json",
5078        ];
5079        let cli = parse_cli(args).expect("Failed to parse");
5080        match *cli.command {
5081            Commands::Rosetta { action } => match action {
5082                RosettaCommands::CompareInference {
5083                    model_a,
5084                    model_b,
5085                    prompt,
5086                    max_tokens,
5087                    temperature,
5088                    tolerance,
5089                    json,
5090                } => {
5091                    assert_eq!(model_a, PathBuf::from("model_a.gguf"));
5092                    assert_eq!(model_b, PathBuf::from("model_b.apr"));
5093                    assert_eq!(prompt, "What is 2+2?");
5094                    assert_eq!(max_tokens, 10);
5095                    assert!((temperature - 0.5).abs() < f32::EPSILON);
5096                    assert!((tolerance - 0.05).abs() < f32::EPSILON);
5097                    assert!(json);
5098                }
5099                _ => panic!("Expected CompareInference subcommand"),
5100            },
5101            _ => panic!("Expected Rosetta command"),
5102        }
5103    }
5104
5105    /// Test parsing rosetta diff-tensors subcommand
5106    #[test]
5107    fn test_parse_rosetta_diff_tensors() {
5108        let args = vec![
5109            "apr",
5110            "rosetta",
5111            "diff-tensors",
5112            "ref.gguf",
5113            "test.apr",
5114            "--mismatches-only",
5115            "--show-values",
5116            "5",
5117            "--filter",
5118            "lm_head",
5119            "--json",
5120        ];
5121        let cli = parse_cli(args).expect("Failed to parse");
5122        match *cli.command {
5123            Commands::Rosetta { action } => match action {
5124                RosettaCommands::DiffTensors {
5125                    model_a,
5126                    model_b,
5127                    mismatches_only,
5128                    show_values,
5129                    filter,
5130                    json,
5131                } => {
5132                    assert_eq!(model_a, PathBuf::from("ref.gguf"));
5133                    assert_eq!(model_b, PathBuf::from("test.apr"));
5134                    assert!(mismatches_only);
5135                    assert_eq!(show_values, 5);
5136                    assert_eq!(filter, Some("lm_head".to_string()));
5137                    assert!(json);
5138                }
5139                _ => panic!("Expected DiffTensors subcommand"),
5140            },
5141            _ => panic!("Expected Rosetta command"),
5142        }
5143    }
5144
5145    /// Test parsing rosetta fingerprint subcommand
5146    #[test]
5147    fn test_parse_rosetta_fingerprint() {
5148        let args = vec![
5149            "apr",
5150            "rosetta",
5151            "fingerprint",
5152            "model.gguf",
5153            "model2.apr",
5154            "--output",
5155            "fingerprints.json",
5156            "--filter",
5157            "encoder",
5158            "--verbose",
5159            "--json",
5160        ];
5161        let cli = parse_cli(args).expect("Failed to parse");
5162        match *cli.command {
5163            Commands::Rosetta { action } => match action {
5164                RosettaCommands::Fingerprint {
5165                    model,
5166                    model_b,
5167                    output,
5168                    filter,
5169                    verbose,
5170                    json,
5171                } => {
5172                    assert_eq!(model, PathBuf::from("model.gguf"));
5173                    assert_eq!(model_b, Some(PathBuf::from("model2.apr")));
5174                    assert_eq!(output, Some(PathBuf::from("fingerprints.json")));
5175                    assert_eq!(filter, Some("encoder".to_string()));
5176                    assert!(verbose);
5177                    assert!(json);
5178                }
5179                _ => panic!("Expected Fingerprint subcommand"),
5180            },
5181            _ => panic!("Expected Rosetta command"),
5182        }
5183    }
5184
5185    /// Test parsing rosetta validate-stats subcommand
5186    #[test]
5187    fn test_parse_rosetta_validate_stats() {
5188        let args = vec![
5189            "apr",
5190            "rosetta",
5191            "validate-stats",
5192            "model.apr",
5193            "--reference",
5194            "ref.gguf",
5195            "--fingerprints",
5196            "fp.json",
5197            "--threshold",
5198            "5.0",
5199            "--strict",
5200            "--json",
5201        ];
5202        let cli = parse_cli(args).expect("Failed to parse");
5203        match *cli.command {
5204            Commands::Rosetta { action } => match action {
5205                RosettaCommands::ValidateStats {
5206                    model,
5207                    reference,
5208                    fingerprints,
5209                    threshold,
5210                    strict,
5211                    json,
5212                } => {
5213                    assert_eq!(model, PathBuf::from("model.apr"));
5214                    assert_eq!(reference, Some(PathBuf::from("ref.gguf")));
5215                    assert_eq!(fingerprints, Some(PathBuf::from("fp.json")));
5216                    assert!((threshold - 5.0).abs() < f32::EPSILON);
5217                    assert!(strict);
5218                    assert!(json);
5219                }
5220                _ => panic!("Expected ValidateStats subcommand"),
5221            },
5222            _ => panic!("Expected Rosetta command"),
5223        }
5224    }
5225
5226    // =========================================================================
5227    // Global flag tests
5228    // =========================================================================
5229
5230    /// Test global --offline flag
5231    #[test]
5232    fn test_global_offline_flag() {
5233        let args = vec!["apr", "--offline", "inspect", "model.apr"];
5234        let cli = parse_cli(args).expect("Failed to parse");
5235        assert!(cli.offline);
5236    }
5237
5238    /// Test global --quiet flag
5239    #[test]
5240    fn test_global_quiet_flag() {
5241        let args = vec!["apr", "--quiet", "inspect", "model.apr"];
5242        let cli = parse_cli(args).expect("Failed to parse");
5243        assert!(cli.quiet);
5244    }
5245
5246    /// Test multiple global flags combined
5247    #[test]
5248    fn test_multiple_global_flags() {
5249        let args = vec![
5250            "apr",
5251            "--verbose",
5252            "--json",
5253            "--offline",
5254            "--quiet",
5255            "--skip-contract",
5256            "inspect",
5257            "model.apr",
5258        ];
5259        let cli = parse_cli(args).expect("Failed to parse");
5260        assert!(cli.verbose);
5261        assert!(cli.json);
5262        assert!(cli.offline);
5263        assert!(cli.quiet);
5264        assert!(cli.skip_contract);
5265    }
5266
5267    /// Test global flags default to false
5268    #[test]
5269    fn test_global_flags_default_false() {
5270        let args = vec!["apr", "inspect", "model.apr"];
5271        let cli = parse_cli(args).expect("Failed to parse");
5272        assert!(!cli.verbose);
5273        assert!(!cli.json);
5274        assert!(!cli.offline);
5275        assert!(!cli.quiet);
5276        assert!(!cli.skip_contract);
5277    }
5278
5279    // =========================================================================
5280    // extract_model_paths: additional command variants
5281    // =========================================================================
5282
5283    /// Test extract_model_paths: Export returns file path
5284    #[test]
5285    fn test_extract_paths_export() {
5286        let cmd = Commands::Export {
5287            file: PathBuf::from("model.apr"),
5288            format: "gguf".to_string(),
5289            output: PathBuf::from("out.gguf"),
5290            quantize: None,
5291        };
5292        let paths = extract_model_paths(&cmd);
5293        assert_eq!(paths, vec![PathBuf::from("model.apr")]);
5294    }
5295
5296    /// Test extract_model_paths: Convert returns file path
5297    #[test]
5298    fn test_extract_paths_convert() {
5299        let cmd = Commands::Convert {
5300            file: PathBuf::from("model.apr"),
5301            quantize: Some("q4k".to_string()),
5302            compress: None,
5303            output: PathBuf::from("out.apr"),
5304            force: false,
5305        };
5306        let paths = extract_model_paths(&cmd);
5307        assert_eq!(paths, vec![PathBuf::from("model.apr")]);
5308    }
5309
5310    /// Test extract_model_paths: Check returns file path
5311    #[test]
5312    fn test_extract_paths_check() {
5313        let cmd = Commands::Check {
5314            file: PathBuf::from("model.gguf"),
5315            no_gpu: false,
5316        };
5317        let paths = extract_model_paths(&cmd);
5318        assert_eq!(paths, vec![PathBuf::from("model.gguf")]);
5319    }
5320
5321    /// Test extract_model_paths: Trace returns file path
5322    #[test]
5323    fn test_extract_paths_trace() {
5324        let cmd = Commands::Trace {
5325            file: PathBuf::from("model.apr"),
5326            layer: None,
5327            reference: None,
5328            json: false,
5329            verbose: false,
5330            payload: false,
5331            diff: false,
5332            interactive: false,
5333        };
5334        let paths = extract_model_paths(&cmd);
5335        assert_eq!(paths, vec![PathBuf::from("model.apr")]);
5336    }
5337
5338    /// Test extract_model_paths: Probar returns file path
5339    #[test]
5340    fn test_extract_paths_probar() {
5341        let cmd = Commands::Probar {
5342            file: PathBuf::from("model.apr"),
5343            output: PathBuf::from("./probar-export"),
5344            format: "both".to_string(),
5345            golden: None,
5346            layer: None,
5347        };
5348        let paths = extract_model_paths(&cmd);
5349        assert_eq!(paths, vec![PathBuf::from("model.apr")]);
5350    }
5351
5352    /// Test extract_model_paths: CompareHf returns file path
5353    #[test]
5354    fn test_extract_paths_compare_hf() {
5355        let cmd = Commands::CompareHf {
5356            file: PathBuf::from("model.apr"),
5357            hf: "openai/whisper-tiny".to_string(),
5358            tensor: None,
5359            threshold: 1e-5,
5360            json: false,
5361        };
5362        let paths = extract_model_paths(&cmd);
5363        assert_eq!(paths, vec![PathBuf::from("model.apr")]);
5364    }
5365
5366    /// Test extract_model_paths: Chat returns file path
5367    #[test]
5368    fn test_extract_paths_chat() {
5369        let cmd = Commands::Chat {
5370            file: PathBuf::from("model.gguf"),
5371            temperature: 0.7,
5372            top_p: 0.9,
5373            max_tokens: 512,
5374            system: None,
5375            inspect: false,
5376            no_gpu: false,
5377            gpu: false,
5378            trace: false,
5379            trace_steps: None,
5380            trace_verbose: false,
5381            trace_output: None,
5382            trace_level: "basic".to_string(),
5383            profile: false,
5384        };
5385        let paths = extract_model_paths(&cmd);
5386        assert_eq!(paths, vec![PathBuf::from("model.gguf")]);
5387    }
5388
5389    /// Test extract_model_paths: Eval returns file path
5390    #[test]
5391    fn test_extract_paths_eval() {
5392        let cmd = Commands::Eval {
5393            file: PathBuf::from("model.gguf"),
5394            dataset: "wikitext-2".to_string(),
5395            text: None,
5396            max_tokens: 512,
5397            threshold: 20.0,
5398        };
5399        let paths = extract_model_paths(&cmd);
5400        assert_eq!(paths, vec![PathBuf::from("model.gguf")]);
5401    }
5402
5403    /// Test extract_model_paths: Profile returns file path
5404    #[test]
5405    fn test_extract_paths_profile() {
5406        let cmd = Commands::Profile {
5407            file: PathBuf::from("model.apr"),
5408            granular: false,
5409            format: "human".to_string(),
5410            focus: None,
5411            detect_naive: false,
5412            threshold: 10.0,
5413            compare_hf: None,
5414            energy: false,
5415            perf_grade: false,
5416            callgraph: false,
5417            fail_on_naive: false,
5418            output: None,
5419            ci: false,
5420            assert_throughput: None,
5421            assert_p99: None,
5422            assert_p50: None,
5423            warmup: 3,
5424            measure: 10,
5425            tokens: 32,
5426            ollama: false,
5427            no_gpu: false,
5428            compare: None,
5429        };
5430        let paths = extract_model_paths(&cmd);
5431        assert_eq!(paths, vec![PathBuf::from("model.apr")]);
5432    }
5433
5434    /// Test extract_model_paths: Import with hf:// URL returns empty (non-local)
5435    #[test]
5436    fn test_extract_paths_import_hf_url() {
5437        let cmd = Commands::Import {
5438            source: "hf://openai/whisper-tiny".to_string(),
5439            output: Some(PathBuf::from("whisper.apr")),
5440            arch: "auto".to_string(),
5441            quantize: None,
5442            strict: false,
5443            preserve_q4k: false,
5444            tokenizer: None,
5445            enforce_provenance: false,
5446        };
5447        let paths = extract_model_paths(&cmd);
5448        assert!(
5449            paths.is_empty(),
5450            "hf:// URLs should not be validated locally for import"
5451        );
5452    }
5453
5454    /// Test extract_model_paths: Import with non-existent local path returns empty
5455    #[test]
5456    fn test_extract_paths_import_nonexistent_local() {
5457        let cmd = Commands::Import {
5458            source: "/tmp/nonexistent_model_abc123.gguf".to_string(),
5459            output: None,
5460            arch: "auto".to_string(),
5461            quantize: None,
5462            strict: false,
5463            preserve_q4k: false,
5464            tokenizer: None,
5465            enforce_provenance: false,
5466        };
5467        let paths = extract_model_paths(&cmd);
5468        assert!(
5469            paths.is_empty(),
5470            "Non-existent local paths return empty for import"
5471        );
5472    }
5473
5474    /// Test extract_model_paths: Tui with file returns file
5475    #[test]
5476    fn test_extract_paths_tui_with_file() {
5477        let cmd = Commands::Tui {
5478            file: Some(PathBuf::from("model.apr")),
5479        };
5480        let paths = extract_model_paths(&cmd);
5481        assert_eq!(paths, vec![PathBuf::from("model.apr")]);
5482    }
5483
5484    /// Test extract_model_paths: Tui without file returns empty
5485    #[test]
5486    fn test_extract_paths_tui_no_file() {
5487        let cmd = Commands::Tui { file: None };
5488        let paths = extract_model_paths(&cmd);
5489        assert!(paths.is_empty());
5490    }
5491
5492    /// Test extract_model_paths: Cbtop with model_path returns it
5493    #[test]
5494    fn test_extract_paths_cbtop_with_model_path() {
5495        let cmd = Commands::Cbtop {
5496            model: None,
5497            attach: None,
5498            model_path: Some(PathBuf::from("model.gguf")),
5499            headless: false,
5500            json: false,
5501            output: None,
5502            ci: false,
5503            throughput: None,
5504            brick_score: None,
5505            warmup: 10,
5506            iterations: 100,
5507            speculative: false,
5508            speculation_k: 4,
5509            draft_model: None,
5510            concurrent: 1,
5511            simulated: false,
5512        };
5513        let paths = extract_model_paths(&cmd);
5514        assert_eq!(paths, vec![PathBuf::from("model.gguf")]);
5515    }
5516
5517    /// Test extract_model_paths: Cbtop without model_path returns empty
5518    #[test]
5519    fn test_extract_paths_cbtop_no_model_path() {
5520        let cmd = Commands::Cbtop {
5521            model: Some("qwen2.5-coder".to_string()),
5522            attach: None,
5523            model_path: None,
5524            headless: false,
5525            json: false,
5526            output: None,
5527            ci: false,
5528            throughput: None,
5529            brick_score: None,
5530            warmup: 10,
5531            iterations: 100,
5532            speculative: false,
5533            speculation_k: 4,
5534            draft_model: None,
5535            concurrent: 1,
5536            simulated: false,
5537        };
5538        let paths = extract_model_paths(&cmd);
5539        assert!(paths.is_empty());
5540    }
5541
5542    /// Test extract_model_paths: Diff is diagnostic (exempt)
5543    #[test]
5544    fn test_extract_paths_diff_exempt() {
5545        let cmd = Commands::Diff {
5546            file1: PathBuf::from("a.apr"),
5547            file2: PathBuf::from("b.apr"),
5548            weights: false,
5549            values: false,
5550            filter: None,
5551            limit: 10,
5552            transpose_aware: false,
5553            json: false,
5554        };
5555        let paths = extract_model_paths(&cmd);
5556        assert!(paths.is_empty(), "Diff is a diagnostic command (exempt)");
5557    }
5558
5559    /// Test extract_model_paths: Hex is diagnostic (exempt)
5560    #[test]
5561    fn test_extract_paths_hex_exempt() {
5562        let cmd = Commands::Hex {
5563            file: PathBuf::from("model.apr"),
5564            tensor: None,
5565            limit: 64,
5566            stats: false,
5567            list: false,
5568            json: false,
5569            header: false,
5570            blocks: false,
5571            distribution: false,
5572            contract: false,
5573            entropy: false,
5574            raw: false,
5575            offset: "0".to_string(),
5576            width: 16,
5577        };
5578        let paths = extract_model_paths(&cmd);
5579        assert!(paths.is_empty(), "Hex is a diagnostic command (exempt)");
5580    }
5581
5582    /// Test extract_model_paths: Tree is diagnostic (exempt)
5583    #[test]
5584    fn test_extract_paths_tree_exempt() {
5585        let cmd = Commands::Tree {
5586            file: PathBuf::from("model.apr"),
5587            filter: None,
5588            format: "ascii".to_string(),
5589            sizes: false,
5590            depth: None,
5591        };
5592        let paths = extract_model_paths(&cmd);
5593        assert!(paths.is_empty(), "Tree is a diagnostic command (exempt)");
5594    }
5595
5596    /// Test extract_model_paths: Flow is diagnostic (exempt)
5597    #[test]
5598    fn test_extract_paths_flow_exempt() {
5599        let cmd = Commands::Flow {
5600            file: PathBuf::from("model.apr"),
5601            layer: None,
5602            component: "full".to_string(),
5603            verbose: false,
5604        };
5605        let paths = extract_model_paths(&cmd);
5606        assert!(paths.is_empty(), "Flow is a diagnostic command (exempt)");
5607    }
5608
5609    /// Test extract_model_paths: Publish is diagnostic (exempt)
5610    #[test]
5611    fn test_extract_paths_publish_exempt() {
5612        let cmd = Commands::Publish {
5613            directory: PathBuf::from("/tmp/models"),
5614            repo_id: "org/repo".to_string(),
5615            model_name: None,
5616            license: "mit".to_string(),
5617            pipeline_tag: "text-generation".to_string(),
5618            library_name: None,
5619            tags: None,
5620            message: None,
5621            dry_run: false,
5622        };
5623        let paths = extract_model_paths(&cmd);
5624        assert!(paths.is_empty(), "Publish is a diagnostic command (exempt)");
5625    }
5626
5627    /// Test extract_model_paths: Tune is diagnostic (exempt)
5628    #[test]
5629    fn test_extract_paths_tune_exempt() {
5630        let cmd = Commands::Tune {
5631            file: Some(PathBuf::from("model.apr")),
5632            method: "auto".to_string(),
5633            rank: None,
5634            vram: 16.0,
5635            plan: false,
5636            model: None,
5637            freeze_base: false,
5638            train_data: None,
5639            json: false,
5640        };
5641        let paths = extract_model_paths(&cmd);
5642        assert!(paths.is_empty(), "Tune is a diagnostic command (exempt)");
5643    }
5644
5645    /// Test extract_model_paths: Pull is diagnostic (exempt)
5646    #[test]
5647    fn test_extract_paths_pull_exempt() {
5648        let cmd = Commands::Pull {
5649            model_ref: "hf://org/repo".to_string(),
5650            force: false,
5651        };
5652        let paths = extract_model_paths(&cmd);
5653        assert!(paths.is_empty(), "Pull is a diagnostic command (exempt)");
5654    }
5655
5656    /// Test extract_model_paths: Rm is diagnostic (exempt)
5657    #[test]
5658    fn test_extract_paths_rm_exempt() {
5659        let cmd = Commands::Rm {
5660            model_ref: "model-name".to_string(),
5661        };
5662        let paths = extract_model_paths(&cmd);
5663        assert!(paths.is_empty(), "Rm is a diagnostic command (exempt)");
5664    }
5665
5666    /// Test extract_model_paths: Canary is diagnostic (exempt)
5667    #[test]
5668    fn test_extract_paths_canary_exempt() {
5669        let cmd = Commands::Canary {
5670            command: CanaryCommands::Check {
5671                file: PathBuf::from("model.apr"),
5672                canary: PathBuf::from("canary.json"),
5673            },
5674        };
5675        let paths = extract_model_paths(&cmd);
5676        assert!(paths.is_empty(), "Canary is a diagnostic command (exempt)");
5677    }
5678
5679    /// Test extract_model_paths: Oracle is diagnostic (exempt)
5680    #[test]
5681    fn test_extract_paths_oracle_exempt() {
5682        let cmd = Commands::Oracle {
5683            source: Some("model.gguf".to_string()),
5684            family: None,
5685            size: None,
5686            compliance: false,
5687            tensors: false,
5688            stats: false,
5689            explain: false,
5690            kernels: false,
5691            validate: false,
5692            full: false,
5693        };
5694        let paths = extract_model_paths(&cmd);
5695        assert!(paths.is_empty(), "Oracle is a diagnostic command (exempt)");
5696    }
5697
5698    /// Test extract_model_paths: Showcase is diagnostic (exempt)
5699    #[test]
5700    fn test_extract_paths_showcase_exempt() {
5701        let cmd = Commands::Showcase {
5702            auto_verify: false,
5703            step: None,
5704            tier: "small".to_string(),
5705            model_dir: PathBuf::from("./models"),
5706            baseline: "llama-cpp,ollama".to_string(),
5707            zram: false,
5708            runs: 30,
5709            gpu: false,
5710            json: false,
5711            verbose: false,
5712            quiet: false,
5713        };
5714        let paths = extract_model_paths(&cmd);
5715        assert!(
5716            paths.is_empty(),
5717            "Showcase is a diagnostic command (exempt)"
5718        );
5719    }
5720
5721    /// Test extract_model_paths: Rosetta Convert returns source path
5722    #[test]
5723    fn test_extract_paths_rosetta_convert() {
5724        let cmd = Commands::Rosetta {
5725            action: RosettaCommands::Convert {
5726                source: PathBuf::from("model.gguf"),
5727                target: PathBuf::from("out.safetensors"),
5728                quantize: None,
5729                verify: false,
5730                json: false,
5731                tokenizer: None,
5732            },
5733        };
5734        let paths = extract_model_paths(&cmd);
5735        assert_eq!(paths, vec![PathBuf::from("model.gguf")]);
5736    }
5737
5738    /// Test extract_model_paths: Rosetta Chain returns source path
5739    #[test]
5740    fn test_extract_paths_rosetta_chain() {
5741        let cmd = Commands::Rosetta {
5742            action: RosettaCommands::Chain {
5743                source: PathBuf::from("model.gguf"),
5744                formats: vec!["safetensors".to_string(), "apr".to_string()],
5745                work_dir: PathBuf::from("/tmp"),
5746                json: false,
5747            },
5748        };
5749        let paths = extract_model_paths(&cmd);
5750        assert_eq!(paths, vec![PathBuf::from("model.gguf")]);
5751    }
5752
5753    /// Test extract_model_paths: Rosetta Verify returns source path
5754    #[test]
5755    fn test_extract_paths_rosetta_verify() {
5756        let cmd = Commands::Rosetta {
5757            action: RosettaCommands::Verify {
5758                source: PathBuf::from("model.apr"),
5759                intermediate: "safetensors".to_string(),
5760                tolerance: 1e-5,
5761                json: false,
5762            },
5763        };
5764        let paths = extract_model_paths(&cmd);
5765        assert_eq!(paths, vec![PathBuf::from("model.apr")]);
5766    }
5767
5768    /// Test extract_model_paths: Rosetta CompareInference returns both paths
5769    #[test]
5770    fn test_extract_paths_rosetta_compare_inference() {
5771        let cmd = Commands::Rosetta {
5772            action: RosettaCommands::CompareInference {
5773                model_a: PathBuf::from("model_a.gguf"),
5774                model_b: PathBuf::from("model_b.apr"),
5775                prompt: "test".to_string(),
5776                max_tokens: 5,
5777                temperature: 0.0,
5778                tolerance: 0.1,
5779                json: false,
5780            },
5781        };
5782        let paths = extract_model_paths(&cmd);
5783        assert_eq!(
5784            paths,
5785            vec![PathBuf::from("model_a.gguf"), PathBuf::from("model_b.apr")]
5786        );
5787    }
5788
5789    /// Test extract_model_paths: Rosetta Inspect is diagnostic (exempt)
5790    #[test]
5791    fn test_extract_paths_rosetta_inspect_exempt() {
5792        let cmd = Commands::Rosetta {
5793            action: RosettaCommands::Inspect {
5794                file: PathBuf::from("model.gguf"),
5795                hexdump: false,
5796                json: false,
5797            },
5798        };
5799        let paths = extract_model_paths(&cmd);
5800        assert!(
5801            paths.is_empty(),
5802            "Rosetta Inspect is a diagnostic command (exempt)"
5803        );
5804    }
5805
5806    /// Test extract_model_paths: Rosetta DiffTensors is diagnostic (exempt)
5807    #[test]
5808    fn test_extract_paths_rosetta_diff_tensors_exempt() {
5809        let cmd = Commands::Rosetta {
5810            action: RosettaCommands::DiffTensors {
5811                model_a: PathBuf::from("a.gguf"),
5812                model_b: PathBuf::from("b.apr"),
5813                mismatches_only: false,
5814                show_values: 0,
5815                filter: None,
5816                json: false,
5817            },
5818        };
5819        let paths = extract_model_paths(&cmd);
5820        assert!(
5821            paths.is_empty(),
5822            "Rosetta DiffTensors is a diagnostic command (exempt)"
5823        );
5824    }
5825
5826    /// Test extract_model_paths: Rosetta Fingerprint is diagnostic (exempt)
5827    #[test]
5828    fn test_extract_paths_rosetta_fingerprint_exempt() {
5829        let cmd = Commands::Rosetta {
5830            action: RosettaCommands::Fingerprint {
5831                model: PathBuf::from("model.gguf"),
5832                model_b: None,
5833                output: None,
5834                filter: None,
5835                verbose: false,
5836                json: false,
5837            },
5838        };
5839        let paths = extract_model_paths(&cmd);
5840        assert!(
5841            paths.is_empty(),
5842            "Rosetta Fingerprint is a diagnostic command (exempt)"
5843        );
5844    }
5845
5846    /// Test extract_model_paths: Rosetta ValidateStats is diagnostic (exempt)
5847    #[test]
5848    fn test_extract_paths_rosetta_validate_stats_exempt() {
5849        let cmd = Commands::Rosetta {
5850            action: RosettaCommands::ValidateStats {
5851                model: PathBuf::from("model.apr"),
5852                reference: None,
5853                fingerprints: None,
5854                threshold: 3.0,
5855                strict: false,
5856                json: false,
5857            },
5858        };
5859        let paths = extract_model_paths(&cmd);
5860        assert!(
5861            paths.is_empty(),
5862            "Rosetta ValidateStats is a diagnostic command (exempt)"
5863        );
5864    }
5865
5866    // =========================================================================
5867    // validate_model_contract: additional edge cases
5868    // =========================================================================
5869
5870    /// Test validate_model_contract: multiple non-existent paths all skipped
5871    #[test]
5872    fn test_validate_contract_multiple_nonexistent() {
5873        let paths = vec![
5874            PathBuf::from("/tmp/nonexistent_a.apr"),
5875            PathBuf::from("/tmp/nonexistent_b.gguf"),
5876            PathBuf::from("/tmp/nonexistent_c.safetensors"),
5877        ];
5878        let result = validate_model_contract(&paths);
5879        assert!(result.is_ok(), "All non-existent paths should be skipped");
5880    }
5881
5882    /// Test validate_model_contract: mix of non-existent paths
5883    #[test]
5884    fn test_validate_contract_mixed_nonexistent() {
5885        let paths = vec![
5886            PathBuf::from("/tmp/does_not_exist_xyz.apr"),
5887            PathBuf::from("/tmp/also_missing_123.gguf"),
5888        ];
5889        let result = validate_model_contract(&paths);
5890        assert!(
5891            result.is_ok(),
5892            "Mixed non-existent paths should all be skipped"
5893        );
5894    }
5895
5896    // =========================================================================
5897    // execute_command: error path tests (file not found)
5898    // =========================================================================
5899
5900    /// Helper: create a Cli struct with the given command and default flags
5901    fn make_cli(command: Commands) -> Cli {
5902        Cli {
5903            command: Box::new(command),
5904            json: false,
5905            verbose: false,
5906            quiet: false,
5907            offline: false,
5908            skip_contract: true, // Skip contract to test command dispatch errors
5909        }
5910    }
5911
5912    /// Test execute_command: Inspect with non-existent file returns error
5913    #[test]
5914    fn test_execute_inspect_file_not_found() {
5915        let cli = make_cli(Commands::Inspect {
5916            file: PathBuf::from("/tmp/nonexistent_model_inspect_test.apr"),
5917            vocab: false,
5918            filters: false,
5919            weights: false,
5920            json: false,
5921        });
5922        let result = execute_command(&cli);
5923        assert!(
5924            result.is_err(),
5925            "Inspect should fail with non-existent file"
5926        );
5927    }
5928
5929    /// Test execute_command: Debug with non-existent file returns error
5930    #[test]
5931    fn test_execute_debug_file_not_found() {
5932        let cli = make_cli(Commands::Debug {
5933            file: PathBuf::from("/tmp/nonexistent_model_debug_test.apr"),
5934            drama: false,
5935            hex: false,
5936            strings: false,
5937            limit: 256,
5938        });
5939        let result = execute_command(&cli);
5940        assert!(result.is_err(), "Debug should fail with non-existent file");
5941    }
5942
5943    /// Test execute_command: Validate with non-existent file returns error
5944    #[test]
5945    fn test_execute_validate_file_not_found() {
5946        let cli = make_cli(Commands::Validate {
5947            file: PathBuf::from("/tmp/nonexistent_model_validate_test.apr"),
5948            quality: false,
5949            strict: false,
5950            min_score: None,
5951        });
5952        let result = execute_command(&cli);
5953        assert!(
5954            result.is_err(),
5955            "Validate should fail with non-existent file"
5956        );
5957    }
5958
5959    /// Test execute_command: Diff with non-existent files returns error
5960    #[test]
5961    fn test_execute_diff_file_not_found() {
5962        let cli = make_cli(Commands::Diff {
5963            file1: PathBuf::from("/tmp/nonexistent_model_diff1.apr"),
5964            file2: PathBuf::from("/tmp/nonexistent_model_diff2.apr"),
5965            weights: false,
5966            values: false,
5967            filter: None,
5968            limit: 10,
5969            transpose_aware: false,
5970            json: false,
5971        });
5972        let result = execute_command(&cli);
5973        assert!(result.is_err(), "Diff should fail with non-existent files");
5974    }
5975
5976    /// Test execute_command: Tensors with non-existent file returns error
5977    #[test]
5978    fn test_execute_tensors_file_not_found() {
5979        let cli = make_cli(Commands::Tensors {
5980            file: PathBuf::from("/tmp/nonexistent_model_tensors_test.apr"),
5981            stats: false,
5982            filter: None,
5983            limit: 0,
5984            json: false,
5985        });
5986        let result = execute_command(&cli);
5987        assert!(
5988            result.is_err(),
5989            "Tensors should fail with non-existent file"
5990        );
5991    }
5992
5993    /// Test execute_command: Lint with non-existent file returns error
5994    #[test]
5995    fn test_execute_lint_file_not_found() {
5996        let cli = make_cli(Commands::Lint {
5997            file: PathBuf::from("/tmp/nonexistent_model_lint_test.apr"),
5998        });
5999        let result = execute_command(&cli);
6000        assert!(result.is_err(), "Lint should fail with non-existent file");
6001    }
6002
6003    /// Test execute_command: Trace with non-existent file returns error
6004    #[test]
6005    fn test_execute_trace_file_not_found() {
6006        let cli = make_cli(Commands::Trace {
6007            file: PathBuf::from("/tmp/nonexistent_model_trace_test.apr"),
6008            layer: None,
6009            reference: None,
6010            json: false,
6011            verbose: false,
6012            payload: false,
6013            diff: false,
6014            interactive: false,
6015        });
6016        let result = execute_command(&cli);
6017        assert!(result.is_err(), "Trace should fail with non-existent file");
6018    }
6019
6020    /// Test execute_command: Export with non-existent file returns error
6021    #[test]
6022    fn test_execute_export_file_not_found() {
6023        let cli = make_cli(Commands::Export {
6024            file: PathBuf::from("/tmp/nonexistent_model_export_test.apr"),
6025            format: "safetensors".to_string(),
6026            output: PathBuf::from("/tmp/out.safetensors"),
6027            quantize: None,
6028        });
6029        let result = execute_command(&cli);
6030        assert!(result.is_err(), "Export should fail with non-existent file");
6031    }
6032
6033    /// Test execute_command: Convert with non-existent file returns error
6034    #[test]
6035    fn test_execute_convert_file_not_found() {
6036        let cli = make_cli(Commands::Convert {
6037            file: PathBuf::from("/tmp/nonexistent_model_convert_test.apr"),
6038            quantize: None,
6039            compress: None,
6040            output: PathBuf::from("/tmp/out.apr"),
6041            force: false,
6042        });
6043        let result = execute_command(&cli);
6044        assert!(
6045            result.is_err(),
6046            "Convert should fail with non-existent file"
6047        );
6048    }
6049
6050    /// Test execute_command: Hex with non-existent file returns error
6051    #[test]
6052    fn test_execute_hex_file_not_found() {
6053        let cli = make_cli(Commands::Hex {
6054            file: PathBuf::from("/tmp/nonexistent_model_hex_test.apr"),
6055            tensor: None,
6056            limit: 64,
6057            stats: false,
6058            list: false,
6059            json: false,
6060            header: false,
6061            blocks: false,
6062            distribution: false,
6063            contract: false,
6064            entropy: false,
6065            raw: false,
6066            offset: String::new(),
6067            width: 16,
6068        });
6069        let result = execute_command(&cli);
6070        assert!(result.is_err(), "Hex should fail with non-existent file");
6071    }
6072
6073    /// Test execute_command: Tree with non-existent file returns error
6074    #[test]
6075    fn test_execute_tree_file_not_found() {
6076        let cli = make_cli(Commands::Tree {
6077            file: PathBuf::from("/tmp/nonexistent_model_tree_test.apr"),
6078            filter: None,
6079            format: "ascii".to_string(),
6080            sizes: false,
6081            depth: None,
6082        });
6083        let result = execute_command(&cli);
6084        assert!(result.is_err(), "Tree should fail with non-existent file");
6085    }
6086
6087    /// Test execute_command: Flow with non-existent file returns error
6088    #[test]
6089    fn test_execute_flow_file_not_found() {
6090        let cli = make_cli(Commands::Flow {
6091            file: PathBuf::from("/tmp/nonexistent_model_flow_test.apr"),
6092            layer: None,
6093            component: "full".to_string(),
6094            verbose: false,
6095        });
6096        let result = execute_command(&cli);
6097        assert!(result.is_err(), "Flow should fail with non-existent file");
6098    }
6099
6100    /// Test execute_command: Probar with non-existent file returns error
6101    #[test]
6102    fn test_execute_probar_file_not_found() {
6103        let cli = make_cli(Commands::Probar {
6104            file: PathBuf::from("/tmp/nonexistent_model_probar_test.apr"),
6105            output: PathBuf::from("/tmp/probar-out"),
6106            format: "both".to_string(),
6107            golden: None,
6108            layer: None,
6109        });
6110        let result = execute_command(&cli);
6111        assert!(result.is_err(), "Probar should fail with non-existent file");
6112    }
6113
6114    /// Test execute_command: Check with non-existent file returns error
6115    #[test]
6116    fn test_execute_check_file_not_found() {
6117        let cli = make_cli(Commands::Check {
6118            file: PathBuf::from("/tmp/nonexistent_model_check_test.apr"),
6119            no_gpu: true,
6120        });
6121        let result = execute_command(&cli);
6122        assert!(result.is_err(), "Check should fail with non-existent file");
6123    }
6124
6125    /// Test execute_command: List succeeds (no file needed)
6126    #[test]
6127    fn test_execute_list_succeeds() {
6128        let cli = make_cli(Commands::List);
6129        // List should succeed even if cache is empty
6130        let result = execute_command(&cli);
6131        assert!(result.is_ok(), "List should succeed without arguments");
6132    }
6133
6134    /// Test execute_command: Explain without args succeeds
6135    #[test]
6136    fn test_execute_explain_no_args() {
6137        let cli = make_cli(Commands::Explain {
6138            code: None,
6139            file: None,
6140            tensor: None,
6141        });
6142        // Explain with no args should still run (shows general help)
6143        let result = execute_command(&cli);
6144        assert!(result.is_ok(), "Explain with no args should succeed");
6145    }
6146
6147    /// Test execute_command: Explain with code succeeds
6148    #[test]
6149    fn test_execute_explain_with_code() {
6150        let cli = make_cli(Commands::Explain {
6151            code: Some("E001".to_string()),
6152            file: None,
6153            tensor: None,
6154        });
6155        let result = execute_command(&cli);
6156        // Should succeed even for unknown error codes (it prints "unknown error code")
6157        assert!(result.is_ok(), "Explain with error code should succeed");
6158    }
6159
6160    /// Test execute_command: Tune --plan without file succeeds
6161    #[test]
6162    fn test_execute_tune_plan_no_file() {
6163        let cli = make_cli(Commands::Tune {
6164            file: None,
6165            method: "auto".to_string(),
6166            rank: None,
6167            vram: 16.0,
6168            plan: true,
6169            model: Some("7B".to_string()),
6170            freeze_base: false,
6171            train_data: None,
6172            json: false,
6173        });
6174        let result = execute_command(&cli);
6175        // Tune with --plan and --model should succeed without a file
6176        assert!(
6177            result.is_ok(),
6178            "Tune --plan --model 7B should succeed without file"
6179        );
6180    }
6181
6182    /// Test execute_command: Qa with non-existent file and all skips still succeeds
6183    /// because QA gates are individually skipped. With no gates enabled, it just
6184    /// prints summary and returns Ok.
6185    #[test]
6186    fn test_execute_qa_all_skips_succeeds() {
6187        let cli = make_cli(Commands::Qa {
6188            file: PathBuf::from("/tmp/nonexistent_model_qa_test.gguf"),
6189            assert_tps: None,
6190            assert_speedup: None,
6191            assert_gpu_speedup: None,
6192            skip_golden: true,
6193            skip_throughput: true,
6194            skip_ollama: true,
6195            skip_gpu_speedup: true,
6196            skip_contract: true,
6197            skip_format_parity: true,
6198            skip_ptx_parity: true,
6199            safetensors_path: None,
6200            iterations: 1,
6201            warmup: 0,
6202            max_tokens: 1,
6203            json: false,
6204            verbose: false,
6205            min_executed: None,
6206            previous_report: None,
6207            regression_threshold: None,
6208            skip_gpu_state: false,
6209            skip_metadata: true,
6210        });
6211        let result = execute_command(&cli);
6212        assert!(
6213            result.is_ok(),
6214            "Qa with all gates skipped should succeed even with non-existent file"
6215        );
6216    }
6217
6218    /// Test execute_command: Qa with non-existent file and gates enabled returns error
6219    #[test]
6220    fn test_execute_qa_with_gates_file_not_found() {
6221        let cli = make_cli(Commands::Qa {
6222            file: PathBuf::from("/tmp/nonexistent_model_qa_gates_test.gguf"),
6223            assert_tps: None,
6224            assert_speedup: None,
6225            assert_gpu_speedup: None,
6226            skip_golden: false, // Gate enabled
6227            skip_throughput: true,
6228            skip_ollama: true,
6229            skip_gpu_speedup: true,
6230            skip_contract: true,
6231            skip_format_parity: true,
6232            skip_ptx_parity: true,
6233            safetensors_path: None,
6234            iterations: 1,
6235            warmup: 0,
6236            max_tokens: 1,
6237            json: false,
6238            verbose: false,
6239            min_executed: None,
6240            previous_report: None,
6241            regression_threshold: None,
6242            skip_gpu_state: false,
6243            skip_metadata: true,
6244        });
6245        let result = execute_command(&cli);
6246        assert!(
6247            result.is_err(),
6248            "Qa with golden gate enabled should fail with non-existent file"
6249        );
6250    }
6251
6252    /// Test execute_command: Import with invalid source returns error
6253    #[test]
6254    fn test_execute_import_invalid_source() {
6255        let cli = make_cli(Commands::Import {
6256            source: "/tmp/nonexistent_model_import_test.gguf".to_string(),
6257            output: None,
6258            arch: "auto".to_string(),
6259            quantize: None,
6260            strict: false,
6261            preserve_q4k: false,
6262            tokenizer: None,
6263            enforce_provenance: false,
6264        });
6265        let result = execute_command(&cli);
6266        assert!(
6267            result.is_err(),
6268            "Import should fail with non-existent source file"
6269        );
6270    }
6271
6272    // =========================================================================
6273    // --chat flag logic: effective_prompt with ChatML wrapping
6274    // =========================================================================
6275
6276    /// Test that --chat flag wraps prompt in ChatML format (verified via parse)
6277    #[test]
6278    fn test_chat_flag_chatml_wrapping_logic() {
6279        // We cannot call execute_command with --chat on a non-existent model
6280        // without error, but we can verify the ChatML wrapping logic directly.
6281        let prompt = "What is the meaning of life?";
6282        let chat = true;
6283
6284        let effective_prompt = if chat {
6285            Some(format!(
6286                "<|im_start|>user\n{}<|im_end|>\n<|im_start|>assistant\n",
6287                prompt
6288            ))
6289        } else {
6290            Some(prompt.to_string())
6291        };
6292
6293        assert!(effective_prompt
6294            .as_ref()
6295            .expect("prompt should exist")
6296            .starts_with("<|im_start|>user\n"));
6297        assert!(effective_prompt
6298            .as_ref()
6299            .expect("prompt should exist")
6300            .ends_with("<|im_start|>assistant\n"));
6301        assert!(effective_prompt
6302            .as_ref()
6303            .expect("prompt should exist")
6304            .contains("What is the meaning of life?"));
6305    }
6306
6307    /// Test that without --chat, prompt is passed through unchanged
6308    #[test]
6309    fn test_no_chat_flag_passthrough() {
6310        let prompt = Some("Hello world".to_string());
6311        let chat = false;
6312
6313        let effective_prompt = if chat {
6314            prompt
6315                .as_ref()
6316                .map(|p| format!("<|im_start|>user\n{}<|im_end|>\n<|im_start|>assistant\n", p))
6317        } else {
6318            prompt.clone()
6319        };
6320
6321        assert_eq!(effective_prompt, Some("Hello world".to_string()));
6322    }
6323
6324    /// Test that --chat with no prompt produces None
6325    #[test]
6326    fn test_chat_flag_no_prompt() {
6327        let prompt: Option<String> = None;
6328        let chat = true;
6329
6330        let effective_prompt = if chat {
6331            prompt
6332                .as_ref()
6333                .map(|p| format!("<|im_start|>user\n{}<|im_end|>\n<|im_start|>assistant\n", p))
6334        } else {
6335            prompt.clone()
6336        };
6337
6338        assert!(effective_prompt.is_none());
6339    }
6340
6341    // =========================================================================
6342    // --trace-payload shorthand logic
6343    // =========================================================================
6344
6345    /// Test trace-payload shorthand enables trace and sets level to payload
6346    #[test]
6347    fn test_trace_payload_shorthand_logic() {
6348        let trace = false;
6349        let trace_payload = true;
6350        let trace_level = "basic".to_string();
6351
6352        let effective_trace = trace || trace_payload;
6353        let effective_trace_level = if trace_payload {
6354            "payload"
6355        } else {
6356            trace_level.as_str()
6357        };
6358
6359        assert!(effective_trace);
6360        assert_eq!(effective_trace_level, "payload");
6361    }
6362
6363    /// Test that without --trace-payload, trace settings are preserved
6364    #[test]
6365    fn test_no_trace_payload_preserves_settings() {
6366        let trace = true;
6367        let trace_payload = false;
6368        let trace_level = "layer".to_string();
6369
6370        let effective_trace = trace || trace_payload;
6371        let effective_trace_level = if trace_payload {
6372            "payload"
6373        } else {
6374            trace_level.as_str()
6375        };
6376
6377        assert!(effective_trace);
6378        assert_eq!(effective_trace_level, "layer");
6379    }
6380
6381    /// Test that neither trace nor trace_payload results in no trace
6382    #[test]
6383    fn test_no_trace_no_trace_payload() {
6384        let trace = false;
6385        let trace_payload = false;
6386        let trace_level = "basic".to_string();
6387
6388        let effective_trace = trace || trace_payload;
6389        let effective_trace_level = if trace_payload {
6390            "payload"
6391        } else {
6392            trace_level.as_str()
6393        };
6394
6395        assert!(!effective_trace);
6396        assert_eq!(effective_trace_level, "basic");
6397    }
6398
6399    // =========================================================================
6400    // Verbose flag inheritance (local vs global)
6401    // =========================================================================
6402
6403    /// Test that local verbose flag overrides global false
6404    #[test]
6405    fn test_verbose_local_true_global_false() {
6406        let local_verbose = true;
6407        let global_verbose = false;
6408        let effective_verbose = local_verbose || global_verbose;
6409        assert!(effective_verbose);
6410    }
6411
6412    /// Test that global verbose flag takes effect when local is false
6413    #[test]
6414    fn test_verbose_local_false_global_true() {
6415        let local_verbose = false;
6416        let global_verbose = true;
6417        let effective_verbose = local_verbose || global_verbose;
6418        assert!(effective_verbose);
6419    }
6420
6421    /// Test that both verbose false means not verbose
6422    #[test]
6423    fn test_verbose_both_false() {
6424        let local_verbose = false;
6425        let global_verbose = false;
6426        let effective_verbose = local_verbose || global_verbose;
6427        assert!(!effective_verbose);
6428    }
6429
6430    /// Test that both verbose true means verbose
6431    #[test]
6432    fn test_verbose_both_true() {
6433        let local_verbose = true;
6434        let global_verbose = true;
6435        let effective_verbose = local_verbose || global_verbose;
6436        assert!(effective_verbose);
6437    }
6438
6439    /// Test verbose inheritance end-to-end via global flag and Run command.
6440    /// Note: clap with `global = true` and matching short flag `-v` means
6441    /// the global verbose flag propagates to both the Cli struct and the
6442    /// Run subcommand's local verbose field.
6443    #[test]
6444    fn test_verbose_inheritance_run_global() {
6445        let args = vec!["apr", "--verbose", "run", "model.gguf", "--prompt", "test"];
6446        let cli = parse_cli(args).expect("Failed to parse");
6447        assert!(cli.verbose);
6448        match *cli.command {
6449            Commands::Run { verbose, .. } => {
6450                // With global = true, clap propagates to both levels
6451                // effective_verbose = local || global = always true
6452                let effective = verbose || cli.verbose;
6453                assert!(effective);
6454            }
6455            _ => panic!("Expected Run command"),
6456        }
6457    }
6458
6459    /// Test verbose inheritance end-to-end with -v after the subcommand.
6460    /// Because clap uses `global = true` + `short = 'v'` on both Cli and
6461    /// Run, -v placed after the subcommand sets the global verbose field.
6462    #[test]
6463    fn test_verbose_inheritance_run_local() {
6464        let args = vec!["apr", "run", "model.gguf", "--prompt", "test", "-v"];
6465        let cli = parse_cli(args).expect("Failed to parse");
6466        // -v after the subcommand still sets the global flag due to global = true
6467        match *cli.command {
6468            Commands::Run { verbose, .. } => {
6469                let effective = verbose || cli.verbose;
6470                assert!(effective, "effective verbose should be true");
6471            }
6472            _ => panic!("Expected Run command"),
6473        }
6474    }
6475
6476    // =========================================================================
6477    // Edge case: conflicting flags (--gpu vs --no-gpu)
6478    // =========================================================================
6479
6480    /// Test that --gpu and --no-gpu conflict (Run command)
6481    #[test]
6482    fn test_parse_run_gpu_nogpu_conflict() {
6483        let args = vec![
6484            "apr",
6485            "run",
6486            "model.gguf",
6487            "--prompt",
6488            "test",
6489            "--gpu",
6490            "--no-gpu",
6491        ];
6492        let result = parse_cli(args);
6493        assert!(result.is_err(), "--gpu and --no-gpu should conflict in Run");
6494    }
6495
6496    /// Test parsing 'apr run' with --gpu flag alone
6497    #[test]
6498    fn test_parse_run_gpu_only() {
6499        let args = vec!["apr", "run", "model.gguf", "--prompt", "test", "--gpu"];
6500        let cli = parse_cli(args).expect("Failed to parse");
6501        match *cli.command {
6502            Commands::Run { gpu, no_gpu, .. } => {
6503                assert!(gpu);
6504                assert!(!no_gpu);
6505            }
6506            _ => panic!("Expected Run command"),
6507        }
6508    }
6509
6510    // =========================================================================
6511    // Missing required args error tests
6512    // =========================================================================
6513
6514    /// Test that 'apr serve' without FILE fails
6515    #[test]
6516    fn test_missing_serve_file() {
6517        let args = vec!["apr", "serve"];
6518        let result = parse_cli(args);
6519        assert!(result.is_err(), "serve requires FILE");
6520    }
6521
6522    /// Test that 'apr diff' with only one file fails
6523    #[test]
6524    fn test_missing_diff_second_file() {
6525        let args = vec!["apr", "diff", "model1.apr"];
6526        let result = parse_cli(args);
6527        assert!(result.is_err(), "diff requires two files");
6528    }
6529
6530    /// Test that 'apr export' without output fails
6531    #[test]
6532    fn test_missing_export_output() {
6533        let args = vec!["apr", "export", "model.apr"];
6534        let result = parse_cli(args);
6535        assert!(result.is_err(), "export requires -o/--output");
6536    }
6537
6538    /// Test that 'apr convert' without output fails
6539    #[test]
6540    fn test_missing_convert_output() {
6541        let args = vec!["apr", "convert", "model.apr"];
6542        let result = parse_cli(args);
6543        assert!(result.is_err(), "convert requires -o/--output");
6544    }
6545
6546    /// Test that 'apr merge' with fewer than 2 files fails
6547    #[test]
6548    fn test_missing_merge_files() {
6549        let args = vec!["apr", "merge", "model1.apr", "-o", "out.apr"];
6550        let result = parse_cli(args);
6551        assert!(result.is_err(), "merge requires at least 2 files");
6552    }
6553
6554    /// Test that 'apr publish' without repo_id fails
6555    #[test]
6556    fn test_missing_publish_repo_id() {
6557        let args = vec!["apr", "publish", "/tmp/models"];
6558        let result = parse_cli(args);
6559        assert!(result.is_err(), "publish requires REPO_ID");
6560    }
6561
6562    /// Test that 'apr pull' without model_ref fails
6563    #[test]
6564    fn test_missing_pull_model_ref() {
6565        let args = vec!["apr", "pull"];
6566        let result = parse_cli(args);
6567        assert!(result.is_err(), "pull requires MODEL");
6568    }
6569
6570    /// Test that 'apr rm' without model_ref fails
6571    #[test]
6572    fn test_missing_rm_model_ref() {
6573        let args = vec!["apr", "rm"];
6574        let result = parse_cli(args);
6575        assert!(result.is_err(), "rm requires MODEL");
6576    }
6577
6578    /// Test that 'apr compare-hf' without --hf fails
6579    #[test]
6580    fn test_missing_compare_hf_hf_arg() {
6581        let args = vec!["apr", "compare-hf", "model.apr"];
6582        let result = parse_cli(args);
6583        assert!(result.is_err(), "compare-hf requires --hf");
6584    }
6585
6586    /// Test that 'apr canary create' without --input fails
6587    #[test]
6588    fn test_missing_canary_create_input() {
6589        let args = vec![
6590            "apr",
6591            "canary",
6592            "create",
6593            "model.apr",
6594            "--output",
6595            "canary.json",
6596        ];
6597        let result = parse_cli(args);
6598        assert!(result.is_err(), "canary create requires --input");
6599    }
6600
6601    /// Test that 'apr canary check' without --canary fails
6602    #[test]
6603    fn test_missing_canary_check_canary() {
6604        let args = vec!["apr", "canary", "check", "model.apr"];
6605        let result = parse_cli(args);
6606        assert!(result.is_err(), "canary check requires --canary");
6607    }
6608
6609    // =========================================================================
6610    // execute_command: contract gate integration
6611    // =========================================================================
6612
6613    /// Test that execute_command with skip_contract=false and non-existent paths
6614    /// still works because non-existent paths are skipped in validate_model_contract
6615    #[test]
6616    fn test_execute_with_contract_gate_nonexistent() {
6617        let cli = Cli {
6618            command: Box::new(Commands::Inspect {
6619                file: PathBuf::from("/tmp/nonexistent_contract_test.apr"),
6620                vocab: false,
6621                filters: false,
6622                weights: false,
6623                json: false,
6624            }),
6625            json: false,
6626            verbose: false,
6627            quiet: false,
6628            offline: false,
6629            skip_contract: false, // Contract enabled, but paths don't exist
6630        };
6631        // The contract gate should pass (non-existent paths are skipped),
6632        // but the command itself should fail (file not found)
6633        let result = execute_command(&cli);
6634        assert!(
6635            result.is_err(),
6636            "Should still fail from command execution, not contract"
6637        );
6638    }
6639
6640    /// Test that execute_command dispatches List even with contract enabled
6641    #[test]
6642    fn test_execute_list_with_contract_enabled() {
6643        let cli = Cli {
6644            command: Box::new(Commands::List),
6645            json: false,
6646            verbose: false,
6647            quiet: false,
6648            offline: false,
6649            skip_contract: false, // Contract enabled
6650        };
6651        let result = execute_command(&cli);
6652        assert!(result.is_ok(), "List should succeed with contract enabled");
6653    }
6654
6655    // =========================================================================
6656    // Rosetta command execution error paths
6657    // =========================================================================
6658
6659    /// Test execute_command: Rosetta inspect with non-existent file returns error
6660    #[test]
6661    fn test_execute_rosetta_inspect_file_not_found() {
6662        let cli = make_cli(Commands::Rosetta {
6663            action: RosettaCommands::Inspect {
6664                file: PathBuf::from("/tmp/nonexistent_rosetta_inspect.gguf"),
6665                hexdump: false,
6666                json: false,
6667            },
6668        });
6669        let result = execute_command(&cli);
6670        assert!(
6671            result.is_err(),
6672            "Rosetta inspect should fail with non-existent file"
6673        );
6674    }
6675
6676    /// Test execute_command: Rosetta convert with non-existent source returns error
6677    #[test]
6678    fn test_execute_rosetta_convert_file_not_found() {
6679        let cli = make_cli(Commands::Rosetta {
6680            action: RosettaCommands::Convert {
6681                source: PathBuf::from("/tmp/nonexistent_rosetta_convert.gguf"),
6682                target: PathBuf::from("/tmp/out.safetensors"),
6683                quantize: None,
6684                verify: false,
6685                json: false,
6686                tokenizer: None,
6687            },
6688        });
6689        let result = execute_command(&cli);
6690        assert!(
6691            result.is_err(),
6692            "Rosetta convert should fail with non-existent source"
6693        );
6694    }
6695
6696    /// Test execute_command: Rosetta fingerprint with non-existent file returns error
6697    #[test]
6698    fn test_execute_rosetta_fingerprint_file_not_found() {
6699        let cli = make_cli(Commands::Rosetta {
6700            action: RosettaCommands::Fingerprint {
6701                model: PathBuf::from("/tmp/nonexistent_rosetta_fingerprint.gguf"),
6702                model_b: None,
6703                output: None,
6704                filter: None,
6705                verbose: false,
6706                json: false,
6707            },
6708        });
6709        let result = execute_command(&cli);
6710        assert!(
6711            result.is_err(),
6712            "Rosetta fingerprint should fail with non-existent file"
6713        );
6714    }
6715
6716    /// Test execute_command: Bench with non-existent file returns error
6717    #[test]
6718    fn test_execute_bench_file_not_found() {
6719        let cli = make_cli(Commands::Bench {
6720            file: PathBuf::from("/tmp/nonexistent_model_bench_test.gguf"),
6721            warmup: 1,
6722            iterations: 1,
6723            max_tokens: 1,
6724            prompt: None,
6725            fast: false,
6726            brick: None,
6727        });
6728        let result = execute_command(&cli);
6729        assert!(result.is_err(), "Bench should fail with non-existent file");
6730    }
6731
6732    /// Test execute_command: Eval with non-existent file returns error
6733    #[test]
6734    fn test_execute_eval_file_not_found() {
6735        let cli = make_cli(Commands::Eval {
6736            file: PathBuf::from("/tmp/nonexistent_model_eval_test.gguf"),
6737            dataset: "wikitext-2".to_string(),
6738            text: None,
6739            max_tokens: 32,
6740            threshold: 20.0,
6741        });
6742        let result = execute_command(&cli);
6743        assert!(result.is_err(), "Eval should fail with non-existent file");
6744    }
6745
6746    /// Test execute_command: Profile with non-existent file returns error
6747    #[test]
6748    fn test_execute_profile_file_not_found() {
6749        let cli = make_cli(Commands::Profile {
6750            file: PathBuf::from("/tmp/nonexistent_model_profile_test.apr"),
6751            granular: false,
6752            format: "human".to_string(),
6753            focus: None,
6754            detect_naive: false,
6755            threshold: 10.0,
6756            compare_hf: None,
6757            energy: false,
6758            perf_grade: false,
6759            callgraph: false,
6760            fail_on_naive: false,
6761            output: None,
6762            ci: false,
6763            assert_throughput: None,
6764            assert_p99: None,
6765            assert_p50: None,
6766            warmup: 3,
6767            measure: 10,
6768            tokens: 32,
6769            ollama: false,
6770            no_gpu: false,
6771            compare: None,
6772        });
6773        let result = execute_command(&cli);
6774        assert!(
6775            result.is_err(),
6776            "Profile should fail with non-existent file"
6777        );
6778    }
6779
6780    /// Test execute_command: CompareHf with non-existent file returns error
6781    #[test]
6782    fn test_execute_compare_hf_file_not_found() {
6783        let cli = make_cli(Commands::CompareHf {
6784            file: PathBuf::from("/tmp/nonexistent_model_compare_hf_test.apr"),
6785            hf: "openai/whisper-tiny".to_string(),
6786            tensor: None,
6787            threshold: 1e-5,
6788            json: false,
6789        });
6790        let result = execute_command(&cli);
6791        assert!(
6792            result.is_err(),
6793            "CompareHf should fail with non-existent file"
6794        );
6795    }
6796
6797    /// Test execute_command: Canary check with non-existent file returns error
6798    #[test]
6799    fn test_execute_canary_check_file_not_found() {
6800        let cli = make_cli(Commands::Canary {
6801            command: CanaryCommands::Check {
6802                file: PathBuf::from("/tmp/nonexistent_canary_check.apr"),
6803                canary: PathBuf::from("/tmp/nonexistent_canary.json"),
6804            },
6805        });
6806        let result = execute_command(&cli);
6807        assert!(
6808            result.is_err(),
6809            "Canary check should fail with non-existent file"
6810        );
6811    }
6812
6813    /// Test execute_command: Publish with non-existent directory returns error
6814    #[test]
6815    fn test_execute_publish_dir_not_found() {
6816        let cli = make_cli(Commands::Publish {
6817            directory: PathBuf::from("/tmp/nonexistent_publish_dir_test"),
6818            repo_id: "test/test".to_string(),
6819            model_name: None,
6820            license: "mit".to_string(),
6821            pipeline_tag: "text-generation".to_string(),
6822            library_name: None,
6823            tags: None,
6824            message: None,
6825            dry_run: true, // Use dry_run to avoid actual upload
6826        });
6827        let result = execute_command(&cli);
6828        assert!(
6829            result.is_err(),
6830            "Publish should fail with non-existent directory"
6831        );
6832    }
6833
6834    // =========================================================================
6835    // Default value verification tests
6836    // =========================================================================
6837
6838    /// Test Run command defaults
6839    #[test]
6840    fn test_parse_run_defaults() {
6841        let args = vec!["apr", "run", "model.gguf"];
6842        let cli = parse_cli(args).expect("Failed to parse");
6843        match *cli.command {
6844            Commands::Run {
6845                max_tokens,
6846                stream,
6847                format,
6848                no_gpu,
6849                gpu,
6850                offline,
6851                benchmark,
6852                trace,
6853                trace_payload,
6854                trace_verbose,
6855                trace_level,
6856                profile,
6857                chat,
6858                verbose,
6859                prompt,
6860                input,
6861                language,
6862                task,
6863                trace_steps,
6864                trace_output,
6865                ..
6866            } => {
6867                assert_eq!(max_tokens, 32);
6868                assert!(!stream);
6869                assert_eq!(format, "text");
6870                assert!(!no_gpu);
6871                assert!(!gpu);
6872                assert!(!offline);
6873                assert!(!benchmark);
6874                assert!(!trace);
6875                assert!(!trace_payload);
6876                assert!(!trace_verbose);
6877                assert_eq!(trace_level, "basic");
6878                assert!(!profile);
6879                assert!(!chat);
6880                assert!(!verbose);
6881                assert!(prompt.is_none());
6882                assert!(input.is_none());
6883                assert!(language.is_none());
6884                assert!(task.is_none());
6885                assert!(trace_steps.is_none());
6886                assert!(trace_output.is_none());
6887            }
6888            _ => panic!("Expected Run command"),
6889        }
6890    }
6891
6892    /// Test Serve command defaults
6893    #[test]
6894    fn test_parse_serve_defaults() {
6895        let args = vec!["apr", "serve", "model.apr"];
6896        let cli = parse_cli(args).expect("Failed to parse");
6897        match *cli.command {
6898            Commands::Serve {
6899                port,
6900                host,
6901                no_cors,
6902                no_metrics,
6903                no_gpu,
6904                gpu,
6905                batch,
6906                trace,
6907                trace_level,
6908                profile,
6909                ..
6910            } => {
6911                assert_eq!(port, 8080);
6912                assert_eq!(host, "127.0.0.1");
6913                assert!(!no_cors);
6914                assert!(!no_metrics);
6915                assert!(!no_gpu);
6916                assert!(!gpu);
6917                assert!(!batch);
6918                assert!(!trace);
6919                assert_eq!(trace_level, "basic");
6920                assert!(!profile);
6921            }
6922            _ => panic!("Expected Serve command"),
6923        }
6924    }
6925
6926    /// Test Bench command defaults
6927    #[test]
6928    fn test_parse_bench_defaults() {
6929        let args = vec!["apr", "bench", "model.gguf"];
6930        let cli = parse_cli(args).expect("Failed to parse");
6931        match *cli.command {
6932            Commands::Bench {
6933                warmup,
6934                iterations,
6935                max_tokens,
6936                prompt,
6937                fast,
6938                brick,
6939                ..
6940            } => {
6941                assert_eq!(warmup, 3);
6942                assert_eq!(iterations, 5);
6943                assert_eq!(max_tokens, 32);
6944                assert!(prompt.is_none());
6945                assert!(!fast);
6946                assert!(brick.is_none());
6947            }
6948            _ => panic!("Expected Bench command"),
6949        }
6950    }
6951
6952    /// Test Cbtop command defaults
6953    #[test]
6954    fn test_parse_cbtop_defaults() {
6955        let args = vec!["apr", "cbtop"];
6956        let cli = parse_cli(args).expect("Failed to parse");
6957        match *cli.command {
6958            Commands::Cbtop {
6959                model,
6960                attach,
6961                model_path,
6962                headless,
6963                json,
6964                output,
6965                ci,
6966                throughput,
6967                brick_score,
6968                warmup,
6969                iterations,
6970                speculative,
6971                speculation_k,
6972                draft_model,
6973                concurrent,
6974                simulated,
6975            } => {
6976                assert!(model.is_none());
6977                assert!(attach.is_none());
6978                assert!(model_path.is_none());
6979                assert!(!headless);
6980                assert!(!json);
6981                assert!(output.is_none());
6982                assert!(!ci);
6983                assert!(throughput.is_none());
6984                assert!(brick_score.is_none());
6985                assert_eq!(warmup, 10);
6986                assert_eq!(iterations, 100);
6987                assert!(!speculative);
6988                assert_eq!(speculation_k, 4);
6989                assert!(draft_model.is_none());
6990                assert_eq!(concurrent, 1);
6991                assert!(!simulated);
6992            }
6993            _ => panic!("Expected Cbtop command"),
6994        }
6995    }
6996
6997    /// Test Profile command defaults
6998    #[test]
6999    fn test_parse_profile_defaults() {
7000        let args = vec!["apr", "profile", "model.apr"];
7001        let cli = parse_cli(args).expect("Failed to parse");
7002        match *cli.command {
7003            Commands::Profile {
7004                granular,
7005                format,
7006                focus,
7007                detect_naive,
7008                threshold,
7009                compare_hf,
7010                energy,
7011                perf_grade,
7012                callgraph,
7013                fail_on_naive,
7014                output,
7015                ci,
7016                assert_throughput,
7017                assert_p99,
7018                assert_p50,
7019                warmup,
7020                measure,
7021                ..
7022            } => {
7023                assert!(!granular);
7024                assert_eq!(format, "human");
7025                assert!(focus.is_none());
7026                assert!(!detect_naive);
7027                assert!((threshold - 10.0).abs() < f64::EPSILON);
7028                assert!(compare_hf.is_none());
7029                assert!(!energy);
7030                assert!(!perf_grade);
7031                assert!(!callgraph);
7032                assert!(!fail_on_naive);
7033                assert!(output.is_none());
7034                assert!(!ci);
7035                assert!(assert_throughput.is_none());
7036                assert!(assert_p99.is_none());
7037                assert!(assert_p50.is_none());
7038                assert_eq!(warmup, 3);
7039                assert_eq!(measure, 10);
7040            }
7041            _ => panic!("Expected Profile command"),
7042        }
7043    }
7044
7045    /// Test Qa command defaults
7046    #[test]
7047    fn test_parse_qa_defaults() {
7048        let args = vec!["apr", "qa", "model.gguf"];
7049        let cli = parse_cli(args).expect("Failed to parse");
7050        match *cli.command {
7051            Commands::Qa {
7052                assert_tps,
7053                assert_speedup,
7054                assert_gpu_speedup,
7055                skip_golden,
7056                skip_throughput,
7057                skip_ollama,
7058                skip_gpu_speedup,
7059                skip_contract,
7060                skip_format_parity,
7061                safetensors_path,
7062                iterations,
7063                warmup,
7064                max_tokens,
7065                json,
7066                verbose,
7067                ..
7068            } => {
7069                assert!(assert_tps.is_none());
7070                assert!(assert_speedup.is_none());
7071                assert!(assert_gpu_speedup.is_none());
7072                assert!(!skip_golden);
7073                assert!(!skip_throughput);
7074                assert!(!skip_ollama);
7075                assert!(!skip_gpu_speedup);
7076                assert!(!skip_contract);
7077                assert!(!skip_format_parity);
7078                assert!(safetensors_path.is_none());
7079                assert_eq!(iterations, 10);
7080                assert_eq!(warmup, 3);
7081                assert_eq!(max_tokens, 32);
7082                assert!(!json);
7083                assert!(!verbose);
7084            }
7085            _ => panic!("Expected Qa command"),
7086        }
7087    }
7088
7089    /// Test Chat command defaults
7090    #[test]
7091    fn test_parse_chat_defaults() {
7092        let args = vec!["apr", "chat", "model.gguf"];
7093        let cli = parse_cli(args).expect("Failed to parse");
7094        match *cli.command {
7095            Commands::Chat {
7096                temperature,
7097                top_p,
7098                max_tokens,
7099                system,
7100                inspect,
7101                no_gpu,
7102                gpu,
7103                trace,
7104                trace_steps,
7105                trace_verbose,
7106                trace_output,
7107                trace_level,
7108                profile,
7109                ..
7110            } => {
7111                assert!((temperature - 0.7).abs() < f32::EPSILON);
7112                assert!((top_p - 0.9).abs() < f32::EPSILON);
7113                assert_eq!(max_tokens, 512);
7114                assert!(system.is_none());
7115                assert!(!inspect);
7116                assert!(!no_gpu);
7117                assert!(!gpu);
7118                assert!(!trace);
7119                assert!(trace_steps.is_none());
7120                assert!(!trace_verbose);
7121                assert!(trace_output.is_none());
7122                assert_eq!(trace_level, "basic");
7123                assert!(!profile);
7124            }
7125            _ => panic!("Expected Chat command"),
7126        }
7127    }
7128}