#![cfg(feature = "mem-repo")]
use std::fs;
use std::path::{Path, PathBuf};
use assert_cmd::Command;
use memstead_base::friction::{FrictionLedger, friction_ledger_path};
use memstead_git_branch::test_support::init_real_mem_repo_from_disk;
use serde_json::Value;
use tempfile::TempDir;
fn make_workspace(tmp: &Path) -> PathBuf {
let mem = tmp.join("cli-write");
fs::create_dir_all(&mem).unwrap();
let store = mem.join(".memstead");
fs::create_dir_all(&store).unwrap();
fs::write(
store.join("config.json"),
r#"{ "schema": "default@1.0.0" }"#,
)
.unwrap();
init_real_mem_repo_from_disk(tmp, &[(&mem, "cli-write")]);
tmp.to_path_buf()
}
fn memstead() -> Command {
Command::cargo_bin("memstead").expect("memstead binary must be built by cargo")
}
fn parse_json(stdout_bytes: &[u8]) -> 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 output must parse as JSON: {e}\n--- stdout ---\n{body}\n--- end ---")
})
}
#[test]
fn cli_refusals_append_successes_do_not_and_health_serves_the_axis() {
let tmp = TempDir::new().unwrap();
let ws = make_workspace(tmp.path());
let ledger = FrictionLedger::for_workspace(&ws);
let distinctive = "XyZzY-distinctive-payload-9a7f";
memstead()
.current_dir(&ws)
.args([
"--json",
"create",
"--title",
"Alpha",
"--type",
"spec",
"--section",
&format!("bogus-section={distinctive}"),
])
.assert()
.failure();
let entries = ledger.entries();
assert_eq!(entries.len(), 1, "one entry per refused CLI call");
assert_eq!(entries[0].surface, "cli");
assert_eq!(entries[0].verb, "create");
assert_eq!(entries[0].code, "UNKNOWN_SECTION");
assert!(entries[0].ts > 0, "entry carries a timestamp");
let raw = fs::read_to_string(friction_ledger_path(&ws)).unwrap_or_default();
assert!(
!raw.contains(distinctive),
"ledger must never contain parameter content"
);
memstead()
.current_dir(&ws)
.args(["--json", "entity", "cli-write--does-not-exist"])
.assert()
.failure();
assert_eq!(ledger.entries().len(), 2);
memstead()
.current_dir(&ws)
.args([
"create",
"--title",
"Gamma",
"--type",
"spec",
"--section",
"identity=I.",
"--section",
"purpose=P.",
])
.assert()
.success();
assert_eq!(
ledger.entries().len(),
2,
"a successful call appends nothing"
);
let out = memstead()
.current_dir(&ws)
.args(["--json", "health", "--include", "friction"])
.assert()
.success()
.get_output()
.stdout
.clone();
let health = parse_json(&out);
assert_eq!(health["friction"]["total"], 2, "{health}");
assert_eq!(health["friction"]["by_code"]["UNKNOWN_SECTION"], 1);
assert_eq!(health["friction"]["by_code"]["ENTITY_NOT_FOUND"], 1);
assert_eq!(health["friction"]["by_verb"]["cli:create"], 1);
assert_eq!(health["friction"]["by_verb"]["cli:entity"], 1);
assert_eq!(health["friction"]["recent_24h"]["total"], 2);
let out = memstead()
.current_dir(&ws)
.args(["--json", "health"])
.assert()
.success()
.get_output()
.stdout
.clone();
let plain = parse_json(&out);
assert!(
plain.get("friction").is_none(),
"health without the include must not carry the axis"
);
let gitignore = friction_ledger_path(&ws)
.parent()
.unwrap()
.join(".gitignore");
assert_eq!(fs::read_to_string(gitignore).unwrap(), "*\n");
}
#[test]
#[cfg(unix)]
fn unwritable_ledger_never_changes_the_refusal() {
use std::os::unix::fs::PermissionsExt;
let tmp = TempDir::new().unwrap();
let ws = make_workspace(tmp.path());
let refuse = || {
let out = memstead()
.current_dir(&ws)
.args([
"--json",
"create",
"--title",
"Alpha",
"--type",
"spec",
"--section",
"bogus-section=x",
])
.assert()
.failure()
.get_output()
.stdout
.clone();
let v = parse_json(&out);
serde_json::json!({ "code": v["code"], "message": v["message"], "details": v["details"] })
};
let with_ledger = refuse();
let ledger = FrictionLedger::for_workspace(&ws);
assert_eq!(ledger.entries().len(), 1);
let dir = friction_ledger_path(&ws).parent().unwrap().to_path_buf();
let file = friction_ledger_path(&ws);
fs::set_permissions(&file, fs::Permissions::from_mode(0o444)).unwrap();
fs::set_permissions(&dir, fs::Permissions::from_mode(0o555)).unwrap();
let without_ledger = refuse();
fs::set_permissions(&dir, fs::Permissions::from_mode(0o755)).unwrap();
fs::set_permissions(&file, fs::Permissions::from_mode(0o644)).unwrap();
assert_eq!(
with_ledger, without_ledger,
"the refusal envelope must be identical with and without a writable ledger"
);
assert_eq!(
ledger.entries().len(),
1,
"the sealed window recorded nothing"
);
}