use std::fs;
use std::io::{ErrorKind, Write};
use std::path::{Path, PathBuf};
use anyhow::{Context, bail};
use crate::fsutil;
const MAX_CLAIM_RETRIES: u32 = 128;
pub(crate) enum Acquired {
Won,
AlreadyHeld,
}
pub(crate) trait Claim {
fn claim(&self, claim: &Path) -> anyhow::Result<Acquired>;
}
pub(crate) struct LocalFs;
impl Claim for LocalFs {
fn claim(&self, claim: &Path) -> anyhow::Result<Acquired> {
match fs::create_dir(claim) {
Ok(()) => Ok(Acquired::Won),
Err(e) if e.kind() == ErrorKind::AlreadyExists => Ok(Acquired::AlreadyHeld),
Err(e) => Err(e).with_context(|| format!("Failed to claim {}", claim.display())),
}
}
}
pub(crate) struct Kind {
pub dir: &'static str,
pub prefix: &'static str,
pub scaffold: fn(&ScaffoldCtx<'_>) -> anyhow::Result<Fileset>,
}
pub(crate) struct ScaffoldCtx<'a> {
pub id: u32,
pub canonical: &'a str,
pub slug: &'a str,
pub title: &'a str,
pub date: &'a str,
}
pub(crate) enum Artifact {
File { rel_path: PathBuf, body: String },
Symlink { rel_path: PathBuf, target: String },
}
pub(crate) type Fileset = Vec<Artifact>;
pub(crate) struct Inputs<'a> {
pub slug: &'a str,
pub title: &'a str,
pub date: &'a str,
}
pub(crate) enum MaterialiseRequest {
Fresh,
InExisting { id: u32 },
}
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum OwnedEntityId {
Numbered { id: u32, canonical: String },
Named { name: String },
}
impl OwnedEntityId {
pub(crate) fn numeric_id(&self) -> Option<u32> {
match self {
OwnedEntityId::Numbered { id, .. } => Some(*id),
OwnedEntityId::Named { .. } => None,
}
}
}
#[derive(Debug)]
pub(crate) struct Materialised {
pub eid: OwnedEntityId,
pub dir: PathBuf,
}
pub(crate) fn candidate_id(existing: &[u32]) -> u32 {
existing.iter().copied().max().map_or(1, |m| m + 1)
}
pub(crate) fn next_id(local: &[u32], trunk: &[u32]) -> u32 {
let union: Vec<u32> = local.iter().copied().chain(trunk.iter().copied()).collect();
candidate_id(&union)
}
pub(crate) fn derive_slug(title: &str) -> String {
let mut slug = String::new();
let mut pending_dash = false;
for ch in title.chars() {
if ch.is_ascii_alphanumeric() {
if pending_dash && !slug.is_empty() {
slug.push('-');
}
pending_dash = false;
slug.push(ch.to_ascii_lowercase());
} else if ch.is_whitespace() || ch == '-' || ch == '_' {
pending_dash = true;
}
}
slug
}
pub(crate) fn scan_ids(tree_root: &Path) -> anyhow::Result<Vec<u32>> {
let mut ids = Vec::new();
let entries = match fs::read_dir(tree_root) {
Ok(entries) => entries,
Err(e) if e.kind() == ErrorKind::NotFound => return Ok(ids),
Err(e) => {
return Err(e).with_context(|| format!("Failed to read {}", tree_root.display()));
}
};
for entry in entries {
let entry = entry?;
if !entry.file_type()?.is_dir() {
continue;
}
if let Some(name) = entry.file_name().to_str()
&& let Ok(n) = name.parse::<u32>()
{
ids.push(n);
}
}
Ok(ids)
}
pub(crate) fn scan_named(tree_root: &Path) -> anyhow::Result<Vec<String>> {
let mut names = Vec::new();
let entries = match fs::read_dir(tree_root) {
Ok(entries) => entries,
Err(e) if e.kind() == ErrorKind::NotFound => return Ok(names),
Err(e) => {
return Err(e).with_context(|| format!("Failed to read {}", tree_root.display()));
}
};
for entry in entries {
let entry = entry?;
if !entry.file_type()?.is_dir() {
continue;
}
if let Some(name) = entry.file_name().to_str() {
names.push(name.to_string());
}
}
Ok(names)
}
pub(crate) fn materialise(
kind: &Kind,
claim: &dyn Claim,
project_root: &Path,
request: &MaterialiseRequest,
inputs: &Inputs<'_>,
trunk_ids: &[u32],
) -> anyhow::Result<Materialised> {
let tree_root = project_root.join(kind.dir);
fs::create_dir_all(&tree_root)
.with_context(|| format!("Failed to create {}", tree_root.display()))?;
match *request {
MaterialiseRequest::Fresh => {
allocate_fresh(kind, claim, &tree_root, inputs, trunk_ids, || {
scan_ids(&tree_root)
})
}
MaterialiseRequest::InExisting { id } => create_in_existing(kind, &tree_root, id, inputs),
}
}
fn allocate_fresh(
kind: &Kind,
claim: &dyn Claim,
tree_root: &Path,
inputs: &Inputs<'_>,
trunk_ids: &[u32],
scan: impl FnMut() -> anyhow::Result<Vec<u32>>,
) -> anyhow::Result<Materialised> {
claim_fresh_id(
claim,
tree_root,
kind.prefix,
trunk_ids,
scan,
|id, canonical| {
let ctx = ScaffoldCtx {
id,
canonical,
slug: inputs.slug,
title: inputs.title,
date: inputs.date,
};
(kind.scaffold)(&ctx)
},
)
}
pub(crate) fn materialise_fresh_prebuilt(
claim: &dyn Claim,
project_root: &Path,
dir: &str,
prefix: &str,
trunk_ids: &[u32],
build: impl FnMut(u32, &str) -> anyhow::Result<Fileset>,
) -> anyhow::Result<Materialised> {
let tree_root = project_root.join(dir);
fs::create_dir_all(&tree_root)
.with_context(|| format!("Failed to create {}", tree_root.display()))?;
claim_fresh_id(
claim,
&tree_root,
prefix,
trunk_ids,
|| scan_ids(&tree_root),
build,
)
}
fn claim_fresh_id(
claim: &dyn Claim,
tree_root: &Path,
prefix: &str,
trunk_ids: &[u32],
mut scan: impl FnMut() -> anyhow::Result<Vec<u32>>,
mut build: impl FnMut(u32, &str) -> anyhow::Result<Fileset>,
) -> anyhow::Result<Materialised> {
for _ in 0..MAX_CLAIM_RETRIES {
let id = next_id(&scan()?, trunk_ids);
let name = format!("{id:03}");
let dir = tree_root.join(&name);
match claim.claim(&dir)? {
Acquired::Won => {
let canonical = format!("{prefix}-{name}");
let written = build(id, &canonical).and_then(|fs| write_fileset(tree_root, &fs));
return match written {
Ok(()) => Ok(Materialised {
eid: OwnedEntityId::Numbered { id, canonical },
dir,
}),
Err(e) => {
drop(fs::remove_dir_all(&dir));
Err(e)
}
};
}
Acquired::AlreadyHeld => {} }
}
bail!("Could not reserve an id after {MAX_CLAIM_RETRIES} attempts");
}
fn create_in_existing(
kind: &Kind,
tree_root: &Path,
id: u32,
inputs: &Inputs<'_>,
) -> anyhow::Result<Materialised> {
let name = format!("{id:03}");
let dir = tree_root.join(&name);
if !dir.is_dir() {
bail!("Parent entity {name} not found at {}", dir.display());
}
let canonical = format!("{}-{name}", kind.prefix);
let ctx = ScaffoldCtx {
id,
canonical: &canonical,
slug: inputs.slug,
title: inputs.title,
date: inputs.date,
};
let fileset = (kind.scaffold)(&ctx)?;
refuse_clobber(tree_root, &fileset)?; write_fileset(tree_root, &fileset)?;
Ok(Materialised {
eid: OwnedEntityId::Numbered { id, canonical },
dir,
})
}
pub(crate) fn materialise_named(
claim: &dyn Claim,
project_root: &Path,
dir: &str,
name: &str,
fileset: &Fileset,
) -> anyhow::Result<Materialised> {
let tree_root = project_root.join(dir);
fs::create_dir_all(&tree_root)
.with_context(|| format!("Failed to create {}", tree_root.display()))?;
let entity_dir = claim_and_write_named(claim, &tree_root, name, fileset)?;
Ok(Materialised {
eid: OwnedEntityId::Named {
name: name.to_string(),
},
dir: entity_dir,
})
}
fn claim_and_write_named(
claim: &dyn Claim,
tree_root: &Path,
name: &str,
fileset: &Fileset,
) -> anyhow::Result<PathBuf> {
let dir = tree_root.join(name);
match claim.claim(&dir)? {
Acquired::Won => match write_fileset(tree_root, fileset) {
Ok(()) => Ok(dir),
Err(e) => {
drop(fs::remove_dir_all(&dir));
Err(e)
}
},
Acquired::AlreadyHeld => bail!("entity {name} already exists"),
}
}
fn refuse_clobber(tree_root: &Path, fileset: &Fileset) -> anyhow::Result<()> {
for art in fileset {
let abs = fsutil::safe_join(tree_root, artifact_rel(art))?;
if abs.exists() {
bail!("Refusing to overwrite existing {}", abs.display());
}
}
Ok(())
}
fn write_fileset(tree_root: &Path, fileset: &Fileset) -> anyhow::Result<()> {
let mut created_paths: Vec<PathBuf> = Vec::new(); let mut created_dirs: Vec<PathBuf> = Vec::new();
match write_fileset_tracked(tree_root, fileset, &mut created_paths, &mut created_dirs) {
Ok(()) => Ok(()),
Err(e) => {
rollback(&created_paths, &created_dirs);
Err(e)
}
}
}
fn write_fileset_tracked(
tree_root: &Path,
fileset: &Fileset,
created_paths: &mut Vec<PathBuf>,
created_dirs: &mut Vec<PathBuf>,
) -> anyhow::Result<()> {
for art in fileset {
let rel = artifact_rel(art);
let abs = fsutil::safe_join(tree_root, rel)?;
ensure_parent_dirs(tree_root, rel, created_dirs)?;
match art {
Artifact::File { body, .. } => {
let mut f = fsutil::create_new_file(&abs)
.with_context(|| format!("Failed to create {}", abs.display()))?;
created_paths.push(abs.clone());
f.write_all(body.as_bytes())
.with_context(|| format!("Failed to write {}", abs.display()))?;
}
Artifact::Symlink { target, .. } => {
std::os::unix::fs::symlink(target, &abs)
.with_context(|| format!("Failed to symlink {}", abs.display()))?;
created_paths.push(abs.clone());
}
}
}
Ok(())
}
fn ensure_parent_dirs(
tree_root: &Path,
rel: &Path,
created_dirs: &mut Vec<PathBuf>,
) -> anyhow::Result<()> {
let Some(parent) = rel.parent() else {
return Ok(());
};
let mut cur = tree_root.to_path_buf();
for comp in parent.components() {
cur.push(comp);
match fs::create_dir(&cur) {
Ok(()) => created_dirs.push(cur.clone()),
Err(e) if e.kind() == ErrorKind::AlreadyExists => {
if !fsutil::is_real_dir(&cur) {
bail!(
"Failed to create {}: a non-directory squats that path",
cur.display()
);
}
}
Err(e) => {
return Err(e).with_context(|| format!("Failed to create {}", cur.display()));
}
}
}
Ok(())
}
fn rollback(created_paths: &[PathBuf], created_dirs: &[PathBuf]) {
for path in created_paths.iter().rev() {
drop(fs::remove_file(path)); }
for dir in created_dirs.iter().rev() {
drop(fs::remove_dir(dir));
}
}
fn artifact_rel(art: &Artifact) -> &Path {
match art {
Artifact::File { rel_path, .. } | Artifact::Symlink { rel_path, .. } => rel_path,
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::Cell;
#[test]
fn candidate_id_empty_is_one() {
assert_eq!(candidate_id(&[]), 1);
}
#[test]
fn candidate_id_is_max_plus_one_ignoring_gaps() {
assert_eq!(candidate_id(&[1, 2, 3]), 4);
assert_eq!(candidate_id(&[1, 3]), 4);
assert_eq!(candidate_id(&[5]), 6);
}
#[test]
fn next_id_empty_union_is_one() {
assert_eq!(next_id(&[], &[]), 1);
}
#[test]
fn next_id_local_only_equals_candidate_id() {
for local in [&[][..], &[1, 2, 3], &[5], &[1, 3]] {
assert_eq!(next_id(local, &[]), candidate_id(local));
}
}
#[test]
fn next_id_is_max_of_union_plus_one() {
assert_eq!(next_id(&[1, 2], &[5, 3]), 6); assert_eq!(next_id(&[7], &[2, 3]), 8); assert_eq!(next_id(&[], &[4]), 5); assert_eq!(next_id(&[4], &[4]), 5); }
#[test]
fn derive_slug_normalises_title() {
assert_eq!(derive_slug("Add skill removal"), "add-skill-removal");
assert_eq!(derive_slug("Hello, World!"), "hello-world");
assert_eq!(derive_slug(" trim edges "), "trim-edges");
assert_eq!(derive_slug("snake_and-dash"), "snake-and-dash");
}
#[test]
fn scan_ids_collects_numeric_dirs_only() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
fs::create_dir(root.join("001")).unwrap();
fs::create_dir(root.join("002")).unwrap();
fs::create_dir(root.join("not-a-slice")).unwrap();
fs::write(root.join("003"), "a file, not a dir").unwrap();
std::os::unix::fs::symlink("001", root.join("001-some-slug")).unwrap();
let mut ids = scan_ids(root).unwrap();
ids.sort_unstable();
assert_eq!(ids, vec![1, 2]);
}
#[test]
fn scan_ids_missing_dir_is_empty() {
let dir = tempfile::tempdir().unwrap();
assert!(scan_ids(&dir.path().join("nope")).unwrap().is_empty());
}
#[test]
fn local_fs_acquire_wins_then_already_held() {
let dir = tempfile::tempdir().unwrap();
let claim = dir.path().join("001");
assert!(matches!(LocalFs.claim(&claim).unwrap(), Acquired::Won));
assert!(matches!(
LocalFs.claim(&claim).unwrap(),
Acquired::AlreadyHeld
));
}
fn one_file(ctx: &ScaffoldCtx<'_>) -> anyhow::Result<Fileset> {
let (id, canonical) = (ctx.id, ctx.canonical);
let name = format!("{id:03}");
Ok(vec![Artifact::File {
rel_path: PathBuf::from(format!("{name}/body.md")),
body: format!("{canonical} :: {}", ctx.title),
}])
}
const TEST_KIND: Kind = Kind {
dir: "tree",
prefix: "TK",
scaffold: one_file,
};
fn inputs() -> Inputs<'static> {
Inputs {
slug: "s",
title: "T",
date: "2026-06-04",
}
}
#[test]
fn allocate_fresh_writes_then_lands_first_id() {
let dir = tempfile::tempdir().unwrap();
let tree = dir.path().join("tree");
fs::create_dir_all(&tree).unwrap();
let out = allocate_fresh(&TEST_KIND, &LocalFs, &tree, &inputs(), &[], || {
scan_ids(&tree)
})
.unwrap();
assert_eq!(out.eid.numeric_id(), Some(1));
let body = fs::read_to_string(tree.join("001/body.md")).unwrap();
assert_eq!(body, "TK-001 :: T");
}
#[test]
fn allocate_fresh_retries_on_collision_through_the_seam() {
let dir = tempfile::tempdir().unwrap();
let tree = dir.path().join("tree");
fs::create_dir_all(&tree).unwrap();
fs::create_dir(tree.join("001")).unwrap();
let calls = Cell::new(0u32);
let scan = || {
let n = calls.get();
calls.set(n + 1);
Ok(if n == 0 { vec![] } else { vec![1] })
};
let out = allocate_fresh(&TEST_KIND, &LocalFs, &tree, &inputs(), &[], scan).unwrap();
assert_eq!(out.eid.numeric_id(), Some(2));
assert!(tree.join("002/body.md").is_file());
assert_eq!(calls.get(), 2, "expected one collision then success");
}
#[test]
fn allocate_fresh_bails_after_bounded_retries() {
struct AlwaysHeld;
impl Claim for AlwaysHeld {
fn claim(&self, _claim: &Path) -> anyhow::Result<Acquired> {
Ok(Acquired::AlreadyHeld)
}
}
let dir = tempfile::tempdir().unwrap();
let tree = dir.path().join("tree");
fs::create_dir_all(&tree).unwrap();
let err = allocate_fresh(&TEST_KIND, &AlwaysHeld, &tree, &inputs(), &[], || {
Ok(vec![])
})
.unwrap_err();
assert!(err.to_string().contains("Could not reserve an id"));
}
fn doomed_fileset(ctx: &ScaffoldCtx<'_>) -> anyhow::Result<Fileset> {
let id = ctx.id;
let name = format!("{id:03}");
Ok(vec![
Artifact::File {
rel_path: PathBuf::from(format!("{name}/a")),
body: "x".to_string(),
},
Artifact::File {
rel_path: PathBuf::from(format!("{name}/a/b")),
body: "y".to_string(),
},
])
}
const DOOMED_KIND: Kind = Kind {
dir: "tree",
prefix: "TK",
scaffold: doomed_fileset,
};
#[test]
fn reserved_materialise_write_failure_cleans_up_the_won_directory() {
let dir = tempfile::tempdir().unwrap();
let tree = dir.path().join("tree");
fs::create_dir_all(&tree).unwrap();
let err = allocate_fresh(&DOOMED_KIND, &LocalFs, &tree, &inputs(), &[], || {
scan_ids(&tree)
})
.unwrap_err();
assert!(err.to_string().contains("Failed to create"));
assert!(!tree.join("001").exists(), "the won dir must be removed");
}
fn escaping_fileset(_ctx: &ScaffoldCtx<'_>) -> anyhow::Result<Fileset> {
Ok(vec![Artifact::File {
rel_path: PathBuf::from("../escape.md"),
body: "x".to_string(),
}])
}
const ESCAPING_KIND: Kind = Kind {
dir: "tree",
prefix: "TK",
scaffold: escaping_fileset,
};
#[test]
fn reserved_materialise_rejects_an_escaping_descriptor_and_cleans_up() {
let dir = tempfile::tempdir().unwrap();
let tree = dir.path().join("tree");
fs::create_dir_all(&tree).unwrap();
let err = allocate_fresh(&ESCAPING_KIND, &LocalFs, &tree, &inputs(), &[], || {
scan_ids(&tree)
})
.unwrap_err();
assert!(err.to_string().contains("must not escape"));
assert!(!tree.join("001").exists());
assert!(!dir.path().join("escape.md").exists());
}
const SUB_KIND: Kind = Kind {
dir: "tree",
prefix: "TK",
scaffold: one_file,
};
#[test]
fn create_in_existing_writes_under_the_parent_without_reserving() {
let dir = tempfile::tempdir().unwrap();
let tree = dir.path().join("tree");
fs::create_dir_all(tree.join("003")).unwrap();
let out = create_in_existing(
&SUB_KIND,
&tree,
3,
&Inputs {
slug: "",
title: "Parent",
date: "2026-06-04",
},
)
.unwrap();
assert_eq!(out.eid.numeric_id(), Some(3));
let body = fs::read_to_string(tree.join("003/body.md")).unwrap();
assert_eq!(body, "TK-003 :: Parent");
}
#[test]
fn create_in_existing_errors_when_parent_absent() {
let dir = tempfile::tempdir().unwrap();
let tree = dir.path().join("tree");
fs::create_dir_all(&tree).unwrap();
let err = create_in_existing(
&SUB_KIND,
&tree,
9,
&Inputs {
slug: "",
title: "T",
date: "2026-06-04",
},
)
.unwrap_err();
assert!(err.to_string().contains("not found"));
}
#[test]
fn create_in_existing_refuses_to_clobber() {
let dir = tempfile::tempdir().unwrap();
let tree = dir.path().join("tree");
fs::create_dir_all(tree.join("003")).unwrap();
fs::write(tree.join("003/body.md"), "already here").unwrap();
let err = create_in_existing(
&SUB_KIND,
&tree,
3,
&Inputs {
slug: "",
title: "T",
date: "2026-06-04",
},
)
.unwrap_err();
assert!(err.to_string().contains("Refusing to overwrite"));
assert_eq!(
fs::read_to_string(tree.join("003/body.md")).unwrap(),
"already here"
);
}
fn two_files(ctx: &ScaffoldCtx<'_>) -> anyhow::Result<Fileset> {
let id = ctx.id;
let name = format!("{id:03}");
Ok(vec![
Artifact::File {
rel_path: PathBuf::from(format!("{name}/plan.toml")),
body: "p".to_string(),
},
Artifact::File {
rel_path: PathBuf::from(format!("{name}/plan.md")),
body: "m".to_string(),
},
])
}
const SUB_TWO_KIND: Kind = Kind {
dir: "tree",
prefix: "TK",
scaffold: two_files,
};
#[test]
fn create_in_existing_writes_a_multi_file_fileset() {
let dir = tempfile::tempdir().unwrap();
let tree = dir.path().join("tree");
fs::create_dir_all(tree.join("003")).unwrap();
create_in_existing(
&SUB_TWO_KIND,
&tree,
3,
&Inputs {
slug: "",
title: "T",
date: "2026-06-04",
},
)
.unwrap();
assert_eq!(fs::read_to_string(tree.join("003/plan.toml")).unwrap(), "p");
assert_eq!(fs::read_to_string(tree.join("003/plan.md")).unwrap(), "m");
}
fn sub_doomed_fileset(ctx: &ScaffoldCtx<'_>) -> anyhow::Result<Fileset> {
let id = ctx.id;
let name = format!("{id:03}");
Ok(vec![
Artifact::File {
rel_path: PathBuf::from(format!("{name}/sub/a")),
body: "x".to_string(),
},
Artifact::Symlink {
rel_path: PathBuf::from(format!("{name}/link")),
target: "sub".to_string(),
},
Artifact::File {
rel_path: PathBuf::from(format!("{name}/sub/a/b")),
body: "y".to_string(),
},
])
}
const SUB_DOOMED_KIND: Kind = Kind {
dir: "tree",
prefix: "TK",
scaffold: sub_doomed_fileset,
};
#[test]
fn create_in_existing_rolls_back_partial_fileset_leaving_parent_intact() {
let dir = tempfile::tempdir().unwrap();
let tree = dir.path().join("tree");
fs::create_dir_all(tree.join("003")).unwrap();
fs::write(tree.join("003/keep.txt"), "keep").unwrap();
let err = create_in_existing(
&SUB_DOOMED_KIND,
&tree,
3,
&Inputs {
slug: "",
title: "T",
date: "2026-06-04",
},
)
.unwrap_err();
assert!(err.to_string().contains("Failed to create"));
assert!(!tree.join("003/sub").exists(), "created dir unwound");
assert!(!tree.join("003/link").exists(), "created symlink unwound");
assert!(tree.join("003").is_dir());
assert_eq!(
fs::read_to_string(tree.join("003/keep.txt")).unwrap(),
"keep"
);
let mut left: Vec<String> = fs::read_dir(tree.join("003"))
.unwrap()
.map(|e| e.unwrap().file_name().to_string_lossy().into_owned())
.collect();
left.sort();
assert_eq!(left, vec!["keep.txt".to_string()]);
}
#[test]
fn rollback_leaves_a_dir_a_concurrent_writer_populated_intact() {
let tmp = tempfile::tempdir().unwrap();
let created = tmp.path().join("created");
fs::create_dir(&created).unwrap();
fs::write(created.join("intruder"), "x").unwrap();
rollback(&[], std::slice::from_ref(&created));
assert!(created.is_dir(), "populated dir survives rollback");
assert_eq!(fs::read_to_string(created.join("intruder")).unwrap(), "x");
}
fn named_fileset(uid: &str, key: Option<&str>) -> Fileset {
let mut fs = vec![
Artifact::File {
rel_path: PathBuf::from(format!("{uid}/memory.toml")),
body: "toml".to_string(),
},
Artifact::File {
rel_path: PathBuf::from(format!("{uid}/memory.md")),
body: "md".to_string(),
},
];
if let Some(k) = key {
fs.push(Artifact::Symlink {
rel_path: PathBuf::from(k),
target: uid.to_string(),
});
}
fs
}
#[test]
fn materialise_named_writes_a_prebuilt_fileset_under_dir_name() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
let out = materialise_named(
&LocalFs,
root,
"tree",
"mem_abc",
&named_fileset("mem_abc", None),
)
.unwrap();
assert_eq!(
out.eid,
OwnedEntityId::Named {
name: "mem_abc".to_string()
}
);
assert_eq!(out.dir, root.join("tree/mem_abc"));
assert_eq!(
fs::read_to_string(root.join("tree/mem_abc/memory.toml")).unwrap(),
"toml"
);
assert_eq!(
fs::read_to_string(root.join("tree/mem_abc/memory.md")).unwrap(),
"md"
);
}
#[test]
fn materialise_named_with_a_key_writes_the_alias_symlink() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
materialise_named(
&LocalFs,
root,
"tree",
"mem_abc",
&named_fileset("mem_abc", Some("mem.a.b")),
)
.unwrap();
let link = root.join("tree/mem.a.b");
assert_eq!(fs::read_link(&link).unwrap(), Path::new("mem_abc"));
assert!(
scan_named(&root.join("tree"))
.unwrap()
.contains(&"mem_abc".to_string())
);
assert!(
!scan_named(&root.join("tree"))
.unwrap()
.contains(&"mem.a.b".to_string())
);
}
#[test]
fn materialise_named_errs_on_a_duplicate_name() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
fs::create_dir_all(root.join("tree/mem_abc")).unwrap();
let err = materialise_named(
&LocalFs,
root,
"tree",
"mem_abc",
&named_fileset("mem_abc", None),
)
.unwrap_err();
assert!(err.to_string().contains("already exists"));
}
#[test]
fn materialise_named_write_failure_cleans_up_the_won_dir() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
let doomed = vec![
Artifact::File {
rel_path: PathBuf::from("mem_abc/a"),
body: "x".to_string(),
},
Artifact::File {
rel_path: PathBuf::from("mem_abc/a/b"),
body: "y".to_string(),
},
];
let err = materialise_named(&LocalFs, root, "tree", "mem_abc", &doomed).unwrap_err();
assert!(err.to_string().contains("Failed to create"));
assert!(
!root.join("tree/mem_abc").exists(),
"the won dir must be removed"
);
}
#[test]
fn materialise_named_rolls_back_the_uid_dir_on_a_pre_existing_key_alias() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
let tree = root.join("tree");
fs::create_dir_all(&tree).unwrap();
fs::write(tree.join("mem.a.b"), "stale").unwrap();
let err = materialise_named(
&LocalFs,
root,
"tree",
"mem_abc",
&named_fileset("mem_abc", Some("mem.a.b")),
)
.unwrap_err();
assert!(err.to_string().contains("Failed to symlink"));
assert!(
!tree.join("mem_abc").exists(),
"the uid dir must be rolled back — no partial record"
);
assert_eq!(fs::read_to_string(tree.join("mem.a.b")).unwrap(), "stale");
}
#[test]
fn scan_named_collects_every_real_subdir_skipping_files_and_symlinks() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
fs::create_dir(root.join("001")).unwrap(); fs::create_dir(root.join("mem_abc")).unwrap(); fs::write(root.join("a-file"), "x").unwrap();
std::os::unix::fs::symlink("001", root.join("a-link")).unwrap();
let mut names = scan_named(root).unwrap();
names.sort();
assert_eq!(names, vec!["001".to_string(), "mem_abc".to_string()]);
}
#[test]
fn scan_named_missing_dir_is_empty() {
let dir = tempfile::tempdir().unwrap();
assert!(scan_named(&dir.path().join("nope")).unwrap().is_empty());
}
}