Skip to main content

apr_cli/
lib.rs

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