#![cfg(feature = "mem-repo")]
use assert_cmd::Command;
use tempfile::TempDir;
fn memstead() -> Command {
Command::cargo_bin("memstead").expect("memstead binary must be built by cargo")
}
fn parse_envelope(stdout_bytes: &[u8]) -> serde_json::Value {
let body = std::str::from_utf8(stdout_bytes).expect("stdout must be UTF-8");
serde_json::from_str(body.trim()).unwrap_or_else(|e| {
panic!("--json error must parse as JSON: {e}\n--- stdout ---\n{body}\n--- end stdout ---")
})
}
fn assert_typed_code(envelope: &serde_json::Value, ctx: &str) {
let code = envelope["code"]
.as_str()
.unwrap_or_else(|| panic!("{ctx}: envelope must carry a string `code`, got: {envelope}"));
assert_ne!(
code, "INTERNAL",
"{ctx}: recoverable path must not leak code=INTERNAL — got envelope: {envelope}",
);
}
#[test]
fn update_missing_entity_returns_typed_code() {
let tmp = TempDir::new().unwrap();
let workspace = tmp.path().join("ws");
memstead()
.args([
"mem-repo",
"init",
workspace.to_str().unwrap(),
"--no-gitignore",
])
.assert()
.success();
let output = memstead()
.current_dir(&workspace)
.args([
"--json",
"update",
"nope--does-not-exist",
"--section",
"Body=hi",
"--auto-hash",
])
.assert()
.failure()
.get_output()
.stdout
.clone();
let env = parse_envelope(&output);
assert_typed_code(&env, "update missing entity");
}
#[test]
fn update_missing_hash_flag_returns_typed_code() {
let tmp = TempDir::new().unwrap();
let workspace = tmp.path().join("ws");
memstead()
.args([
"mem-repo",
"init",
workspace.to_str().unwrap(),
"--no-gitignore",
])
.assert()
.success();
let output = memstead()
.current_dir(&workspace)
.args(["--json", "update", "anything--here", "--section", "Body=hi"])
.assert()
.failure()
.get_output()
.stdout
.clone();
let env = parse_envelope(&output);
assert_typed_code(&env, "update missing hash flag");
assert_eq!(
env["code"], "HASH_FLAG_REQUIRED",
"expected HASH_FLAG_REQUIRED, got: {env}",
);
}
#[test]
fn overview_chunk_past_end_returns_typed_code() {
let tmp = TempDir::new().unwrap();
let workspace = tmp.path().join("ws");
memstead()
.args([
"mem-repo",
"init",
workspace.to_str().unwrap(),
"--no-gitignore",
])
.assert()
.success();
let output = memstead()
.current_dir(&workspace)
.args(["--json", "overview", "--chunk", "99"])
.assert()
.failure()
.get_output()
.stdout
.clone();
let env = parse_envelope(&output);
assert_typed_code(&env, "overview chunk past end");
assert_eq!(
env["code"], "CHUNK_OUT_OF_RANGE",
"expected CHUNK_OUT_OF_RANGE, got: {env}",
);
}
#[test]
fn init_non_empty_returns_typed_code() {
let tmp = TempDir::new().unwrap();
std::fs::write(tmp.path().join("preexisting.md"), "hi").unwrap();
let output = memstead()
.args([
"--json",
"init",
"--name",
"tmp",
"--schema",
"default@1.0.0",
tmp.path().to_str().unwrap(),
])
.assert()
.failure()
.get_output()
.stdout
.clone();
let env = parse_envelope(&output);
assert_typed_code(&env, "init non-empty target");
assert_eq!(
env["code"], "TARGET_NOT_EMPTY",
"expected TARGET_NOT_EMPTY, got: {env}",
);
}
#[test]
fn type_unknown_returns_typed_code() {
let tmp = TempDir::new().unwrap();
let workspace = tmp.path().join("ws");
memstead()
.args([
"mem-repo",
"init",
workspace.to_str().unwrap(),
"--no-gitignore",
])
.assert()
.success();
let output = memstead()
.current_dir(&workspace)
.args(["--json", "type", "nonexistent-type"])
.assert()
.failure()
.get_output()
.stdout
.clone();
let env = parse_envelope(&output);
assert_typed_code(&env, "type unknown");
}
#[test]
fn cold_start_returns_typed_code() {
let tmp = TempDir::new().unwrap();
let output = memstead()
.current_dir(tmp.path())
.args(["--json", "status"])
.assert()
.failure()
.get_output()
.stdout
.clone();
let env = parse_envelope(&output);
assert_typed_code(&env, "status outside workspace");
}
#[test]
fn projection_migrate_outside_workspace_returns_typed_code() {
let tmp = TempDir::new().unwrap();
let output = memstead()
.current_dir(tmp.path())
.args(["--json", "projection", "migrate"])
.assert()
.failure()
.get_output()
.stdout
.clone();
let env = parse_envelope(&output);
assert_typed_code(&env, "projection migrate outside workspace");
assert_eq!(env["code"], "WORKSPACE_NOT_INITIALISED", "got: {env}");
}
#[test]
fn projection_init_outside_workspace_returns_typed_code() {
let tmp = TempDir::new().unwrap();
let output = memstead()
.current_dir(tmp.path())
.args([
"--json",
"projection",
"init",
"--mem",
"m",
"--source",
"../x",
"--medium-type",
"codebase",
])
.assert()
.failure()
.get_output()
.stdout
.clone();
let env = parse_envelope(&output);
assert_typed_code(&env, "projection init outside workspace");
assert_eq!(env["code"], "WORKSPACE_NOT_INITIALISED", "got: {env}");
}
#[test]
fn projection_init_existing_binding_returns_typed_code() {
let tmp = TempDir::new().unwrap();
let workspace = tmp.path().join("ws");
memstead()
.args([
"mem-repo",
"init",
workspace.to_str().unwrap(),
"--no-gitignore",
])
.assert()
.success();
let init_args = [
"projection",
"init",
"--mem",
"engine",
"--source",
"../public",
"--medium-type",
"codebase",
"--name",
"graph",
];
memstead()
.current_dir(&workspace)
.args(init_args)
.assert()
.success();
let output = memstead()
.current_dir(&workspace)
.args(
["--json"]
.iter()
.chain(init_args.iter())
.copied()
.collect::<Vec<_>>(),
)
.assert()
.failure()
.get_output()
.stdout
.clone();
let env = parse_envelope(&output);
assert_typed_code(&env, "projection init existing binding");
assert_eq!(env["code"], "PROJECTION_EXISTS", "got: {env}");
}
#[test]
fn projection_enable_missing_binding_returns_typed_code() {
let tmp = TempDir::new().unwrap();
let workspace = tmp.path().join("ws");
memstead()
.args([
"mem-repo",
"init",
workspace.to_str().unwrap(),
"--no-gitignore",
])
.assert()
.success();
let output = memstead()
.current_dir(&workspace)
.args(["--json", "projection", "enable", "sync", "engine/nope"])
.assert()
.failure()
.get_output()
.stdout
.clone();
let env = parse_envelope(&output);
assert_typed_code(&env, "projection enable missing binding");
assert_eq!(env["code"], "PROJECTION_NOT_FOUND", "got: {env}");
}
#[test]
fn projection_advance_missing_binding_returns_typed_code() {
let tmp = TempDir::new().unwrap();
let workspace = tmp.path().join("ws");
memstead()
.args([
"mem-repo",
"init",
workspace.to_str().unwrap(),
"--no-gitignore",
])
.assert()
.success();
let output = memstead()
.current_dir(&workspace)
.args([
"--json",
"projection",
"advance",
"engine/nope",
"--dispositions",
"{}",
])
.assert()
.failure()
.get_output()
.stdout
.clone();
let env = parse_envelope(&output);
assert_typed_code(&env, "projection advance missing binding");
assert_eq!(env["code"], "PROJECTION_NOT_FOUND", "got: {env}");
}
#[test]
fn projection_brief_missing_binding_returns_typed_code() {
let tmp = TempDir::new().unwrap();
let workspace = tmp.path().join("ws");
memstead()
.args([
"mem-repo",
"init",
workspace.to_str().unwrap(),
"--no-gitignore",
])
.assert()
.success();
let output = memstead()
.current_dir(&workspace)
.args(["--json", "projection", "brief", "engine/nope"])
.assert()
.failure()
.get_output()
.stdout
.clone();
let env = parse_envelope(&output);
assert_typed_code(&env, "projection brief missing binding");
assert_eq!(env["code"], "PROJECTION_NOT_FOUND", "got: {env}");
}
#[test]
fn projection_migrate_dangling_ref_returns_typed_code() {
let tmp = TempDir::new().unwrap();
let workspace = tmp.path().join("ws");
memstead()
.args([
"mem-repo",
"init",
workspace.to_str().unwrap(),
"--no-gitignore",
])
.assert()
.success();
let ingests = workspace.join(".memstead").join("ingests");
std::fs::create_dir_all(&ingests).unwrap();
std::fs::write(
ingests.join("engine-graph.json"),
r#"{"projection":"engine/missing","mode":"discovery","trigger":"loop","batch_size":20,"deny_paths":[]}"#,
)
.unwrap();
let output = memstead()
.current_dir(&workspace)
.args(["--json", "projection", "migrate"])
.assert()
.failure()
.get_output()
.stdout
.clone();
let env = parse_envelope(&output);
assert_typed_code(&env, "projection migrate dangling ref");
assert_eq!(env["code"], "PROJECTION_MIGRATE_DANGLING_REF", "got: {env}");
}
#[test]
fn install_refusals_are_all_typed() {
let tmp = TempDir::new().unwrap();
let workspace = tmp.path().join("ws");
let cache = tmp.path().join("cache");
memstead()
.args([
"mem-repo",
"init",
workspace.to_str().unwrap(),
"--no-gitignore",
])
.assert()
.success();
let missing = tmp.path().join("nowhere.mem");
let garbage = tmp.path().join("garbage.mem");
std::fs::write(&garbage, b"definitely not a zip").unwrap();
let cases: [(&str, &str); 4] = [
("missing local path", missing.to_str().unwrap()),
("non-archive bytes", garbage.to_str().unwrap()),
("legacy at-scope ref", "@memstead/knowledge"),
("directory as source", tmp.path().to_str().unwrap()),
];
for (label, source) in cases {
let output = memstead()
.current_dir(&workspace)
.env("MEMSTEAD_MEM_CACHE", &cache)
.args(["--json", "install", source])
.assert()
.failure()
.get_output()
.stdout
.clone();
let env = parse_envelope(&output);
assert_typed_code(&env, &format!("install: {label}"));
}
let folder_ws = tmp.path().join("folder-ws");
std::fs::create_dir_all(folder_ws.join(".memstead")).unwrap();
std::fs::write(folder_ws.join(".memstead").join("workspace.toml"), "").unwrap();
let output = memstead()
.current_dir(&folder_ws)
.env("MEMSTEAD_MEM_CACHE", &cache)
.args(["--json", "install", garbage.to_str().unwrap()])
.assert()
.failure()
.get_output()
.stdout
.clone();
let env = parse_envelope(&output);
assert_typed_code(&env, "install into a folder workspace");
}