use super::*;
#[test]
fn file_backed_compile_cache_resolves_agent_spec_and_rejects_invalid_options() {
let root = tempfile::tempdir().unwrap();
let path = root.path().join("main.harn");
let source = include_str!("../../api/tests/fixtures/task_retry.harn");
std::fs::write(&path, source).unwrap();
let mut server = AcpServer::new(AcpServerConfig::new(Some(
path.to_string_lossy().into_owned(),
)));
let (_, cached) = server
.compile_pipeline_cached(source, Some(&path), Some("main"))
.unwrap();
assert!(!cached);
let (_, cached) = server
.compile_pipeline_cached(source, Some(&path), Some("main"))
.unwrap();
assert!(cached);
let invalid = source.replace("provider: \"mock\"", "provider: 7");
assert!(server
.compile_pipeline_cached(&invalid, Some(&path), Some("main"))
.unwrap_err()
.contains("AgentSpec"));
}
#[test]
fn compile_cache_invalidates_transitive_types_reexports_and_bodies() {
let root = tempfile::tempdir().unwrap();
let dependency = root.path().join("dependency.harn");
let facade = root.path().join("facade.harn");
std::fs::write(
&dependency,
"pub type Options = {count: int}\npub fn value() -> int { 1 }\n",
)
.unwrap();
std::fs::write(
&facade,
"pub import {Options, value} from \"./dependency.harn\"\n",
)
.unwrap();
let path = root.path().join("main.harn");
let source = "import {Options, value} from \"./facade.harn\"\npipeline main(harness: Harness) { const options: Options = {count: 1}; assert_eq(value(), 1) }";
std::fs::write(&path, source).unwrap();
let root_mtime = std::fs::metadata(&path).unwrap().modified().unwrap();
let mut server = AcpServer::new(AcpServerConfig::new(Some(
path.to_string_lossy().into_owned(),
)));
let (_, hit) = server
.compile_pipeline_cached(source, Some(&path), None)
.unwrap();
assert!(!hit);
let (_, hit) = server
.compile_pipeline_cached(source, Some(&path), None)
.unwrap();
assert!(hit);
std::fs::write(
&dependency,
"pub type Options = {count: int}\npub fn value() -> int { 2 }\n",
)
.unwrap();
assert_eq!(
std::fs::metadata(&path).unwrap().modified().unwrap(),
root_mtime
);
let (_, hit) = server
.compile_pipeline_cached(source, Some(&path), None)
.unwrap();
assert!(
!hit,
"body-only dependency changes invalidate compiled entries"
);
std::fs::write(
&dependency,
"pub type Options = {count: string}\npub fn value() -> int { 2 }\n",
)
.unwrap();
assert!(server
.compile_pipeline_cached(source, Some(&path), None)
.unwrap_err()
.contains("Options"));
std::fs::write(&facade, "pub import {value} from \"./dependency.harn\"\n").unwrap();
assert!(server
.compile_pipeline_cached(source, Some(&path), None)
.unwrap_err()
.contains("does not exist"));
}
#[tokio::test(flavor = "current_thread")]
async fn vm_baseline_cache_invalidates_transitive_import_changes() {
let root = tempfile::tempdir().unwrap();
let dependency = root.path().join("dependency.harn");
let facade = root.path().join("facade.harn");
std::fs::write(&dependency, "pub fn value() -> int { 1 }\n").unwrap();
std::fs::write(&facade, "pub import {value} from \"./dependency.harn\"\n").unwrap();
let path = root.path().join("main.harn");
let source =
"import {value} from \"./facade.harn\"\npipeline main(harness: Harness) { value() }";
std::fs::write(&path, source).unwrap();
let root_mtime = std::fs::metadata(&path).unwrap().modified().unwrap();
let scope = harn_vm::SessionStoreScope::resolve(
root.path(),
&harn_vm::security::LauncherEnvironment::from_snapshot(Default::default()),
)
.unwrap();
let mut server = AcpServer::new(AcpServerConfig::new(Some(
path.to_string_lossy().into_owned(),
)));
let (_, hit, _) = server
.prepare_vm_baseline_cached(
source,
Some(&path),
None,
root.path(),
root.path(),
&scope,
"code",
)
.await
.unwrap();
assert_eq!(hit, Some(false));
let (_, hit, _) = server
.prepare_vm_baseline_cached(
source,
Some(&path),
None,
root.path(),
root.path(),
&scope,
"code",
)
.await
.unwrap();
assert_eq!(hit, Some(true));
std::fs::write(&dependency, "pub fn value() -> int { 2 }\n").unwrap();
assert_eq!(
std::fs::metadata(&path).unwrap().modified().unwrap(),
root_mtime
);
let (_, hit, _) = server
.prepare_vm_baseline_cached(
source,
Some(&path),
None,
root.path(),
root.path(),
&scope,
"code",
)
.await
.unwrap();
assert_eq!(hit, Some(false), "import body changes invalidate VM setup");
std::fs::write(&facade, "pub import \"./dependency.harn\"\n").unwrap();
let (_, hit, _) = server
.prepare_vm_baseline_cached(
source,
Some(&path),
None,
root.path(),
root.path(),
&scope,
"code",
)
.await
.unwrap();
assert_eq!(hit, Some(false), "re-export changes invalidate VM setup");
}
#[test]
fn compile_pipeline_cached_serves_cached_chunk_until_mtime_advances() {
let dir = tempfile::tempdir().expect("tempdir");
let pipeline_path = dir.path().join("p.harn");
let initial = "pipeline main(harness: Harness) { harness.stdio.println(\"first\") }\n";
std::fs::write(&pipeline_path, initial).expect("write initial");
let mut server = AcpServer::new(AcpServerConfig::new(Some(
pipeline_path.to_string_lossy().to_string(),
)));
let (_chunk, hit1) = server
.compile_pipeline_cached(initial, Some(pipeline_path.as_path()), None)
.expect("first compile");
assert!(!hit1, "first compile must miss the cache");
let (_chunk, hit2) = server
.compile_pipeline_cached(initial, Some(pipeline_path.as_path()), None)
.expect("second compile");
assert!(hit2, "second compile of unchanged source must hit");
let named_source = "@command(name: \"alpha\") pipeline alpha(harness: Harness) { harness.stdio.println(\"alpha\") }\n\
pipeline main(harness: Harness) { harness.stdio.println(\"main\") }\n";
std::fs::write(&pipeline_path, named_source).expect("write named");
let bumped = filetime::FileTime::from_unix_time(2_000_000_000, 0);
filetime::set_file_mtime(&pipeline_path, bumped).expect("bump mtime");
let (_chunk, hit3) = server
.compile_pipeline_cached(named_source, Some(pipeline_path.as_path()), Some("alpha"))
.expect("named compile");
assert!(
!hit3,
"different mtime + target_pipeline must miss the previous slot"
);
let (_chunk, hit4) = server
.compile_pipeline_cached(named_source, Some(pipeline_path.as_path()), Some("alpha"))
.expect("named compile second");
assert!(hit4, "repeated named compile must hit");
}
#[test]
fn compile_pipeline_cached_does_not_cache_inline_prompts() {
let mut server = AcpServer::new(AcpServerConfig::new(None));
let source = "pipeline main(harness: Harness) { harness.stdio.println(\"inline\") }\n";
let (_chunk, hit1) = server
.compile_pipeline_cached(source, None, None)
.expect("first inline compile");
assert!(!hit1);
let (_chunk, hit2) = server
.compile_pipeline_cached(source, None, None)
.expect("second inline compile");
assert!(
!hit2,
"inline-mode compiles must not be cached (per-turn source is dynamic)"
);
}
#[tokio::test(flavor = "current_thread")]
async fn vm_baseline_cached_serves_file_backed_context_until_key_changes() {
let dir = tempfile::tempdir().expect("tempdir");
let pipeline_path = dir.path().join("baseline.harn");
let source = "pipeline main(harness: Harness) { harness.stdio.println(\"baseline\") }\n";
std::fs::write(&pipeline_path, source).expect("write pipeline");
let store_scope = harn_vm::SessionStoreScope::resolve(
dir.path(),
&harn_vm::security::LauncherEnvironment::from_snapshot(Default::default()),
)
.unwrap();
let mut server = AcpServer::new(AcpServerConfig::new(Some(
pipeline_path.to_string_lossy().to_string(),
)));
let (_baseline, hit1, _ms1) = server
.prepare_vm_baseline_cached(
source,
Some(pipeline_path.as_path()),
None,
dir.path(),
dir.path(),
&store_scope,
"code",
)
.await
.expect("first prepare");
assert_eq!(hit1, Some(false), "first prepare must fill the cache");
let (_baseline, hit2, _ms2) = server
.prepare_vm_baseline_cached(
source,
Some(pipeline_path.as_path()),
None,
dir.path(),
dir.path(),
&store_scope,
"code",
)
.await
.expect("second prepare");
assert_eq!(hit2, Some(true), "unchanged file-backed context must hit");
let (_baseline, hit3, _ms3) = server
.prepare_vm_baseline_cached(
source,
Some(pipeline_path.as_path()),
Some("review"),
dir.path(),
dir.path(),
&store_scope,
"code",
)
.await
.expect("target prepare");
assert_eq!(
hit3,
Some(false),
"target pipeline is part of baseline invalidation"
);
let (_baseline, hit4, _ms4) = server
.prepare_vm_baseline_cached(
source,
Some(pipeline_path.as_path()),
Some("review"),
dir.path(),
dir.path(),
&store_scope,
"plan",
)
.await
.expect("mode prepare");
assert_eq!(
hit4,
Some(false),
"ACP mode is part of baseline invalidation"
);
let other_root = tempfile::tempdir().expect("other project root");
let (_baseline, hit5, _ms5) = server
.prepare_vm_baseline_cached(
source,
Some(pipeline_path.as_path()),
Some("review"),
dir.path(),
other_root.path(),
&store_scope,
"plan",
)
.await
.expect("different project-root prepare");
assert_eq!(
hit5,
Some(false),
"session project root is part of baseline invalidation"
);
let (baseline, hit6, _ms6) = server
.prepare_vm_baseline_cached(
source,
None,
None,
dir.path(),
dir.path(),
&store_scope,
"code",
)
.await
.expect("inline prepare");
assert!(
baseline.is_some(),
"inline VM setup must precede prompt admission"
);
assert_eq!(hit6, None);
}