#![cfg(feature = "mem-repo")]
use std::fs;
use std::path::Path;
use std::sync::{Mutex, OnceLock};
use assert_cmd::Command;
use memstead_git_branch::test_support::init_real_mem_repo_from_disk;
use predicates::str::contains;
use tempfile::TempDir;
fn cache_guard() -> &'static Mutex<()> {
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
LOCK.get_or_init(|| Mutex::new(()))
}
fn commit_mem_branch(root: &Path, mem_name: &str, entries: &[(&str, &str)]) {
use memstead_git_branch::storage::MemWriter;
use memstead_git_branch::storage::git_tree::GitTreeMemWriter;
use memstead_git_branch::vcs::CommitContext;
let gitdir = root.join("mem-repo").join(".git");
let writer = GitTreeMemWriter::new(gitdir, format!("refs/heads/{mem_name}"));
for (rel, content) in entries {
writer
.write_entity(Path::new(rel), content.as_bytes())
.unwrap();
}
writer.commit("seed", &CommitContext::internal()).unwrap();
}
fn make_sender_mem(root: &Path) -> std::path::PathBuf {
let dir = root.join("sender-mem");
fs::create_dir_all(&dir).unwrap();
let store = dir.join(".memstead");
fs::create_dir_all(&store).unwrap();
fs::write(
store.join("config.json"),
r#"{
"version": "1.0.0",
"description": "Fixture write mem used to exercise export → install",
"schema": "default@1.0.0"
}"#,
)
.unwrap();
init_real_mem_repo_from_disk(root, &[(&dir, "sender-mem")]);
let alpha_body = r#"---
type: spec
created_date: 2026-01-01
last_modified: 2026-01-01
level: M0
---
# Alpha
## Identity
First entity in the sender mem. Used to verify a read mem's entities
become discoverable from a second project after install.
## Purpose
Exercises the export → install round trip via the CLI.
"#;
fs::write(dir.join("alpha.md"), alpha_body).unwrap();
let beta_body = r#"---
type: spec
created_date: 2026-01-02
last_modified: 2026-01-02
level: M0
---
# Beta
## Identity
Second entity in the sender mem.
## Purpose
Provides a second hit for the search test below.
"#;
fs::write(dir.join("beta.md"), beta_body).unwrap();
commit_mem_branch(
root,
"sender-mem",
&[("alpha.md", alpha_body), ("beta.md", beta_body)],
);
dir
}
fn make_receiver_mem(root: &Path) -> std::path::PathBuf {
let dir = root.join("receiver-mem");
fs::create_dir_all(&dir).unwrap();
let store = dir.join(".memstead");
fs::create_dir_all(&store).unwrap();
fs::write(
store.join("config.json"),
r#"{ "version": "0.1.0", "schema": "default@1.0.0" }"#,
)
.unwrap();
init_real_mem_repo_from_disk(root, &[(&dir, "receiver-mem")]);
dir
}
fn memstead() -> Command {
Command::cargo_bin("memstead").expect("memstead binary must be built by cargo")
}
#[test]
fn export_markdown_default_runs() {
let tmp = TempDir::new().unwrap();
let _mem = make_sender_mem(tmp.path());
memstead()
.current_dir(tmp.path())
.arg("export")
.assert()
.success()
.stdout(contains("Export — markdown"));
}
#[test]
fn export_markdown_workspace_wide_reports_skipped_git_branch_mounts() {
let tmp = TempDir::new().unwrap();
let _mem = make_sender_mem(tmp.path());
memstead()
.current_dir(tmp.path())
.arg("export")
.assert()
.success()
.stdout(contains("Export — markdown"))
.stdout(contains("Skipped mounts"))
.stdout(contains("sender-mem"))
.stdout(contains("git-branch"))
.stdout(contains("backend_does_not_support_markdown_export"));
}
#[test]
fn export_markdown_per_mem_refuses_on_git_branch_backend() {
let tmp = TempDir::new().unwrap();
let _mem = make_sender_mem(tmp.path());
memstead()
.current_dir(tmp.path())
.args(["export", "--format", "markdown", "--mem", "sender-mem"])
.assert()
.failure()
.stderr(contains("MARKDOWN_EXPORT_UNSUPPORTED_BACKEND"))
.stderr(contains("git-branch"))
.stderr(contains("--format mem"));
}
#[test]
fn export_markdown_per_mem_refuses_with_json_envelope() {
let tmp = TempDir::new().unwrap();
let _mem = make_sender_mem(tmp.path());
let assert = memstead()
.current_dir(tmp.path())
.args([
"--json",
"export",
"--format",
"markdown",
"--mem",
"sender-mem",
])
.assert()
.failure();
let stdout = String::from_utf8(assert.get_output().stdout.clone()).unwrap();
let env: serde_json::Value = serde_json::from_str(stdout.trim()).unwrap_or_else(|e| {
panic!("expected JSON error envelope on stdout; got:\n{stdout}\n({e})")
});
assert_eq!(env["code"], "MARKDOWN_EXPORT_UNSUPPORTED_BACKEND");
assert_eq!(env["details"]["mem"], "sender-mem");
assert_eq!(env["details"]["active_backend"], "git-branch");
assert_eq!(
env["details"]["supported_backends"],
serde_json::json!(["folder"])
);
}
#[test]
fn export_markdown_workspace_wide_json_carries_skipped_mounts() {
let tmp = TempDir::new().unwrap();
let _mem = make_sender_mem(tmp.path());
let assert = memstead()
.current_dir(tmp.path())
.args(["--json", "export"])
.assert()
.success();
let stdout = String::from_utf8(assert.get_output().stdout.clone()).unwrap();
let env: serde_json::Value = serde_json::from_str(stdout.trim())
.unwrap_or_else(|e| panic!("expected JSON envelope on stdout; got:\n{stdout}\n({e})"));
assert_eq!(env["written"], 0);
assert_eq!(env["unchanged"], 0);
let skipped = env["skipped_mounts"].as_array().expect(
"skipped_mounts must be a JSON array under the workspace-wide partial-success shape",
);
assert_eq!(skipped.len(), 1, "one git-branch-mount in this fixture");
assert_eq!(skipped[0]["mem"], "sender-mem");
assert_eq!(skipped[0]["active_backend"], "git-branch");
assert_eq!(
skipped[0]["reason"],
"backend_does_not_support_markdown_export"
);
}
#[test]
fn export_mem_produces_memstead_archive() {
let _guard = cache_guard().lock().unwrap();
let tmp = TempDir::new().unwrap();
let mem = make_sender_mem(tmp.path());
let output_path = mem.join("sender-mem-1.0.0.mem");
memstead()
.current_dir(tmp.path())
.args(["export", "--format", "mem", "-o"])
.arg(&output_path)
.assert()
.success()
.stdout(contains("Exported `sender-mem` v1.0.0"))
.stdout(contains("sender-mem-1.0.0.mem"));
assert!(output_path.exists(), "archive must be written to disk");
assert!(
fs::metadata(&output_path).unwrap().len() > 0,
"archive must not be empty"
);
}
#[test]
fn export_mem_fails_without_version() {
let tmp = TempDir::new().unwrap();
let mem = tmp.path().join("unversioned");
let memstead_dir = mem.join(".memstead");
fs::create_dir_all(&memstead_dir).unwrap();
fs::write(
memstead_dir.join("config.json"),
r#"{ "schema": "default@1.0.0" }"#,
)
.unwrap();
init_real_mem_repo_from_disk(tmp.path(), &[(&mem, "unversioned")]);
memstead()
.current_dir(tmp.path())
.args(["export", "--format", "mem", "-o"])
.arg(mem.join("out.mem"))
.assert()
.failure()
.stderr(contains("version"));
}
#[test]
fn mem_set_version_persists_through_mem_repo_backend() {
let tmp = TempDir::new().unwrap();
let _mem = make_sender_mem(tmp.path());
memstead()
.current_dir(tmp.path())
.args(["mem", "set-version", "sender-mem", "1.5.0"])
.assert()
.success()
.stdout(contains("New version: 1.5.0"));
memstead()
.current_dir(tmp.path())
.args(["mem", "set-version", "sender-mem", "2.0.0"])
.assert()
.success()
.stdout(contains("Old version: 1.5.0"))
.stdout(contains("New version: 2.0.0"));
memstead()
.current_dir(tmp.path())
.args(["mem", "set-version", "sender-mem", "not-a-semver"])
.assert()
.failure();
}
#[test]
fn mem_set_description_persists_and_clears() {
let tmp = TempDir::new().unwrap();
let _mem = make_sender_mem(tmp.path());
memstead()
.current_dir(tmp.path())
.args([
"mem",
"set-description",
"sender-mem",
"Typed knowledge about senders.",
])
.assert()
.success()
.stdout(contains("New: Typed knowledge about senders."));
memstead()
.current_dir(tmp.path())
.args(["mem", "set-description", "sender-mem", "Sharper card text."])
.assert()
.success()
.stdout(contains("Old: Typed knowledge about senders."))
.stdout(contains("New: Sharper card text."));
memstead()
.current_dir(tmp.path())
.args(["mem", "set-description", "sender-mem", ""])
.assert()
.success()
.stdout(contains("New: <cleared>"));
}
#[test]
fn mem_set_version_note_lands_on_commit() {
let tmp = TempDir::new().unwrap();
let _mem = make_sender_mem(tmp.path());
memstead()
.current_dir(tmp.path())
.args([
"mem",
"set-version",
"sender-mem",
"1.5.0",
"--note",
"bump for the auth release",
])
.assert()
.success()
.stdout(contains("New version: 1.5.0"));
let gitdir = tmp.path().join("mem-repo").join(".git");
let repo = gix::open(&gitdir).expect("open mem-repo gitdir");
let commit = repo
.find_reference("refs/heads/__MEMSTEAD")
.expect("find __MEMSTEAD")
.into_fully_peeled_id()
.expect("peel __MEMSTEAD to id")
.object()
.expect("load __MEMSTEAD object")
.try_into_commit()
.expect("__MEMSTEAD is a commit");
let message = commit.message_raw().expect("commit message").to_string();
assert!(
message.contains("bump for the auth release"),
"version-bump note must land on the __MEMSTEAD commit body; got:\n{message}"
);
}
#[test]
fn mem_set_sync_state_persists_and_reports() {
let tmp = TempDir::new().unwrap();
let _mem = make_sender_mem(tmp.path());
memstead()
.current_dir(tmp.path())
.args([
"mem",
"set-sync-state",
"sender-mem",
"engine-graph/source-files",
"cafef00d",
])
.assert()
.success()
.stdout(contains("sync state set"))
.stdout(contains("engine-graph/source-files"));
memstead()
.current_dir(tmp.path())
.args([
"mem",
"set-sync-state",
"sender-mem",
"engine-graph/source-files",
"f00dcafe",
])
.assert()
.success()
.stdout(contains("sync state overwrote"));
memstead()
.current_dir(tmp.path())
.args([
"mem",
"set-sync-state",
"sender-mem",
"engine-graph/source-files",
"",
])
.assert()
.success()
.stdout(contains("sync state cleared"));
memstead()
.current_dir(tmp.path())
.args(["mem", "set-sync-state", "no-such-mem", "k", "v"])
.assert()
.failure();
}
#[test]
fn install_round_trip_entities_discoverable() {
let _guard = cache_guard().lock().unwrap();
let sender = TempDir::new().unwrap();
let receiver = TempDir::new().unwrap();
let cache = TempDir::new().unwrap();
let sender_mem = make_sender_mem(sender.path());
let _receiver_mem = make_receiver_mem(receiver.path());
let archive = sender_mem.join("sender-mem-1.0.0.mem");
memstead()
.current_dir(sender.path())
.args(["export", "--format", "mem", "-o"])
.arg(&archive)
.env("MEMSTEAD_MEM_CACHE", cache.path())
.assert()
.success();
assert!(archive.exists(), "export must produce the archive");
memstead()
.current_dir(receiver.path())
.arg("install")
.arg(&archive)
.env("MEMSTEAD_MEM_CACHE", cache.path())
.assert()
.success()
.stdout(contains("Installed `sender-mem`"));
memstead()
.current_dir(receiver.path())
.args(["search", "Alpha"])
.env("MEMSTEAD_MEM_CACHE", cache.path())
.assert()
.success()
.stdout(contains("sender-mem--alpha"));
memstead()
.current_dir(receiver.path())
.args(["entity", "sender-mem--alpha"])
.env("MEMSTEAD_MEM_CACHE", cache.path())
.assert()
.success()
.stdout(contains("# Alpha"));
}
#[test]
fn install_refuses_when_archive_shadows_writable() {
let _guard = cache_guard().lock().unwrap();
let sender = TempDir::new().unwrap();
let receiver = TempDir::new().unwrap();
let cache = TempDir::new().unwrap();
let sender_mem = make_sender_mem(sender.path());
let archive = sender_mem.join("sender-mem-1.0.0.mem");
memstead()
.current_dir(sender.path())
.args(["export", "--format", "mem", "-o"])
.arg(&archive)
.env("MEMSTEAD_MEM_CACHE", cache.path())
.assert()
.success();
let receiver_mem_dir = receiver.path().join("sender-mem");
fs::create_dir_all(&receiver_mem_dir).unwrap();
let memstead_dir = receiver_mem_dir.join(".memstead");
fs::create_dir_all(&memstead_dir).unwrap();
fs::write(
memstead_dir.join("config.json"),
r#"{ "version": "0.1.0", "schema": "default@1.0.0" }"#,
)
.unwrap();
init_real_mem_repo_from_disk(receiver.path(), &[(&receiver_mem_dir, "sender-mem")]);
memstead()
.current_dir(receiver.path())
.arg("install")
.arg(&archive)
.env("MEMSTEAD_MEM_CACHE", cache.path())
.assert()
.failure()
.stderr(contains("READ_MEM_SHADOWS_WRITABLE"))
.stderr(contains("already exists as a writable mount"));
}
#[test]
fn install_is_idempotent() {
let _guard = cache_guard().lock().unwrap();
let sender = TempDir::new().unwrap();
let receiver = TempDir::new().unwrap();
let cache = TempDir::new().unwrap();
let sender_mem = make_sender_mem(sender.path());
let _receiver_mem = make_receiver_mem(receiver.path());
let archive = sender_mem.join("sender-mem-1.0.0.mem");
memstead()
.current_dir(sender.path())
.args(["export", "--format", "mem", "-o"])
.arg(&archive)
.env("MEMSTEAD_MEM_CACHE", cache.path())
.assert()
.success();
memstead()
.current_dir(receiver.path())
.arg("install")
.arg(&archive)
.env("MEMSTEAD_MEM_CACHE", cache.path())
.assert()
.success()
.stdout(contains("copied into cache"))
.stdout(contains("registered as a workspace-level read-only mount"));
memstead()
.current_dir(receiver.path())
.arg("install")
.arg(&archive)
.env("MEMSTEAD_MEM_CACHE", cache.path())
.assert()
.success()
.stdout(contains("already in cache"))
.stdout(contains(
"already registered as a read-mem mount (unchanged)",
));
}
#[test]
fn workspace_dump_and_type_resolve_for_ro_mount_after_install() {
let _guard = cache_guard().lock().unwrap();
let sender = TempDir::new().unwrap();
let receiver = TempDir::new().unwrap();
let cache = TempDir::new().unwrap();
let sender_mem = make_sender_mem(sender.path());
let _receiver_mem = make_receiver_mem(receiver.path());
let archive = sender_mem.join("sender-mem-1.0.0.mem");
memstead()
.current_dir(sender.path())
.args(["export", "--format", "mem", "-o"])
.arg(&archive)
.env("MEMSTEAD_MEM_CACHE", cache.path())
.assert()
.success();
memstead()
.current_dir(receiver.path())
.arg("install")
.arg(&archive)
.env("MEMSTEAD_MEM_CACHE", cache.path())
.assert()
.success();
let dump = memstead()
.current_dir(receiver.path())
.args(["workspace", "dump", "--json"])
.env("MEMSTEAD_MEM_CACHE", cache.path())
.assert()
.success()
.get_output()
.stdout
.clone();
let dump_str = String::from_utf8(dump).unwrap();
let dump_json: serde_json::Value =
serde_json::from_str(&dump_str).expect("workspace dump must emit valid JSON");
let mems = dump_json["mems"]
.as_array()
.expect("dump must carry a mems array");
let ro_entry = mems
.iter()
.find(|v| v["name"] == "sender-mem")
.expect("installed sender-mem must appear in dump");
assert_eq!(
ro_entry["capability"], "read_only",
"RO mount must carry the `read_only` capability marker; got {ro_entry}"
);
assert_eq!(
ro_entry["schema_ref"], "default@1.0.0",
"RO mount's schema_ref must match the archive's bundled pin (pre-fix \
the dump would have crashed before reaching this assertion); got {ro_entry}"
);
memstead()
.current_dir(receiver.path())
.args(["type", "--mem", "sender-mem"])
.env("MEMSTEAD_MEM_CACHE", cache.path())
.assert()
.success();
}
#[test]
fn mem_title_and_subject_lifecycle() {
let tmp = TempDir::new().unwrap();
let _mem = make_sender_mem(tmp.path());
memstead()
.current_dir(tmp.path())
.args([
"mem",
"set-title",
"sender-mem",
"Einrichtungsbezogene Impfpflicht Deutschland",
])
.assert()
.success()
.stdout(contains("title updated"));
let out = memstead()
.current_dir(tmp.path())
.args(["--json", "mem", "list"])
.assert()
.success()
.get_output()
.stdout
.clone();
let json: serde_json::Value = serde_json::from_slice(&out).unwrap();
let row = &json["mems"][0];
assert_eq!(row["name"], "sender-mem");
assert_eq!(row["title"], "Einrichtungsbezogene Impfpflicht Deutschland");
memstead()
.current_dir(tmp.path())
.args(["mem", "list"])
.assert()
.success()
.stdout(contains(
"Einrichtungsbezogene Impfpflicht Deutschland (`sender-mem`)",
));
memstead()
.current_dir(tmp.path())
.args([
"--json",
"search",
"--mem",
"Einrichtungsbezogene Impfpflicht Deutschland",
])
.assert()
.failure()
.stdout(contains("UNKNOWN_MEM"));
memstead()
.current_dir(tmp.path())
.args(["--json", "search", "--mem", "sender-mem"])
.assert()
.success();
memstead()
.current_dir(tmp.path())
.args([
"mem",
"set-subject",
"sender-mem",
"--scope",
"Die Impfpflicht in Einrichtungen",
"--method",
"Primärquellen, händisch geprüft",
"--exclusion",
"Länderverordnungen nach 2023",
"--exclusion",
"Presseberichte",
])
.assert()
.success()
.stdout(contains("subject updated"));
memstead()
.current_dir(tmp.path())
.args([
"mem",
"set-subject",
"sender-mem",
"--method",
"nur Methode",
])
.assert()
.failure()
.stderr(contains("--scope"));
memstead()
.current_dir(tmp.path())
.args(["--json", "mem", "set-subject", "sender-mem"])
.assert()
.success()
.stdout(predicates::prelude::PredicateBooleanExt::not(contains(
"\"new_subject\"",
)));
memstead()
.current_dir(tmp.path())
.args(["mem", "set-title", "sender-mem", ""])
.assert()
.success();
memstead()
.current_dir(tmp.path())
.args(["mem", "list"])
.assert()
.success()
.stdout(contains("- `sender-mem`"));
}
const DELTA_IDENTITY: &str = "Multi-section fixture entity for the JSON bulk export.\n\nSecond paragraph with `inline code` and *emphasis* to make the\nround-trip non-trivial.";
const DELTA_PURPOSE: &str = "Verifies byte-faithful section content, metadata, relationships\n(including a cross-mem edge), and `_hash` in one document.";
const GAMMA_IDENTITY: &str = "Cross-mem edge target living in the second writable mem.";
fn make_json_export_workspace(root: &Path) {
let sender = root.join("sender-mem");
let second = root.join("second-mem");
for dir in [&sender, &second] {
let store = dir.join(".memstead");
fs::create_dir_all(&store).unwrap();
fs::write(
store.join("config.json"),
r#"{ "version": "1.0.0", "schema": "default@1.0.0" }"#,
)
.unwrap();
}
let delta_body = format!(
"---\ntype: spec\ncreated_date: 2026-02-01\nlast_modified: 2026-02-01\nlevel: M1\n---\n# Delta\n\n## Identity\n\n{DELTA_IDENTITY}\n\n## Purpose\n\n{DELTA_PURPOSE}\n\n## Relationships\n\n- **USES**: [[second-mem--gamma]]\n"
);
let gamma_body = format!(
"---\ntype: spec\ncreated_date: 2026-02-02\nlast_modified: 2026-02-02\nlevel: M0\n---\n# Gamma\n\n## Identity\n\n{GAMMA_IDENTITY}\n"
);
fs::write(sender.join("delta.md"), &delta_body).unwrap();
fs::write(second.join("gamma.md"), &gamma_body).unwrap();
init_real_mem_repo_from_disk(root, &[(&sender, "sender-mem"), (&second, "second-mem")]);
commit_mem_branch(root, "sender-mem", &[("delta.md", &delta_body)]);
commit_mem_branch(root, "second-mem", &[("gamma.md", &gamma_body)]);
}
fn export_json(root: &Path, extra: &[&str]) -> serde_json::Value {
let out = memstead()
.current_dir(root)
.args(["export", "--format", "json"])
.args(extra)
.assert()
.success()
.get_output()
.stdout
.clone();
serde_json::from_slice(&out).expect("--format json must emit one valid JSON document")
}
#[test]
fn export_json_git_branch_round_trips_values() {
let tmp = TempDir::new().unwrap();
make_json_export_workspace(tmp.path());
let doc = export_json(tmp.path(), &["--mem", "sender-mem"]);
assert_eq!(doc["format"], "memstead-export/v1");
let group = &doc["mems"]["sender-mem"];
assert_eq!(group["schema"], "default@1.0.0");
assert_eq!(group["read_only"], false);
assert_eq!(group["entity_count"], 1);
let entities = group["entities"].as_array().expect("entities array");
let delta = entities
.iter()
.find(|e| e["id"] == "sender-mem--delta")
.expect("delta must be exported");
assert_eq!(delta["entity_type"], "spec");
assert_eq!(delta["title"], "Delta");
assert_eq!(delta["mem"], "sender-mem");
assert_eq!(delta["metadata"]["level"], "M1");
assert_eq!(delta["metadata"]["created_date"], "2026-02-01");
assert_eq!(
delta["sections"]["identity"], DELTA_IDENTITY,
"identity section must round-trip byte-faithfully; got {}",
delta["sections"]["identity"]
);
assert_eq!(delta["sections"]["purpose"], DELTA_PURPOSE);
let rels = delta["relationships"].as_array().expect("relationships");
let uses = rels
.iter()
.find(|r| r["rel_type"] == "USES")
.expect("cross-mem USES edge must be exported");
assert_eq!(uses["target"], "second-mem--gamma");
let hash = delta["_hash"].as_str().expect("_hash string");
assert_eq!(hash.len(), 16, "truncated sha-256 hex expected; got {hash}");
assert!(hash.chars().all(|c| c.is_ascii_hexdigit()));
}
#[test]
fn export_json_workspace_wide_groups_every_writable_mem() {
let tmp = TempDir::new().unwrap();
make_json_export_workspace(tmp.path());
let doc = export_json(tmp.path(), &[]);
let mems = doc["mems"].as_object().expect("mems object");
assert!(mems.contains_key("sender-mem"), "sender-mem group missing");
assert!(mems.contains_key("second-mem"), "second-mem group missing");
let gamma = mems["second-mem"]["entities"]
.as_array()
.unwrap()
.iter()
.find(|e| e["id"] == "second-mem--gamma")
.expect("gamma must be exported in its own group");
assert_eq!(gamma["title"], "Gamma");
assert_eq!(gamma["sections"]["identity"], GAMMA_IDENTITY);
for (name, group) in mems {
for e in group["entities"].as_array().unwrap() {
assert!(
e.get("_stub_kind").is_none(),
"stub leaked into export group `{name}`: {e}"
);
}
}
}
#[test]
fn export_json_read_only_mount_named_vs_default() {
let _guard = cache_guard().lock().unwrap();
let sender = TempDir::new().unwrap();
let receiver = TempDir::new().unwrap();
let cache = TempDir::new().unwrap();
let sender_mem = make_sender_mem(sender.path());
let _receiver_mem = make_receiver_mem(receiver.path());
let archive = sender_mem.join("sender-mem-1.0.0.mem");
memstead()
.current_dir(sender.path())
.args(["export", "--format", "mem", "-o"])
.arg(&archive)
.env("MEMSTEAD_MEM_CACHE", cache.path())
.assert()
.success();
memstead()
.current_dir(receiver.path())
.arg("install")
.arg(&archive)
.env("MEMSTEAD_MEM_CACHE", cache.path())
.assert()
.success();
let wide = memstead()
.current_dir(receiver.path())
.args(["export", "--format", "json"])
.env("MEMSTEAD_MEM_CACHE", cache.path())
.assert()
.success()
.get_output()
.stdout
.clone();
let wide: serde_json::Value = serde_json::from_slice(&wide).unwrap();
let mems = wide["mems"].as_object().unwrap();
assert!(
!mems.contains_key("sender-mem"),
"read-only mount must be absent from the workspace-wide default; got {:?}",
mems.keys().collect::<Vec<_>>()
);
assert!(mems.contains_key("receiver-mem"));
let named = memstead()
.current_dir(receiver.path())
.args(["export", "--format", "json", "--mem", "sender-mem"])
.env("MEMSTEAD_MEM_CACHE", cache.path())
.assert()
.success()
.get_output()
.stdout
.clone();
let named: serde_json::Value = serde_json::from_slice(&named).unwrap();
let group = &named["mems"]["sender-mem"];
assert_eq!(group["read_only"], true);
let ids: Vec<&str> = group["entities"]
.as_array()
.unwrap()
.iter()
.map(|e| e["id"].as_str().unwrap())
.collect();
assert!(
ids.contains(&"sender-mem--alpha") && ids.contains(&"sender-mem--beta"),
"named RO export must carry the mount's entities; got {ids:?}"
);
}
#[test]
fn export_json_leaves_mem_repo_refs_untouched() {
let tmp = TempDir::new().unwrap();
make_json_export_workspace(tmp.path());
let gitdir = tmp.path().join("mem-repo").join(".git");
let refs = |label: &str| -> String {
let out = std::process::Command::new("git")
.arg("--git-dir")
.arg(&gitdir)
.args(["for-each-ref", "--format=%(refname) %(objectname)"])
.output()
.unwrap_or_else(|e| panic!("git for-each-ref ({label}): {e}"));
String::from_utf8(out.stdout).unwrap()
};
let before = refs("before");
assert!(
before.contains("refs/heads/sender-mem"),
"fixture must have seeded the branch; got: {before}"
);
let _ = export_json(tmp.path(), &[]);
let after = refs("after");
assert_eq!(before, after, "export must not move any mem-repo ref");
}
#[test]
fn export_json_folder_mem_round_trips_and_mutates_nothing() {
let tmp = TempDir::new().unwrap();
let root = tmp.path();
let store = root.join(".memstead");
fs::create_dir_all(store.join("state")).unwrap();
fs::write(
store.join("workspace.toml"),
"format = \"memstead-git-branch-2\"\n\n[persistence_adapter]\nname = \"file-two-layer\"\n",
)
.unwrap();
fs::write(
store.join("state").join("mounts.json"),
r#"{"format":"memstead-mounts-3","mounts":[{"mem":"engine","schema":"default@1.0.0","storage":{"type":"folder","path":"engine-mem"},"capability":"write","lifecycle":"eager","cross_linkable":false}]}"#,
)
.unwrap();
let mem_dir = root.join("engine-mem");
fs::create_dir_all(mem_dir.join(".memstead")).unwrap();
fs::write(
mem_dir.join(".memstead").join("config.json"),
r#"{"format":1,"schema":"default@1.0.0"}"#,
)
.unwrap();
let epsilon_body = format!(
"---\ntype: spec\ncreated_date: 2026-02-03\nlast_modified: 2026-02-03\nlevel: M0\n---\n# Epsilon\n\n## Identity\n\n{DELTA_IDENTITY}\n\n## Purpose\n\n{DELTA_PURPOSE}\n"
);
fs::write(mem_dir.join("epsilon.md"), &epsilon_body).unwrap();
let snapshot = |label: &str| -> Vec<(String, Vec<u8>)> {
let mut files: Vec<(String, Vec<u8>)> = walk_files(&mem_dir)
.into_iter()
.map(|p| {
let rel = p
.strip_prefix(&mem_dir)
.unwrap()
.to_string_lossy()
.into_owned();
let bytes = fs::read(&p).unwrap_or_else(|e| panic!("read {p:?} ({label}): {e}"));
(rel, bytes)
})
.collect();
files.sort();
files
};
let before = snapshot("before");
let doc = export_json(root, &["--mem", "engine"]);
let group = &doc["mems"]["engine"];
assert_eq!(group["read_only"], false);
let epsilon = group["entities"]
.as_array()
.unwrap()
.iter()
.find(|e| e["id"] == "engine--epsilon")
.expect("epsilon must be exported from the folder mount");
assert_eq!(epsilon["title"], "Epsilon");
assert_eq!(epsilon["sections"]["identity"], DELTA_IDENTITY);
assert_eq!(epsilon["sections"]["purpose"], DELTA_PURPOSE);
let after = snapshot("after");
assert_eq!(before, after, "export must not change any mem-content file");
}
fn walk_files(dir: &Path) -> Vec<std::path::PathBuf> {
let mut out = Vec::new();
for entry in fs::read_dir(dir).unwrap() {
let p = entry.unwrap().path();
if p.is_dir() {
out.extend(walk_files(&p));
} else {
out.push(p);
}
}
out
}
#[test]
fn export_json_refusals_are_typed() {
let tmp = TempDir::new().unwrap();
make_json_export_workspace(tmp.path());
let unknown = memstead()
.current_dir(tmp.path())
.args(["export", "--format", "json", "--mem", "no-such-mem"])
.assert()
.failure()
.get_output()
.stderr
.clone();
let unknown = String::from_utf8(unknown).unwrap();
assert!(unknown.contains("UNKNOWN_MEM"), "got: {unknown}");
assert!(!unknown.contains("INTERNAL"), "got: {unknown}");
let with_output = memstead()
.current_dir(tmp.path())
.args(["export", "--format", "json", "-o", "dump.json"])
.assert()
.failure()
.get_output()
.stderr
.clone();
let with_output = String::from_utf8(with_output).unwrap();
assert!(with_output.contains("INVALID_INPUT"), "got: {with_output}");
assert!(!with_output.contains("INTERNAL"), "got: {with_output}");
assert!(
!tmp.path().join("dump.json").exists(),
"refusal must not have written the file"
);
}
fn make_rename_workspace(root: &Path) {
let m = |args: &[&str]| {
memstead()
.current_dir(root)
.env("MEMSTEAD_OPERATOR_MODE", "1")
.args(args)
.assert()
.success();
};
m(&["mem-repo", "init", "."]);
m(&["mem", "init", "alpha", "--no-gitignore"]);
m(&["mem", "init", "beta", "--no-gitignore"]);
m(&["workspace", "grant-cross-link", "beta", "alpha"]);
fs::create_dir_all(root.join("src")).unwrap();
fs::write(root.join("src/lib.rs"), "// lib").unwrap();
let corpus = serde_json::json!({ "creates": [
{"title": "One", "entity_type": "spec", "mem": "alpha",
"sections": {"identity": "First entity.", "purpose": "Rename fixture."}},
{"title": "Two", "entity_type": "spec", "mem": "alpha",
"sections": {"identity": "Second entity, see [[alpha--one]] full form.", "purpose": "Rename fixture."},
"relations": [{"rel_type": "USES", "target": "alpha--one"}]},
{"title": "Watcher", "entity_type": "spec", "mem": "beta",
"sections": {"identity": "Watches [[alpha--one]] from beta.", "purpose": "Cross-mem referrer."},
"relations": [{"rel_type": "DEPENDS_ON", "target": "alpha--two"}]},
]});
let corpus_path = root.join("corpus.json");
fs::write(&corpus_path, serde_json::to_string(&corpus).unwrap()).unwrap();
m(&["batch-create", "--from", corpus_path.to_str().unwrap()]);
fs::remove_file(&corpus_path).unwrap();
m(&[
"update",
"alpha--one",
"--auto-hash",
"--append",
"purpose= Anchored.",
"--anchor",
r#"{"artifact": "src/lib.rs", "grain": "file", "class": "anchored"}"#,
]);
m(&[
"mem",
"set-sync-state",
"alpha",
"alpha/graph/tree#synced",
"abc123",
]);
}
#[test]
fn mem_rename_git_branch_full_surface() {
let tmp = TempDir::new().unwrap();
let root = tmp.path();
make_rename_workspace(root);
memstead()
.current_dir(root)
.env("MEMSTEAD_OPERATOR_MODE", "1")
.args(["mem", "rename", "alpha", "gamma"])
.assert()
.success()
.stdout(contains("renamed to `gamma`"));
memstead()
.current_dir(root)
.args(["entity", "gamma--one"])
.assert()
.success();
memstead()
.current_dir(root)
.args(["entity", "alpha--one"])
.assert()
.failure()
.stderr(contains("ENTITY_NOT_FOUND"));
memstead()
.current_dir(root)
.args(["entity", "beta--watcher"])
.assert()
.success()
.stdout(contains("[[gamma--one]]"))
.stdout(contains("[[gamma--two]]"))
.stdout(predicates::prelude::PredicateBooleanExt::not(contains(
"alpha--",
)));
let toml = fs::read_to_string(root.join(".memstead/workspace.toml")).unwrap();
assert!(
toml.contains(r#"beta = ["gamma"]"#),
"grant must name the new mem; got:\n{toml}"
);
assert!(
!toml.contains("alpha"),
"no grant may still name the old mem:\n{toml}"
);
let log = std::process::Command::new("git")
.arg("--git-dir")
.arg(root.join("mem-repo/.git"))
.args(["log", "--oneline", "refs/heads/gamma"])
.output()
.unwrap();
let log = String::from_utf8(log.stdout).unwrap();
assert!(
log.contains("create mem alpha"),
"history must be preserved across the rename; got:\n{log}"
);
let refs = std::process::Command::new("git")
.arg("--git-dir")
.arg(root.join("mem-repo/.git"))
.args(["for-each-ref", "--format=%(refname)"])
.output()
.unwrap();
let refs = String::from_utf8(refs.stdout).unwrap();
assert!(
!refs.contains("refs/heads/alpha"),
"old branch must be gone:\n{refs}"
);
let sidecar = std::process::Command::new("git")
.arg("--git-dir")
.arg(root.join("mem-repo/.git"))
.args(["show", "refs/heads/gamma:.memstead/anchors.json"])
.output()
.unwrap();
let sidecar = String::from_utf8(sidecar.stdout).unwrap();
assert!(
sidecar.contains("gamma--one") && !sidecar.contains("alpha--one"),
"anchors must key on the new id; got:\n{sidecar}"
);
let config = std::process::Command::new("git")
.arg("--git-dir")
.arg(root.join("mem-repo/.git"))
.args(["show", "refs/heads/__MEMSTEAD:mems/gamma/config.json"])
.output()
.unwrap();
let config = String::from_utf8(config.stdout).unwrap();
assert!(
config.contains("gamma/graph/tree#synced") && !config.contains("alpha/"),
"sync-state keys must carry the new mem name; got:\n{config}"
);
memstead()
.current_dir(root)
.args(["health", "--strict"])
.assert()
.success();
}
#[test]
fn mem_rename_refusals_are_typed_and_effect_free() {
let tmp = TempDir::new().unwrap();
let root = tmp.path();
make_rename_workspace(root);
let snapshot = |label: &str| -> (String, String, String) {
let refs = std::process::Command::new("git")
.arg("--git-dir")
.arg(root.join("mem-repo/.git"))
.args(["for-each-ref", "--format=%(refname) %(objectname)"])
.output()
.unwrap_or_else(|e| panic!("for-each-ref ({label}): {e}"));
(
String::from_utf8(refs.stdout).unwrap(),
fs::read_to_string(root.join(".memstead/workspace.toml")).unwrap(),
fs::read_to_string(root.join(".memstead/state/mounts.json")).unwrap(),
)
};
let before = snapshot("before");
let refusals: &[(&[&str], &str, bool)] = &[
(&["mem", "rename", "nope", "other"], "UNKNOWN_MEM", true),
(
&["mem", "rename", "alpha", "beta"],
"MEM_NAME_COLLISION",
true,
),
(
&["mem", "rename", "alpha", "Bad Name"],
"INVALID_MEM_NAME",
true,
),
(&["mem", "rename", "alpha", "alpha"], "INVALID_INPUT", true),
(
&["mem", "rename", "alpha", "delta"],
"MEM_PATH_NOT_ALLOWED",
false,
),
];
for (args, code, operator) in refusals {
let mut cmd = memstead();
cmd.current_dir(root);
if *operator {
cmd.env("MEMSTEAD_OPERATOR_MODE", "1");
} else {
cmd.env_remove("MEMSTEAD_OPERATOR_MODE");
}
let out = cmd
.args(*args)
.assert()
.failure()
.get_output()
.stderr
.clone();
let stderr = String::from_utf8(out).unwrap();
assert!(stderr.contains(code), "expected {code}; got: {stderr}");
assert!(!stderr.contains("INTERNAL"), "INTERNAL leaked: {stderr}");
if *code == "MEM_PATH_NOT_ALLOWED" {
assert!(
stderr.contains("mem_management.delete"),
"policy_table disambiguator missing: {stderr}"
);
}
}
let after = snapshot("after");
assert_eq!(
before, after,
"a refused rename must leave the workspace byte-identical"
);
}
#[test]
fn mem_rename_interrupted_half_state_detectable_and_completable() {
let tmp = TempDir::new().unwrap();
let root = tmp.path();
make_rename_workspace(root);
let watcher_body = "---\ntype: spec\ncreated_date: 2026-02-01\nlast_modified: 2026-02-01\nlevel: M0\n---\n# Watcher\n\n## Identity\n\nWatches [[gamma--one]] from beta.\n\n## Purpose\n\nCross-mem referrer.\n\n## Relationships\n\n- **DEPENDS_ON**: [[gamma--two]]\n";
commit_mem_branch(root, "beta", &[("watcher.md", watcher_body)]);
memstead()
.current_dir(root)
.args(["health", "--strict"])
.assert()
.stdout(contains("Stubs: 1"));
memstead()
.current_dir(root)
.env("MEMSTEAD_OPERATOR_MODE", "1")
.args(["mem", "rename", "alpha", "gamma"])
.assert()
.success();
memstead()
.current_dir(root)
.args(["entity", "gamma--one"])
.assert()
.success();
memstead()
.current_dir(root)
.args(["health", "--strict"])
.assert()
.success()
.stdout(contains("Stubs: 0"));
}
#[test]
fn mem_rename_folder_mount() {
let tmp = TempDir::new().unwrap();
let root = tmp.path();
let store = root.join(".memstead");
fs::create_dir_all(store.join("state")).unwrap();
fs::write(
store.join("workspace.toml"),
"format = \"memstead-git-branch-2\"\n\n[persistence_adapter]\nname = \"file-two-layer\"\n",
)
.unwrap();
memstead_git_branch::test_support::init_real_mem_repo(root, &[]);
fs::write(
store.join("state").join("mounts.json"),
r#"{"format":"memstead-mounts-3","mounts":[{"mem":"engine","schema":"default@1.0.0","storage":{"type":"folder","path":"engine-mem"},"capability":"write","lifecycle":"eager","cross_linkable":false}]}"#,
)
.unwrap();
let mem_dir = root.join("engine-mem");
fs::create_dir_all(mem_dir.join(".memstead")).unwrap();
fs::write(
mem_dir.join(".memstead").join("config.json"),
r#"{"format":1,"schema":"default@1.0.0"}"#,
)
.unwrap();
fs::write(
mem_dir.join("solo.md"),
"---\ntype: spec\ncreated_date: 2026-02-01\nlast_modified: 2026-02-01\nlevel: M0\n---\n# Solo\n\n## Identity\n\nFolder-mount rename fixture.\n\n## Purpose\n\nTesting.\n",
)
.unwrap();
memstead()
.current_dir(root)
.env("MEMSTEAD_OPERATOR_MODE", "1")
.args(["mem", "rename", "engine", "motor"])
.assert()
.success();
memstead()
.current_dir(root)
.args(["entity", "motor--solo"])
.assert()
.success();
memstead()
.current_dir(root)
.args(["entity", "engine--solo"])
.assert()
.failure();
assert!(
root.join("engine-mem").join("solo.md").exists(),
"folder location is not identity — the directory stays in place"
);
let mounts = fs::read_to_string(store.join("state").join("mounts.json")).unwrap();
assert!(
mounts.contains(r#""motor""#) && !mounts.contains(r#""engine""#),
"mounts.json must carry the new name; got:\n{mounts}"
);
}
#[test]
fn mem_rename_read_only_mount_refuses() {
let _guard = cache_guard().lock().unwrap();
let sender = TempDir::new().unwrap();
let receiver = TempDir::new().unwrap();
let cache = TempDir::new().unwrap();
let sender_mem = make_sender_mem(sender.path());
let _receiver_mem = make_receiver_mem(receiver.path());
let archive = sender_mem.join("sender-mem-1.0.0.mem");
memstead()
.current_dir(sender.path())
.args(["export", "--format", "mem", "-o"])
.arg(&archive)
.env("MEMSTEAD_MEM_CACHE", cache.path())
.assert()
.success();
memstead()
.current_dir(receiver.path())
.arg("install")
.arg(&archive)
.env("MEMSTEAD_MEM_CACHE", cache.path())
.assert()
.success();
let out = memstead()
.current_dir(receiver.path())
.env("MEMSTEAD_OPERATOR_MODE", "1")
.env("MEMSTEAD_MEM_CACHE", cache.path())
.args(["mem", "rename", "sender-mem", "other-name"])
.assert()
.failure()
.get_output()
.stderr
.clone();
let stderr = String::from_utf8(out).unwrap();
assert!(
stderr.contains("READ_ONLY_MOUNT") || stderr.contains("read-only"),
"renaming an RO mount must refuse with the read-only code; got: {stderr}"
);
assert!(!stderr.contains("INTERNAL"), "INTERNAL leaked: {stderr}");
}
#[test]
fn mem_rename_rewrites_grant_table_keys() {
let tmp = TempDir::new().unwrap();
let root = tmp.path();
make_rename_workspace(root);
memstead()
.current_dir(root)
.env("MEMSTEAD_OPERATOR_MODE", "1")
.args(["mem", "rename", "beta", "zeta"])
.assert()
.success();
let toml = fs::read_to_string(root.join(".memstead/workspace.toml")).unwrap();
assert!(
toml.contains(r#"zeta = ["alpha"]"#),
"the grant key must carry the new mem name; got:\n{toml}"
);
assert!(
!toml.contains("beta"),
"no grant may still key on the old mem name:\n{toml}"
);
}
#[test]
fn mem_rename_preserves_outgoing_cross_mem_edges() {
let tmp = TempDir::new().unwrap();
let root = tmp.path();
make_rename_workspace(root);
memstead()
.current_dir(root)
.env("MEMSTEAD_OPERATOR_MODE", "1")
.args(["mem", "rename", "beta", "zeta"])
.assert()
.success();
memstead()
.current_dir(root)
.args(["entity", "zeta--watcher"])
.assert()
.success()
.stdout(contains("[[alpha--one]]"))
.stdout(contains("alpha--two"));
memstead()
.current_dir(root)
.args(["entity", "beta--watcher"])
.assert()
.failure()
.stderr(contains("ENTITY_NOT_FOUND"));
memstead()
.current_dir(root)
.args(["--json", "relations", "alpha--two"])
.assert()
.success()
.stdout(contains("zeta--watcher"));
memstead()
.current_dir(root)
.args(["health", "--strict"])
.assert()
.success();
}
#[test]
fn mem_rename_create_allowlist_gates_the_new_name() {
let tmp = TempDir::new().unwrap();
let root = tmp.path();
make_rename_workspace(root);
let m = |args: &[&str]| {
memstead()
.current_dir(root)
.env("MEMSTEAD_OPERATOR_MODE", "1")
.args(args)
.assert()
.success();
};
m(&["workspace", "allow-delete", "alpha"]);
m(&["workspace", "allow-create", "exec-*", "--schema", "*"]);
let out = memstead()
.current_dir(root)
.args(["--json", "mem", "rename", "alpha", "gamma"])
.assert()
.failure()
.get_output()
.stdout
.clone();
let err = String::from_utf8(out).unwrap();
assert!(
err.contains("MEM_PATH_NOT_ALLOWED"),
"typed refusal expected; got: {err}"
);
assert!(
err.contains("mem_management.create"),
"the refusal names the create policy table; got: {err}"
);
memstead()
.current_dir(root)
.args(["entity", "alpha--one"])
.assert()
.success();
memstead()
.current_dir(root)
.args(["mem", "rename", "alpha", "exec-alpha"])
.assert()
.success();
memstead()
.current_dir(root)
.args(["entity", "exec-alpha--one"])
.assert()
.success();
}
#[test]
fn mem_rename_relocates_bindings_and_findings() {
let tmp = TempDir::new().unwrap();
let root = tmp.path();
make_rename_workspace(root);
memstead()
.current_dir(root)
.env("MEMSTEAD_OPERATOR_MODE", "1")
.args([
"projection",
"init",
"--mem",
"alpha",
"--source",
"src",
"--medium-type",
"codebase",
"--intent",
"rename fixture",
"--name",
"graph",
])
.assert()
.success();
let findings_dir = root.join(".memstead/state/findings/alpha");
fs::create_dir_all(&findings_dir).unwrap();
fs::write(findings_dir.join("graph.json"), "{\"findings\":[]}").unwrap();
memstead()
.current_dir(root)
.env("MEMSTEAD_OPERATOR_MODE", "1")
.args(["mem", "rename", "alpha", "gamma"])
.assert()
.success();
let binding_new = root.join(".memstead/projections/gamma/graph.json");
assert!(binding_new.is_file(), "binding must move with the mem");
assert!(
!root.join(".memstead/projections/alpha").exists(),
"old projections dir must be gone"
);
let binding: serde_json::Value =
serde_json::from_str(&fs::read_to_string(&binding_new).unwrap()).unwrap();
assert_eq!(
binding["destination_mem"], "gamma",
"destination_mem must be rewritten; got: {binding}"
);
assert!(
root.join(".memstead/state/findings/gamma/graph.json")
.is_file(),
"findings store must move with the mem"
);
assert!(
!root.join(".memstead/state/findings/alpha").exists(),
"old findings dir must be gone"
);
}
fn export_sender_archive(sender_root: &Path, sender_mem: &Path) -> std::path::PathBuf {
let archive = sender_mem.join("sender-mem-1.0.0.mem");
memstead()
.current_dir(sender_root)
.args(["export", "--format", "mem", "-o"])
.arg(&archive)
.assert()
.success();
archive
}
#[test]
fn install_registers_workspace_read_only_mount() {
let _guard = cache_guard().lock().unwrap_or_else(|e| e.into_inner());
let sender = TempDir::new().unwrap();
let receiver = TempDir::new().unwrap();
let cache = TempDir::new().unwrap();
let sender_mem = make_sender_mem(sender.path());
let _receiver_mem = make_receiver_mem(receiver.path());
let archive = export_sender_archive(sender.path(), &sender_mem);
memstead()
.current_dir(receiver.path())
.arg("install")
.arg(&archive)
.env("MEMSTEAD_MEM_CACHE", cache.path())
.assert()
.success()
.stdout(contains("registered as a workspace-level read-only mount"));
memstead()
.current_dir(receiver.path())
.args(["mem", "list"])
.env("MEMSTEAD_MEM_CACHE", cache.path())
.assert()
.success()
.stdout(contains("`sender-mem` (read_only)"));
memstead()
.current_dir(receiver.path())
.args(["overview"])
.env("MEMSTEAD_MEM_CACHE", cache.path())
.assert()
.success()
.stdout(contains("sender-mem"));
let mounts = fs::read_to_string(receiver.path().join(".memstead/state/mounts.json")).unwrap();
assert!(
mounts.contains(r#""sender-mem""#) && mounts.contains(r#""archive""#),
"mounts.json must carry the archive mount; got:\n{mounts}"
);
let cfg = memstead_git_branch::mem_repo_config::read_config(receiver.path(), "receiver-mem")
.expect("receiver config readable");
assert!(
cfg.read_mems.is_empty(),
"install must not write readMems; got {:?}",
cfg.read_mems
);
}
#[test]
fn uninstall_round_trip_cache_survives_and_reads_have_parity() {
let _guard = cache_guard().lock().unwrap_or_else(|e| e.into_inner());
let sender = TempDir::new().unwrap();
let receiver = TempDir::new().unwrap();
let cache = TempDir::new().unwrap();
let sender_mem = make_sender_mem(sender.path());
let _receiver_mem = make_receiver_mem(receiver.path());
let archive = export_sender_archive(sender.path(), &sender_mem);
memstead()
.current_dir(receiver.path())
.arg("install")
.arg(&archive)
.env("MEMSTEAD_MEM_CACHE", cache.path())
.assert()
.success();
memstead()
.current_dir(receiver.path())
.args(["search", "Alpha"])
.env("MEMSTEAD_MEM_CACHE", cache.path())
.assert()
.success()
.stdout(contains("sender-mem--alpha"));
memstead()
.current_dir(receiver.path())
.env("MEMSTEAD_OPERATOR_MODE", "1")
.args([
"workspace",
"grant-cross-link",
"receiver-mem",
"sender-mem",
])
.env("MEMSTEAD_MEM_CACHE", cache.path())
.assert()
.success();
memstead()
.current_dir(receiver.path())
.env("MEMSTEAD_MEM_CACHE", cache.path())
.args([
"create",
"--mem",
"receiver-mem",
"--title",
"Bridge",
"--type",
"spec",
"--section",
"identity=Links into [[sender-mem--alpha]] from the writable side.",
"--section",
"purpose=Cross-mem parity probe.",
])
.assert()
.success();
memstead()
.current_dir(receiver.path())
.args(["entity", "receiver-mem--bridge"])
.env("MEMSTEAD_MEM_CACHE", cache.path())
.assert()
.success()
.stdout(contains("[[sender-mem--alpha]]"));
memstead()
.current_dir(receiver.path())
.arg("uninstall")
.arg("sender-mem")
.env("MEMSTEAD_MEM_CACHE", cache.path())
.assert()
.failure()
.stderr(contains("MEM_HAS_INCOMING_REFS"))
.stderr(contains("receiver-mem--bridge"));
memstead()
.current_dir(receiver.path())
.env("MEMSTEAD_MEM_CACHE", cache.path())
.args(["delete", "receiver-mem--bridge"])
.assert()
.success();
memstead()
.current_dir(receiver.path())
.arg("uninstall")
.arg("sender-mem")
.env("MEMSTEAD_MEM_CACHE", cache.path())
.assert()
.success()
.stdout(contains("archive copy retained"));
memstead()
.current_dir(receiver.path())
.args(["search", "Alpha"])
.env("MEMSTEAD_MEM_CACHE", cache.path())
.assert()
.success()
.stdout(predicates::prelude::PredicateBooleanExt::not(contains(
"sender-mem--alpha",
)));
let cached: Vec<_> = fs::read_dir(cache.path())
.unwrap()
.map(|e| e.unwrap().file_name().to_string_lossy().into_owned())
.filter(|n| n.starts_with("sender-mem-") && n.ends_with(".mem"))
.collect();
assert_eq!(
cached.len(),
1,
"cache copy must survive uninstall: {cached:?}"
);
memstead()
.current_dir(receiver.path())
.arg("install")
.arg(&archive)
.env("MEMSTEAD_MEM_CACHE", cache.path())
.assert()
.success()
.stdout(contains("already in cache"))
.stdout(contains("registered as a workspace-level read-only mount"));
}
#[test]
fn legacy_read_mems_config_migrates_at_boot() {
let _guard = cache_guard().lock().unwrap_or_else(|e| e.into_inner());
let sender = TempDir::new().unwrap();
let receiver = TempDir::new().unwrap();
let cache = TempDir::new().unwrap();
let sender_mem = make_sender_mem(sender.path());
let _receiver_mem = make_receiver_mem(receiver.path());
let archive = export_sender_archive(sender.path(), &sender_mem);
unsafe {
std::env::set_var("MEMSTEAD_MEM_CACHE", cache.path());
}
let cached = memstead_git_branch::mem_cache::install_to_cache(&archive, &[]).unwrap();
unsafe {
std::env::remove_var("MEMSTEAD_MEM_CACHE");
}
let mut cfg =
memstead_git_branch::mem_repo_config::read_config(receiver.path(), "receiver-mem")
.expect("receiver config readable");
cfg.read_mems.insert(
"sender-mem".to_string(),
memstead_schema::config::ReadMemSpec {
source: memstead_schema::config::ReadMemSource::Local,
cache_key: Some(cached.cache_key.clone()),
},
);
let mut bytes = serde_json::to_vec_pretty(&cfg).unwrap();
bytes.push(b'\n');
memstead_git_branch::mem_repo_config::commit_config(
receiver.path(),
"receiver-mem",
&bytes,
&memstead_git_branch::vcs::CommitContext::internal(),
"test: legacy readMems fixture",
)
.unwrap();
let health = memstead()
.current_dir(receiver.path())
.args(["--json", "health"])
.env("MEMSTEAD_MEM_CACHE", cache.path())
.assert()
.success()
.get_output()
.stdout
.clone();
let health = String::from_utf8(health).unwrap();
assert!(
health.contains("READ_MEMS_MIGRATED_TO_MOUNTS") && health.contains("sender-mem"),
"boot must surface the migration warning naming the mem; got:\n{health}"
);
let mounts = fs::read_to_string(receiver.path().join(".memstead/state/mounts.json")).unwrap();
assert!(
mounts.contains(r#""sender-mem""#),
"migrated mount must persist; got:\n{mounts}"
);
let cfg_after =
memstead_git_branch::mem_repo_config::read_config(receiver.path(), "receiver-mem").unwrap();
assert!(
cfg_after.read_mems.is_empty(),
"legacy key must be removed; got {:?}",
cfg_after.read_mems
);
let health2 = memstead()
.current_dir(receiver.path())
.args(["--json", "health"])
.env("MEMSTEAD_MEM_CACHE", cache.path())
.assert()
.success()
.get_output()
.stdout
.clone();
let health2 = String::from_utf8(health2).unwrap();
assert!(
!health2.contains("READ_MEMS_MIGRATED_TO_MOUNTS"),
"second boot must not re-warn; got:\n{health2}"
);
memstead()
.current_dir(receiver.path())
.args(["search", "Alpha"])
.env("MEMSTEAD_MEM_CACHE", cache.path())
.assert()
.success()
.stdout(contains("sender-mem--alpha"));
memstead()
.current_dir(receiver.path())
.env("MEMSTEAD_OPERATOR_MODE", "1")
.args([
"workspace",
"grant-cross-link",
"receiver-mem",
"sender-mem",
])
.env("MEMSTEAD_MEM_CACHE", cache.path())
.assert()
.success();
memstead()
.current_dir(receiver.path())
.env("MEMSTEAD_MEM_CACHE", cache.path())
.args([
"create",
"--mem",
"receiver-mem",
"--title",
"Bridge",
"--type",
"spec",
"--section",
"identity=Links into [[sender-mem--alpha]] post-migration.",
"--section",
"purpose=Migration parity probe.",
])
.assert()
.success();
memstead()
.current_dir(receiver.path())
.args(["entity", "receiver-mem--bridge"])
.env("MEMSTEAD_MEM_CACHE", cache.path())
.assert()
.success()
.stdout(contains("[[sender-mem--alpha]]"));
}
#[test]
fn uninstall_refusals_are_typed_and_effect_free() {
let _guard = cache_guard().lock().unwrap_or_else(|e| e.into_inner());
let receiver = TempDir::new().unwrap();
let _receiver_mem = make_receiver_mem(receiver.path());
let mounts_before =
fs::read_to_string(receiver.path().join(".memstead/state/mounts.json")).unwrap();
for (name, code) in [
("no-such-mem", "UNKNOWN_MEM"),
("receiver-mem", "MEM_NOT_READ_ONLY"),
] {
let out = memstead()
.current_dir(receiver.path())
.arg("uninstall")
.arg(name)
.assert()
.failure()
.get_output()
.stderr
.clone();
let stderr = String::from_utf8(out).unwrap();
assert!(stderr.contains(code), "expected {code}; got: {stderr}");
assert!(!stderr.contains("INTERNAL"), "INTERNAL leaked: {stderr}");
}
let mounts_after =
fs::read_to_string(receiver.path().join(".memstead/state/mounts.json")).unwrap();
assert_eq!(
mounts_before, mounts_after,
"refusals must not touch mount state"
);
}
fn make_anchor_workspace(root: &Path) {
let m = |args: &[&str]| {
memstead()
.current_dir(root)
.env("MEMSTEAD_OPERATOR_MODE", "1")
.args(args)
.assert()
.success();
};
m(&["mem-repo", "init", "."]);
m(&["mem", "init", "hold", "--no-gitignore"]);
m(&[
"create",
"--mem",
"hold",
"--title",
"Holder",
"--type",
"spec",
"--section",
"identity=Anchored fixture entity.",
"--section",
"purpose=Verification states.",
]);
for (name, content) in [
("src-a.txt", "alpha"),
("src-b.txt", "beta"),
("src-c.txt", "gamma"),
("src-d.txt", "delta"),
] {
fs::write(root.join(name), content).unwrap();
}
let h = |content: &str| memstead_base::anchor::prepared_content_hash(content.as_bytes());
let anchors = [
format!(
r#"{{"artifact":"src-a.txt","grain":"file","class":"anchored","hash":"{}","hash_stability":"stable"}}"#,
h("alpha")
),
format!(
r#"{{"artifact":"src-b.txt","grain":"file","class":"anchored","hash":"{}","hash_stability":"stable"}}"#,
h("beta")
),
format!(
r#"{{"artifact":"src-c.txt","grain":"file","class":"anchored","hash":"{}","hash_stability":"unstable"}}"#,
h("gamma")
),
format!(
r#"{{"artifact":"src-d.txt","grain":"file","class":"anchored","hash":"{}","hash_stability":"stable"}}"#,
h("delta")
),
];
let mut args: Vec<String> = vec![
"update".into(),
"hold--holder".into(),
"--auto-hash".into(),
"--append".into(),
"purpose= Anchored.".into(),
];
for a in &anchors {
args.push("--anchor".into());
args.push(a.clone());
}
let arg_refs: Vec<&str> = args.iter().map(String::as_str).collect();
m(&arg_refs);
fs::write(root.join("src-b.txt"), "beta CHANGED").unwrap();
fs::write(root.join("src-c.txt"), "gamma CHANGED").unwrap();
fs::remove_file(root.join("src-d.txt")).unwrap();
}
#[test]
fn source_dialect_anchors_join_fallback_collide_and_refuse() {
let tmp = TempDir::new().unwrap();
let root = tmp.path();
let m = |args: &[&str]| {
memstead()
.current_dir(root)
.env("MEMSTEAD_OPERATOR_MODE", "1")
.args(args)
.assert()
.success();
};
m(&["mem-repo", "init", ".", "--no-gitignore"]);
m(&["mem", "init", "hold", "--no-gitignore"]);
fs::create_dir_all(root.join("srcdir")).unwrap();
fs::write(root.join("srcdir/a.txt"), "alpha").unwrap();
fs::write(root.join("srcdir/x.txt"), "source copy").unwrap();
fs::write(root.join("x.txt"), "workspace copy").unwrap();
m(&[
"projection",
"init",
"--mem",
"hold",
"--source",
"./srcdir",
"--medium-type",
"codebase",
"--name",
"main-app",
]);
let h = |content: &str| memstead_base::anchor::prepared_content_hash(content.as_bytes());
let src_a = format!(
r#"{{"artifact":"a.txt","source":"main-app","grain":"file","class":"anchored","hash":"{}","hash_stability":"stable"}}"#,
h("alpha")
);
let src_x = format!(
r#"{{"artifact":"x.txt","source":"main-app","grain":"file","class":"anchored","hash":"{}","hash_stability":"stable"}}"#,
h("source copy")
);
let ws_a = format!(
r#"{{"artifact":"srcdir/a.txt","grain":"file","class":"anchored","hash":"{}","hash_stability":"stable"}}"#,
h("alpha")
);
m(&[
"create",
"--mem",
"hold",
"--title",
"Dialects",
"--type",
"spec",
"--section",
"identity=Anchor dialect fixture.",
"--section",
"purpose=Join, fallback, collision.",
"--anchor",
&src_a,
"--anchor",
&src_x,
"--anchor",
&ws_a,
]);
let out = memstead()
.current_dir(root)
.args(["--json", "verify-anchors", "--mem", "hold"])
.assert()
.success()
.get_output()
.stdout
.clone();
let v: serde_json::Value = serde_json::from_slice(&out).unwrap();
let state_of = |artifact: &str| -> String {
v["anchors"]
.as_array()
.unwrap()
.iter()
.find(|a| a["artifact"] == artifact)
.map(|a| a["state"].as_str().unwrap().to_string())
.unwrap_or_else(|| panic!("anchor for {artifact} missing: {v}"))
};
assert_eq!(
state_of("a.txt"),
"resolves",
"source-relative resolves via the pointer-join: {v}"
);
assert_eq!(
state_of("x.txt"),
"resolves",
"collision resolves to the SOURCE-join target (its hash matches the \
source copy; the workspace twin would read drifted): {v}"
);
assert_eq!(
state_of("srcdir/a.txt"),
"resolves",
"workspace-relative fallback unbroken: {v}"
);
let dead =
r#"{"artifact":"missing.txt","source":"main-app","grain":"file","class":"anchored"}"#;
let out = memstead()
.current_dir(root)
.env("MEMSTEAD_OPERATOR_MODE", "1")
.args([
"--json",
"create",
"--mem",
"hold",
"--title",
"Dead Ref",
"--type",
"spec",
"--section",
"identity=Should refuse.",
"--section",
"purpose=Dead anchor.",
"--anchor",
dead,
])
.assert()
.failure()
.get_output()
.clone();
let err = format!(
"{}{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
assert!(err.contains("INVALID_ANCHOR"), "typed refusal: {err}");
assert!(
err.contains("missing.txt"),
"payload names the artifact: {err}"
);
assert!(
err.contains("srcdir/missing.txt"),
"payload names the source-join candidate tried: {err}"
);
let typo =
r#"{"artifact":"missing2.txt","source":"main-apppp","grain":"file","class":"anchored"}"#;
let out = memstead()
.current_dir(root)
.env("MEMSTEAD_OPERATOR_MODE", "1")
.args([
"--json",
"create",
"--mem",
"hold",
"--title",
"Typo Source",
"--type",
"spec",
"--section",
"identity=Should refuse.",
"--section",
"purpose=Typo'd source.",
"--anchor",
typo,
])
.assert()
.failure()
.get_output()
.clone();
let err = format!(
"{}{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
assert!(err.contains("INVALID_ANCHOR"), "typed refusal: {err}");
assert!(err.contains("missing2.txt"), "names the artifact: {err}");
let legacy =
r#"{"artifact":"srcdir/a.txt","source":"legacy-name","grain":"file","class":"anchored"}"#;
memstead()
.current_dir(root)
.env("MEMSTEAD_OPERATOR_MODE", "1")
.args([
"create",
"--mem",
"hold",
"--title",
"Legacy Source",
"--type",
"spec",
"--section",
"identity=Accepted.",
"--section",
"purpose=Legacy source name over a live path.",
"--anchor",
legacy,
])
.assert()
.success();
}
#[test]
fn verify_anchors_reports_four_states_without_binding() {
let tmp = TempDir::new().unwrap();
let root = tmp.path();
make_anchor_workspace(root);
let refs_before = std::process::Command::new("git")
.arg("--git-dir")
.arg(root.join("mem-repo/.git"))
.args(["for-each-ref", "--format=%(refname) %(objectname)"])
.output()
.unwrap()
.stdout;
let out = memstead()
.current_dir(root)
.args(["--json", "verify-anchors", "--mem", "hold"])
.assert()
.success()
.get_output()
.stdout
.clone();
let v: serde_json::Value = serde_json::from_slice(&out).unwrap();
assert_eq!(v["resolves"], 1, "full: {v}");
assert_eq!(v["drifted"], 1, "full: {v}");
assert_eq!(v["recheck"], 1, "full: {v}");
assert_eq!(v["unresolvable"], 1, "full: {v}");
let state_of = |artifact: &str| -> String {
v["anchors"]
.as_array()
.unwrap()
.iter()
.find(|a| a["artifact"] == artifact)
.map(|a| a["state"].as_str().unwrap().to_string())
.unwrap_or_else(|| panic!("anchor for {artifact} missing: {v}"))
};
assert_eq!(state_of("src-a.txt"), "resolves");
assert_eq!(state_of("src-b.txt"), "drifted");
assert_eq!(state_of("src-c.txt"), "recheck");
assert_eq!(state_of("src-d.txt"), "unresolvable");
let refs_after = std::process::Command::new("git")
.arg("--git-dir")
.arg(root.join("mem-repo/.git"))
.args(["for-each-ref", "--format=%(refname) %(objectname)"])
.output()
.unwrap()
.stdout;
assert_eq!(
refs_before, refs_after,
"verify-anchors must not move any ref"
);
}
#[test]
fn verify_anchors_observations_adjudicate_and_age_url_rows() {
let tmp = TempDir::new().unwrap();
let root = tmp.path();
make_anchor_workspace(root);
let proj = root.join(".memstead/projections/hold");
fs::create_dir_all(&proj).unwrap();
fs::write(
proj.join("graph.json"),
r#"{"version":2,"intent":"t","sources":[{"name":"s","type":"codebase","pointer":".","change_detection":"git","scope":[{"path":"**","mode":"allow"}]}],"reference_mems":[],"destination_mem":"hold","deny_paths":[],"coverage_semantics":"exhaustive","operations":{"build":{"mode":"discovery","trigger":"loop","batch_size":20},"sync":{"trigger":"manual","batch_size":20}}}"#,
)
.unwrap();
let refused = memstead()
.current_dir(root)
.env("MEMSTEAD_OPERATOR_MODE", "1")
.args([
"--json",
"update",
"hold--holder",
"--auto-hash",
"--append",
"purpose= Bad span.",
"--anchor",
r#"{"artifact":"https://w.test/doc#L1-L4","grain":"span","class":"informed-by"}"#,
])
.assert()
.failure()
.get_output()
.stdout
.clone();
let e: serde_json::Value = serde_json::from_slice(&refused).unwrap();
assert_eq!(e["code"], "INVALID_ANCHOR", "{e}");
assert!(
e["message"]
.as_str()
.is_some_and(|m| m.contains("never enters a path namespace")),
"{e}"
);
let h = |content: &str| memstead_base::anchor::prepared_content_hash(content.as_bytes());
memstead()
.current_dir(root)
.env("MEMSTEAD_OPERATOR_MODE", "1")
.args([
"update",
"hold--holder",
"--auto-hash",
"--append",
"purpose= Url anchors.",
"--anchor",
r#"{"artifact":"https://w.test/stable","grain":"url","class":"anchored","content":"immutable text","hash_stability":"stable"}"#,
"--anchor",
r#"{"artifact":"https://w.test/living","grain":"url","class":"anchored","content":"page v1"}"#,
"--anchor",
r#"{"artifact":"https://w.test/gone","grain":"url","class":"anchored","content":"was here"}"#,
"--anchor",
r#"{"artifact":"https://w.test/never","grain":"url","class":"anchored","content":"never observed again"}"#,
])
.assert()
.success();
let before = memstead()
.current_dir(root)
.args(["--json", "anchors", "hold--holder"])
.assert()
.success()
.get_output()
.stdout
.clone();
let v: serde_json::Value = serde_json::from_slice(&before).unwrap();
let url_rows: Vec<&serde_json::Value> = v["anchors"]
.as_array()
.unwrap()
.iter()
.filter(|a| a["grain"] == "url")
.collect();
assert_eq!(url_rows.len(), 4, "{v}");
assert!(
url_rows
.iter()
.all(|a| a["hash_source"] == "author" && a.get("state").is_none()),
"content supplied at write → author hash; nothing observed yet: {v}"
);
fs::write(
root.join("bad.json"),
r#"[{"artifact":"https://w.test/stable","hash":"x","content":"y"}]"#,
)
.unwrap();
let refused = memstead()
.current_dir(root)
.args([
"--json",
"verify-anchors",
"--mem",
"hold",
"--observations",
"bad.json",
])
.assert()
.failure()
.get_output()
.stdout
.clone();
let e: serde_json::Value = serde_json::from_slice(&refused).unwrap();
assert_eq!(e["code"], "INVALID_OBSERVATION", "{e}");
fs::write(
root.join("obs.json"),
format!(
r#"{{"observations":[
{{"artifact":"https://w.test/stable","hash":"{}"}},
{{"artifact":"https://w.test/living","content":"page v2","observed_at":"2026-08-03T09:00:00Z"}},
{{"artifact":"https://w.test/gone","absent":true}}
]}}"#,
h("immutable text")
),
)
.unwrap();
let out = memstead()
.current_dir(root)
.args([
"--json",
"verify-anchors",
"--mem",
"hold",
"--observations",
"obs.json",
])
.assert()
.success()
.get_output()
.stdout
.clone();
let v: serde_json::Value = serde_json::from_slice(&out).unwrap();
let state_of = |v: &serde_json::Value, artifact: &str| -> String {
v["anchors"]
.as_array()
.unwrap()
.iter()
.find(|a| a["artifact"] == artifact)
.map(|a| a["state"].as_str().unwrap().to_string())
.unwrap_or_else(|| panic!("no row for {artifact}: {v}"))
};
assert_eq!(state_of(&v, "https://w.test/stable"), "resolves");
assert_eq!(state_of(&v, "https://w.test/living"), "recheck");
assert_eq!(state_of(&v, "https://w.test/gone"), "recheck");
assert_eq!(state_of(&v, "https://w.test/never"), "unobserved");
assert_eq!(v["observations"]["supplied"], 3, "{v}");
assert_eq!(v["observations"]["recorded"], 3, "{v}");
let living = v["anchors"]
.as_array()
.unwrap()
.iter()
.find(|a| a["artifact"] == "https://w.test/living")
.unwrap();
assert_eq!(living["observed_at"], "2026-08-03T09:00:00Z");
assert!(
living["unobserved_for_days"]
.as_u64()
.is_some_and(|d| d >= 30),
"backdated observation ages: {living}"
);
assert!(
v["findings"]["items"].as_array().is_some_and(|items| items
.iter()
.all(|f| { !serde_json::to_string(f).unwrap().contains("w.test/never") })),
"{v}"
);
let read = memstead()
.current_dir(root)
.args(["--json", "anchors", "hold--holder"])
.assert()
.success()
.get_output()
.stdout
.clone();
let v: serde_json::Value = serde_json::from_slice(&read).unwrap();
let row = |artifact: &str| {
v["anchors"]
.as_array()
.unwrap()
.iter()
.find(|a| a["artifact"] == artifact)
.cloned()
.unwrap()
};
assert_eq!(row("https://w.test/stable")["state"], "resolves");
assert_eq!(row("https://w.test/living")["state"], "recheck");
assert_eq!(
row("https://w.test/living")["last_observed"]["state"],
"recheck"
);
assert!(
row("https://w.test/living")["unobserved_for_days"]
.as_u64()
.is_some_and(|d| d >= 30)
);
assert!(row("https://w.test/never").get("state").is_none());
let health = memstead()
.current_dir(root)
.args(["--json", "health", "--include", "anchors,open_questions"])
.assert()
.get_output()
.stdout
.clone();
let hv: serde_json::Value = serde_json::from_slice(&health).unwrap();
let text = hv.to_string();
assert!(
text.contains("unobserved for 3"),
"aging note missing: {text}"
);
assert!(
hv["anchors"]["hold"]["aging"]
.as_array()
.is_some_and(|a| a.iter().any(|r| r["artifact"] == "https://w.test/living")),
"{hv}"
);
assert!(
hv["open_questions"]["hold"]["anchors_aging"]["items"]
.as_array()
.is_some_and(|a| a.iter().any(|r| r["artifact"] == "https://w.test/living")),
"{hv}"
);
fs::write(
root.join("obs2.json"),
r#"[{"artifact":"https://w.test/stable","content":"immutable text, edited"}]"#,
)
.unwrap();
let out = memstead()
.current_dir(root)
.args([
"--json",
"verify-anchors",
"--mem",
"hold",
"--observations",
"obs2.json",
])
.assert()
.success()
.get_output()
.stdout
.clone();
let v: serde_json::Value = serde_json::from_slice(&out).unwrap();
assert_eq!(state_of(&v, "https://w.test/stable"), "drifted");
assert_eq!(state_of(&v, "https://w.test/living"), "recheck");
assert_eq!(state_of(&v, "https://w.test/never"), "unobserved");
}
#[test]
fn check_findings_persist_and_open_kinds_are_recorded_not_interpreted() {
let tmp = TempDir::new().unwrap();
let root = tmp.path();
make_anchor_workspace(root);
let ledger = root.join(".memstead/state/checks/checks.jsonl");
let lines = || {
fs::read_to_string(&ledger)
.map(|t| t.lines().count())
.unwrap_or(0)
};
fs::write(
root.join("f.json"),
r#"{"checks":[{"id":"hold--holder","verdict":"failed","method":"walked the step","finding":{"code":"hidden-premise","section":"purpose","message":"The purpose assumes a premise the identity never states."}}]}"#,
)
.unwrap();
let out = memstead()
.current_dir(root)
.args([
"--json",
"check",
"--from",
"f.json",
"--identity",
"checker-one",
"--role",
"checker",
])
.assert()
.success()
.get_output()
.stdout
.clone();
let v: serde_json::Value = serde_json::from_slice(&out).unwrap();
assert_eq!(v["checks"][0]["finding"]["code"], "hidden-premise", "{v}");
assert_eq!(lines(), 1);
let line = fs::read_to_string(&ledger).unwrap();
assert!(
line.contains("\"finding\":{\"code\":\"hidden-premise\""),
"{line}"
);
let health = memstead()
.current_dir(root)
.args(["--json", "health", "--include", "checks"])
.assert()
.get_output()
.stdout
.clone();
let h: serde_json::Value = serde_json::from_slice(&health).unwrap();
let f = &h["checks"]["hold"]["findings"]["hold--holder"];
assert_eq!(f["verdict"], "failed", "{h}");
assert_eq!(f["finding"]["section"], "purpose", "{h}");
assert_eq!(h["checks"]["hold"]["check_failed"], 1, "{h}");
for bad in [
r#"{"checks":[{"id":"hold--holder","verdict":"ok","finding":{"code":"x"}}]}"#,
r#"{"checks":[{"id":"hold--holder","verdict":"ok","finding":{"code":"x","message":"m","severity":"high"}}]}"#,
] {
fs::write(root.join("bad.json"), bad).unwrap();
let out = memstead()
.current_dir(root)
.args(["--json", "check", "--from", "bad.json"])
.assert()
.failure()
.get_output()
.stdout
.clone();
let e: serde_json::Value = serde_json::from_slice(&out).unwrap();
assert_eq!(e["code"], "BATCH_REFUSED", "{e}");
assert_eq!(
e["details"]["failed_entries"][0]["errors"][0]["code"], "INVALID_CHECK_FINDING",
"{e}"
);
assert_eq!(lines(), 1, "nothing appended on refusal");
}
let out = memstead()
.current_dir(root)
.args([
"--json",
"check",
"hold--holder",
"--verdict",
"ok",
"--finding",
r#"{"message":"no code"}"#,
])
.assert()
.failure()
.get_output()
.stdout
.clone();
let e: serde_json::Value = serde_json::from_slice(&out).unwrap();
assert_eq!(e["code"], "INVALID_CHECK_FINDING", "{e}");
assert!(e["message"].as_str().unwrap().contains("shape"), "{e}");
assert_eq!(lines(), 1);
let out = memstead()
.current_dir(root)
.args([
"--json",
"check",
"hold--holder",
"--verdict",
"ok",
"--kind",
"x-step-walk",
"--identity",
"checker-one",
])
.assert()
.success()
.get_output()
.stdout
.clone();
let v: serde_json::Value = serde_json::from_slice(&out).unwrap();
assert_eq!(v["kind"], "x-step-walk", "{v}");
assert_eq!(
v["check_state"], "check_failed",
"a foreign ok moves no verification state: {v}"
);
assert_eq!(lines(), 2);
let health = memstead()
.current_dir(root)
.args(["--json", "health", "--include", "checks"])
.assert()
.get_output()
.stdout
.clone();
let h: serde_json::Value = serde_json::from_slice(&health).unwrap();
assert_eq!(
h["checks"]["hold"]["foreign_kinds"]["x-step-walk"], 1,
"{h}"
);
assert_eq!(h["checks"]["hold"]["check_failed"], 1, "{h}");
let out = memstead()
.current_dir(root)
.args([
"--json",
"check",
"hold--holder",
"--verdict",
"ok",
"--kind",
"step-walk",
])
.assert()
.failure()
.get_output()
.stdout
.clone();
let e: serde_json::Value = serde_json::from_slice(&out).unwrap();
assert_eq!(e["code"], "INVALID_CHECK_KIND", "{e}");
assert!(e["message"].as_str().unwrap().contains("x-<name>"), "{e}");
assert_eq!(lines(), 2);
let out = memstead()
.current_dir(root)
.args([
"--json",
"check",
"hold--holder",
"--verdict",
"ok",
"--kind",
"conformance",
])
.assert()
.success()
.get_output()
.stdout
.clone();
let v: serde_json::Value = serde_json::from_slice(&out).unwrap();
assert_eq!(v["kind"], "conformance");
assert!(
v["schema_ref"]
.as_str()
.is_some_and(|s| s.starts_with("default@")),
"{v}"
);
assert_eq!(lines(), 3);
}
#[test]
fn verify_anchors_multi_binding_mem_no_longer_nulls() {
let tmp = TempDir::new().unwrap();
let root = tmp.path();
make_anchor_workspace(root);
let proj = root.join(".memstead/projections/hold");
fs::create_dir_all(&proj).unwrap();
for (name, pointer) in [("graph-a", "src-a.txt"), ("graph-b", "src-b.txt")] {
fs::write(
proj.join(format!("{name}.json")),
format!(
r#"{{"version":2,"intent":"t","sources":[{{"name":"s","type":"codebase","pointer":"{pointer}","change_detection":"git","scope":[{{"path":"**","mode":"allow"}}]}}],"reference_mems":[],"destination_mem":"hold","deny_paths":[],"coverage_semantics":"exhaustive","operations":{{"build":{{"mode":"discovery","trigger":"loop","batch_size":20}},"sync":{{"trigger":"manual","batch_size":20}}}}}}"#
),
)
.unwrap();
}
memstead()
.current_dir(root)
.env("MEMSTEAD_OPERATOR_MODE", "1")
.args([
"update",
"hold--holder",
"--auto-hash",
"--append",
"purpose= Url anchor.",
"--anchor",
r#"{"artifact":"https://example.com/spec","grain":"url","class":"informed-by"}"#,
])
.assert()
.success();
let out = memstead()
.current_dir(root)
.args(["--json", "verify-anchors", "--mem", "hold"])
.assert()
.success()
.get_output()
.stdout
.clone();
let v: serde_json::Value = serde_json::from_slice(&out).unwrap();
assert_eq!(v["resolves"], 1, "{v}");
assert_eq!(v["drifted"], 1, "{v}");
assert_eq!(v["recheck"], 1, "{v}");
assert_eq!(v["unresolvable"], 1, "{v}");
assert_eq!(v["unobserved"], 1, "{v}");
assert!(
v["population"]
.as_str()
.is_some_and(|p| p.contains("adjudicated") && p.contains("counted row(s)")),
"{v}"
);
assert_eq!(v["fully_adjudicated"], false, "{v}");
}
#[test]
fn folder_backend_same_transport_check_reads_unconfirmable() {
let tmp = TempDir::new().unwrap();
let root = tmp.path();
let ws = root.join("gatews");
fs::create_dir_all(&ws).unwrap();
memstead()
.current_dir(&ws)
.args(["quickstart"])
.assert()
.success();
memstead()
.current_dir(&ws)
.args([
"--role",
"author",
"create",
"--title",
"Gate Probe",
"--type",
"memo",
"--section",
"claim=Recorded.",
"--section",
"context=Gate test.",
])
.assert()
.success();
memstead()
.current_dir(&ws)
.args([
"--role",
"checker",
"check",
"gatews--gate-probe",
"--verdict",
"ok",
])
.assert()
.success();
let out = memstead()
.current_dir(&ws)
.args(["--json", "health", "--include", "checks"])
.assert()
.success()
.get_output()
.stdout
.clone();
let v: serde_json::Value = serde_json::from_slice(&out).unwrap();
let gate = &v["checks"]["gatews"]["independence"];
assert_eq!(
gate["unconfirmable"]["items"],
serde_json::json!(["gatews--gate-probe"]),
"transport is not identity — neither self_checked nor \
confirmed_independent without a caller-declared identity: {v}"
);
assert_eq!(gate["self_checked"]["count"], 0, "{v}");
assert_eq!(gate["confirmed_independent"]["count"], 0, "{v}");
}
#[test]
fn health_markdown_renders_checks_and_stale_derivations_sections() {
let tmp = TempDir::new().unwrap();
let ws = tmp.path().join("mdws");
fs::create_dir_all(&ws).unwrap();
memstead()
.current_dir(&ws)
.args(["quickstart"])
.assert()
.success();
let out = memstead()
.current_dir(&ws)
.args(["health", "--include", "checks,stale_derivations"])
.assert()
.success()
.get_output()
.stdout
.clone();
let md = String::from_utf8(out).unwrap();
assert!(md.contains("## Checks (1 mems)"), "{md}");
assert!(md.contains("never_checked 1"), "{md}");
assert!(md.contains("unconfirmable 0"), "{md}");
assert!(md.contains("## Stale derivations (0 findings)"), "{md}");
let plain = memstead()
.current_dir(&ws)
.args(["health"])
.assert()
.success()
.get_output()
.stdout
.clone();
let plain = String::from_utf8(plain).unwrap();
assert!(
!plain.contains("## Checks") && !plain.contains("## Stale derivations"),
"{plain}"
);
}
#[test]
fn verify_anchors_persists_standalone_findings_across_passes() {
let tmp = TempDir::new().unwrap();
let root = tmp.path();
make_anchor_workspace(root);
let out = memstead()
.current_dir(root)
.args(["--json", "verify-anchors", "--mem", "hold"])
.assert()
.success()
.get_output()
.stdout
.clone();
let v: serde_json::Value = serde_json::from_slice(&out).unwrap();
assert_eq!(v["findings"]["new"], 2, "{v}");
assert_eq!(v["findings"]["already_seen"], 0, "{v}");
assert_eq!(v["findings"]["items"].as_array().unwrap().len(), 2);
let store_path = root
.join(".memstead/state/findings/hold/standalone.json")
.to_path_buf();
let store = fs::read_to_string(&store_path).expect("standalone store persisted");
let parsed: serde_json::Value = serde_json::from_str(&store).unwrap();
assert_eq!(
parsed["batches"][0]["key"]["binding_hash"], "standalone",
"{store}"
);
let out = memstead()
.current_dir(root)
.args(["--json", "verify-anchors", "--mem", "hold"])
.assert()
.success()
.get_output()
.stdout
.clone();
let v: serde_json::Value = serde_json::from_slice(&out).unwrap();
assert_eq!(v["findings"]["new"], 0, "{v}");
assert_eq!(v["findings"]["already_seen"], 2, "{v}");
fs::write(root.join("src-d.txt"), "delta").unwrap();
let out = memstead()
.current_dir(root)
.args(["--json", "verify-anchors", "--mem", "hold"])
.assert()
.success()
.get_output()
.stdout
.clone();
let v: serde_json::Value = serde_json::from_slice(&out).unwrap();
assert_eq!(v["findings"]["already_seen"], 1, "{v}");
assert_eq!(v["findings"]["new"], 0, "{v}");
}
#[test]
fn verify_anchors_health_axis_and_refusals() {
let tmp = TempDir::new().unwrap();
let root = tmp.path();
make_anchor_workspace(root);
let plain = memstead()
.current_dir(root)
.args(["--json", "health"])
.assert()
.success()
.get_output()
.stdout
.clone();
let plain: serde_json::Value = serde_json::from_slice(&plain).unwrap();
assert!(
plain.get("anchors").is_none(),
"anchors axis must be include-gated: {plain}"
);
let with = memstead()
.current_dir(root)
.args(["--json", "health", "--include", "anchors"])
.assert()
.success()
.get_output()
.stdout
.clone();
let with: serde_json::Value = serde_json::from_slice(&with).unwrap();
let axis = with.get("anchors").expect("axis present under include");
assert_eq!(axis["hold"]["drifted"], 1, "{with}");
assert_eq!(axis["hold"]["unresolvable"], 1, "{with}");
memstead()
.current_dir(root)
.env("MEMSTEAD_OPERATOR_MODE", "1")
.args(["mem", "init", "bare", "--no-gitignore"])
.assert()
.success();
let empty = memstead()
.current_dir(root)
.args(["--json", "verify-anchors", "--mem", "bare"])
.assert()
.success()
.get_output()
.stdout
.clone();
let empty: serde_json::Value = serde_json::from_slice(&empty).unwrap();
assert_eq!(empty["resolves"], 0);
assert_eq!(empty["anchors"].as_array().map(|a| a.len()), Some(0));
let unknown = memstead()
.current_dir(root)
.args(["verify-anchors", "--mem", "nope"])
.assert()
.failure()
.get_output()
.stderr
.clone();
let unknown = String::from_utf8(unknown).unwrap();
assert!(unknown.contains("UNKNOWN_MEM"), "got: {unknown}");
assert!(!unknown.contains("INTERNAL"), "INTERNAL leaked: {unknown}");
}
#[test]
fn mem_init_succeeds_without_a_configured_git_identity() {
let tmp = TempDir::new().unwrap();
let empty_home = TempDir::new().unwrap();
let scrubbed = |args: &[&str]| -> assert_cmd::assert::Assert {
memstead()
.current_dir(tmp.path())
.env("MEMSTEAD_OPERATOR_MODE", "1")
.env("HOME", empty_home.path())
.env("GIT_CONFIG_GLOBAL", "/dev/null")
.env("GIT_CONFIG_SYSTEM", "/dev/null")
.env("GIT_CONFIG_NOSYSTEM", "1")
.env_remove("GIT_AUTHOR_NAME")
.env_remove("GIT_AUTHOR_EMAIL")
.env_remove("GIT_COMMITTER_NAME")
.env_remove("GIT_COMMITTER_EMAIL")
.args(args)
.assert()
};
scrubbed(&["mem-repo", "init", "."]).success();
scrubbed(&["mem", "init", "alpha", "--no-gitignore"]).success();
let listed = scrubbed(&["mem", "list"])
.success()
.get_output()
.stdout
.clone();
let listed = String::from_utf8(listed).unwrap();
assert!(listed.contains("alpha"), "mem not listed: {listed}");
}
#[test]
fn mem_list_counts_entities_in_lazy_mems() {
let tmp = TempDir::new().unwrap();
let root = tmp.path();
let store = root.join(".memstead");
fs::create_dir_all(store.join("state")).unwrap();
fs::write(
store.join("workspace.toml"),
"format = \"memstead-git-branch-2\"\n\n[persistence_adapter]\nname = \"file-two-layer\"\n",
)
.unwrap();
fs::write(
store.join("state").join("mounts.json"),
r#"{"format":"memstead-mounts-3","mounts":[{"mem":"ma","schema":"default@1.0.0","storage":{"type":"folder","path":"ma-mem"},"capability":"write","lifecycle":"eager","cross_linkable":false},{"mem":"mb","schema":"default@1.0.0","storage":{"type":"folder","path":"mb-mem"},"capability":"write","lifecycle":"lazy","cross_linkable":false}]}"#,
)
.unwrap();
Command::new("git")
.args(["init", "-q"])
.arg(root.join("mem-repo"))
.assert()
.success();
for m in ["ma-mem", "mb-mem"] {
let mem_dir = root.join(m);
fs::create_dir_all(mem_dir.join(".memstead")).unwrap();
fs::write(
mem_dir.join(".memstead").join("config.json"),
r#"{"format":1,"schema":"default@1.0.0"}"#,
)
.unwrap();
let body = format!(
"---\ntype: spec\ncreated_date: 2026-02-03\nlast_modified: 2026-02-03\nlevel: M0\n---\n# Epsilon {m}\n\n## Identity\n\n{DELTA_IDENTITY}\n\n## Purpose\n\n{DELTA_PURPOSE}\n"
);
fs::write(mem_dir.join("epsilon.md"), &body).unwrap();
}
let out = memstead()
.current_dir(root)
.args(["--json", "mem", "list"])
.assert()
.success()
.get_output()
.stdout
.clone();
let json: serde_json::Value = serde_json::from_slice(&out).unwrap();
let mems = json["mems"].as_array().expect("mems array");
let count_of = |name: &str| -> i64 {
mems.iter()
.find(|r| r["name"] == name)
.unwrap_or_else(|| panic!("mem {name} missing from roster"))["entity_count"]
.as_i64()
.unwrap()
};
assert_eq!(count_of("ma"), 1, "eager mem counts its entity");
assert_eq!(
count_of("mb"),
1,
"lazy mem must report its TRUE count, never a partial-store zero"
);
}
#[test]
fn quickstart_workspace_exports_a_mem_archive_that_installs() {
let tmp = TempDir::new().unwrap();
let root = tmp.path();
let ws = root.join("repo");
fs::create_dir_all(&ws).unwrap();
memstead()
.current_dir(&ws)
.args(["quickstart", "--repo", "."])
.assert()
.success();
memstead()
.current_dir(&ws)
.args([
"create",
"--title",
"Front Door Probe",
"--type",
"memo",
"--section",
"claim=Exported from a quickstart workspace.",
"--section",
"context=F6 round-trip pin.",
])
.assert()
.success();
memstead()
.current_dir(&ws)
.args(["export", "--format", "mem", "-o", "out.mem"])
.assert()
.success();
let archive = ws.join("out.mem");
assert!(archive.exists(), "export must write the archive");
let scratch = root.join("scratch");
fs::create_dir_all(&scratch).unwrap();
memstead()
.current_dir(&scratch)
.args(["mem-repo", "init", "."])
.assert()
.success();
memstead()
.current_dir(&scratch)
.args(["install", archive.to_str().unwrap()])
.assert()
.success();
let out = memstead()
.current_dir(&scratch)
.args(["--json", "entity", "repo--front-door-probe"])
.assert()
.success();
let text = String::from_utf8_lossy(&out.get_output().stdout).to_string();
assert!(
text.contains("Front Door Probe"),
"installed archive must carry the entity intact: {text}"
);
}
#[test]
fn quickstart_workspace_bare_publish_dry_run_assembles() {
let tmp = TempDir::new().unwrap();
let ws = tmp.path().join("repo");
fs::create_dir_all(&ws).unwrap();
memstead()
.current_dir(&ws)
.args(["quickstart", "--repo", "."])
.assert()
.success();
let out = memstead()
.current_dir(&ws)
.args(["publish", "--dry-run"])
.assert()
.success();
let text = String::from_utf8_lossy(&out.get_output().stdout).to_string();
assert!(
text.contains("Nothing was published"),
"dry run must assemble and stop: {text}"
);
}
#[test]
fn cli_kinded_check_records_and_refuses() {
let tmp = TempDir::new().unwrap();
let ws = tmp.path().join("kindws");
fs::create_dir_all(&ws).unwrap();
memstead()
.current_dir(&ws)
.args(["quickstart"])
.assert()
.success();
memstead()
.current_dir(&ws)
.args([
"--role",
"author",
"create",
"--title",
"Kind Probe",
"--type",
"memo",
"--section",
"claim=Recorded.",
"--section",
"context=Kind test.",
])
.assert()
.success();
let refused = memstead()
.current_dir(&ws)
.args([
"--json",
"check",
"kindws--kind-probe",
"--verdict",
"ok",
"--kind",
"semantic",
])
.assert()
.failure()
.get_output()
.clone();
let e = format!(
"{}{}",
String::from_utf8_lossy(&refused.stdout),
String::from_utf8_lossy(&refused.stderr)
);
assert!(e.contains("INVALID_CHECK_KIND"), "{e}");
assert!(
e.contains("verification") && e.contains("conformance"),
"{e}"
);
let out = memstead()
.current_dir(&ws)
.args([
"--json",
"--role",
"checker",
"check",
"kindws--kind-probe",
"--verdict",
"failed",
"--kind",
"conformance",
"--method",
"judged against write_rules by test-model",
])
.assert()
.success()
.get_output()
.stdout
.clone();
let v: serde_json::Value = serde_json::from_slice(&out).unwrap();
assert_eq!(v["kind"], "conformance", "{v}");
assert_eq!(v["check_state"], "check_failed", "{v}");
let pin = v["schema_ref"].as_str().expect("engine stamps the pin");
assert!(pin.contains('@'), "pin is name@version: {pin}");
let out2 = memstead()
.current_dir(&ws)
.args([
"--json",
"--role",
"checker",
"check",
"kindws--kind-probe",
"--verdict",
"ok",
])
.assert()
.success()
.get_output()
.stdout
.clone();
let v2: serde_json::Value = serde_json::from_slice(&out2).unwrap();
assert_eq!(v2["kind"], "verification", "{v2}");
assert_eq!(v2["check_state"], "checked_ok", "{v2}");
assert!(v2["schema_ref"].is_null(), "{v2}");
let health = memstead()
.current_dir(&ws)
.args(["--json", "health", "--include", "checks"])
.assert()
.success()
.get_output()
.stdout
.clone();
let h: serde_json::Value = serde_json::from_slice(&health).unwrap();
let axis = &h["checks"]["kindws"];
assert_eq!(axis["checked_ok"], 1, "{h}");
assert_eq!(axis["conformance"]["check_failed"], 1, "{h}");
}
struct FixtureRegistry {
base: String,
_runtime: tokio::runtime::Runtime,
}
fn spawn_fixture_registry(
scope: &'static str,
name: &'static str,
body: Vec<u8>,
) -> FixtureRegistry {
use axum::{Router, extract::Path as AxumPath, http::StatusCode, routing::get};
use std::sync::Arc;
let runtime = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.unwrap();
let body = Arc::new(body);
let base = runtime.block_on(async move {
let app: Router = Router::new().route(
"/api/mem/{scope}/{file}",
get({
let body = body.clone();
move |AxumPath((got_scope, got_file)): AxumPath<(String, String)>| {
let body = body.clone();
async move {
if got_scope == scope && got_file == format!("{name}.mem") {
(StatusCode::OK, (*body).clone())
} else {
(StatusCode::NOT_FOUND, vec![])
}
}
}
}),
);
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
tokio::spawn(async move {
let _ = axum::serve(listener, app).await;
});
format!("http://{addr}")
});
FixtureRegistry {
base,
_runtime: runtime,
}
}
fn assert_linked(root: &Path, cache: &Path) {
let mounts = fs::read_to_string(root.join(".memstead/state/mounts.json"))
.unwrap_or_else(|e| panic!("mounts.json must exist at {}: {e}", root.display()));
assert!(
mounts.contains(r#""sender-mem""#) && mounts.contains(r#""archive""#),
"link must record the attachment in the mount roster; got:\n{mounts}"
);
memstead()
.current_dir(root)
.args(["search", "Alpha"])
.env("MEMSTEAD_MEM_CACHE", cache)
.assert()
.success()
.stdout(contains("sender-mem"));
}
#[test]
fn install_attaches_the_registry_mem_on_every_layout() {
let _guard = cache_guard().lock().unwrap_or_else(|e| e.into_inner());
let sender = TempDir::new().unwrap();
let cache = TempDir::new().unwrap();
let sender_mem = make_sender_mem(sender.path());
let archive = export_sender_archive(sender.path(), &sender_mem);
let registry = spawn_fixture_registry("fixture", "published", fs::read(&archive).unwrap());
let install = |root: &Path| {
memstead()
.current_dir(root)
.args(["install", "fixture/published", "--registry", ®istry.base])
.env("MEMSTEAD_MEM_CACHE", cache.path())
.assert()
.success()
.stdout(contains("Installed"));
};
let collapsed = TempDir::new().unwrap();
memstead()
.current_dir(collapsed.path())
.args([
"init",
"--name",
"collapsed-mem",
"--schema",
"default@1.0.0",
])
.assert()
.success();
install(collapsed.path());
assert_linked(collapsed.path(), cache.path());
let overlapping = TempDir::new().unwrap();
fs::create_dir_all(overlapping.path().join(".git")).unwrap();
memstead()
.current_dir(overlapping.path())
.args([
"quickstart",
"--name",
"overlapping-mem",
"--repo",
".",
"--agent",
"claude-code",
])
.assert()
.success();
install(overlapping.path());
assert_linked(overlapping.path(), cache.path());
let receiver = TempDir::new().unwrap();
let _receiver_mem = make_receiver_mem(receiver.path());
install(receiver.path());
assert_linked(receiver.path(), cache.path());
let fs_multi = TempDir::new().unwrap();
{
let store = fs_multi.path().join(".memstead");
fs::create_dir_all(store.join("state")).unwrap();
fs::write(
store.join("workspace.toml"),
"format = \"memstead-git-branch-2\"\n\n[persistence_adapter]\nname = \"file-two-layer\"\n",
)
.unwrap();
let mut wire = String::from("{\n \"format\": \"memstead-mounts-3\",\n \"mounts\": [\n");
for (i, name) in ["mem-one", "mem-two"].iter().enumerate() {
let dir = fs_multi.path().join(name);
fs::create_dir_all(dir.join(".memstead")).unwrap();
fs::write(
dir.join(".memstead").join("config.json"),
r#"{ "format": 1, "version": "0.1.0", "schema": "default@1.0.0" }"#,
)
.unwrap();
wire.push_str(&format!(
" {{\n \"mem\": \"{name}\",\n \"schema\": \"default@1.0.0\",\n \"storage\": {{ \"type\": \"folder\", \"path\": \"{name}\" }},\n \"capability\": \"write\",\n \"lifecycle\": \"eager\",\n \"cross_linkable\": true\n }}{}\n",
if i == 0 { "," } else { "" }
));
}
wire.push_str(" ]\n}\n");
fs::write(store.join("state").join("mounts.json"), wire).unwrap();
}
install(fs_multi.path());
assert_linked(fs_multi.path(), cache.path());
let multi = TempDir::new().unwrap();
for name in ["mem-one", "mem-two"] {
let dir = multi.path().join(name);
fs::create_dir_all(dir.join(".memstead")).unwrap();
fs::write(
dir.join(".memstead").join("config.json"),
r#"{ "version": "0.1.0", "schema": "default@1.0.0" }"#,
)
.unwrap();
}
init_real_mem_repo_from_disk(
multi.path(),
&[
(&multi.path().join("mem-one"), "mem-one"),
(&multi.path().join("mem-two"), "mem-two"),
],
);
install(multi.path());
assert_linked(multi.path(), cache.path());
}
#[test]
fn install_without_a_workspace_refuses_typed() {
let nowhere = TempDir::new().unwrap();
memstead()
.current_dir(nowhere.path())
.args(["--json", "install", "fixture/published"])
.assert()
.failure()
.stdout(contains("workspace.toml"));
}
#[test]
fn a_long_lived_engine_state_write_keeps_a_sibling_processs_install() {
let _guard = cache_guard().lock().unwrap_or_else(|e| e.into_inner());
let sender = TempDir::new().unwrap();
let receiver = TempDir::new().unwrap();
let cache = TempDir::new().unwrap();
let sender_mem = make_sender_mem(sender.path());
let _receiver_mem = make_receiver_mem(receiver.path());
let archive = export_sender_archive(sender.path(), &sender_mem);
let registry = spawn_fixture_registry("fixture", "published", fs::read(&archive).unwrap());
let long_lived = memstead_base::Engine::from_workspace_root(receiver.path())
.expect("receiver workspace boots");
assert!(
long_lived.mount("sender-mem").is_none(),
"the long-lived engine must boot before the link, or the test proves nothing"
);
memstead()
.current_dir(receiver.path())
.args(["install", "fixture/published", "--registry", ®istry.base])
.env("MEMSTEAD_MEM_CACHE", cache.path())
.assert()
.success();
let after_link =
fs::read_to_string(receiver.path().join(".memstead/state/mounts.json")).unwrap();
assert!(
after_link.contains(r#""sender-mem""#),
"link must have registered the mount; got:\n{after_link}"
);
long_lived.persist_state().expect("state write succeeds");
let after_write =
fs::read_to_string(receiver.path().join(".memstead/state/mounts.json")).unwrap();
assert!(
after_write.contains(r#""sender-mem""#),
"the sibling process's attachment must survive the long-lived engine's \
state write; got:\n{after_write}"
);
assert!(
after_write.contains(r#""receiver-mem""#),
"the long-lived engine's own mount must still be there; got:\n{after_write}"
);
memstead()
.current_dir(receiver.path())
.args(["search", "Alpha"])
.env("MEMSTEAD_MEM_CACHE", cache.path())
.assert()
.success()
.stdout(contains("sender-mem"));
}
#[test]
fn a_state_write_still_removes_what_the_writer_unregistered() {
let _guard = cache_guard().lock().unwrap_or_else(|e| e.into_inner());
let sender = TempDir::new().unwrap();
let receiver = TempDir::new().unwrap();
let cache = TempDir::new().unwrap();
let sender_mem = make_sender_mem(sender.path());
let _receiver_mem = make_receiver_mem(receiver.path());
let archive = export_sender_archive(sender.path(), &sender_mem);
memstead()
.current_dir(receiver.path())
.arg("install")
.arg(&archive)
.env("MEMSTEAD_MEM_CACHE", cache.path())
.assert()
.success();
let mut engine = memstead_base::Engine::from_workspace_root(receiver.path())
.expect("receiver workspace boots");
engine
.unregister_read_mount("sender-mem")
.expect("unregister succeeds");
engine.persist_state().expect("state write succeeds");
let after = fs::read_to_string(receiver.path().join(".memstead/state/mounts.json")).unwrap();
assert!(
!after.contains(r#""sender-mem""#),
"an unregistered mount must not be resurrected by the merge; got:\n{after}"
);
}
#[test]
fn cli_check_from_records_batch_and_refuses_atomically() {
let tmp = TempDir::new().unwrap();
let ws = tmp.path().join("batchcheckws");
fs::create_dir_all(&ws).unwrap();
memstead()
.current_dir(&ws)
.args(["quickstart"])
.assert()
.success();
for title in ["Probe One", "Probe Two"] {
memstead()
.current_dir(&ws)
.args([
"create",
"--title",
title,
"--type",
"memo",
"--section",
"claim=Recorded.",
"--section",
"context=Batch test.",
])
.assert()
.success();
}
let batch = ws.join("checks.json");
fs::write(
&batch,
r#"{"checks": [
{"id": "batchcheckws--probe-one", "verdict": "ok", "method": "read in full"},
{"id": "batchcheckws--probe-two", "verdict": "failed", "kind": "conformance"}
]}"#,
)
.unwrap();
let out = memstead()
.current_dir(&ws)
.args(["--json", "check", "--from", batch.to_str().unwrap()])
.assert()
.success()
.get_output()
.stdout
.clone();
let v: serde_json::Value = serde_json::from_slice(&out).unwrap();
assert_eq!(v["recorded"], 2, "{v}");
assert_eq!(v["checks"][0]["entity"], "batchcheckws--probe-one", "{v}");
assert_eq!(v["checks"][1]["kind"], "conformance", "{v}");
assert!(
v["checks"][1]["schema_ref"]
.as_str()
.is_some_and(|s| s.contains('@')),
"conformance entries carry the engine-stamped pin: {v}"
);
fs::write(
&batch,
r#"{"checks": [
{"id": "batchcheckws--probe-one", "verdict": "maybe"},
{"id": "batchcheckws--does-not-exist", "verdict": "ok"},
{"id": "batchcheckws--probe-two", "verdict": "ok"}
]}"#,
)
.unwrap();
let refused = memstead()
.current_dir(&ws)
.args(["--json", "check", "--from", batch.to_str().unwrap()])
.assert()
.failure()
.get_output()
.clone();
let e = format!(
"{}{}",
String::from_utf8_lossy(&refused.stdout),
String::from_utf8_lossy(&refused.stderr)
);
assert!(e.contains("BATCH_REFUSED"), "{e}");
assert!(e.contains("INVALID_VERDICT"), "{e}");
assert!(e.contains("ENTITY_NOT_FOUND"), "{e}");
assert!(e.contains("nothing recorded"), "{e}");
memstead()
.current_dir(&ws)
.args(["check", "batchcheckws--probe-one", "--verdict", "ok"])
.assert()
.success();
}
#[test]
fn export_json_include_anchors_rides_the_envelope() {
let tmp = TempDir::new().unwrap();
let ws = tmp.path().join("anchorexport");
fs::create_dir_all(&ws).unwrap();
memstead()
.current_dir(&ws)
.args(["quickstart"])
.assert()
.success();
memstead()
.current_dir(&ws)
.args([
"create",
"--title",
"Anchored Probe",
"--type",
"memo",
"--section",
"claim=Anchored.",
"--section",
"context=Export include test.",
])
.assert()
.success();
fs::create_dir_all(ws.join("src")).unwrap();
fs::write(ws.join("src/probe.rs"), "fn probe() {}\n").unwrap();
memstead()
.current_dir(&ws)
.args([
"update",
"anchorexport--anchored-probe",
"--anchor",
r#"{"artifact": "src/probe.rs", "grain": "file", "class": "authored"}"#,
])
.assert()
.success();
let out = memstead()
.current_dir(&ws)
.args(["export", "--format", "json", "--include", "anchors"])
.assert()
.success()
.get_output()
.stdout
.clone();
let doc: serde_json::Value = serde_json::from_slice(&out).unwrap();
let entities = doc["mems"]["anchorexport"]["entities"]
.as_array()
.expect("entities array");
let probe = entities
.iter()
.find(|e| e["id"] == "anchorexport--anchored-probe")
.expect("probe exported");
let anchors = probe["anchors"].as_array().expect("anchors array present");
assert_eq!(anchors.len(), 1, "{probe}");
assert_eq!(anchors[0]["artifact"], "src/probe.rs", "{probe}");
assert_eq!(anchors[0]["class"], "authored", "{probe}");
let plain = memstead()
.current_dir(&ws)
.args(["export", "--format", "json"])
.assert()
.success()
.get_output()
.stdout
.clone();
let plain_doc: serde_json::Value = serde_json::from_slice(&plain).unwrap();
assert!(
plain_doc["mems"]["anchorexport"]["entities"][0]
.get("anchors")
.is_none(),
"no anchors key without the include"
);
let refused = memstead()
.current_dir(&ws)
.args(["export", "--format", "json", "--include", "signals"])
.assert()
.failure()
.get_output()
.clone();
let e = String::from_utf8_lossy(&refused.stderr).to_string();
assert!(e.contains("unknown --include key"), "{e}");
assert!(e.contains("anchors"), "{e}");
memstead()
.current_dir(&ws)
.args(["export", "--format", "llms-txt", "--include", "anchors"])
.assert()
.failure();
}
#[test]
fn cli_update_multi_patch_per_section_and_ambiguous_separator_refusal() {
let tmp = TempDir::new().unwrap();
make_json_export_workspace(tmp.path());
memstead()
.current_dir(tmp.path())
.args([
"update",
"sender-mem--delta",
"--force",
"--patch",
"identity=Multi-section=>ONE",
"--patch",
"identity=ONE fixture=>TWO",
])
.assert()
.success();
let out = memstead()
.current_dir(tmp.path())
.args(["--json", "entity", "sender-mem--delta"])
.assert()
.success()
.get_output()
.stdout
.clone();
let v: serde_json::Value = serde_json::from_slice(&out).unwrap();
let body = v["sections"]["identity"].as_str().unwrap();
assert!(
body.starts_with("TWO entity"),
"patches applied in order against the evolving body: {body}"
);
let refused = memstead()
.current_dir(tmp.path())
.args([
"update",
"sender-mem--delta",
"--force",
"--patch",
"identity=a=>b=>c",
])
.assert()
.failure()
.get_output()
.clone();
let e = String::from_utf8_lossy(&refused.stderr).to_string();
assert!(e.contains("more than one `=>`"), "{e}");
assert!(e.contains("--from"), "{e}");
}
#[test]
fn verify_anchors_backfills_hashless_anchor_and_recheck_drains() {
let tmp = TempDir::new().unwrap();
let ws = tmp.path().join("backfillws");
fs::create_dir_all(&ws).unwrap();
memstead()
.current_dir(&ws)
.args(["quickstart"])
.assert()
.success();
fs::create_dir_all(ws.join("src")).unwrap();
fs::write(ws.join("src/pinned.rs"), "fn pinned() {}\n").unwrap();
memstead()
.current_dir(&ws)
.args([
"create",
"--title",
"Pinned Probe",
"--type",
"memo",
"--section",
"claim=Pinned.",
"--section",
"context=Backfill test.",
])
.assert()
.success();
memstead()
.current_dir(&ws)
.args([
"update",
"backfillws--pinned-probe",
"--anchor",
r#"{"artifact": "src/pinned.rs", "grain": "file", "class": "anchored"}"#,
])
.assert()
.success();
let out = memstead()
.current_dir(&ws)
.args(["--json", "verify-anchors", "--mem", "backfillws"])
.assert()
.success()
.get_output()
.stdout
.clone();
let v: serde_json::Value = serde_json::from_slice(&out).unwrap();
assert_eq!(v["recheck"], 1, "hash-less anchor queues once: {v}");
assert_eq!(v["hash_backfilled"], 1, "…and is backfilled this pass: {v}");
let out2 = memstead()
.current_dir(&ws)
.args(["--json", "verify-anchors", "--mem", "backfillws"])
.assert()
.success()
.get_output()
.stdout
.clone();
let v2: serde_json::Value = serde_json::from_slice(&out2).unwrap();
assert_eq!(v2["resolves"], 1, "recheck drained: {v2}");
assert_eq!(v2["recheck"], 0, "{v2}");
assert_eq!(
v2["hash_backfilled"], 0,
"idempotent — nothing staged: {v2}"
);
}