1use std::collections::HashMap;
2
3use serde::{Deserialize, Serialize};
4
5use super::context_field::{
6 ContextItemId, ContextKind, ContextState, Provenance, ViewCosts, ViewKind,
7};
8
9const DEFAULT_CONTEXT_WINDOW: usize = 128_000;
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct ContextLedger {
13 pub window_size: usize,
14 pub entries: Vec<LedgerEntry>,
15 pub total_tokens_sent: usize,
16 pub total_tokens_saved: usize,
17}
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct LedgerEntry {
21 pub path: String,
22 pub mode: String,
23 pub original_tokens: usize,
24 pub sent_tokens: usize,
25 pub timestamp: i64,
26 #[serde(default)]
27 pub id: Option<ContextItemId>,
28 #[serde(default)]
29 pub kind: Option<ContextKind>,
30 #[serde(default)]
31 pub source_hash: Option<String>,
32 #[serde(default)]
33 pub state: Option<ContextState>,
34 #[serde(default)]
35 pub phi: Option<f64>,
36 #[serde(default)]
37 pub view_costs: Option<ViewCosts>,
38 #[serde(default)]
39 pub active_view: Option<ViewKind>,
40 #[serde(default)]
41 pub provenance: Option<Provenance>,
42}
43
44#[derive(Debug, Clone)]
45pub struct ContextPressure {
46 pub utilization: f64,
47 pub remaining_tokens: usize,
48 pub entries_count: usize,
49 pub recommendation: PressureAction,
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub enum PressureAction {
54 NoAction,
55 SuggestCompression,
56 ForceCompression,
57 EvictLeastRelevant,
58}
59
60impl ContextLedger {
61 pub fn new() -> Self {
62 Self {
63 window_size: DEFAULT_CONTEXT_WINDOW,
64 entries: Vec::new(),
65 total_tokens_sent: 0,
66 total_tokens_saved: 0,
67 }
68 }
69
70 pub fn with_window_size(size: usize) -> Self {
71 Self {
72 window_size: size,
73 entries: Vec::new(),
74 total_tokens_sent: 0,
75 total_tokens_saved: 0,
76 }
77 }
78
79 pub fn record(&mut self, path: &str, mode: &str, original_tokens: usize, sent_tokens: usize) {
80 let item_id = ContextItemId::from_file(path);
81 if let Some(existing) = self.entries.iter_mut().find(|e| e.path == path) {
82 self.total_tokens_sent -= existing.sent_tokens;
83 self.total_tokens_saved -= existing
84 .original_tokens
85 .saturating_sub(existing.sent_tokens);
86 existing.mode = mode.to_string();
87 existing.original_tokens = original_tokens;
88 existing.sent_tokens = sent_tokens;
89 existing.timestamp = chrono::Utc::now().timestamp();
90 existing.active_view = Some(ViewKind::parse(mode));
91 if existing.id.is_none() {
92 existing.id = Some(item_id);
93 }
94 if existing.state.is_none() || existing.state == Some(ContextState::Candidate) {
95 existing.state = Some(ContextState::Included);
96 }
97 } else {
98 self.entries.push(LedgerEntry {
99 path: path.to_string(),
100 mode: mode.to_string(),
101 original_tokens,
102 sent_tokens,
103 timestamp: chrono::Utc::now().timestamp(),
104 id: Some(item_id),
105 kind: Some(ContextKind::File),
106 source_hash: None,
107 state: Some(ContextState::Included),
108 phi: None,
109 view_costs: Some(ViewCosts::from_full_tokens(original_tokens)),
110 active_view: Some(ViewKind::parse(mode)),
111 provenance: None,
112 });
113 }
114 self.total_tokens_sent += sent_tokens;
115 self.total_tokens_saved += original_tokens.saturating_sub(sent_tokens);
116 }
117
118 pub fn upsert(
120 &mut self,
121 path: &str,
122 mode: &str,
123 original_tokens: usize,
124 sent_tokens: usize,
125 source_hash: Option<&str>,
126 kind: ContextKind,
127 provenance: Option<Provenance>,
128 ) {
129 self.record(path, mode, original_tokens, sent_tokens);
130 if let Some(entry) = self.entries.iter_mut().find(|e| e.path == path) {
131 entry.kind = Some(kind);
132 if let Some(h) = source_hash {
133 if entry.source_hash.as_deref() != Some(h) {
134 if entry.source_hash.is_some() {
135 entry.state = Some(ContextState::Stale);
136 }
137 entry.source_hash = Some(h.to_string());
138 }
139 }
140 if let Some(prov) = provenance {
141 entry.provenance = Some(prov);
142 }
143 }
144 }
145
146 pub fn update_phi(&mut self, path: &str, phi: f64) {
148 if let Some(entry) = self.entries.iter_mut().find(|e| e.path == path) {
149 entry.phi = Some(phi);
150 }
151 }
152
153 pub fn set_state(&mut self, path: &str, state: ContextState) {
155 if let Some(entry) = self.entries.iter_mut().find(|e| e.path == path) {
156 entry.state = Some(state);
157 }
158 }
159
160 pub fn find_by_id(&self, id: &ContextItemId) -> Option<&LedgerEntry> {
162 self.entries.iter().find(|e| e.id.as_ref() == Some(id))
163 }
164
165 pub fn items_by_state(&self, state: ContextState) -> Vec<&LedgerEntry> {
167 self.entries
168 .iter()
169 .filter(|e| e.state == Some(state))
170 .collect()
171 }
172
173 pub fn eviction_candidates_by_phi(&self, keep_count: usize) -> Vec<String> {
176 if self.entries.len() <= keep_count {
177 return Vec::new();
178 }
179 let mut sorted = self.entries.clone();
180 sorted.sort_by(|a, b| {
181 let a_phi = a.phi.unwrap_or(0.0);
182 let b_phi = b.phi.unwrap_or(0.0);
183 a_phi
184 .partial_cmp(&b_phi)
185 .unwrap_or(std::cmp::Ordering::Equal)
186 .then_with(|| a.timestamp.cmp(&b.timestamp))
187 });
188 sorted
189 .iter()
190 .filter(|e| e.state != Some(ContextState::Pinned))
191 .take(self.entries.len() - keep_count)
192 .map(|e| e.path.clone())
193 .collect()
194 }
195
196 pub fn mark_stale_by_hash(&mut self, path: &str, new_hash: &str) {
198 if let Some(entry) = self.entries.iter_mut().find(|e| e.path == path) {
199 if let Some(ref old_hash) = entry.source_hash {
200 if old_hash != new_hash {
201 entry.state = Some(ContextState::Stale);
202 entry.source_hash = Some(new_hash.to_string());
203 }
204 }
205 }
206 }
207
208 pub fn pressure(&self) -> ContextPressure {
209 let utilization = self.total_tokens_sent as f64 / self.window_size as f64;
210 let remaining = self.window_size.saturating_sub(self.total_tokens_sent);
211
212 let pinned_count = self
213 .entries
214 .iter()
215 .filter(|e| e.state == Some(ContextState::Pinned))
216 .count();
217 let stale_count = self
218 .entries
219 .iter()
220 .filter(|e| e.state == Some(ContextState::Stale))
221 .count();
222 let pinned_pressure = pinned_count as f64 * 0.02;
223 let stale_penalty = stale_count as f64 * 0.01;
224 let effective_utilization = (utilization + pinned_pressure + stale_penalty).min(1.0);
225
226 let recommendation = if effective_utilization > 0.9 {
227 PressureAction::EvictLeastRelevant
228 } else if effective_utilization > 0.75 {
229 PressureAction::ForceCompression
230 } else if effective_utilization > 0.5 {
231 PressureAction::SuggestCompression
232 } else {
233 PressureAction::NoAction
234 };
235
236 ContextPressure {
237 utilization: effective_utilization,
238 remaining_tokens: remaining,
239 entries_count: self.entries.len(),
240 recommendation,
241 }
242 }
243
244 pub fn compression_ratio(&self) -> f64 {
245 let total_original: usize = self.entries.iter().map(|e| e.original_tokens).sum();
246 if total_original == 0 {
247 return 1.0;
248 }
249 self.total_tokens_sent as f64 / total_original as f64
250 }
251
252 pub fn files_by_token_cost(&self) -> Vec<(String, usize)> {
253 let mut costs: Vec<(String, usize)> = self
254 .entries
255 .iter()
256 .map(|e| (e.path.clone(), e.sent_tokens))
257 .collect();
258 costs.sort_by_key(|b| std::cmp::Reverse(b.1));
259 costs
260 }
261
262 pub fn mode_distribution(&self) -> HashMap<String, usize> {
263 let mut dist: HashMap<String, usize> = HashMap::new();
264 for entry in &self.entries {
265 *dist.entry(entry.mode.clone()).or_insert(0) += 1;
266 }
267 dist
268 }
269
270 pub fn eviction_candidates(&self, keep_count: usize) -> Vec<String> {
271 if self.entries.len() <= keep_count {
272 return Vec::new();
273 }
274 let mut sorted = self.entries.clone();
275 sorted.sort_by_key(|e| e.timestamp);
276 sorted
277 .iter()
278 .take(self.entries.len() - keep_count)
279 .map(|e| e.path.clone())
280 .collect()
281 }
282
283 pub fn remove(&mut self, path: &str) {
284 if let Some(idx) = self.entries.iter().position(|e| e.path == path) {
285 let entry = &self.entries[idx];
286 self.total_tokens_sent -= entry.sent_tokens;
287 self.total_tokens_saved -= entry.original_tokens.saturating_sub(entry.sent_tokens);
288 self.entries.remove(idx);
289 }
290 }
291
292 pub fn save(&self) {
293 if let Ok(dir) = crate::core::data_dir::lean_ctx_data_dir() {
294 let path = dir.join("context_ledger.json");
295 if let Ok(json) = serde_json::to_string(self) {
296 let _ = std::fs::write(path, json);
297 }
298 }
299 }
300
301 pub fn load() -> Self {
302 crate::core::data_dir::lean_ctx_data_dir()
303 .ok()
304 .map(|d| d.join("context_ledger.json"))
305 .and_then(|p| std::fs::read_to_string(p).ok())
306 .and_then(|s| serde_json::from_str(&s).ok())
307 .unwrap_or_default()
308 }
309
310 pub fn format_summary(&self) -> String {
311 let pressure = self.pressure();
312 format!(
313 "CTX: {}/{} tokens ({:.0}%), {} files, ratio {:.2}, action: {:?}",
314 self.total_tokens_sent,
315 self.window_size,
316 pressure.utilization * 100.0,
317 self.entries.len(),
318 self.compression_ratio(),
319 pressure.recommendation,
320 )
321 }
322}
323
324#[derive(Debug, Clone)]
325pub struct ReinjectionAction {
326 pub path: String,
327 pub current_mode: String,
328 pub new_mode: String,
329 pub tokens_freed: usize,
330}
331
332#[derive(Debug, Clone)]
333pub struct ReinjectionPlan {
334 pub actions: Vec<ReinjectionAction>,
335 pub total_tokens_freed: usize,
336 pub new_utilization: f64,
337}
338
339impl ContextLedger {
340 pub fn reinjection_plan(
341 &self,
342 intent: &super::intent_engine::StructuredIntent,
343 target_utilization: f64,
344 ) -> ReinjectionPlan {
345 let current_util = self.total_tokens_sent as f64 / self.window_size as f64;
346 if current_util <= target_utilization {
347 return ReinjectionPlan {
348 actions: Vec::new(),
349 total_tokens_freed: 0,
350 new_utilization: current_util,
351 };
352 }
353
354 let tokens_to_free =
355 self.total_tokens_sent - (self.window_size as f64 * target_utilization) as usize;
356
357 let target_set: std::collections::HashSet<&str> = intent
358 .targets
359 .iter()
360 .map(std::string::String::as_str)
361 .collect();
362
363 let mut candidates: Vec<(usize, &LedgerEntry)> = self
364 .entries
365 .iter()
366 .enumerate()
367 .filter(|(_, e)| !target_set.iter().any(|t| e.path.contains(t)))
368 .collect();
369
370 candidates.sort_by(|a, b| {
371 let a_phi = a.1.phi.unwrap_or(0.0);
372 let b_phi = b.1.phi.unwrap_or(0.0);
373 a_phi
374 .partial_cmp(&b_phi)
375 .unwrap_or_else(|| a.1.timestamp.cmp(&b.1.timestamp))
376 });
377
378 let mut actions = Vec::new();
379 let mut freed = 0usize;
380
381 for (_, entry) in &candidates {
382 if freed >= tokens_to_free {
383 break;
384 }
385 if let Some((new_mode, new_tokens)) = downgrade_mode(&entry.mode, entry.sent_tokens) {
386 let saving = entry.sent_tokens.saturating_sub(new_tokens);
387 if saving > 0 {
388 actions.push(ReinjectionAction {
389 path: entry.path.clone(),
390 current_mode: entry.mode.clone(),
391 new_mode,
392 tokens_freed: saving,
393 });
394 freed += saving;
395 }
396 }
397 }
398
399 let new_sent = self.total_tokens_sent.saturating_sub(freed);
400 let new_utilization = new_sent as f64 / self.window_size as f64;
401
402 ReinjectionPlan {
403 actions,
404 total_tokens_freed: freed,
405 new_utilization,
406 }
407 }
408}
409
410fn downgrade_mode(current_mode: &str, current_tokens: usize) -> Option<(String, usize)> {
411 match current_mode {
412 "full" => Some(("signatures".to_string(), current_tokens / 5)),
413 "aggressive" => Some(("signatures".to_string(), current_tokens / 3)),
414 "signatures" => Some(("map".to_string(), current_tokens / 2)),
415 "map" => Some(("reference".to_string(), current_tokens / 4)),
416 _ => None,
417 }
418}
419
420impl Default for ContextLedger {
421 fn default() -> Self {
422 Self::new()
423 }
424}
425
426#[cfg(test)]
427mod tests {
428 use super::*;
429
430 #[test]
431 fn new_ledger_is_empty() {
432 let ledger = ContextLedger::new();
433 assert_eq!(ledger.total_tokens_sent, 0);
434 assert_eq!(ledger.entries.len(), 0);
435 assert_eq!(ledger.pressure().recommendation, PressureAction::NoAction);
436 }
437
438 #[test]
439 fn record_tracks_tokens() {
440 let mut ledger = ContextLedger::with_window_size(10000);
441 ledger.record("src/main.rs", "full", 500, 500);
442 ledger.record("src/lib.rs", "signatures", 1000, 200);
443 assert_eq!(ledger.total_tokens_sent, 700);
444 assert_eq!(ledger.total_tokens_saved, 800);
445 assert_eq!(ledger.entries.len(), 2);
446 }
447
448 #[test]
449 fn record_updates_existing_entry() {
450 let mut ledger = ContextLedger::with_window_size(10000);
451 ledger.record("src/main.rs", "full", 500, 500);
452 ledger.record("src/main.rs", "signatures", 500, 100);
453 assert_eq!(ledger.entries.len(), 1);
454 assert_eq!(ledger.total_tokens_sent, 100);
455 assert_eq!(ledger.total_tokens_saved, 400);
456 }
457
458 #[test]
459 fn pressure_escalates() {
460 let mut ledger = ContextLedger::with_window_size(1000);
461 ledger.record("a.rs", "full", 600, 600);
462 assert_eq!(
463 ledger.pressure().recommendation,
464 PressureAction::SuggestCompression
465 );
466 ledger.record("b.rs", "full", 200, 200);
467 assert_eq!(
468 ledger.pressure().recommendation,
469 PressureAction::ForceCompression
470 );
471 ledger.record("c.rs", "full", 150, 150);
472 assert_eq!(
473 ledger.pressure().recommendation,
474 PressureAction::EvictLeastRelevant
475 );
476 }
477
478 #[test]
479 fn compression_ratio_accurate() {
480 let mut ledger = ContextLedger::with_window_size(10000);
481 ledger.record("a.rs", "full", 1000, 1000);
482 ledger.record("b.rs", "signatures", 1000, 200);
483 let ratio = ledger.compression_ratio();
484 assert!((ratio - 0.6).abs() < 0.01);
485 }
486
487 #[test]
488 fn eviction_returns_oldest() {
489 let mut ledger = ContextLedger::with_window_size(10000);
490 ledger.record("old.rs", "full", 100, 100);
491 std::thread::sleep(std::time::Duration::from_millis(10));
492 ledger.record("new.rs", "full", 100, 100);
493 let candidates = ledger.eviction_candidates(1);
494 assert_eq!(candidates, vec!["old.rs"]);
495 }
496
497 #[test]
498 fn remove_updates_totals() {
499 let mut ledger = ContextLedger::with_window_size(10000);
500 ledger.record("a.rs", "full", 500, 500);
501 ledger.record("b.rs", "full", 300, 300);
502 ledger.remove("a.rs");
503 assert_eq!(ledger.total_tokens_sent, 300);
504 assert_eq!(ledger.entries.len(), 1);
505 }
506
507 #[test]
508 fn mode_distribution_counts() {
509 let mut ledger = ContextLedger::new();
510 ledger.record("a.rs", "full", 100, 100);
511 ledger.record("b.rs", "signatures", 100, 50);
512 ledger.record("c.rs", "full", 100, 100);
513 let dist = ledger.mode_distribution();
514 assert_eq!(dist.get("full"), Some(&2));
515 assert_eq!(dist.get("signatures"), Some(&1));
516 }
517
518 #[test]
519 fn format_summary_includes_key_info() {
520 let mut ledger = ContextLedger::with_window_size(10000);
521 ledger.record("a.rs", "full", 500, 500);
522 let summary = ledger.format_summary();
523 assert!(summary.contains("500/10000"));
524 assert!(summary.contains("1 files"));
525 }
526
527 #[test]
528 fn reinjection_no_action_when_low_pressure() {
529 use crate::core::intent_engine::StructuredIntent;
530
531 let mut ledger = ContextLedger::with_window_size(10000);
532 ledger.record("a.rs", "full", 100, 100);
533 let intent = StructuredIntent::from_query("fix bug in a.rs");
534 let plan = ledger.reinjection_plan(&intent, 0.7);
535 assert!(plan.actions.is_empty());
536 assert_eq!(plan.total_tokens_freed, 0);
537 }
538
539 #[test]
540 fn reinjection_downgrades_non_target_files() {
541 use crate::core::intent_engine::StructuredIntent;
542
543 let mut ledger = ContextLedger::with_window_size(1000);
544 ledger.record("src/target.rs", "full", 400, 400);
545 std::thread::sleep(std::time::Duration::from_millis(10));
546 ledger.record("src/other.rs", "full", 400, 400);
547 std::thread::sleep(std::time::Duration::from_millis(10));
548 ledger.record("src/utils.rs", "full", 200, 200);
549
550 let intent = StructuredIntent::from_query("fix bug in target.rs");
551 let plan = ledger.reinjection_plan(&intent, 0.5);
552
553 assert!(!plan.actions.is_empty());
554 assert!(
555 plan.actions.iter().all(|a| !a.path.contains("target")),
556 "should not downgrade target file"
557 );
558 assert!(plan.total_tokens_freed > 0);
559 }
560
561 #[test]
562 fn reinjection_preserves_targets() {
563 use crate::core::intent_engine::StructuredIntent;
564
565 let mut ledger = ContextLedger::with_window_size(1000);
566 ledger.record("src/auth.rs", "full", 900, 900);
567 let intent = StructuredIntent::from_query("fix bug in auth.rs");
568 let plan = ledger.reinjection_plan(&intent, 0.5);
569 assert!(
570 plan.actions.is_empty(),
571 "should not downgrade target files even under pressure"
572 );
573 }
574
575 #[test]
576 fn downgrade_mode_chain() {
577 assert_eq!(
578 downgrade_mode("full", 1000),
579 Some(("signatures".to_string(), 200))
580 );
581 assert_eq!(
582 downgrade_mode("signatures", 200),
583 Some(("map".to_string(), 100))
584 );
585 assert_eq!(
586 downgrade_mode("map", 100),
587 Some(("reference".to_string(), 25))
588 );
589 assert_eq!(downgrade_mode("reference", 25), None);
590 }
591
592 #[test]
593 fn record_assigns_item_id() {
594 let mut ledger = ContextLedger::new();
595 ledger.record("src/main.rs", "full", 500, 500);
596 let entry = &ledger.entries[0];
597 assert!(entry.id.is_some());
598 assert_eq!(entry.id.as_ref().unwrap().as_str(), "file:src/main.rs");
599 }
600
601 #[test]
602 fn record_sets_state_to_included() {
603 let mut ledger = ContextLedger::new();
604 ledger.record("src/main.rs", "full", 500, 500);
605 assert_eq!(
606 ledger.entries[0].state,
607 Some(crate::core::context_field::ContextState::Included)
608 );
609 }
610
611 #[test]
612 fn record_generates_view_costs() {
613 let mut ledger = ContextLedger::new();
614 ledger.record("src/main.rs", "full", 5000, 5000);
615 let vc = ledger.entries[0].view_costs.as_ref().unwrap();
616 assert_eq!(vc.get(&crate::core::context_field::ViewKind::Full), 5000);
617 assert_eq!(
618 vc.get(&crate::core::context_field::ViewKind::Signatures),
619 1000
620 );
621 }
622
623 #[test]
624 fn update_phi_works() {
625 let mut ledger = ContextLedger::new();
626 ledger.record("a.rs", "full", 100, 100);
627 ledger.update_phi("a.rs", 0.85);
628 assert_eq!(ledger.entries[0].phi, Some(0.85));
629 }
630
631 #[test]
632 fn set_state_works() {
633 let mut ledger = ContextLedger::new();
634 ledger.record("a.rs", "full", 100, 100);
635 ledger.set_state("a.rs", crate::core::context_field::ContextState::Pinned);
636 assert_eq!(
637 ledger.entries[0].state,
638 Some(crate::core::context_field::ContextState::Pinned)
639 );
640 }
641
642 #[test]
643 fn items_by_state_filters() {
644 let mut ledger = ContextLedger::new();
645 ledger.record("a.rs", "full", 100, 100);
646 ledger.record("b.rs", "full", 100, 100);
647 ledger.set_state("b.rs", crate::core::context_field::ContextState::Excluded);
648 let included = ledger.items_by_state(crate::core::context_field::ContextState::Included);
649 assert_eq!(included.len(), 1);
650 assert_eq!(included[0].path, "a.rs");
651 }
652
653 #[test]
654 fn eviction_by_phi_prefers_low_phi() {
655 let mut ledger = ContextLedger::with_window_size(10000);
656 ledger.record("high.rs", "full", 100, 100);
657 ledger.update_phi("high.rs", 0.9);
658 ledger.record("low.rs", "full", 100, 100);
659 ledger.update_phi("low.rs", 0.1);
660 let candidates = ledger.eviction_candidates_by_phi(1);
661 assert_eq!(candidates, vec!["low.rs"]);
662 }
663
664 #[test]
665 fn eviction_by_phi_skips_pinned() {
666 let mut ledger = ContextLedger::with_window_size(10000);
667 ledger.record("pinned.rs", "full", 100, 100);
668 ledger.update_phi("pinned.rs", 0.01);
669 ledger.set_state(
670 "pinned.rs",
671 crate::core::context_field::ContextState::Pinned,
672 );
673 ledger.record("normal.rs", "full", 100, 100);
674 ledger.update_phi("normal.rs", 0.5);
675 let candidates = ledger.eviction_candidates_by_phi(1);
676 assert_eq!(candidates, vec!["normal.rs"]);
677 }
678
679 #[test]
680 fn mark_stale_by_hash_detects_change() {
681 let mut ledger = ContextLedger::new();
682 ledger.record("a.rs", "full", 100, 100);
683 ledger.entries[0].source_hash = Some("hash_v1".to_string());
684 ledger.mark_stale_by_hash("a.rs", "hash_v2");
685 assert_eq!(
686 ledger.entries[0].state,
687 Some(crate::core::context_field::ContextState::Stale)
688 );
689 }
690
691 #[test]
692 fn find_by_id_works() {
693 let mut ledger = ContextLedger::new();
694 ledger.record("src/lib.rs", "full", 100, 100);
695 let id = crate::core::context_field::ContextItemId::from_file("src/lib.rs");
696 assert!(ledger.find_by_id(&id).is_some());
697 }
698
699 #[test]
700 fn upsert_sets_source_hash_and_kind() {
701 let mut ledger = ContextLedger::new();
702 ledger.upsert(
703 "src/main.rs",
704 "full",
705 500,
706 500,
707 Some("sha256_abc"),
708 crate::core::context_field::ContextKind::File,
709 None,
710 );
711 let entry = &ledger.entries[0];
712 assert_eq!(entry.source_hash.as_deref(), Some("sha256_abc"));
713 assert_eq!(
714 entry.kind,
715 Some(crate::core::context_field::ContextKind::File)
716 );
717 }
718}