1use std::path::Path;
2
3use crate::core::compressor;
4use crate::core::deps as dep_extract;
5use crate::core::entropy;
6use crate::core::io_boundary;
7use crate::core::patterns::deps_cmd;
8use crate::core::protocol;
9use crate::core::roles;
10use crate::core::signatures;
11
12#[cfg(unix)]
16fn is_failed_daemon_result(output: &str) -> bool {
17 let first_line = output.lines().next().unwrap_or("");
18 if first_line.starts_with("Cannot read file:") || first_line.starts_with("File is empty:") {
19 return true;
20 }
21 if !first_line.ends_with(" 0L") {
22 return false;
23 }
24 let body: String = output
25 .lines()
26 .skip(1)
27 .filter(|l| {
28 let t = l.trim();
29 !t.is_empty()
30 && !t.starts_with("[no extractable structure")
31 && !t.starts_with("[lean-ctx]")
32 && !t.starts_with('[')
33 })
34 .collect();
35 body.is_empty()
36}
37
38fn resolve_cli_path(raw: &str) -> String {
39 if let Ok(abs) = std::path::Path::new(raw).canonicalize() {
40 return abs.to_string_lossy().to_string();
41 }
42 if Path::new(raw).is_relative()
43 && let Ok(cwd) = std::env::current_dir()
44 {
45 return cwd.join(raw).to_string_lossy().into_owned();
46 }
47 raw.to_string()
48}
49use crate::core::tokens::count_tokens;
50
51use super::common::print_savings;
52
53fn cap_cli_to_raw(framed: String, raw_content: &str, raw_tokens: usize) -> String {
60 if raw_tokens > 0 && count_tokens(&framed) > raw_tokens {
61 raw_content.to_string()
62 } else {
63 framed
64 }
65}
66
67fn should_force_fresh(args: &[String], hook_child: bool) -> bool {
72 hook_child || args.iter().any(|a| a == "--fresh" || a == "--no-cache")
73}
74
75fn resolve_cli_read_mode(args: &[String]) -> &str {
81 if let Some(m) = args
82 .iter()
83 .position(|a| a == "--mode" || a == "-m")
84 .and_then(|i| args.get(i + 1))
85 {
86 return m.as_str();
87 }
88 args.iter()
89 .skip(1)
90 .find(|a| !a.starts_with('-') && a.parse::<crate::tools::ctx_read::ReadMode>().is_ok())
91 .map_or("auto", std::string::String::as_str)
92}
93
94pub fn cmd_read(args: &[String]) {
95 if args.is_empty() {
96 eprintln!(
97 "Usage: lean-ctx read <file> [--mode auto|full|map|signatures|aggressive|entropy] [--fresh]"
98 );
99 std::process::exit(1);
100 }
101
102 let raw_path = &args[0];
103 let path = if Path::new(raw_path).is_relative() {
104 std::env::current_dir().ok().map_or_else(
105 || raw_path.clone(),
106 |cwd| cwd.join(raw_path).to_string_lossy().into_owned(),
107 )
108 } else {
109 raw_path.clone()
110 };
111 let path = path.as_str();
112 let mode = resolve_cli_read_mode(args);
113 let force_fresh = should_force_fresh(args, crate::core::runtime_flags::hook_child_enabled());
120 let requested_auto = mode == "auto";
123
124 let short = protocol::shorten_path(path);
125
126 if let Ok(abs) = std::fs::canonicalize(path) {
129 match io_boundary::check_secret_path_for_tool("cli_read", &abs) {
130 Ok(Some(w)) => eprintln!("{w}"),
131 Ok(None) => {}
132 Err(e) => {
133 eprintln!("{e}");
134 std::process::exit(1);
135 }
136 }
137 } else {
138 let raw = std::path::Path::new(path);
140 match io_boundary::check_secret_path_for_tool("cli_read", raw) {
141 Ok(Some(w)) => eprintln!("{w}"),
142 Ok(None) => {}
143 Err(e) => {
144 eprintln!("{e}");
145 std::process::exit(1);
146 }
147 }
148 }
149
150 #[cfg(unix)]
151 {
152 #[cfg(unix)]
153 if let Some(out) = crate::daemon_client::try_daemon_tool_call_blocking_text(
154 "ctx_read",
155 Some(serde_json::json!({
156 "path": path,
157 "mode": mode,
158 "fresh": force_fresh,
159 })),
160 ) {
161 let filtered = super::common::filter_daemon_output(&out);
162 if !filtered.trim().is_empty() && !is_failed_daemon_result(&filtered) {
163 println!("{filtered}");
164 return;
165 }
166 }
168 }
169 super::common::daemon_fallback_hint();
170
171 let read_start = std::time::Instant::now();
174
175 if !force_fresh && mode == "full" {
176 use crate::core::cli_cache::{self, CacheResult};
177 match cli_cache::check_and_read(path) {
178 CacheResult::Hit { entry, file_ref } => {
179 let msg = cli_cache::format_hit(&entry, &file_ref, &short);
180 println!("{msg}");
181 let sent = count_tokens(&msg);
182 super::common::cli_track_read_cached(
183 path,
184 "full",
185 entry.original_tokens,
186 sent,
187 &msg,
188 read_start.elapsed(),
189 );
190 return;
191 }
192 CacheResult::Miss { content } if content.is_empty() => {
193 eprintln!("Error: could not read {path}");
194 std::process::exit(1);
195 }
196 CacheResult::Miss { content } => {
197 let line_count = content.lines().count();
198 let raw_tokens = count_tokens(&content);
199 let framed = format!("{short} [{line_count}L]\n{content}");
200 let output = cap_cli_to_raw(framed, &content, raw_tokens);
201 println!("{output}");
202 let sent = count_tokens(&output);
203 super::common::cli_track_read(
204 path,
205 "full",
206 raw_tokens,
207 sent,
208 &output,
209 read_start.elapsed(),
210 );
211 return;
212 }
213 }
214 }
215
216 let content = match crate::tools::ctx_read::read_file_lossy(path) {
217 Ok(c) => c,
218 Err(e) => {
219 eprintln!("Error: {e}");
220 std::process::exit(1);
221 }
222 };
223
224 let ext = Path::new(path)
225 .extension()
226 .and_then(|e| e.to_str())
227 .unwrap_or("");
228 let line_count = content.lines().count();
229 let original_tokens = count_tokens(&content);
230
231 let mode = if mode == "auto" {
232 crate::core::auto_mode_resolver::resolve(
238 &crate::core::auto_mode_resolver::AutoModeContext {
239 path,
240 token_count: original_tokens,
241 line_count: None,
242 task: None,
243 cache: None,
244 },
245 )
246 .mode
247 } else if mode != "full" && crate::tools::ctx_read::is_instruction_file(path) {
248 "full".to_string()
249 } else {
250 mode.to_string()
251 };
252 let mode = if (mode == "map" || mode == "signatures") && original_tokens <= 400 {
253 "full".to_string()
254 } else {
255 mode
256 };
257 let mode = mode.as_str();
258
259 match mode {
260 "map" => {
261 let structured = match ext {
262 "md" | "mdx" | "rst" => {
263 crate::core::structured_read::extract_markdown_outline(&content)
264 }
265 "json" => crate::core::structured_read::extract_json_structure(&content),
266 "yaml" | "yml" => crate::core::structured_read::extract_yaml_structure(&content),
267 "toml" => crate::core::structured_read::extract_toml_structure(&content),
268 _ if path.to_lowercase().ends_with(".lock")
269 || path.to_lowercase().ends_with("go.sum") =>
270 {
271 crate::core::structured_read::extract_lock_summary(&content, path)
272 }
273 _ => String::new(),
274 };
275
276 let mut output_buf = if structured.is_empty() {
277 let sigs = signatures::extract_signatures(&content, ext);
278 let dep_info = dep_extract::extract_deps(&content, ext);
279 let mut buf = format!("{short} [{line_count}L]");
280 if !dep_info.imports.is_empty() {
281 buf.push_str(&format!("\n deps: {}", dep_info.imports.join(", ")));
282 }
283 let key_sigs: Vec<&signatures::Signature> = sigs
284 .iter()
285 .filter(|s| s.is_exported || s.indent == 0)
286 .collect();
287 let extra_exports =
291 signatures::exports_not_in_signatures(&dep_info.exports, &key_sigs);
292 if !extra_exports.is_empty() {
293 buf.push_str(&format!("\n exports: {}", extra_exports.join(", ")));
294 }
295 if !key_sigs.is_empty() {
296 buf.push_str("\n API:");
297 for sig in &key_sigs {
298 buf.push_str(&format!("\n {}", sig.to_compact_located()));
299 }
300 }
301 if key_sigs.is_empty() && dep_info.imports.is_empty() && extra_exports.is_empty() {
304 buf.push_str(&crate::tools::ctx_read::no_structure_marker(ext));
305 }
306 buf
307 } else {
308 format!("{short} [{line_count}L]\n{structured}")
309 };
310
311 let sent = count_tokens(&output_buf);
312 output_buf = protocol::append_savings(&output_buf, original_tokens, sent);
313 if requested_auto {
314 output_buf = cap_cli_to_raw(output_buf, &content, original_tokens);
315 }
316 let sent = count_tokens(&output_buf);
317 println!("{output_buf}");
318 super::common::cli_track_read(
319 path,
320 "map",
321 original_tokens,
322 sent,
323 &output_buf,
324 read_start.elapsed(),
325 );
326 }
327 "signatures" => {
328 let sigs = signatures::extract_signatures(&content, ext);
329 let mut output_buf = format!("{short} [{line_count}L]");
330 for sig in &sigs {
331 output_buf.push_str(&format!("\n{}", sig.to_compact_located()));
332 }
333 if sigs.is_empty() {
335 output_buf.push_str(&crate::tools::ctx_read::no_structure_marker(ext));
336 }
337 if requested_auto {
338 output_buf = cap_cli_to_raw(output_buf, &content, original_tokens);
339 }
340 println!("{output_buf}");
341 let sent = count_tokens(&output_buf);
342 print_savings(original_tokens, sent);
343 super::common::cli_track_read(
344 path,
345 "signatures",
346 original_tokens,
347 sent,
348 &output_buf,
349 read_start.elapsed(),
350 );
351 }
352 "aggressive" => {
353 let compressed = compressor::aggressive_compress(&content, Some(ext));
354 println!("{short} [{line_count}L]");
355 println!("{compressed}");
356 let sent = count_tokens(&compressed);
357 print_savings(original_tokens, sent);
358 super::common::cli_track_read(
359 path,
360 "aggressive",
361 original_tokens,
362 sent,
363 &compressed,
364 read_start.elapsed(),
365 );
366 }
367 "entropy" => {
368 let result = entropy::entropy_compress(&content);
369 let avg_h = entropy::analyze_entropy(&content).avg_entropy;
370 println!("{short} [{line_count}L] (H̄={avg_h:.1})");
371 for tech in &result.techniques {
372 println!("{tech}");
373 }
374 println!("{}", result.output);
375 let sent = count_tokens(&result.output);
376 print_savings(original_tokens, sent);
377 super::common::cli_track_read(
378 path,
379 "entropy",
380 original_tokens,
381 sent,
382 &result.output,
383 read_start.elapsed(),
384 );
385 }
386 m if m.starts_with("lines:") => {
387 let range_str = &m[6..];
391 let extracted = crate::tools::ctx_read::extract_line_range(&content, range_str);
392 let multi_hint = if range_str.contains(',') {
393 crate::tools::ctx_read::LINES_COMMA_HINT
394 } else {
395 ""
396 };
397 let output =
398 format!("{short} [{line_count}L] lines:{range_str}\n{extracted}{multi_hint}");
399 println!("{output}");
400 let sent = count_tokens(&output);
401 print_savings(original_tokens, sent);
402 super::common::cli_track_read(
403 path,
404 "lines",
405 original_tokens,
406 sent,
407 &output,
408 read_start.elapsed(),
409 );
410 }
411 "cognitive" | "mdl" => {
412 let (output, _) = crate::tools::ctx_read::process_mode(
413 &content,
414 mode,
415 "",
416 &short,
417 ext,
418 original_tokens,
419 crate::core::protocol::CrpMode::Off,
420 path,
421 None,
422 );
423 let output = if requested_auto {
424 cap_cli_to_raw(output, &content, original_tokens)
425 } else {
426 output
427 };
428 let sent = count_tokens(&output);
429 println!("{output}");
430 print_savings(original_tokens, sent);
431 super::common::cli_track_read(
432 path,
433 mode,
434 original_tokens,
435 sent,
436 &output,
437 read_start.elapsed(),
438 );
439 }
440 _ => {
441 let mut output = format!("{short} [{line_count}L]\n{content}");
448 if !crate::core::terse::is_verbatim_read("ctx_read", Some(mode)) {
449 let config = crate::core::config::Config::load();
450 let level = crate::core::config::CompressionLevel::effective(&config);
451 if level.is_active() {
452 let terse_result =
453 crate::core::terse::pipeline::compress(&output, &level, None);
454 if terse_result.quality_passed && terse_result.savings_pct >= 3.0 {
455 output = terse_result.output;
456 }
457 }
458 }
459 let output = cap_cli_to_raw(output, &content, original_tokens);
462 println!("{output}");
463 let sent = count_tokens(&output);
464 super::common::cli_track_read(
465 path,
466 "full",
467 original_tokens,
468 sent,
469 &output,
470 read_start.elapsed(),
471 );
472 }
473 }
474}
475
476pub fn cmd_diff(args: &[String]) {
477 if args.len() < 2 {
478 eprintln!("Usage: lean-ctx diff <file1> <file2>");
479 std::process::exit(1);
480 }
481
482 let content1 = match crate::tools::ctx_read::read_file_lossy(&args[0]) {
483 Ok(c) => c,
484 Err(e) => {
485 eprintln!("Error reading {}: {e}", args[0]);
486 std::process::exit(1);
487 }
488 };
489
490 let content2 = match crate::tools::ctx_read::read_file_lossy(&args[1]) {
491 Ok(c) => c,
492 Err(e) => {
493 eprintln!("Error reading {}: {e}", args[1]);
494 std::process::exit(1);
495 }
496 };
497
498 let diff = compressor::diff_content(&content1, &content2);
499 let original = count_tokens(&content1) + count_tokens(&content2);
500 let sent = count_tokens(&diff);
501
502 println!(
503 "diff {} {}",
504 protocol::shorten_path(&args[0]),
505 protocol::shorten_path(&args[1])
506 );
507 println!("{diff}");
508 print_savings(original, sent);
509 crate::core::stats::record("cli_diff", original, sent);
510}
511
512pub fn cmd_grep(args: &[String]) {
513 if args.is_empty() {
514 eprintln!("Usage: lean-ctx grep <pattern> [path]");
515 std::process::exit(1);
516 }
517
518 let pattern = &args[0];
519 let raw_path = args.get(1).map_or(".", std::string::String::as_str);
520 let abs_path = resolve_cli_path(raw_path);
521 let path = abs_path.as_str();
522
523 #[cfg(unix)]
524 {
525 #[cfg(unix)]
526 if let Some(out) = crate::daemon_client::try_daemon_tool_call_blocking_text(
527 "ctx_search",
528 Some(serde_json::json!({
529 "pattern": pattern,
530 "path": path,
531 })),
532 ) {
533 let out = super::common::filter_daemon_output(&out);
534 println!("{out}");
535 if out.trim_start().starts_with("0 matches") {
536 std::process::exit(1);
537 }
538 return;
539 }
540 }
541 super::common::daemon_fallback_hint();
542
543 let search_start = std::time::Instant::now();
545
546 let outcome = crate::tools::ctx_search::handle(
547 pattern,
548 path,
549 None,
550 20,
551 crate::tools::CrpMode::effective(),
552 true,
553 roles::active_role().io.allow_secret_paths,
554 false,
555 );
556 let out = outcome.text;
557 println!("{out}");
558 super::common::cli_track_search(
559 outcome.modeled_baseline,
560 outcome.observed_tokens,
561 count_tokens(&out),
562 pattern,
563 path,
564 &out,
565 search_start.elapsed(),
566 );
567 if outcome.modeled_baseline == 0 && out.trim_start().starts_with("0 matches") {
568 std::process::exit(1);
569 }
570}
571
572pub fn cmd_glob(args: &[String]) {
577 if args.is_empty() {
578 eprintln!("Usage: lean-ctx glob <pattern> [path]");
579 std::process::exit(1);
580 }
581
582 let pattern = &args[0];
583 let raw_path = args.get(1).map_or(".", std::string::String::as_str);
584 let abs_path = resolve_cli_path(raw_path);
585 let path = abs_path.as_str();
586
587 #[cfg(unix)]
588 if let Some(out) = crate::daemon_client::try_daemon_tool_call_blocking_text(
589 "ctx_glob",
590 Some(serde_json::json!({
591 "pattern": pattern,
592 "path": path,
593 })),
594 ) {
595 let out = super::common::filter_daemon_output(&out);
596 println!("{out}");
597 return;
598 }
599 super::common::daemon_fallback_hint();
600
601 let (out, _original) = crate::tools::ctx_glob::handle(
602 pattern,
603 path,
604 true,
605 roles::active_role().io.allow_secret_paths,
606 200,
607 );
608 println!("{out}");
609 crate::core::stats::record("cli_glob", 0, 0);
610 if out.starts_with("ERROR:") {
611 std::process::exit(1);
612 }
613}
614
615pub fn cmd_find(args: &[String]) {
616 if args.is_empty() {
617 eprintln!("Usage: lean-ctx find <pattern> [path]");
618 std::process::exit(1);
619 }
620
621 let raw_pattern = &args[0];
622 let path = args.get(1).map_or(".", std::string::String::as_str);
623
624 let is_glob = raw_pattern.contains('*') || raw_pattern.contains('?');
625 let glob_matcher = if is_glob {
626 glob::Pattern::new(&raw_pattern.to_lowercase()).ok()
627 } else {
628 None
629 };
630 let substring = raw_pattern.to_lowercase();
631
632 let mut found = false;
633 let walk_root = crate::core::walk_filter::explicit_walk_root(std::path::Path::new(path));
634 for entry in ignore::WalkBuilder::new(&walk_root)
635 .hidden(true)
636 .git_ignore(true)
637 .git_global(true)
638 .git_exclude(true)
639 .require_git(false)
640 .max_depth(Some(10))
641 .filter_entry(crate::core::walk_filter::keep_entry)
642 .build()
643 .flatten()
644 {
645 let name = entry.file_name().to_string_lossy().to_lowercase();
646 let matches = if let Some(ref g) = glob_matcher {
647 g.matches(&name)
648 } else {
649 name.contains(&substring)
650 };
651 if matches {
652 println!("{}", entry.path().display());
653 found = true;
654 }
655 }
656
657 crate::core::stats::record("cli_find", 0, 0);
658
659 if !found {
660 std::process::exit(1);
661 }
662}
663
664pub fn cmd_ls(args: &[String]) {
665 let mut raw_path = ".";
666 let mut depth = 3usize;
667 let mut show_hidden = false;
668 let mut respect_gitignore = true;
669 let mut i = 0;
670
671 while i < args.len() {
672 let arg = &args[i];
673 if arg == "--depth" {
674 i += 1;
675 if let Some(d) = args.get(i).and_then(|s| s.parse::<usize>().ok()) {
676 depth = d.min(10);
677 }
678 } else if arg == "--all" || arg == "-a" {
679 show_hidden = true;
680 } else if arg == "--no-gitignore" {
681 respect_gitignore = false;
682 } else if arg.starts_with('-') {
683 eprintln!("Error: lean-ctx ls does not support flag '{arg}'.\n");
684 eprintln!(
685 "lean-ctx ls is a compressed directory tree viewer for AI context, not a drop-in ls replacement."
686 );
687 eprintln!(
688 "The shell hook (lean-ctx -t ls {arg} ...) passes flags to system ls transparently.\n"
689 );
690 eprintln!("Usage: lean-ctx ls [path] [--depth N] [--all] [--no-gitignore]");
691 std::process::exit(1);
692 } else {
693 raw_path = arg;
694 }
695 i += 1;
696 }
697
698 let abs_path = resolve_cli_path(raw_path);
699 let path = abs_path.as_str();
700
701 #[cfg(unix)]
702 {
703 #[cfg(unix)]
704 if let Some(out) = crate::daemon_client::try_daemon_tool_call_blocking_text(
705 "ctx_tree",
706 Some(serde_json::json!({
707 "path": path,
708 "depth": depth,
709 "show_hidden": show_hidden,
710 "respect_gitignore": respect_gitignore,
711 })),
712 ) {
713 println!("{}", super::common::filter_daemon_output(&out));
714 return;
715 }
716 }
717 super::common::daemon_fallback_hint();
718
719 let (out, original) =
720 crate::tools::ctx_tree::handle(path, depth, show_hidden, respect_gitignore);
721 println!("{out}");
722 super::common::cli_track_tree(original, count_tokens(&out));
723}
724
725pub fn cmd_deps(args: &[String]) {
726 let path = args.first().map_or(".", std::string::String::as_str);
727
728 if let Some(result) = deps_cmd::detect_and_compress(path) {
729 println!("{result}");
730 crate::core::stats::record("cli_deps", 0, 0);
731 } else {
732 eprintln!("No dependency file found in {path}");
733 std::process::exit(1);
734 }
735}
736
737#[cfg(all(test, unix))]
738mod empty_daemon_tests {
739 use super::is_failed_daemon_result;
740
741 #[test]
742 fn detects_tcc_empty_stub() {
743 assert!(is_failed_daemon_result(
744 "file.rs 0L
745"
746 ));
747 assert!(is_failed_daemon_result(
748 "F1=file.rs 0L
749[lean-ctx: 0 tok saved]"
750 ));
751 }
752
753 #[test]
754 fn accepts_real_content() {
755 assert!(!is_failed_daemon_result(
756 "file.rs 10L
757fn main() {}
758"
759 ));
760 }
761
762 #[test]
763 fn accepts_nonzero_line_count() {
764 assert!(!is_failed_daemon_result(
765 "file.rs 1L
766"
767 ));
768 }
769
770 #[test]
771 fn ignores_structure_markers_in_body() {
772 assert!(is_failed_daemon_result(
773 "file.rs 0L
774[no extractable structure for .rs]
775"
776 ));
777 }
778}
779
780#[cfg(test)]
781mod cap_tests {
782 use super::{cap_cli_to_raw, count_tokens};
783
784 #[test]
785 fn caps_to_raw_when_framing_inflates() {
786 let raw = "x = 1\n";
790 let raw_tokens = count_tokens(raw);
791 let framed = format!("some/very/long/path/header.rs [1L]\n{raw}\n[lean-ctx: 0 tok saved]");
792 assert!(count_tokens(&framed) > raw_tokens, "fixture must inflate");
793 assert_eq!(cap_cli_to_raw(framed, raw, raw_tokens), raw);
794 }
795
796 #[test]
797 fn keeps_framing_when_it_saves() {
798 let raw = "fn a() {}\n".repeat(300);
800 let raw_tokens = count_tokens(&raw);
801 let framed = "f.rs [300L]\nfn a() {} …".to_string();
802 assert!(count_tokens(&framed) < raw_tokens);
803 assert_eq!(cap_cli_to_raw(framed.clone(), &raw, raw_tokens), framed);
804 }
805
806 #[test]
807 fn keeps_framing_for_empty_file() {
808 let framed = "empty.rs [0L]\n".to_string();
810 assert_eq!(cap_cli_to_raw(framed.clone(), "", 0), framed);
811 }
812
813 #[test]
814 fn break_even_is_not_inflation() {
815 let raw = "alpha beta gamma delta";
817 let raw_tokens = count_tokens(raw);
818 let framed = raw.to_string();
819 assert_eq!(count_tokens(&framed), raw_tokens);
820 assert_eq!(cap_cli_to_raw(framed.clone(), raw, raw_tokens), framed);
821 }
822
823 #[test]
824 fn emitted_never_exceeds_raw_across_sizes() {
825 for n in [1usize, 5, 50, 500] {
828 let raw = "data line here\n".repeat(n);
829 let raw_tokens = count_tokens(&raw);
830 let framed = format!("a/b/c/path.txt [{n}L]\n{raw}\n[lean-ctx: {n} tok saved ({n}%)]");
831 let out = cap_cli_to_raw(framed, &raw, raw_tokens);
832 assert!(
833 count_tokens(&out) <= raw_tokens,
834 "n={n}: emitted {} tok exceeds raw {raw_tokens}",
835 count_tokens(&out)
836 );
837 }
838 }
839}
840
841#[cfg(test)]
842mod fresh_tests {
843 use super::should_force_fresh;
844
845 #[test]
846 fn hook_child_forces_fresh_even_without_flags() {
847 assert!(should_force_fresh(&[], true));
850 assert!(!should_force_fresh(&[], false));
851 }
852
853 #[test]
854 fn explicit_flags_force_fresh() {
855 assert!(should_force_fresh(&["--fresh".to_string()], false));
856 assert!(should_force_fresh(&["--no-cache".to_string()], false));
857 assert!(!should_force_fresh(&["file.rs".to_string()], false));
858 }
859}
860
861#[cfg(test)]
862mod mode_arg_tests {
863 use super::resolve_cli_read_mode;
864
865 fn args(list: &[&str]) -> Vec<String> {
866 list.iter().map(ToString::to_string).collect()
867 }
868
869 #[test]
873 fn positional_mode_is_honoured() {
874 assert_eq!(resolve_cli_read_mode(&args(&["f.ps1", "map"])), "map");
875 assert_eq!(
876 resolve_cli_read_mode(&args(&["f.rs", "lines:5-10"])),
877 "lines:5-10"
878 );
879 }
880
881 #[test]
882 fn mode_flag_wins_over_positional() {
883 assert_eq!(
884 resolve_cli_read_mode(&args(&["f.rs", "map", "--mode", "signatures"])),
885 "signatures"
886 );
887 assert_eq!(
888 resolve_cli_read_mode(&args(&["f.rs", "-m", "full"])),
889 "full"
890 );
891 }
892
893 #[test]
894 fn defaults_to_auto_and_skips_flags_and_junk() {
895 assert_eq!(resolve_cli_read_mode(&args(&["f.rs"])), "auto");
896 assert_eq!(resolve_cli_read_mode(&args(&["f.rs", "--fresh"])), "auto");
897 assert_eq!(resolve_cli_read_mode(&args(&["f.rs", "banana"])), "auto");
899 }
900}