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