blob: cce13243c6e468fcdfa243b3cdf6760b822f7cfb [file]
//! The projection: the record as a read-only file view beside the working tree, so a
//! harness can pull an entry with the cheapest tool it has, a file read. One directory
//! per granted domain: a summaries page, which is the index as a file, and one file per
//! active entry under the session's filter, which is the body as a file. It is a static
//! render the wrapper refreshes on events; the launcher mounts it read-only and installs
//! the hook that turns an open into a `get`, so the filter, the label and the funnel
//! apply without the agent reporting anything. Rendering here is not a read: the hook's
//! `get` is.
use crate::{Error, Memory};
use dm_core::{digest, Entry};
use dm_index::Hit;
use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
/// The summaries page in each domain's directory.
pub const SUMMARIES: &str = "README.md";
/// The directory, beside the domains, where a session's recalls land as files.
pub const RECALLS: &str = "recall";
/// Where the launcher renders each session's projection under the root, one directory
/// per session as `<harness>/<session>/`. Kept in step with `dm_sandbox::PROJECTION`.
pub const PROJECTIONS: &str = "state/projection";
/// The file an entry is projected as.
#[must_use]
pub fn entry_file(id: &str) -> String {
format!("{id}.md")
}
/// The entry id a projected path names, if its file name is `<id>.md` with an id of the
/// record's shape; the summaries page and anything else are `None`.
#[must_use]
pub fn id_of(path: &Path) -> Option<&str> {
let name = path.file_name()?.to_str()?;
let id = name.strip_suffix(".md")?;
let shaped = id.starts_with("mem-")
&& id.len() > 4
&& id
.bytes()
.all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_');
shaped.then_some(id)
}
/// The summaries page: what the index serves before anyone opens a body, one line per
/// entry, newest first, with the file to open beside it.
#[must_use]
pub fn render_summaries(domain: &str, hits: &[Hit]) -> String {
let mut out = format!(
"# {domain}: {} entries\n\nThe memory's summaries for this domain, newest first. Each \
line names the file holding the entry; opening one is a read of the memory and \
raises this session's label to the entry's. Search is cheaper than reading: use \
the memory's `search` tool before opening files here.\n\n",
hits.len()
);
for h in hits {
out.push_str(&format!(
"- [{title}]({file}) · {kind} · {tier} · {label}: {summary}\n",
title = h.title.replace(['[', ']', '\n', '\r'], " "),
file = entry_file(&h.id),
kind = h.entry_type,
tier = h.trust_tier,
label = h.classification,
summary = h.summary.lines().next().unwrap_or_default()
));
}
out
}
/// What one render did.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Projected {
/// Entry files written because they were new or changed.
pub written: usize,
/// Entry files removed because the filter no longer lists them.
pub removed: usize,
/// Entries listed in all, across the domains.
pub listed: usize,
}
impl std::ops::AddAssign for Projected {
fn add_assign(&mut self, other: Projected) {
self.written += other.written;
self.removed += other.removed;
self.listed += other.listed;
}
}
fn io(path: &Path) -> impl FnOnce(std::io::Error) -> Error + '_ {
move |source| Error::Io {
path: path.display().to_string(),
source,
}
}
/// A directory of the projection, made if missing and never a symlink: the projection
/// is written by the launcher, and a link planted where it writes would carry its files
/// elsewhere.
fn own_dir(path: &Path) -> Result<(), Error> {
if let Ok(meta) = std::fs::symlink_metadata(path) {
if meta.file_type().is_symlink() || !meta.is_dir() {
return Err(Error::Denied(format!(
"{} is not a directory of the projection's own",
path.display()
)));
}
return Ok(());
}
std::fs::create_dir_all(path).map_err(io(path))
}
impl Memory {
/// Render every granted domain under `dir`: `<dir>/<domain>/README.md` and one file
/// per active entry the session's filter lists, as the record holds it now: an entry
/// the index lists but the record has since demoted or raised above the session's
/// handling is left out, as `get` would refuse it. A file whose text is already
/// current is left alone, so a refresh touches only what changed; a file for an
/// entry no longer listed, erased, demoted or above the handling, is removed. A
/// symlink anywhere in the way is refused rather than followed.
///
/// # Errors
///
/// An index, store or I/O error, or a path in the projection that is not its own.
pub async fn project_into(&self, dir: &Path) -> Result<Projected, Error> {
self.own_below(dir)?;
let mut done = Projected::default();
// a launch's render: what another session left as recall pages is not this
// session's to read, and goes
done.removed += self.sweep_recalls(dir, true)?;
done += self.render(dir).await?;
Ok(done)
}
/// The domains rendered under `dir`, without touching the recall pages: what a
/// write refreshes.
async fn render(&self, dir: &Path) -> Result<Projected, Error> {
let mut done = Projected::default();
for domain in self.domains() {
if domain == RECALLS {
// a domain by that name would sit where the recall pages do
eprintln!(
"amem: the domain {RECALLS} is not projected: the name is the recall pages'"
);
continue;
}
let f = self.filter_for(Some(&domain))?;
let hits = self.index.lock().await.list(&f, None, usize::MAX / 2)?;
let dom_dir = dir.join(&domain);
own_dir(&dom_dir)?;
let mut keep: BTreeSet<PathBuf> = BTreeSet::new();
let mut listed = Vec::with_capacity(hits.len());
for h in &hits {
let Some(text) = self.store.get_text(&domain, &h.id).await? else {
continue;
};
let served = Entry::parse(&text).is_ok_and(|e| {
e.is_active()
&& e.front.classification <= f.handling
&& e.front.id == h.id
&& e.front.domain == domain
});
if !served {
continue;
}
let path = dom_dir.join(entry_file(&h.id));
keep.insert(path.clone());
let current = read_own(&path);
if current.as_deref().map(digest) != Some(digest(&text)) {
write_atomically(&path, &text)?;
done.written += 1;
}
listed.push(h.clone());
}
let summaries = dom_dir.join(SUMMARIES);
write_atomically(&summaries, &render_summaries(&domain, &listed))?;
keep.insert(summaries);
for stale in std::fs::read_dir(&dom_dir).map_err(io(&dom_dir))?.flatten() {
let path = stale.path();
let kind = stale.file_type().map_err(io(&path))?;
if (kind.is_file() || kind.is_symlink()) && !keep.contains(&path) {
std::fs::remove_file(&path).map_err(io(&path))?;
done.removed += 1;
}
}
done.listed += listed.len();
}
Ok(done)
}
}
impl Memory {
/// Every component of `dir` below the root, or below the directory's parent when it
/// is elsewhere, checked and made in turn, so a link higher up is not followed.
fn own_below(&self, dir: &Path) -> Result<(), Error> {
let mut step = if dir.starts_with(&self.root) {
self.root.clone()
} else {
dir.parent().map(Path::to_path_buf).unwrap_or_default()
};
let below = dir.strip_prefix(&step).unwrap_or(dir).to_path_buf();
for component in below.components() {
step.push(component);
own_dir(&step)?;
}
Ok(())
}
/// Land a recall as a file under the projection's `recall/<session>/` directory,
/// when the session has a projection, and name it on the recall relative to the
/// projection. The pack was served to this session already, so the file is read
/// free; the directory is this session's, swept by the next session's first render
/// and by an erasure, since a page holds bodies. A render that fails is said on
/// standard error and does not fail the recall.
pub(crate) async fn project_recall(&self, r: &mut crate::recall::Recall) {
let Some(dir) = &self.projection else {
return;
};
let mine = dir.join(RECALLS).join(&self.session_id);
let landed = self.own_below(&mine).and_then(|()| {
let now = time::OffsetDateTime::now_utc();
let stamp = now
.format(time::macros::format_description!(
"[year][month][day]T[hour][minute][second]"
))
.unwrap_or_default();
write_fresh(
&mine,
&format!("{stamp}-{:09}", now.nanosecond()),
&render_recall(r),
)
});
match landed {
Ok(name) => r.projected = Some(format!("{RECALLS}/{}/{name}", self.session_id)),
Err(e) => eprintln!("amem: projection at {}: {e}", dir.display()),
}
}
/// Remove every recall page under the projection: another session's on this
/// session's first render, every session's after an erasure, since a page holds
/// bodies as they were served.
fn sweep_recalls(&self, dir: &Path, keep_own: bool) -> Result<usize, Error> {
let recalls = dir.join(RECALLS);
match std::fs::symlink_metadata(&recalls) {
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(0),
Err(e) => return Err(io(&recalls)(e)),
Ok(meta) if meta.file_type().is_symlink() || !meta.is_dir() => {
return Err(Error::Denied(format!(
"{} is not a directory of the projection's own",
recalls.display()
)))
}
Ok(_) => {}
}
let entries = std::fs::read_dir(&recalls).map_err(io(&recalls))?;
let mut swept = 0;
for entry in entries {
let entry = entry.map_err(io(&recalls))?;
let path = entry.path();
let own = entry.file_name() == std::ffi::OsStr::new(&self.session_id);
if own && keep_own {
continue;
}
let kind = entry.file_type().map_err(io(&path))?;
if kind.is_dir() {
std::fs::remove_dir_all(&path).map_err(io(&path))?;
} else {
std::fs::remove_file(&path).map_err(io(&path))?;
}
swept += 1;
}
Ok(swept)
}
/// After a write: the domains rendered again, the recall pages left as they are.
pub(crate) async fn refresh_projection(&self) {
if let Some(dir) = &self.projection {
if let Err(e) = self.render(dir).await {
eprintln!("amem: projection at {}: {e}", dir.display());
}
}
}
/// After an erasure, in every session's projection under the root, not only this
/// one's: a live session's, which nothing else would reach before its own next
/// write, one a killed session left behind, which nothing renders again, and one
/// rendered at `<harness>/` itself before renders were per session. The entry's file
/// and any half-written copy of it go, its line leaves the domain's summaries, and
/// every recall page goes, since a page holds bodies as they were served. Nothing is
/// walked through a link: `state` and `state/projection` must be directories of the
/// root's own. What could not be looked at or cleaned comes back, one line each, for
/// the receipt.
pub(crate) fn erase_from_projections(&self, domain: &str, id: &str) -> Vec<String> {
let base = self.root.join(PROJECTIONS);
let mut missed = Vec::new();
for step in [self.root.join("state"), base.clone()] {
match std::fs::symlink_metadata(&step) {
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return missed,
Ok(m) if m.is_dir() => {}
_ => {
missed.push(format!(
"the projections: {} is not a directory of the root's own",
step.display()
));
return missed;
}
}
}
let mut dirs = Vec::new();
for harness in real_subdirs(&base, &mut missed) {
dirs.extend(real_subdirs(&harness, &mut missed));
dirs.push(harness);
}
for dir in dirs {
if let Err(e) = self.erase_from(&dir, domain, id) {
missed.push(format!("the projection at {}: {e}", dir.display()));
}
}
missed
}
fn erase_from(&self, dir: &Path, domain: &str, id: &str) -> Result<(), Error> {
let dom = dir.join(domain);
if std::fs::symlink_metadata(&dom).is_ok_and(|m| m.is_dir()) {
let file = entry_file(id);
for name in [
file.clone(),
format!("{file}.tmp"),
format!("{SUMMARIES}.tmp"),
] {
let path = dom.join(name);
match std::fs::remove_file(&path) {
Err(e) if e.kind() != std::io::ErrorKind::NotFound => {
return Err(io(&path)(e));
}
_ => {}
}
}
let summaries = dom.join(SUMMARIES);
if let Some(text) = summaries_of(&summaries)? {
let own = format!("]({file})");
let gone = own_lines(&text, &own);
if gone.iter().any(|g| *g) {
let kept: Vec<&str> = text
.lines()
.zip(&gone)
.filter(|(_, g)| !**g)
.map(|(line, _)| line)
.collect();
// the header counts the lines, as render_summaries wrote it
let listed = kept.iter().filter(|line| line.starts_with("- [")).count();
let page: String = kept
.iter()
.enumerate()
.map(|(n, line)| match n {
0 if line.starts_with("# ") => {
format!("# {domain}: {listed} entries\n")
}
_ => format!("{line}\n"),
})
.collect();
write_atomically(&summaries, &page)?;
}
}
}
self.sweep_recalls(dir, false)?;
Ok(())
}
/// After an erasure: every recall page goes, this session's included, and the
/// domains are rendered again.
pub(crate) async fn refresh_projection_after_erasure(&self) {
if let Some(dir) = &self.projection {
if let Err(e) = self.sweep_recalls(dir, false) {
eprintln!("amem: projection at {}: {e}", dir.display());
}
}
self.refresh_projection().await;
}
}
/// A new file in `dir` named `stem.md`, or `stem-1.md` and on when the name is taken,
/// written once and never over another; the name it got.
fn write_fresh(dir: &Path, stem: &str, text: &str) -> Result<String, Error> {
use std::io::Write;
for n in 0..1000 {
let name = if n == 0 {
format!("{stem}.md")
} else {
format!("{stem}-{n}.md")
};
let path = dir.join(&name);
match std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&path)
{
Ok(reserved) => {
drop(reserved);
// written beside and renamed over the reserved name, so a reader never
// sees half a page
let tmp = dir.join(format!("{name}.tmp"));
let _ = std::fs::remove_file(&tmp);
let mut f = std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&tmp)
.map_err(io(&tmp))?;
f.write_all(text.as_bytes()).map_err(io(&tmp))?;
drop(f);
std::fs::rename(&tmp, &path).map_err(io(&path))?;
return Ok(name);
}
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {}
Err(e) => return Err(io(&path)(e)),
}
}
Err(Error::Denied(format!(
"{} has a thousand pages of one stamp",
dir.display()
)))
}
/// A recall as a page: the question, the brief or the note that synthesis is owed, the
/// citations, and the pack's pieces as they were served.
#[must_use]
pub fn render_recall(r: &crate::recall::Recall) -> String {
use crate::recall::Synthesis;
let mut out = format!("# {}\n\n", r.question.trim());
match &r.synthesis {
Synthesis::Server { model } => {
out.push_str(&format!("Brief by {model}.\n\n"));
if let Some(brief) = &r.brief {
out.push_str(brief.trim());
out.push_str("\n\n");
}
}
Synthesis::Owed => out.push_str(
"Synthesis owed: no model wrote a brief; the pieces below are the pack, at the \
depth asked for, for the reader to answer from.\n\n",
),
}
out.push_str(&format!(
"Cites: {}. Pack: {} pieces at {} depth, {} tokens, {} hop(s).\n\n",
r.citations.join(", "),
r.pack.pieces.len(),
r.pack.depth,
r.pack.tokens,
r.hops
));
for p in &r.pack.pieces {
out.push_str(&format!(
"## {} ({}, {}, {})\n\n{}\n\n",
p.title, p.id, p.entry_type, p.classification, p.summary
));
if let Some(body) = &p.body {
out.push_str(body.trim_end());
out.push_str("\n\n");
}
}
out
}
/// Which lines of a summaries page are the entry's whose link is `own`, `](<file>)`. A
/// line is when its own link names the file: a title holds no bracket, so its first `](`
/// is the link, and a summary citing the file further along is another entry's line. A
/// page rendered before titles lost their newlines can hold a title split over lines:
/// there the line holding the link is the entry's, and so is the line it continues.
fn own_lines(text: &str, own: &str) -> Vec<bool> {
let lines: Vec<&str> = text.lines().collect();
let mut gone = vec![false; lines.len()];
for (n, line) in lines.iter().enumerate() {
let link_first = line.find("](").is_some_and(|i| line[i..].starts_with(own));
if line.starts_with("- [") {
gone[n] = link_first;
} else if link_first {
gone[n] = true;
if let Some(start) = (0..n).rev().find(|&k| lines[k].starts_with("- [")) {
if (start..n).all(|k| !lines[k].contains("](")) {
gone[start..n].iter_mut().for_each(|g| *g = true);
}
}
}
}
gone
}
/// A summaries page's text: none when there is none or a link stands in its place, and
/// an error when a page of its own cannot be read, so the sweep says it missed it.
fn summaries_of(path: &Path) -> Result<Option<String>, Error> {
match std::fs::symlink_metadata(path) {
Ok(meta) if meta.is_file() => std::fs::read_to_string(path).map(Some).map_err(io(path)),
_ => Ok(None),
}
}
/// The directories directly under `dir`, links and files left out. One that cannot be
/// read is said in `missed`, not taken for an empty one.
fn real_subdirs(dir: &Path, missed: &mut Vec<String>) -> Vec<PathBuf> {
let entries = match std::fs::read_dir(dir) {
Ok(entries) => entries,
Err(e) => {
missed.push(format!("the projections under {}: {e}", dir.display()));
return Vec::new();
}
};
let mut dirs = Vec::new();
for entry in entries {
match entry.and_then(|e| e.file_type().map(|t| (e.path(), t))) {
Ok((path, kind)) if kind.is_dir() => dirs.push(path),
Ok(_) => {}
Err(e) => missed.push(format!("the projections under {}: {e}", dir.display())),
}
}
dirs
}
/// The text of a projected file, if it is a plain file of the projection's own; a
/// symlink in its place is not read and is replaced.
fn read_own(path: &Path) -> Option<String> {
let meta = std::fs::symlink_metadata(path).ok()?;
meta.is_file().then(|| std::fs::read_to_string(path).ok())?
}
/// Write through a sibling and rename, so a reader never sees half a file. The sibling
/// is made fresh: whatever stood under its name, a link included, is removed first,
/// and the rename replaces the target's name rather than writing through it.
fn write_atomically(path: &Path, text: &str) -> Result<(), Error> {
use std::io::Write;
let mut name = path.file_name().unwrap_or_default().to_os_string();
name.push(".tmp");
let tmp = path.with_file_name(name);
let _ = std::fs::remove_file(&tmp);
let mut f = std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&tmp)
.map_err(io(&tmp))?;
f.write_all(text.as_bytes()).map_err(io(&tmp))?;
drop(f);
std::fs::rename(&tmp, path).map_err(io(path))?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::WriteRequest;
use dm_core::erasure::Reason;
use dm_core::Label;
#[test]
fn a_projected_path_names_its_entry_and_nothing_else_does() {
assert_eq!(
id_of(Path::new("/memory/dom-me/mem-me-abc123.md")),
Some("mem-me-abc123")
);
assert_eq!(id_of(Path::new("/memory/dom-me/README.md")), None);
assert_eq!(
id_of(Path::new("/memory/dom-me/mem-me-abc123.md.tmp")),
None
);
assert_eq!(id_of(Path::new("/memory/dom-me/notes.md")), None);
assert_eq!(id_of(Path::new("/memory/dom-me/mem-.md")), None);
assert_eq!(id_of(Path::new("mem-me-a/../b.md")), None);
}
#[tokio::test]
async fn the_projection_lists_what_the_filter_lists_and_follows_the_record() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path().join("root");
let view = dir.path().join("view");
let m = Memory::init(&root, "human:test:me", "dom-me")
.unwrap()
.without_read_log();
let a = m
.write(WriteRequest {
title: Some("Retry policy".into()),
body: "Operation R retries with backoff.\n".into(),
..WriteRequest::default()
})
.await
.unwrap();
let secret = m
.write(WriteRequest {
title: Some("The key".into()),
body: "The signing key lives in the vault.\n".into(),
classification: Some(Label::Restricted),
..WriteRequest::default()
})
.await
.unwrap();
let first = m.project_into(&view).await.unwrap();
assert_eq!((first.written, first.removed, first.listed), (2, 0, 2));
let dom = view.join("dom-me");
let page = std::fs::read_to_string(dom.join(SUMMARIES)).unwrap();
assert!(
page.contains(&format!("[Retry policy]({}.md)", a.id)),
"{page}"
);
assert!(page.contains("restricted: The signing key"), "{page}");
let body = std::fs::read_to_string(dom.join(entry_file(&a.id))).unwrap();
assert!(body.starts_with("---\n") && body.contains("backoff"));
// nothing changed: nothing rewritten
let again = m.project_into(&view).await.unwrap();
assert_eq!((again.written, again.removed), (0, 0));
// a session capped at internal does not get the restricted body as a file
let capped = Memory::open(&root)
.unwrap()
.without_read_log()
.with_handling_cap(Label::Internal);
let p = capped.project_into(&view).await.unwrap();
assert_eq!((p.removed, p.listed), (1, 1), "{p:?}");
assert!(!dom.join(entry_file(&secret.id)).exists());
assert!(!std::fs::read_to_string(dom.join(SUMMARIES))
.unwrap()
.contains("signing key"));
// erasure removes the file on the next render, and no sibling is left behind
m.erase(&a.id, Reason::Mistaken).await.unwrap();
let after = m.project_into(&view).await.unwrap();
assert_eq!(after.removed, 1, "{after:?}");
assert!(!dom.join(entry_file(&a.id)).exists());
let names: Vec<String> = std::fs::read_dir(&dom)
.unwrap()
.flatten()
.map(|e| e.file_name().to_string_lossy().into_owned())
.collect();
assert!(names.iter().all(|n| !n.ends_with(".tmp")), "{names:?}");
}
#[tokio::test]
async fn a_session_with_a_projection_keeps_it_current() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path().join("root");
let view = dir.path().join("view");
let m = Memory::init(&root, "human:test:me", "dom-me")
.unwrap()
.without_read_log()
.with_projection(view.clone());
let w = m
.write(WriteRequest {
title: Some("Fresh".into()),
body: "Written a moment ago.\n".into(),
..WriteRequest::default()
})
.await
.unwrap();
let file = view.join("dom-me").join(entry_file(&w.id));
assert!(file.is_file(), "a write renders");
assert!(std::fs::read_to_string(view.join("dom-me").join(SUMMARIES))
.unwrap()
.contains("[Fresh]"));
m.erase(&w.id, Reason::Mistaken).await.unwrap();
assert!(!file.exists(), "an erasure removes the file at once");
let out = dir.path().join("out");
let other = Memory::init(&dir.path().join("other"), "human:test:me", "dom-me")
.unwrap()
.without_read_log();
other
.write(WriteRequest {
title: Some("From elsewhere".into()),
body: "Imported.\n".into(),
..WriteRequest::default()
})
.await
.unwrap();
other
.export(Some("dom-me"), Label::Internal, &out)
.await
.unwrap();
let im = m.import(&out, Some("dom-me")).await.unwrap();
assert!(
view.join("dom-me")
.join(entry_file(&im.written[0]))
.is_file(),
"an import renders"
);
}
#[cfg(unix)]
#[tokio::test]
async fn a_symlink_in_the_projection_is_refused_not_followed() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path().join("root");
let view = dir.path().join("view");
let outside = dir.path().join("outside");
std::fs::create_dir_all(&outside).unwrap();
std::fs::write(outside.join("victim.txt"), "keep me").unwrap();
let m = Memory::init(&root, "human:test:me", "dom-me")
.unwrap()
.without_read_log();
m.write(WriteRequest {
body: "An entry.\n".into(),
..WriteRequest::default()
})
.await
.unwrap();
// the domain's directory planted as a link to somewhere else
std::fs::create_dir_all(&view).unwrap();
std::os::unix::fs::symlink(&outside, view.join("dom-me")).unwrap();
assert!(
matches!(m.project_into(&view).await, Err(Error::Denied(_))),
"a linked directory is refused"
);
assert_eq!(
std::fs::read_to_string(outside.join("victim.txt")).unwrap(),
"keep me"
);
assert!(
!outside.join(SUMMARIES).exists(),
"nothing was written outside"
);
// the view itself as a link
let view2 = dir.path().join("view2");
std::os::unix::fs::symlink(&outside, &view2).unwrap();
assert!(matches!(
m.project_into(&view2).await,
Err(Error::Denied(_))
));
// a link above the leaf, under the root: state/projection itself
std::os::unix::fs::symlink(&outside, root.join("state/projection")).unwrap();
assert!(
matches!(
m.project_into(&root.join("state/projection/claude")).await,
Err(Error::Denied(_))
),
"a linked parent is refused, not walked"
);
assert!(
!outside.join("claude").exists(),
"nothing was made in the link's target"
);
// a sibling planted where the atomic write lands: replaced, never written through
std::fs::remove_file(view.join("dom-me")).unwrap();
std::fs::create_dir_all(view.join("dom-me")).unwrap();
let planted = outside.join("planted.txt");
std::fs::write(&planted, "mine").unwrap();
std::os::unix::fs::symlink(&planted, view.join("dom-me").join("README.md.tmp")).unwrap();
std::os::unix::fs::symlink(&planted, view.join("dom-me").join("stray.md")).unwrap();
let p = m.project_into(&view).await.unwrap();
assert_eq!(std::fs::read_to_string(&planted).unwrap(), "mine");
assert_eq!(
p.removed, 1,
"the stray link is removed, the sibling replaced: {p:?}"
);
assert!(!view.join("dom-me").join("stray.md").exists());
assert!(
std::fs::symlink_metadata(view.join("dom-me").join(SUMMARIES))
.unwrap()
.is_file()
);
}
#[tokio::test]
async fn the_record_s_word_decides_what_is_rendered_not_the_index_s() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path().join("root");
let view = dir.path().join("view");
let m = Memory::init(&root, "human:test:me", "dom-me")
.unwrap()
.without_read_log();
let demoted = m
.write(WriteRequest {
body: "Demoted behind the index's back.\n".into(),
..WriteRequest::default()
})
.await
.unwrap();
let raised = m
.write(WriteRequest {
title: Some("Raised later".into()),
body: "secret body B\n".into(),
..WriteRequest::default()
})
.await
.unwrap();
// another process moved the record on and its index step was lost
m.store.demote("dom-me", &demoted.id).await.unwrap();
let mut e = m.store.get("dom-me", &raised.id).await.unwrap().unwrap();
e.front.classification = Label::Restricted;
m.store.put(&e).await.unwrap();
let capped = Memory::open(&root)
.unwrap()
.without_read_log()
.with_handling_cap(Label::Internal);
let p = capped.project_into(&view).await.unwrap();
assert_eq!((p.written, p.listed), (0, 0), "{p:?}");
let dom = view.join("dom-me");
assert!(!dom.join(entry_file(&demoted.id)).exists());
assert!(!dom.join(entry_file(&raised.id)).exists());
assert!(!std::fs::read_to_string(dom.join(SUMMARIES))
.unwrap()
.contains("Raised later"));
}
#[tokio::test]
async fn a_recall_lands_as_a_file_the_session_can_read_free() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path().join("root");
let view = dir.path().join("view");
let m = Memory::init(&root, "human:test:me", "dom-me")
.unwrap()
.without_read_log()
.with_projection(view.clone());
m.write(WriteRequest {
title: Some("Retry policy".into()),
body: "Operation R retries with exponential backoff.\n".into(),
..WriteRequest::default()
})
.await
.unwrap();
let r = m
.recall(
"how does the retry work?",
None,
None,
crate::recall::Budget::default(),
)
.await
.unwrap();
let rel = r.projected.clone().expect("the recall landed");
assert!(
rel.starts_with(&format!("recall/{}/", m.session_id())) && rel.ends_with(".md"),
"{rel}"
);
let text = std::fs::read_to_string(view.join(&rel)).unwrap();
assert!(text.starts_with("# how does the retry work?"), "{text}");
assert!(
text.contains("Synthesis owed") && text.contains("Retry policy"),
"{text}"
);
assert!(
text.contains("exponential backoff"),
"the pack's body is in the file"
);
let json = crate::recall::to_json(&r);
assert_eq!(json["projected"], rel);
// a second recall in the same session is another page beside it
let again = m
.recall("retry", None, None, crate::recall::Budget::default())
.await
.unwrap();
assert_ne!(again.projected, r.projected);
assert!(view.join(&rel).is_file() && view.join(again.projected.unwrap()).is_file());
// without a projection, nothing lands and nothing is named
let plain = Memory::open(&root).unwrap().without_read_log();
let r = plain
.recall("retry", None, None, crate::recall::Budget::default())
.await
.unwrap();
assert_eq!(r.projected, None);
}
#[tokio::test]
async fn recall_pages_do_not_outlive_the_session_or_the_bodies_they_hold() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path().join("root");
let view = dir.path().join("view");
let m = Memory::init(&root, "human:test:me", "dom-me")
.unwrap()
.without_read_log()
.with_projection(view.clone());
let secret = m
.write(WriteRequest {
title: Some("The key".into()),
body: "The signing key lives in the vault.\n".into(),
classification: Some(Label::Restricted),
..WriteRequest::default()
})
.await
.unwrap();
let r = m
.recall(
"where is the key?",
None,
None,
crate::recall::Budget::default(),
)
.await
.unwrap();
let page = view.join(r.projected.unwrap());
assert!(std::fs::read_to_string(&page).unwrap().contains("vault"));
// another session at a lower handling renders: the page is not its to read
let capped = Memory::open(&root)
.unwrap()
.without_read_log()
.with_handling_cap(Label::Internal)
.with_projection(view.clone());
capped.project_into(&view).await.unwrap();
assert!(!page.exists(), "another session's render sweeps the page");
// this session recalls again, then erases: its own page goes too
let r = m
.recall(
"where is the key?",
None,
None,
crate::recall::Budget::default(),
)
.await
.unwrap();
let page = view.join(r.projected.unwrap());
assert!(page.exists());
m.erase(&secret.id, Reason::Sensitive).await.unwrap();
assert!(!page.exists(), "an erasure sweeps every recall page");
assert!(!view.join(RECALLS).join(m.session_id()).exists());
// a domain named like the recall directory is left out, and the launch goes on
let clash = Memory::init(&dir.path().join("clash"), "human:test:me", RECALLS)
.unwrap()
.without_read_log();
let clash_view = dir.path().join("clash-view");
let p = clash.project_into(&clash_view).await.unwrap();
assert_eq!(p.listed, 0);
assert!(!clash_view.join(RECALLS).join(SUMMARIES).exists());
}
#[tokio::test]
async fn a_write_keeps_another_session_s_pages_and_the_sweep_follows_no_link() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path().join("root");
let view = dir.path().join("view");
let a = Memory::init(&root, "human:test:me", "dom-me")
.unwrap()
.without_read_log()
.with_projection(view.clone());
a.write(WriteRequest {
body: "First.\n".into(),
..WriteRequest::default()
})
.await
.unwrap();
let page_a = view.join(
a.recall("first", None, None, crate::recall::Budget::default())
.await
.unwrap()
.projected
.unwrap(),
);
let b = Memory::open(&root)
.unwrap()
.without_read_log()
.with_projection(view.clone());
b.project_into(&view).await.unwrap();
assert!(
!page_a.exists(),
"a launch's render sweeps the other session's pages"
);
let page_b = view.join(
b.recall("first", None, None, crate::recall::Budget::default())
.await
.unwrap()
.projected
.unwrap(),
);
a.write(WriteRequest {
body: "Second.\n".into(),
..WriteRequest::default()
})
.await
.unwrap();
assert!(
page_b.exists(),
"a write's refresh leaves the other session's pages"
);
#[cfg(unix)]
{
let outside = dir.path().join("outside");
std::fs::create_dir_all(outside.join("keep")).unwrap();
std::fs::remove_dir_all(view.join(RECALLS)).unwrap();
std::os::unix::fs::symlink(&outside, view.join(RECALLS)).unwrap();
assert!(matches!(a.project_into(&view).await, Err(Error::Denied(_))));
assert!(outside.join("keep").is_dir(), "the sweep followed no link");
}
}
}