use std::time::Duration;
use super::handle_with_task_resolved;
use super::resolve_explicit_delta_mode;
use super::try_stub_hit_readonly_scoped;
use crate::core::cache::SessionCache;
use crate::tools::CrpMode;
fn primed_full_cache(p: &str) -> SessionCache {
let mut cache = SessionCache::new();
let _ = handle_with_task_resolved(&mut cache, p, "full", CrpMode::Off, None);
debug_assert!(
cache.is_full_delivered(p),
"fixture must deliver full content"
);
cache
}
#[test]
fn auto_reread_of_fully_delivered_file_serves_unchanged_stub() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("warm.rs");
let p = path.to_string_lossy().to_string();
let body = (0..48)
.map(|i| format!("fn function_number_{i}() {{ let value_{i} = {i} * 2; }}"))
.collect::<Vec<_>>()
.join("\n");
std::fs::write(&path, format!("{body}\n")).unwrap();
let mut cold = SessionCache::new();
let full = handle_with_task_resolved(&mut cold, &p, "full", CrpMode::Off, None);
assert!(
!full.content.contains("[unchanged"),
"cold full read must deliver the body, not a stub"
);
let mut cache = primed_full_cache(&p);
let reread = handle_with_task_resolved(&mut cache, &p, "auto", CrpMode::Off, None);
assert!(
reread.content.contains("[unchanged"),
"auto re-read of an unchanged fully-delivered file must serve the stub, got: {}",
reread.content
);
assert!(
reread.output_tokens.saturating_mul(4) < full.output_tokens,
"stub ({} tok) must be far cheaper than a full re-delivery ({} tok)",
reread.output_tokens,
full.output_tokens
);
}
#[test]
fn mode_change_clears_full_delivered_flag() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("readme.md");
let body = (0..50)
.map(|i| {
format!(
"## Section {i}
Content for section {i}."
)
})
.collect::<Vec<_>>()
.join(
"
",
);
std::fs::write(
&path,
format!(
"{body}
"
),
)
.unwrap();
let p = crate::core::pathutil::normalize_tool_path(&path.to_string_lossy());
let mut cache = SessionCache::new();
let full1 = handle_with_task_resolved(&mut cache, &p, "full", CrpMode::Off, None);
assert!(
!full1.content.contains("[unchanged"),
"first full read must deliver the body"
);
assert!(
cache.is_full_delivered(&p),
"full_content_delivered must be set"
);
let task = handle_with_task_resolved(
&mut cache,
&p,
"task",
CrpMode::Off,
Some("find section 42"),
);
assert!(
!task.content.contains("[unchanged"),
"task read must deliver filtered content, not a stub"
);
assert!(
!cache.is_full_delivered(&p),
"full_content_delivered must be cleared after a non-full read (#841)"
);
let full2 = handle_with_task_resolved(&mut cache, &p, "full", CrpMode::Off, None);
assert!(
!full2.content.contains("[unchanged"),
"full read after a task read must deliver the body, not stub (#841): {}",
full2.content
);
assert!(
full2.content.contains("Section 0") || full2.content.contains("Section 49"),
"re-delivered full read must contain actual file content"
);
}
#[test]
fn full_reread_still_serves_stub_when_no_mode_change() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("stable.rs");
let p = path.to_string_lossy().to_string();
let body = (0..48)
.map(|i| format!("fn function_{i}() {{ let v = {i}; }}"))
.collect::<Vec<_>>()
.join(
"
",
);
std::fs::write(
&path,
format!(
"{body}
"
),
)
.unwrap();
let mut cache = SessionCache::new();
let full1 = handle_with_task_resolved(&mut cache, &p, "full", CrpMode::Off, None);
assert!(
!full1.content.contains("[unchanged"),
"first read must deliver body"
);
let full2 = handle_with_task_resolved(&mut cache, &p, "full", CrpMode::Off, None);
assert!(
full2.content.contains("[unchanged"),
"full->full re-read of unchanged file must serve the stub: {}",
full2.content
);
}
#[test]
fn conversation_scoped_stub_served_for_same_conversation() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("warm.rs");
let p = path.to_string_lossy().to_string();
std::fs::write(&path, "fn main() { let x = 1; }\n").unwrap();
let cache = primed_full_cache(&p);
let delivered = cache.get(&p).unwrap().delivered_conversation.clone();
let out = try_stub_hit_readonly_scoped(&cache, &p, delivered.as_deref());
assert!(
out.is_some_and(|o| o.content.contains("[unchanged")),
"same-conversation re-read must serve the stub"
);
}
#[test]
fn conversation_scoped_stub_withheld_for_other_conversation() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("warm.rs");
let p = path.to_string_lossy().to_string();
std::fs::write(&path, "fn main() { let x = 1; }\n").unwrap();
let cache = primed_full_cache(&p);
let foreign = "conversation-that-never-read-this-file";
assert_ne!(
cache.get(&p).unwrap().delivered_conversation.as_deref(),
Some(foreign),
"test fixture id collided with the foreign id"
);
let out = try_stub_hit_readonly_scoped(&cache, &p, Some(foreign));
assert!(
out.is_none(),
"a foreign conversation must get a full re-read, never a misleading [unchanged] stub"
);
}
#[test]
fn full_read_fallback_never_serves_a_stub() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("warm.rs");
let p = path.to_string_lossy().to_string();
std::fs::write(&path, "fn main() { let x = 1; }\n").unwrap();
let mut cache = primed_full_cache(&p);
let (out, _) = super::handle_full_with_auto_delta(&mut cache, &p, "F1", &p, "rs", None);
assert!(
!out.contains("[unchanged"),
"full-read fallback must not decide stubbing on its own: {out}"
);
assert!(
out.contains("fn main"),
"full-read fallback must deliver content: {out}"
);
}
#[test]
fn conversation_scoped_stub_served_when_no_context() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("warm.rs");
let p = path.to_string_lossy().to_string();
std::fs::write(&path, "fn main() { let x = 1; }\n").unwrap();
let cache = primed_full_cache(&p);
let out = try_stub_hit_readonly_scoped(&cache, &p, None);
assert!(
out.is_some_and(|o| o.content.contains("[unchanged")),
"absent conversation context must keep legacy stub behavior"
);
}
fn seed_cold_record(p: &str, conv: &str) {
crate::core::read_stub_index::clear_for_test();
let primed = primed_full_cache(p);
let entry = primed.get(p).unwrap();
let rec = crate::core::read_stub_index::StubRecord::new(
crate::core::pathutil::normalize_tool_path(p),
entry.hash.clone(),
entry.stored_mtime,
entry.line_count,
primed.get_file_ref_readonly(p).unwrap_or_default(),
Some(conv.to_string()),
);
crate::core::read_stub_index::clear_for_test();
crate::core::read_stub_index::record(rec);
}
#[test]
#[serial_test::serial(stub_index)]
fn cold_fallback_serves_stub_for_same_conversation_after_restart() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("warm.rs");
let p = path.to_string_lossy().to_string();
std::fs::write(&path, "fn main() { let x = 1; }\n").unwrap();
seed_cold_record(&p, "conv-a");
let cold = SessionCache::new();
let out = try_stub_hit_readonly_scoped(&cold, &p, Some("conv-a"));
crate::core::read_stub_index::clear_for_test();
assert!(
out.is_some_and(|o| o.content.contains("[unchanged")),
"same-conversation re-read after restart must serve the persisted stub"
);
}
#[test]
#[serial_test::serial(stub_index)]
fn cold_fallback_withheld_for_other_conversation() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("warm.rs");
let p = path.to_string_lossy().to_string();
std::fs::write(&path, "fn main() { let x = 1; }\n").unwrap();
seed_cold_record(&p, "conv-a");
let cold = SessionCache::new();
let out = try_stub_hit_readonly_scoped(&cold, &p, Some("conv-b"));
crate::core::read_stub_index::clear_for_test();
assert!(
out.is_none(),
"a different conversation must get a cold full read, never a persisted stub"
);
}
#[test]
#[serial_test::serial(stub_index)]
fn cold_fallback_withheld_without_conversation_context() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("warm.rs");
let p = path.to_string_lossy().to_string();
std::fs::write(&path, "fn main() { let x = 1; }\n").unwrap();
seed_cold_record(&p, "conv-a");
let cold = SessionCache::new();
let out = try_stub_hit_readonly_scoped(&cold, &p, None);
crate::core::read_stub_index::clear_for_test();
assert!(
out.is_none(),
"absent conversation context must NOT serve a cold persisted stub"
);
}
#[test]
#[serial_test::serial(stub_index)]
fn cold_fallback_withheld_when_file_changed() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("warm.rs");
let p = path.to_string_lossy().to_string();
std::fs::write(&path, "fn main() { let x = 1; }\n").unwrap();
seed_cold_record(&p, "conv-a");
std::fs::write(&path, "fn main() { let x = 2; let y = 3; }\n").unwrap();
let cold = SessionCache::new();
let out = try_stub_hit_readonly_scoped(&cold, &p, Some("conv-a"));
crate::core::read_stub_index::clear_for_test();
assert!(
out.is_none(),
"a file changed on disk must get a cold full read, never a stale stub"
);
}
#[test]
fn delta_explicit_changed_file_diverts_full_reread_to_diff() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("changed.rs");
let p = path.to_string_lossy().to_string();
std::fs::write(&path, "fn main() {}\n").unwrap();
let mut cache = primed_full_cache(&p);
std::thread::sleep(Duration::from_secs(1));
std::fs::write(&path, "fn main() { changed(); }\n").unwrap();
let decision = resolve_explicit_delta_mode(
&cache, &p, "full", true, false, true,
);
assert_eq!(
decision.mode, "diff",
"changed full re-read must divert to diff"
);
let note = decision
.note
.expect("a diff diversion must carry an advisory note");
assert!(
note.contains("[delta-explicit]"),
"note tag missing: {note}"
);
assert!(
note.contains("fresh=true"),
"note must mention the bypass: {note}"
);
let out = handle_with_task_resolved(&mut cache, &p, "diff", CrpMode::Off, None);
assert_eq!(out.resolved_mode, "diff");
assert!(
out.content.contains("[diff]"),
"engine must emit a diff: {}",
out.content
);
assert!(
out.content.contains("changed()"),
"diff must reflect the new on-disk content: {}",
out.content
);
}
#[test]
fn delta_explicit_changed_lines_request_diverts_to_diff() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("lines.rs");
let p = path.to_string_lossy().to_string();
std::fs::write(&path, "fn a() {}\nfn b() {}\n").unwrap();
let cache = primed_full_cache(&p);
std::thread::sleep(Duration::from_secs(1));
std::fs::write(&path, "fn a() { x(); }\nfn b() {}\n").unwrap();
let decision = resolve_explicit_delta_mode(&cache, &p, "lines:1-1", true, false, true);
assert_eq!(
decision.mode, "diff",
"a changed-file lines: re-read must divert to diff, not re-extract a window"
);
assert!(decision.note.is_some());
}
#[test]
fn delta_explicit_diff_base_is_full_cached_content_not_compressed() {
let _iso = crate::core::data_dir::isolated_data_dir();
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("big.rs");
let p = path.to_string_lossy().to_string();
let mut content = String::new();
for i in 0..60 {
content.push_str(&format!(
"pub fn original_fn_{i}(x: i32) -> i32 {{ x + {i} }}\n"
));
}
std::fs::write(&path, &content).unwrap();
let mut cache = SessionCache::new();
let _ = handle_with_task_resolved(&mut cache, &p, "full", CrpMode::Off, None);
let _ = handle_with_task_resolved(&mut cache, &p, "map", CrpMode::Off, None);
std::thread::sleep(Duration::from_secs(1));
let changed = content.replace(
"pub fn original_fn_7(x: i32) -> i32 { x + 7 }",
"pub fn original_fn_7(x: i32) -> i32 { x + 70707 }",
);
std::fs::write(&path, &changed).unwrap();
let out = handle_with_task_resolved(&mut cache, &p, "diff", CrpMode::Off, None);
assert!(
out.content.contains("[diff]"),
"expected a diff: {}",
out.content
);
assert!(
out.content.contains("70707"),
"diff must be computed against full cached source, got: {}",
out.content
);
assert!(
out.content.contains("+1/-1") || out.content.contains("-1/+1"),
"single-line change should diff as +1/-1: {}",
out.content
);
}
#[test]
fn delta_explicit_unchanged_lines_collapse_to_full_stub() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("same.rs");
let p = path.to_string_lossy().to_string();
std::fs::write(&path, "fn a() {}\nfn b() {}\n").unwrap();
let cache = primed_full_cache(&p);
let decision = resolve_explicit_delta_mode(&cache, &p, "lines:1-1", true, false, true);
assert_eq!(
decision.mode, "full",
"unchanged lines: of a full file must collapse to the stub"
);
assert!(
decision.note.is_none(),
"a silent stub collapse must not carry a note"
);
}
#[test]
fn delta_explicit_unchanged_full_reread_is_untouched() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("same.rs");
let p = path.to_string_lossy().to_string();
std::fs::write(&path, "fn a() {}\n").unwrap();
let cache = primed_full_cache(&p);
let decision = resolve_explicit_delta_mode(&cache, &p, "full", true, false, true);
assert_eq!(decision.mode, "full");
assert!(decision.note.is_none());
}
#[test]
fn delta_explicit_off_preserves_current_behavior() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("changed.rs");
let p = path.to_string_lossy().to_string();
std::fs::write(&path, "fn main() {}\n").unwrap();
let cache = primed_full_cache(&p);
std::thread::sleep(Duration::from_secs(1));
std::fs::write(&path, "fn main() { changed(); }\n").unwrap();
let decision =
resolve_explicit_delta_mode(&cache, &p, "full", true, false, false);
assert_eq!(
decision.mode, "full",
"feature OFF must preserve the requested mode"
);
assert!(decision.note.is_none());
let lines = resolve_explicit_delta_mode(&cache, &p, "lines:1-1", true, false, false);
assert_eq!(
lines.mode, "lines:1-1",
"feature OFF must not touch lines: either"
);
assert!(lines.note.is_none());
}
#[test]
fn delta_explicit_fresh_bypasses() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("changed.rs");
let p = path.to_string_lossy().to_string();
std::fs::write(&path, "fn main() {}\n").unwrap();
let cache = primed_full_cache(&p);
std::thread::sleep(Duration::from_secs(1));
std::fs::write(&path, "fn main() { changed(); }\n").unwrap();
let decision = resolve_explicit_delta_mode(&cache, &p, "full", true, true, true);
assert_eq!(
decision.mode, "full",
"fresh=true must bypass the diff diversion"
);
assert!(decision.note.is_none());
}
#[test]
fn delta_explicit_first_read_unaffected() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("new.rs");
let p = path.to_string_lossy().to_string();
std::fs::write(&path, "fn main() {}\n").unwrap();
let cache = SessionCache::new();
let decision = resolve_explicit_delta_mode(&cache, &p, "full", true, false, true);
assert_eq!(
decision.mode, "full",
"an uncached first read must be served normally"
);
assert!(decision.note.is_none());
let lines = resolve_explicit_delta_mode(&cache, &p, "lines:1-1", true, false, true);
assert_eq!(lines.mode, "lines:1-1");
assert!(lines.note.is_none());
}
#[test]
fn delta_explicit_only_fires_for_explicit_mode() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("changed.rs");
let p = path.to_string_lossy().to_string();
std::fs::write(&path, "fn main() {}\n").unwrap();
let cache = primed_full_cache(&p);
std::thread::sleep(Duration::from_secs(1));
std::fs::write(&path, "fn main() { changed(); }\n").unwrap();
let decision =
resolve_explicit_delta_mode(&cache, &p, "full", false, false, true);
assert_eq!(
decision.mode, "full",
"auto-resolved modes must not be diverted to diff"
);
assert!(decision.note.is_none());
}
#[test]
fn delta_explicit_decision_is_byte_stable() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("changed.rs");
let p = path.to_string_lossy().to_string();
std::fs::write(&path, "fn main() {}\n").unwrap();
let cache = primed_full_cache(&p);
std::thread::sleep(Duration::from_secs(1));
std::fs::write(&path, "fn main() { changed(); }\n").unwrap();
let d1 = resolve_explicit_delta_mode(&cache, &p, "full", true, false, true);
let d2 = resolve_explicit_delta_mode(&cache, &p, "full", true, false, true);
assert_eq!(
d1, d2,
"delta-explicit decision drifted between identical calls"
);
}