#[cfg(all(feature = "localfs", feature = "overlay"))]
mod overlay_tests {
use kaish_kernel::{Kernel, KernelConfig};
use std::path::Path;
fn overlay_kernel(dir: &Path) -> Kernel {
let config = KernelConfig::mcp_with_root(dir.to_path_buf())
.with_overlay(true)
.with_latch(false)
.with_trash(false)
.with_allow_external_commands(false);
Kernel::new(config).expect("failed to create overlay kernel")
}
async fn run(kernel: &Kernel, script: &str) -> (String, i64) {
let result = kernel.execute(script).await.expect("kernel execute");
(result.text_out().trim().to_string(), result.code)
}
fn status_field<'a>(status_out: &'a str, key: &str) -> &'a str {
for line in status_out.lines() {
if let Some(rest) = line.strip_prefix(key) {
if let Some(value) = rest.strip_prefix('\t') {
return value;
}
}
}
panic!("kaish-vfs status: field '{}' not found in:\n{}", key, status_out);
}
#[tokio::test]
async fn overlay_end_to_end_write_diff_commit() {
let dir = tempfile::tempdir().expect("tempdir");
let root = dir.path();
std::fs::write(root.join("hello.txt"), b"original content\n").unwrap();
let kernel = overlay_kernel(root);
let cwd = root.to_string_lossy();
let (_, code) = run(&kernel, &format!(
"echo 'edited content' > \"{}/hello.txt\"", cwd
)).await;
assert_eq!(code, 0, "overlay write should succeed");
let real_bytes = std::fs::read(root.join("hello.txt")).unwrap();
assert_eq!(real_bytes, b"original content\n",
"real file must be byte-identical after virtual write");
let (status_out, code) = run(&kernel, "kaish-vfs status").await;
assert_eq!(code, 0, "kaish-vfs status failed: {}", status_out);
assert_eq!(status_field(&status_out, "dirty"), "yes",
"overlay should be dirty: {}", status_out);
let display_path = format!("{}/hello.txt", cwd);
let (diff_out, diff_code) = run(&kernel, "kaish-vfs diff").await;
assert_eq!(diff_code, 1, "diff should exit 1 when dirty");
assert!(diff_out.lines().any(|l| l == "-original content"),
"diff output must contain '-original content' line: {}", diff_out);
assert!(diff_out.lines().any(|l| l == "+edited content"),
"diff output must contain '+edited content' line: {}", diff_out);
let triple_minus_count = diff_out.lines()
.filter(|l| l.starts_with("---") && l.contains(&*display_path))
.count();
assert_eq!(triple_minus_count, 1,
"--- header for {} must appear exactly once: {}", display_path, diff_out);
let triple_plus_count = diff_out.lines()
.filter(|l| l.starts_with("+++") && l.contains(&*display_path))
.count();
assert_eq!(triple_plus_count, 1,
"+++ header for {} must appear exactly once: {}", display_path, diff_out);
let (commit_out, code) = run(&kernel, "kaish-vfs commit").await;
assert_eq!(code, 0, "commit failed: {}", commit_out);
let committed_bytes = std::fs::read(root.join("hello.txt")).unwrap();
assert_eq!(committed_bytes, b"edited content\n",
"real file should have new content after commit");
let (status_after, code) = run(&kernel, "kaish-vfs status").await;
assert_eq!(code, 0);
assert_eq!(status_field(&status_after, "dirty"), "no",
"overlay should be clean after commit: {}", status_after);
}
#[tokio::test]
async fn overlay_commit_add_remove_nested() {
let dir = tempfile::tempdir().expect("tempdir");
let root = dir.path();
std::fs::write(root.join("modify.txt"), b"before\n").unwrap();
std::fs::write(root.join("to_remove.txt"), b"will be gone\n").unwrap();
let kernel = overlay_kernel(root);
let cwd = root.to_string_lossy();
let (_, code) = run(&kernel, &format!(
"echo 'after' > \"{}/modify.txt\"", cwd
)).await;
assert_eq!(code, 0);
let (_, code) = run(&kernel, &format!(
"mkdir -p \"{}/sub/dir\" && echo 'new nested' > \"{}/sub/dir/new.txt\"", cwd, cwd
)).await;
assert_eq!(code, 0, "mkdir+write in overlay should succeed");
let (_, code) = run(&kernel, &format!(
"rm \"{}/to_remove.txt\"", cwd
)).await;
assert_eq!(code, 0);
assert_eq!(std::fs::read(root.join("modify.txt")).unwrap(), b"before\n",
"modify.txt must still have original bytes before commit");
assert!(!root.join("sub").exists(),
"sub/ dir must not exist on real disk before commit");
assert!(root.join("to_remove.txt").exists(),
"to_remove.txt must still exist on real disk before commit");
let (commit_out, code) = run(&kernel, "kaish-vfs commit").await;
assert_eq!(code, 0, "commit failed: {}", commit_out);
assert_eq!(std::fs::read(root.join("modify.txt")).unwrap(), b"after\n",
"modify.txt must have new bytes after commit");
let nested_bytes = std::fs::read(root.join("sub/dir/new.txt"))
.expect("sub/dir/new.txt must exist after commit");
assert_eq!(nested_bytes, b"new nested\n",
"nested file must have exact bytes after commit");
assert!(!root.join("to_remove.txt").exists(),
"to_remove.txt must be GONE from real tree after commit");
}
#[tokio::test]
async fn overlay_diff_added_file_header_appears_once() {
let dir = tempfile::tempdir().expect("tempdir");
let root = dir.path();
let kernel = overlay_kernel(root);
let cwd = root.to_string_lossy();
let (_, code) = run(&kernel, &format!(
"echo 'brand new' > \"{}/added.txt\"", cwd
)).await;
assert_eq!(code, 0);
let (diff_out, diff_code) = run(&kernel, "kaish-vfs diff").await;
assert_eq!(diff_code, 1, "diff should exit 1 when there are changes");
let display_path = format!("{}/added.txt", cwd);
let minus_headers: Vec<&str> = diff_out.lines()
.filter(|l| l.starts_with("---"))
.collect();
assert_eq!(minus_headers.len(), 1, "exactly one --- header: {:?}", minus_headers);
assert!(minus_headers[0].contains("/dev/null"),
"--- header for Added file must reference /dev/null: {}", minus_headers[0]);
let plus_headers: Vec<&str> = diff_out.lines()
.filter(|l| l.starts_with("+++"))
.collect();
assert_eq!(plus_headers.len(), 1, "exactly one +++ header: {:?}", plus_headers);
assert!(plus_headers[0].contains(&*display_path),
"+++ header must name the file path: {}", plus_headers[0]);
assert!(diff_out.lines().any(|l| l == "+brand new"),
"diff must contain '+brand new' line: {}", diff_out);
}
#[tokio::test]
async fn overlay_reset_discards_edits() {
let dir = tempfile::tempdir().expect("tempdir");
let root = dir.path();
std::fs::write(root.join("data.txt"), b"original\n").unwrap();
let kernel = overlay_kernel(root);
let cwd = root.to_string_lossy();
let (_, code) = run(&kernel, &format!(
"echo 'virtual' > \"{}/data.txt\"", cwd
)).await;
assert_eq!(code, 0);
let (cat_out, code) = run(&kernel, &format!(
"cat \"{}/data.txt\"", cwd
)).await;
assert_eq!(code, 0);
assert!(cat_out.contains("virtual"), "overlay should see new content: {}", cat_out);
let (reset_out, code) = run(&kernel, "kaish-vfs reset").await;
assert_eq!(code, 0, "reset failed: {}", reset_out);
let (status, code) = run(&kernel, "kaish-vfs status").await;
assert_eq!(code, 0);
assert_eq!(status_field(&status, "dirty"), "no",
"overlay should be clean after reset: {}", status);
let real_bytes = std::fs::read(root.join("data.txt")).unwrap();
assert_eq!(real_bytes, b"original\n", "real file should be untouched after reset");
let (cat_after, code) = run(&kernel, &format!(
"cat \"{}/data.txt\"", cwd
)).await;
assert_eq!(code, 0);
assert!(cat_after.contains("original"), "overlay should show lower content after reset: {}", cat_after);
}
#[tokio::test]
async fn overlay_reset_single_path_leaves_other_dirty() {
let dir = tempfile::tempdir().expect("tempdir");
let root = dir.path();
std::fs::write(root.join("file_a.txt"), b"aaa\n").unwrap();
std::fs::write(root.join("file_b.txt"), b"bbb\n").unwrap();
let kernel = overlay_kernel(root);
let cwd = root.to_string_lossy();
let (_, code) = run(&kernel, &format!(
"echo 'modified_a' > \"{}/file_a.txt\"", cwd
)).await;
assert_eq!(code, 0);
let (_, code) = run(&kernel, &format!(
"echo 'modified_b' > \"{}/file_b.txt\"", cwd
)).await;
assert_eq!(code, 0);
let rel_a = "file_a.txt";
let (reset_out, code) = run(&kernel, &format!(
"cd \"{}\" && kaish-vfs reset {}", cwd, rel_a
)).await;
assert_eq!(code, 0, "single-path reset failed: {}", reset_out);
let (cat_a, _) = run(&kernel, &format!("cat \"{}/file_a.txt\"", cwd)).await;
assert!(cat_a.contains("aaa"), "file_a should revert: {}", cat_a);
let (cat_b, _) = run(&kernel, &format!("cat \"{}/file_b.txt\"", cwd)).await;
assert!(cat_b.contains("modified_b"), "file_b should still be modified: {}", cat_b);
let (status, code) = run(&kernel, "kaish-vfs status").await;
assert_eq!(code, 0);
assert_eq!(status_field(&status, "mode"), "transaction");
assert_eq!(status_field(&status, "dirty"), "yes",
"overlay should still be dirty after partial reset: {}", status);
assert_eq!(status_field(&status, "modified"), "1",
"should have exactly 1 modified file remaining: {}", status);
}
#[tokio::test]
async fn kaish_vfs_without_overlay() {
let kernel = Kernel::new(
KernelConfig::isolated()
).expect("kernel");
let result = kernel.execute("kaish-vfs status").await.expect("execute");
assert_eq!(
result.code, 0,
"kaish-vfs status must work in any session: {}",
result.err
);
let status = result.text_out();
assert_eq!(status_field(&status, "mode"), "direct");
assert_eq!(status_field(&status, "budget"), "unlimited");
for subcmd in ["diff", "commit", "reset"] {
let result = kernel
.execute(&format!("kaish-vfs {subcmd}"))
.await
.expect("execute");
assert_ne!(
result.code, 0,
"kaish-vfs {subcmd} on a non-overlay kernel should exit non-zero"
);
let combined = format!("{}{}", result.text_out(), result.err);
assert!(
combined.contains("no overlay active"),
"kaish-vfs {subcmd} should say 'no overlay active': {}",
combined
);
}
}
#[test]
fn nolocal_plus_overlay_fails_loudly() {
let result = Kernel::new(
KernelConfig::isolated().with_overlay(true)
);
assert!(result.is_err(), "NoLocal + overlay should fail at construction");
let err_msg = result.map(|_| "ok".to_string()).unwrap_or_else(|e| e.to_string());
assert!(err_msg.contains("NoLocal") || err_msg.contains("overlay") || err_msg.contains("virtual"),
"error should mention NoLocal or overlay: {}", err_msg);
}
#[tokio::test]
async fn overlay_budget_exceeded_fails_inband() {
let dir = tempfile::tempdir().expect("tempdir");
let root = dir.path();
let config = KernelConfig::mcp_with_root(root.to_path_buf())
.with_overlay(true)
.with_vfs_budget(100)
.with_latch(false)
.with_trash(false)
.with_allow_external_commands(false);
let kernel = Kernel::new(config).expect("kernel");
let cwd = root.to_string_lossy();
let big_script = format!(
"echo 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' > \"{}/big.txt\"",
cwd
);
let (out, code) = run(&kernel, &big_script).await;
assert_ne!(code, 0, "write exceeding budget should fail: out={}", out);
let err = kernel.execute(&big_script).await.expect("execute").err;
assert!(err.contains("vfs-memory") || out.contains("vfs-memory"),
"error should name 'vfs-memory' budget: err={:?}", err);
}
#[tokio::test]
async fn overlay_glob_over_merged_dir() {
let dir = tempfile::tempdir().expect("tempdir");
let root = dir.path();
std::fs::write(root.join("a.txt"), b"a").unwrap();
std::fs::write(root.join("b.txt"), b"b").unwrap();
let kernel = overlay_kernel(root);
let cwd = root.to_string_lossy();
let (_, code) = run(&kernel, &format!(
"echo 'c' > \"{}/c.txt\"", cwd
)).await;
assert_eq!(code, 0);
let (_, code) = run(&kernel, &format!(
"rm \"{}/b.txt\"", cwd
)).await;
assert_eq!(code, 0);
let (glob_out, code) = run(&kernel, &format!(
"for f in \"{}/\"*.txt; do echo $f; done", cwd
)).await;
assert_eq!(code, 0, "glob over merged dir failed: {}", glob_out);
assert!(glob_out.contains("a.txt"), "a.txt should be in merged glob: {}", glob_out);
assert!(glob_out.contains("c.txt"), "c.txt should be in merged glob: {}", glob_out);
assert!(!glob_out.contains("b.txt"), "b.txt should be whiteouted: {}", glob_out);
assert!(!root.join("c.txt").exists(),
"c.txt must NOT exist on real disk (overlay-only file)");
assert!(root.join("b.txt").exists(),
"b.txt must still exist on real disk (only whiteouted in overlay)");
assert!(root.join("a.txt").exists(),
"a.txt must still exist on real disk (unchanged)");
}
#[tokio::test]
async fn overlay_diff_path_filter_excludes_other_files() {
let dir = tempfile::tempdir().expect("tempdir");
let root = dir.path();
std::fs::write(root.join("alpha.txt"), b"alpha\n").unwrap();
std::fs::write(root.join("beta.txt"), b"beta\n").unwrap();
let kernel = overlay_kernel(root);
let cwd = root.to_string_lossy();
let (_, code) = run(&kernel, &format!(
"echo 'ALPHA' > \"{}/alpha.txt\"", cwd
)).await;
assert_eq!(code, 0);
let (_, code) = run(&kernel, &format!(
"echo 'BETA' > \"{}/beta.txt\"", cwd
)).await;
assert_eq!(code, 0);
let (diff_out, diff_code) = run(&kernel, &format!(
"cd \"{}\" && kaish-vfs diff alpha.txt", cwd
)).await;
assert_eq!(diff_code, 1, "filtered diff should exit 1 (changes present): {}", diff_out);
assert!(diff_out.contains("alpha"), "filtered diff must mention alpha.txt: {}", diff_out);
assert!(!diff_out.contains("beta") && !diff_out.contains("BETA"),
"filtered diff must NOT include beta.txt changes: {}", diff_out);
let (full_diff, _) = run(&kernel, "kaish-vfs diff").await;
assert!(full_diff.contains("alpha"), "unfiltered diff must show alpha: {}", full_diff);
assert!(full_diff.contains("beta"), "unfiltered diff must show beta: {}", full_diff);
}
#[tokio::test]
async fn overlay_commit_conflict_fails_inband() {
let dir = tempfile::tempdir().expect("tempdir");
let root = dir.path();
let kernel = overlay_kernel(root);
let cwd = root.to_string_lossy();
let (_, code) = run(&kernel, &format!(
"echo 'overlay version' > \"{}/conflict.txt\"", cwd
)).await;
assert_eq!(code, 0);
std::fs::write(root.join("conflict.txt"), b"real version\n").unwrap();
let (commit_out, commit_code) = run(&kernel, "kaish-vfs commit").await;
assert_ne!(commit_code, 0,
"commit should fail when Added file conflicts with existing lower: {}", commit_out);
let combined = format!("{}{}", commit_out,
kernel.execute("kaish-vfs commit").await.map(|r| r.err).unwrap_or_default());
assert!(
combined.to_lowercase().contains("conflict") || commit_out.to_lowercase().contains("conflict"),
"commit error must mention 'conflict': {}", commit_out
);
let (status_after, status_code) = run(&kernel, "kaish-vfs status").await;
assert_eq!(status_code, 0);
assert_eq!(status_field(&status_after, "dirty"), "yes",
"overlay must remain dirty after failed commit: {}", status_after);
}
#[tokio::test]
async fn overlay_diff_json_is_structured() {
let dir = tempfile::tempdir().expect("tempdir");
let root = dir.path();
std::fs::write(root.join("src.txt"), b"old line\n").unwrap();
let kernel = overlay_kernel(root);
let cwd = root.to_string_lossy();
let (_, code) = run(&kernel, &format!(
"echo 'new line' > \"{}/src.txt\"", cwd
)).await;
assert_eq!(code, 0);
let (_, code) = run(&kernel, &format!(
"echo 'added' > \"{}/new.txt\"", cwd
)).await;
assert_eq!(code, 0);
let result = kernel.execute("kaish-vfs diff --json").await.expect("execute");
assert_eq!(result.code, 1, "kaish-vfs diff --json should exit 1 when dirty");
let json_str = result.text_out();
let v: serde_json::Value = serde_json::from_str(json_str.trim())
.unwrap_or_else(|e| panic!("kaish-vfs diff --json must produce valid JSON: {e}\ngot: {json_str:?}"));
let arr = v.as_array()
.unwrap_or_else(|| panic!("kaish-vfs diff --json must be a JSON array, got: {v}"));
assert!(!arr.is_empty(), "diff --json must not be empty when changes exist");
for entry in arr {
let obj = entry.as_object()
.unwrap_or_else(|| panic!("each diff entry must be a JSON object: {entry}"));
assert!(obj.contains_key("path"), "entry missing 'path': {entry}");
assert!(obj.contains_key("kind"), "entry missing 'kind': {entry}");
assert!(obj.contains_key("base_bytes"), "entry missing 'base_bytes': {entry}");
assert!(obj.contains_key("current_bytes"), "entry missing 'current_bytes': {entry}");
let kind = obj["kind"].as_str().expect("kind must be a string");
assert!(
kind == "added" || kind == "modified" || kind == "removed",
"kind must be 'added', 'modified', or 'removed': {kind}"
);
}
let src_entry = arr.iter().find(|e| {
e["path"].as_str().map(|p| p.ends_with("src.txt")).unwrap_or(false)
});
assert!(src_entry.is_some(), "must have entry for src.txt: {arr:?}");
assert_eq!(src_entry.unwrap()["kind"].as_str(), Some("modified"),
"src.txt must be 'modified': {src_entry:?}");
let new_entry = arr.iter().find(|e| {
e["path"].as_str().map(|p| p.ends_with("new.txt")).unwrap_or(false)
});
assert!(new_entry.is_some(), "must have entry for new.txt: {arr:?}");
assert_eq!(new_entry.unwrap()["kind"].as_str(), Some("added"),
"new.txt must be 'added': {new_entry:?}");
}
#[tokio::test]
async fn overlay_diff_exits_0_when_clean() {
let dir = tempfile::tempdir().expect("tempdir");
let root = dir.path();
let kernel = overlay_kernel(root);
let (out, code) = run(&kernel, "kaish-vfs diff").await;
assert_eq!(code, 0, "diff should exit 0 when overlay is clean: {}", out);
}
}
#[cfg(all(feature = "localfs", feature = "overlay"))]
mod diff_header_tests {
use kaish_kernel::{Kernel, KernelConfig};
async fn run(kernel: &Kernel, script: &str) -> (String, i64) {
let result = kernel.execute(script).await.expect("kernel execute");
(result.text_out().trim().to_string(), result.code)
}
#[tokio::test]
async fn diff_added_file_shows_devnull_header() {
let dir = tempfile::tempdir().expect("tempdir");
let root = dir.path();
let config = KernelConfig::mcp_with_root(root.to_path_buf())
.with_overlay(true)
.with_latch(false)
.with_trash(false)
.with_allow_external_commands(false);
let kernel = Kernel::new(config).expect("kernel");
let cwd = root.to_string_lossy();
let (_, code) = run(&kernel, &format!(
"echo 'new file' > \"{}/new.txt\"", cwd
)).await;
assert_eq!(code, 0);
let (diff_out, _) = run(&kernel, "kaish-vfs diff").await;
assert!(diff_out.contains("/dev/null"),
"added file diff should reference /dev/null: {}", diff_out);
let minus_headers: Vec<&str> = diff_out.lines()
.filter(|l| l.starts_with("---"))
.collect();
assert_eq!(minus_headers.len(), 1,
"exactly one --- header expected, got: {:?}", minus_headers);
assert!(diff_out.lines().any(|l| l == "+new file"),
"added file diff must contain '+new file' line: {}", diff_out);
}
}