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