1use crate::core::context_field::{ContextItemId, ContextState};
2use crate::core::context_ledger::{ContextLedger, PressureAction};
3use crate::core::context_overlay::{OverlayOp, OverlayStore};
4
5#[derive(Debug, Clone)]
6pub struct PreDispatchResult {
7 pub overridden_mode: Option<String>,
8 pub reason: Option<&'static str>,
9 pub pressure_downgraded: bool,
10 pub budget_blocked: bool,
11 pub budget_warning: Option<String>,
12}
13
14#[derive(Debug, Clone)]
15pub struct PostDispatchResult {
16 pub eviction_hint: Option<String>,
17 pub elicitation_hint: Option<String>,
18 pub resource_changed: bool,
19}
20
21pub fn pre_dispatch_read(
22 path: &str,
23 requested_mode: &str,
24 task: Option<&str>,
25 project_root: Option<&str>,
26 pressure: Option<&PressureAction>,
27) -> PreDispatchResult {
28 pre_dispatch_read_for_agent(path, requested_mode, task, project_root, pressure, None)
29}
30
31pub fn pre_dispatch_read_for_agent(
32 path: &str,
33 requested_mode: &str,
34 task: Option<&str>,
35 project_root: Option<&str>,
36 pressure: Option<&PressureAction>,
37 agent_id: Option<&str>,
38) -> PreDispatchResult {
39 let no_change = PreDispatchResult {
40 overridden_mode: None,
41 reason: None,
42 pressure_downgraded: false,
43 budget_blocked: false,
44 budget_warning: None,
45 };
46
47 if let Some(aid) = agent_id {
48 let estimated_tokens = estimate_read_tokens(path, requested_mode);
49 match crate::core::agent_budget::check_budget(aid, estimated_tokens) {
50 crate::core::agent_budget::BudgetCheckResult::Exceeded { limit, consumed } => {
51 return PreDispatchResult {
52 overridden_mode: None,
53 reason: Some("agent-budget-exceeded"),
54 pressure_downgraded: false,
55 budget_blocked: true,
56 budget_warning: Some(format!(
57 "Agent budget exceeded: {consumed}/{limit} tokens consumed. Reset via ctx_session or set a higher limit."
58 )),
59 };
60 }
61 crate::core::agent_budget::BudgetCheckResult::Warning {
62 remaining,
63 percent_used,
64 } => {
65 let warning = format!(
66 "[BUDGET WARNING] Agent '{aid}' at {:.0}% budget ({remaining} tokens remaining)",
67 percent_used * 100.0
68 );
69 let mut result = no_change.clone();
70 result.budget_warning = Some(warning);
71 if requested_mode == "diff" || requested_mode.starts_with("lines") {
72 return result;
73 }
74 let rest = pre_dispatch_inner(path, requested_mode, task, project_root, pressure);
75 return PreDispatchResult {
76 budget_warning: result.budget_warning,
77 ..rest
78 };
79 }
80 crate::core::agent_budget::BudgetCheckResult::Allowed { .. } => {}
81 }
82 }
83
84 pre_dispatch_inner(path, requested_mode, task, project_root, pressure)
85}
86
87fn pre_dispatch_inner(
88 path: &str,
89 requested_mode: &str,
90 task: Option<&str>,
91 project_root: Option<&str>,
92 pressure: Option<&PressureAction>,
93) -> PreDispatchResult {
94 let no_change = PreDispatchResult {
95 overridden_mode: None,
96 reason: None,
97 pressure_downgraded: false,
98 budget_blocked: false,
99 budget_warning: None,
100 };
101
102 if requested_mode == "diff" || requested_mode.starts_with("lines") {
103 return no_change;
104 }
105
106 if let Some(root) = project_root {
107 let overlay = OverlayStore::load_project(&std::path::PathBuf::from(root));
108 if let Some(result) = check_overlay_mode_override(path, requested_mode, &overlay) {
109 return result;
110 }
111 }
112
113 if let Some(action) = pressure {
114 let no_degrade = crate::core::config::Config::load().no_degrade_effective();
115 if !no_degrade {
116 if let Some(downgraded) = pressure_downgrade(requested_mode, action) {
117 return PreDispatchResult {
118 overridden_mode: Some(downgraded),
119 reason: Some("pressure-auto-downgrade"),
120 pressure_downgraded: true,
121 budget_blocked: false,
122 budget_warning: None,
123 };
124 }
125 }
126 }
127
128 if requested_mode == "full" {
129 return no_change;
130 }
131
132 if let Ok(bt) = crate::core::bounce_tracker::global().lock() {
133 if bt.should_force_full(path) {
134 return PreDispatchResult {
135 overridden_mode: Some("full".to_string()),
136 reason: Some("bounce-prevention"),
137 pressure_downgraded: false,
138 budget_blocked: false,
139 budget_warning: None,
140 };
141 }
142 }
143
144 if let Some(task_str) = task {
145 let intent = crate::core::intent_engine::StructuredIntent::from_query(task_str);
146 let norm = crate::core::pathutil::normalize_tool_path(path);
147 let is_target = intent
148 .targets
149 .iter()
150 .any(|t| norm.ends_with(t) || norm.contains(t));
151 if is_target {
152 return PreDispatchResult {
153 overridden_mode: Some("full".to_string()),
154 reason: Some("intent-target"),
155 pressure_downgraded: false,
156 budget_blocked: false,
157 budget_warning: None,
158 };
159 }
160 }
161
162 if let Some(root) = project_root {
163 if let Some(index) = try_load_graph(root) {
164 let related = index.get_related(path, 1);
165 if let Some(task_str) = task {
166 let intent = crate::core::intent_engine::StructuredIntent::from_query(task_str);
167 for target in &intent.targets {
168 let target_related = index.get_related(target, 1);
169 let norm = crate::core::pathutil::normalize_tool_path(path);
170 if target_related
171 .iter()
172 .any(|r| r.contains(&norm) || norm.contains(r))
173 {
174 return PreDispatchResult {
175 overridden_mode: Some("map".to_string()),
176 reason: Some("graph-direct-import"),
177 pressure_downgraded: false,
178 budget_blocked: false,
179 budget_warning: None,
180 };
181 }
182 }
183 }
184 if !related.is_empty() && requested_mode == "auto" {
185 let reverse_deps = index.get_reverse_deps(path, 1);
186 if reverse_deps.len() > 3 {
187 return PreDispatchResult {
188 overridden_mode: Some("map".to_string()),
189 reason: Some("graph-hub-file"),
190 pressure_downgraded: false,
191 budget_blocked: false,
192 budget_warning: None,
193 };
194 }
195 }
196 }
197 }
198
199 if let Some(root) = project_root {
200 if let Some(knowledge) = crate::core::knowledge::ProjectKnowledge::load(root) {
201 let norm = crate::core::pathutil::normalize_tool_path(path);
202 let mentions = knowledge
203 .facts
204 .iter()
205 .filter(|f| f.value.contains(&norm) || f.key.contains(&norm))
206 .count();
207 if mentions >= 3 {
208 return PreDispatchResult {
209 overridden_mode: Some("map".to_string()),
210 reason: Some("knowledge-high-relevance"),
211 pressure_downgraded: false,
212 budget_blocked: false,
213 budget_warning: None,
214 };
215 }
216 }
217 }
218
219 no_change
220}
221
222fn estimate_read_tokens(path: &str, mode: &str) -> usize {
223 let file_size = std::fs::metadata(path).map_or(4000, |m| m.len() as usize);
224 let char_estimate = file_size;
225 let full_tokens = char_estimate / 4;
226 match mode {
227 "signatures" => full_tokens / 5,
228 "map" => full_tokens / 3,
229 "aggressive" | "entropy" => full_tokens / 4,
230 "diff" => full_tokens / 10,
231 _ if mode.starts_with("lines:") => {
232 if let Some(range) = mode.strip_prefix("lines:") {
233 let parts: Vec<&str> = range.split('-').collect();
234 if parts.len() == 2 {
235 let start = parts[0].parse::<usize>().unwrap_or(1);
236 let end = parts[1].parse::<usize>().unwrap_or(start + 100);
237 (end.saturating_sub(start) + 1) * 10
238 } else {
239 full_tokens / 10
240 }
241 } else {
242 full_tokens / 10
243 }
244 }
245 _ => full_tokens,
246 }
247}
248
249fn pressure_downgrade(requested_mode: &str, action: &PressureAction) -> Option<String> {
250 match action {
251 PressureAction::SuggestCompression => match requested_mode {
252 "auto" => Some("map".to_string()),
253 _ => None,
254 },
255 PressureAction::ForceCompression => match requested_mode {
256 "full" => Some("map".to_string()),
257 "auto" | "map" => Some("signatures".to_string()),
258 _ => None,
259 },
260 PressureAction::EvictLeastRelevant => match requested_mode {
261 "full" => Some("map".to_string()),
262 "auto" | "map" => Some("signatures".to_string()),
263 _ => None,
264 },
265 PressureAction::NoAction => None,
266 }
267}
268
269fn check_overlay_mode_override(
270 path: &str,
271 requested_mode: &str,
272 overlay: &OverlayStore,
273) -> Option<PreDispatchResult> {
274 let item_id = ContextItemId::from_file(path);
275 let overlays = overlay.for_item(&item_id);
276
277 for ov in overlays.iter().rev() {
278 match &ov.operation {
279 OverlayOp::SetView(view) => {
280 let mode_str = view.as_str();
281 if mode_str != requested_mode {
282 return Some(PreDispatchResult {
283 overridden_mode: Some(mode_str.to_string()),
284 reason: Some("overlay-set-view"),
285 pressure_downgraded: false,
286 budget_blocked: false,
287 budget_warning: None,
288 });
289 }
290 }
291 OverlayOp::Pin { .. } if requested_mode != "full" => {
292 return Some(PreDispatchResult {
293 overridden_mode: Some("full".to_string()),
294 reason: Some("pinned"),
295 pressure_downgraded: false,
296 budget_blocked: false,
297 budget_warning: None,
298 });
299 }
300 OverlayOp::Exclude { .. } if requested_mode != "signatures" => {
301 return Some(PreDispatchResult {
302 overridden_mode: Some("signatures".to_string()),
303 reason: Some("excluded"),
304 pressure_downgraded: false,
305 budget_blocked: false,
306 budget_warning: None,
307 });
308 }
309 _ => {}
310 }
311 }
312 None
313}
314
315pub fn post_dispatch_record(
316 path: &str,
317 mode: &str,
318 original_tokens: usize,
319 sent_tokens: usize,
320 ledger: &mut ContextLedger,
321 overlay: &OverlayStore,
322) -> PostDispatchResult {
323 post_dispatch_record_with_task(
324 path,
325 mode,
326 original_tokens,
327 sent_tokens,
328 ledger,
329 overlay,
330 None,
331 )
332}
333
334pub fn post_dispatch_record_with_task(
335 path: &str,
336 mode: &str,
337 original_tokens: usize,
338 sent_tokens: usize,
339 ledger: &mut ContextLedger,
340 overlay: &OverlayStore,
341 task: Option<&str>,
342) -> PostDispatchResult {
343 let prev_count = ledger.entries.len();
344 let prev_pressure = ledger.pressure().recommendation;
345
346 ledger.record_with_task(path, mode, original_tokens, sent_tokens, task);
347
348 let item_id = ContextItemId::from_file(path);
349 let state = overlay.apply_to_state(&item_id, ContextState::Included);
350
351 if state == ContextState::Excluded {
352 return PostDispatchResult {
353 eviction_hint: Some(format!("File '{path}' is excluded by overlay.")),
354 elicitation_hint: None,
355 resource_changed: true,
356 };
357 }
358
359 let elicitation =
360 super::elicitation::check_elicitation_needed(ledger, Some(path), Some(sent_tokens))
361 .map(|s| s.format_fallback_hint());
362
363 let pressure = ledger.pressure();
364
365 apply_reinjection_plan(ledger, &pressure.recommendation);
366
367 let new_entry = ledger.entries.len() != prev_count;
368 let pressure_shifted = pressure.recommendation != prev_pressure;
369 let resource_changed = new_entry || pressure_shifted;
370
371 if pressure.utilization > 0.9 {
372 let candidates = ledger.eviction_candidates_by_phi(3);
373 if !candidates.is_empty() {
374 let names: Vec<_> = candidates
375 .iter()
376 .take(3)
377 .map(|p| crate::core::protocol::shorten_path(p))
378 .collect();
379 return PostDispatchResult {
380 eviction_hint: Some(format!(
381 "Context pressure {:.0}%. Evict: ctx_ledger(action=\"evict\", targets=\"{}\")",
382 pressure.utilization * 100.0,
383 names.join(", ")
384 )),
385 elicitation_hint: elicitation,
386 resource_changed,
387 };
388 }
389 }
390
391 PostDispatchResult {
392 eviction_hint: None,
393 elicitation_hint: elicitation,
394 resource_changed,
395 }
396}
397
398fn apply_reinjection_plan(ledger: &mut ContextLedger, action: &PressureAction) {
399 if *action != PressureAction::ForceCompression && *action != PressureAction::EvictLeastRelevant
400 {
401 return;
402 }
403 for entry in &mut ledger.entries {
404 if entry.mode == "full" {
405 entry.mode = "map".to_string();
406 }
407 }
408}
409
410fn try_load_graph(project_root: &str) -> Option<crate::core::graph_index::ProjectIndex> {
411 crate::core::graph_index::ProjectIndex::load(project_root)
412}
413
414#[cfg(test)]
415mod tests {
416 use super::*;
417
418 #[test]
419 fn pre_dispatch_passthrough_for_full() {
420 let result = pre_dispatch_read("src/main.rs", "full", None, None, None);
421 assert!(result.overridden_mode.is_none());
422 }
423
424 #[test]
425 fn pre_dispatch_passthrough_for_diff() {
426 let result = pre_dispatch_read("src/main.rs", "diff", None, None, None);
427 assert!(result.overridden_mode.is_none());
428 }
429
430 #[test]
431 fn pre_dispatch_no_override_without_signals() {
432 let result = pre_dispatch_read("src/unknown.rs", "auto", None, None, None);
433 assert!(result.overridden_mode.is_none());
434 }
435
436 #[test]
437 fn pre_dispatch_bounce_prevention_forces_full() {
438 {
439 let mut bt = crate::core::bounce_tracker::global().lock().unwrap();
440 bt.set_seq(1);
441 bt.record_read("src/bouncy.yml", "map", 30, 400);
442 bt.set_seq(2);
443 bt.record_read("src/bouncy.yml", "full", 400, 400);
444 bt.set_seq(3);
445 bt.record_read("a2.yml", "map", 30, 400);
446 bt.set_seq(4);
447 bt.record_read("a2.yml", "full", 400, 400);
448 bt.set_seq(5);
449 bt.record_read("a3.yml", "map", 30, 400);
450 bt.set_seq(6);
451 bt.record_read("a3.yml", "full", 400, 400);
452 }
453 let result = pre_dispatch_read("new.yml", "auto", None, None, None);
454 assert_eq!(result.overridden_mode, Some("full".to_string()));
455 assert_eq!(result.reason, Some("bounce-prevention"));
456 }
457
458 #[test]
459 fn pressure_downgrade_full_to_map() {
460 let result = pre_dispatch_read(
461 "c.rs",
462 "full",
463 None,
464 None,
465 Some(&PressureAction::ForceCompression),
466 );
467 assert_eq!(result.overridden_mode, Some("map".to_string()));
468 assert_eq!(result.reason, Some("pressure-auto-downgrade"));
469 assert!(result.pressure_downgraded);
470 }
471
472 #[test]
473 fn pressure_downgrade_map_to_signatures_on_evict() {
474 let result = pre_dispatch_read(
475 "c.rs",
476 "map",
477 None,
478 None,
479 Some(&PressureAction::EvictLeastRelevant),
480 );
481 assert_eq!(result.overridden_mode, Some("signatures".to_string()));
482 assert!(result.pressure_downgraded);
483 }
484
485 #[test]
486 fn no_pressure_downgrade_when_low() {
487 let result = pre_dispatch_read("c.rs", "full", None, None, Some(&PressureAction::NoAction));
488 assert!(result.overridden_mode.is_none());
489 assert!(!result.pressure_downgraded);
490 }
491
492 #[test]
493 fn suggest_compression_downgrades_auto_to_map() {
494 let result = pre_dispatch_read(
495 "c.rs",
496 "auto",
497 None,
498 None,
499 Some(&PressureAction::SuggestCompression),
500 );
501 assert_eq!(result.overridden_mode, Some("map".to_string()));
502 assert!(result.pressure_downgraded);
503 }
504
505 #[test]
506 fn suggest_compression_does_not_touch_explicit_full() {
507 let result = pre_dispatch_read(
508 "c.rs",
509 "full",
510 None,
511 None,
512 Some(&PressureAction::SuggestCompression),
513 );
514 assert!(result.overridden_mode.is_none());
515 assert!(!result.pressure_downgraded);
516 }
517
518 #[test]
519 fn post_dispatch_reinjection_downgrades_entries() {
520 let mut ledger = ContextLedger::with_window_size(1000);
521 ledger.record("a.rs", "full", 400, 400);
522 ledger.record("b.rs", "full", 400, 400);
523 let overlay = OverlayStore::new();
524 let result = post_dispatch_record("c.rs", "full", 300, 300, &mut ledger, &overlay);
525 assert!(result.resource_changed);
526 let a_entry = ledger.entries.iter().find(|e| e.path == "a.rs").unwrap();
527 assert_eq!(a_entry.mode, "map");
528 }
529
530 #[test]
531 fn overlay_pin_forces_full_mode() {
532 let dir = tempfile::tempdir().expect("tmp dir");
533 let root = dir.path();
534 let mut store = OverlayStore::new();
535 let target = ContextItemId::from_file("src/important.rs");
536 store.add(crate::core::context_overlay::ContextOverlay::new(
537 target,
538 OverlayOp::Pin { verbatim: false },
539 crate::core::context_overlay::OverlayScope::Project,
540 String::new(),
541 crate::core::context_overlay::OverlayAuthor::User,
542 ));
543 store.save_project(root).unwrap();
544
545 let result = pre_dispatch_read(
546 "src/important.rs",
547 "auto",
548 None,
549 Some(root.to_str().unwrap()),
550 None,
551 );
552 assert_eq!(result.overridden_mode, Some("full".to_string()));
553 assert_eq!(result.reason, Some("pinned"));
554 }
555
556 #[test]
557 fn overlay_exclude_forces_signatures_mode() {
558 let dir = tempfile::tempdir().expect("tmp dir");
559 let root = dir.path();
560 let mut store = OverlayStore::new();
561 let target = ContextItemId::from_file("src/noisy.rs");
562 store.add(crate::core::context_overlay::ContextOverlay::new(
563 target,
564 OverlayOp::Exclude {
565 reason: "noise".to_string(),
566 },
567 crate::core::context_overlay::OverlayScope::Project,
568 String::new(),
569 crate::core::context_overlay::OverlayAuthor::User,
570 ));
571 store.save_project(root).unwrap();
572
573 let result = pre_dispatch_read(
574 "src/noisy.rs",
575 "auto",
576 None,
577 Some(root.to_str().unwrap()),
578 None,
579 );
580 assert_eq!(result.overridden_mode, Some("signatures".to_string()));
581 assert_eq!(result.reason, Some("excluded"));
582 }
583
584 #[test]
587 fn pressure_downgrade_suggest_auto_to_map() {
588 let result = pressure_downgrade("auto", &PressureAction::SuggestCompression);
589 assert_eq!(result, Some("map".to_string()));
590 }
591
592 #[test]
593 fn pressure_downgrade_suggest_does_not_touch_full() {
594 let result = pressure_downgrade("full", &PressureAction::SuggestCompression);
595 assert!(result.is_none());
596 }
597
598 #[test]
599 fn pressure_downgrade_suggest_does_not_touch_signatures() {
600 let result = pressure_downgrade("signatures", &PressureAction::SuggestCompression);
601 assert!(result.is_none());
602 }
603
604 #[test]
605 fn pressure_downgrade_suggest_does_not_touch_diff() {
606 let result = pressure_downgrade("diff", &PressureAction::SuggestCompression);
607 assert!(result.is_none());
608 }
609
610 #[test]
611 fn pressure_downgrade_force_full_to_map() {
612 let result = pressure_downgrade("full", &PressureAction::ForceCompression);
613 assert_eq!(result, Some("map".to_string()));
614 }
615
616 #[test]
617 fn pressure_downgrade_force_auto_to_signatures() {
618 let result = pressure_downgrade("auto", &PressureAction::ForceCompression);
619 assert_eq!(result, Some("signatures".to_string()));
620 }
621
622 #[test]
623 fn pressure_downgrade_force_map_to_signatures() {
624 let result = pressure_downgrade("map", &PressureAction::ForceCompression);
625 assert_eq!(result, Some("signatures".to_string()));
626 }
627
628 #[test]
629 fn pressure_downgrade_force_does_not_touch_signatures() {
630 let result = pressure_downgrade("signatures", &PressureAction::ForceCompression);
631 assert!(result.is_none());
632 }
633
634 #[test]
635 fn pressure_downgrade_force_does_not_touch_lines() {
636 let result = pressure_downgrade("lines:1-50", &PressureAction::ForceCompression);
637 assert!(result.is_none());
638 }
639
640 #[test]
641 fn pressure_downgrade_evict_full_to_map() {
642 let result = pressure_downgrade("full", &PressureAction::EvictLeastRelevant);
643 assert_eq!(result, Some("map".to_string()));
644 }
645
646 #[test]
647 fn pressure_downgrade_evict_auto_to_signatures() {
648 let result = pressure_downgrade("auto", &PressureAction::EvictLeastRelevant);
649 assert_eq!(result, Some("signatures".to_string()));
650 }
651
652 #[test]
653 fn pressure_downgrade_evict_map_to_signatures() {
654 let result = pressure_downgrade("map", &PressureAction::EvictLeastRelevant);
655 assert_eq!(result, Some("signatures".to_string()));
656 }
657
658 #[test]
659 fn pressure_downgrade_noaction_returns_none() {
660 let result = pressure_downgrade("full", &PressureAction::NoAction);
661 assert!(result.is_none());
662 }
663
664 #[test]
665 fn pressure_downgrade_noaction_auto_returns_none() {
666 let result = pressure_downgrade("auto", &PressureAction::NoAction);
667 assert!(result.is_none());
668 }
669
670 #[test]
674 fn pre_dispatch_downgrades_under_force_when_degrade_enabled() {
675 if std::env::var("LCTX_NO_DEGRADE").is_ok() {
676 return;
677 }
678 let result = pre_dispatch_read(
679 "nd_test.rs",
680 "full",
681 None,
682 None,
683 Some(&PressureAction::ForceCompression),
684 );
685 assert_eq!(result.overridden_mode, Some("map".to_string()));
686 assert!(result.pressure_downgraded);
687 }
688
689 #[test]
690 fn pre_dispatch_downgrades_auto_under_evict_when_degrade_enabled() {
691 if std::env::var("LCTX_NO_DEGRADE").is_ok() {
692 return;
693 }
694 let result = pre_dispatch_read(
695 "nd_test2.rs",
696 "auto",
697 None,
698 None,
699 Some(&PressureAction::EvictLeastRelevant),
700 );
701 assert_eq!(result.overridden_mode, Some("signatures".to_string()));
702 assert!(result.pressure_downgraded);
703 }
704
705 #[test]
708 fn estimate_tokens_diff_mode_is_small() {
709 let tokens = estimate_read_tokens("nonexistent.rs", "diff");
710 assert!(tokens < 500, "diff mode should estimate low: got {tokens}");
711 }
712
713 #[test]
714 fn estimate_tokens_signatures_smaller_than_full() {
715 let sig = estimate_read_tokens("nonexistent.rs", "signatures");
716 let full = estimate_read_tokens("nonexistent.rs", "full");
717 assert!(sig < full, "signatures={sig} should be < full={full}");
718 }
719
720 #[test]
721 fn estimate_tokens_lines_range() {
722 let tokens = estimate_read_tokens("nonexistent.rs", "lines:1-10");
723 assert!(tokens <= 200, "lines:1-10 should be small: got {tokens}");
724 }
725
726 #[test]
727 fn overlay_set_view_forces_specified_mode() {
728 let dir = tempfile::tempdir().expect("tmp dir");
729 let root = dir.path();
730 let mut store = OverlayStore::new();
731 let target = ContextItemId::from_file("src/big.rs");
732 store.add(crate::core::context_overlay::ContextOverlay::new(
733 target,
734 OverlayOp::SetView(crate::core::context_field::ViewKind::Map),
735 crate::core::context_overlay::OverlayScope::Project,
736 String::new(),
737 crate::core::context_overlay::OverlayAuthor::User,
738 ));
739 store.save_project(root).unwrap();
740
741 let result = pre_dispatch_read(
742 "src/big.rs",
743 "auto",
744 None,
745 Some(root.to_str().unwrap()),
746 None,
747 );
748 assert_eq!(result.overridden_mode, Some("map".to_string()));
749 assert_eq!(result.reason, Some("overlay-set-view"));
750 }
751}