use std::path::Path;
use crate::state::{AphroditeState, MarkerEntry};
const MAX_PREFETCH_SIZE:u64 = 10 * 1024 * 1024;
pub enum ReadOutcome {
Missing,
Error,
SkippedSize { size:u64 },
Loaded { content:String, size:u64 },
}
pub fn read_paths(paths:&[String]) -> Vec<(String, ReadOutcome)> {
paths
.iter()
.map(|path_str| {
let path = Path::new(path_str);
if !path.is_file() {
return (path_str.clone(), ReadOutcome::Missing);
}
let size = match std::fs::metadata(path) {
Ok(m) => m.len(),
Err(_) => return (path_str.clone(), ReadOutcome::Error),
};
if size > MAX_PREFETCH_SIZE {
return (path_str.clone(), ReadOutcome::SkippedSize { size });
}
match std::fs::read_to_string(path) {
Ok(content) => (path_str.clone(), ReadOutcome::Loaded { content, size }),
Err(_) => (path_str.clone(), ReadOutcome::Error),
}
})
.collect()
}
pub fn insert_outcomes(state:&mut AphroditeState, outcomes:Vec<(String, ReadOutcome)>) -> serde_json::Value {
let total = outcomes.len();
let mut results = Vec::with_capacity(total);
let mut loaded = 0u32;
let mut skipped_size = 0u32;
let mut missing = 0u32;
for (path_str, outcome) in outcomes {
match outcome {
ReadOutcome::Missing | ReadOutcome::Error => {
missing += 1;
results.push(serde_json::json!({"path": path_str, "status": "missing"}));
},
ReadOutcome::SkippedSize { size } => {
skipped_size += 1;
results.push(serde_json::json!({
"path": path_str,
"status": "skipped",
"reason": format!(
"exceeds per-file prefetch limit ({} bytes > {} byte max)",
size, MAX_PREFETCH_SIZE
),
"size": size,
}));
},
ReadOutcome::Loaded { content, size } => {
let ct = headroom_core::transforms::detect(&content);
let hash = headroom_core::ccr::compute_key(content.as_bytes());
let type_str = ct.as_str();
let preview = crate::build_preview(type_str, &content);
state.inline_store_put(hash.clone(), content);
state.record_marker(MarkerEntry {
hash:hash.clone(),
ccr_type:type_str.to_string(),
size:size as usize,
preview:preview.clone(),
turn:state.turn_counter,
center:None,
meta:Some({
let mut m = std::collections::HashMap::new();
m.insert("path".to_string(), path_str.clone());
m
}),
});
loaded += 1;
state.record_file(path_str.clone(), "prefetch".to_string());
results.push(serde_json::json!({
"path": path_str,
"status": "loaded",
"hash": &hash,
"type": type_str,
"size": size,
"preview": preview,
}));
},
}
}
serde_json::json!({
"total": total,
"loaded": loaded,
"skipped_size": skipped_size,
"missing": missing,
"results": results,
"inline_store_bytes": state.inline_store_bytes(),
"inline_store_byte_budget": state.inline_store_byte_budget(),
})
}
pub fn prefetch_files(state:&mut AphroditeState, paths:&[String]) -> serde_json::Value {
insert_outcomes(state, read_paths(paths))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_prefetch_missing() {
let mut s = AphroditeState::default();
let r = prefetch_files(&mut s, &["/nonexistent/file/xyzzy.txt".to_string()]);
assert_eq!(r["missing"], 1);
assert_eq!(r["loaded"], 0);
}
#[test]
fn test_prefetch_real_file() {
let mut s = AphroditeState::default();
let src = env!("CARGO_MANIFEST_DIR").to_string() + "/src/prefetch.rs";
let r = prefetch_files(&mut s, &[src.clone()]);
assert_eq!(r["loaded"], 1, "prefetch failed: {:?}", r);
assert_eq!(s.recent_markers.len(), 1);
}
#[test]
fn test_prefetch_response_surfaces_inline_store_budget() {
let mut s = AphroditeState::default();
let src = env!("CARGO_MANIFEST_DIR").to_string() + "/src/prefetch.rs";
let r = prefetch_files(&mut s, &[src]);
assert_eq!(r["inline_store_byte_budget"], 256 * 1024 * 1024);
assert!(r["inline_store_bytes"].as_u64().unwrap() > 0);
}
#[test]
fn test_prefetch_skip_size_reason_mentions_byte_limit() {
let dir = std::env::temp_dir();
let path = dir.join(format!("aphrodite_prefetch_oversize_test_{}.txt", std::process::id()));
std::fs::write(&path, vec![b'x'; (MAX_PREFETCH_SIZE + 1) as usize]).unwrap();
let mut s = AphroditeState::default();
let r = prefetch_files(&mut s, &[path.to_string_lossy().to_string()]);
assert_eq!(r["skipped_size"], 1);
let reason = r["results"][0]["reason"].as_str().unwrap();
assert!(
reason.contains(&MAX_PREFETCH_SIZE.to_string()),
"reason should surface the byte limit: {reason}"
);
let _ = std::fs::remove_file(&path);
}
#[test]
fn test_prefetch_multiple() {
let mut s = AphroditeState::default();
let src = env!("CARGO_MANIFEST_DIR").to_string() + "/src/prefetch.rs";
let r = prefetch_files(&mut s, &[src, "/nonexistent/abc".to_string()]);
assert_eq!(r["loaded"], 1);
assert_eq!(r["missing"], 1);
}
}