use std::path::Path;
use assert_cmd::Command;
use tempfile::TempDir;
fn memstead() -> Command {
Command::cargo_bin("memstead").expect("memstead binary must be built by cargo")
}
fn stdout_of(assert: assert_cmd::assert::Assert) -> String {
String::from_utf8(assert.get_output().stdout.clone()).expect("stdout is UTF-8")
}
fn stderr_of(assert: assert_cmd::assert::Assert) -> String {
String::from_utf8(assert.get_output().stderr.clone()).expect("stderr is UTF-8")
}
#[test]
fn quickstart_fresh_dir_bootstraps_workspace_seed_and_wiring() {
let tmp = TempDir::new().unwrap();
let root = tmp.path().join("my-fresh-graph");
let assert = memstead()
.args(["quickstart", "--json"])
.arg(&root)
.assert()
.success();
let payload: serde_json::Value =
serde_json::from_str(&stdout_of(assert)).expect("quickstart --json emits JSON");
assert_eq!(payload["name"], "my-fresh-graph");
assert_eq!(payload["schema"], "default@1.3.0");
assert_eq!(payload["agents_defaulted"], true);
assert_eq!(payload["agents"][0]["target"], "claude-code");
assert!(root.join(".memstead").join("workspace.toml").is_file());
assert!(root.join(".memstead").join("config.json").is_file());
let seed_id = payload["seed_entity"].as_str().expect("seed entity id");
assert_eq!(seed_id, "my-fresh-graph--welcome-to-memstead");
assert!(root.join("welcome-to-memstead.md").is_file());
let mcp: serde_json::Value =
serde_json::from_slice(&std::fs::read(root.join(".mcp.json")).unwrap()).unwrap();
let command = mcp["mcpServers"]["memstead"]["command"]
.as_str()
.expect("server entry has a command");
assert!(
command.contains("memstead-mcp"),
"command must launch memstead-mcp, got: {command}",
);
assert!(
payload["next_action"].as_str().unwrap().contains("Restart"),
"next action must name the restart, got: {}",
payload["next_action"],
);
memstead()
.current_dir(&root)
.arg("overview")
.assert()
.success();
}
#[test]
fn quickstart_tolerates_dotfiles_and_readme_without_ingesting() {
let tmp = TempDir::new().unwrap();
let root = tmp.path().to_path_buf();
std::fs::write(root.join(".gitignore"), "target/\n").unwrap();
std::fs::write(root.join("README"), "my project\n").unwrap();
std::fs::write(root.join("LICENSE"), "MIT\n").unwrap();
std::fs::create_dir(root.join(".git")).unwrap();
memstead().arg("quickstart").arg(&root).assert().success();
assert_eq!(
std::fs::read_to_string(root.join("README")).unwrap(),
"my project\n"
);
let assert = memstead()
.current_dir(&root)
.args(["list", "--json"])
.assert()
.success();
let listed: serde_json::Value = serde_json::from_str(&stdout_of(assert)).unwrap();
let hits = listed["hits"]
.as_array()
.unwrap_or_else(|| panic!("list --json carries hits[]; got {listed}"));
assert_eq!(hits.len(), 1, "seed entity only; got {hits:?}");
}
#[test]
fn quickstart_refuses_markdown_readme_naming_the_ingestion_risk() {
let tmp = TempDir::new().unwrap();
std::fs::write(tmp.path().join("README.md"), "# my project\n").unwrap();
let err = stderr_of(
memstead()
.arg("quickstart")
.arg(tmp.path())
.assert()
.failure(),
);
assert!(err.contains("README.md"), "names the file; got: {err}");
assert!(
err.contains("adopt"),
"explains the ingestion risk; got: {err}"
);
assert!(
err.contains("memstead quickstart"),
"carries the alternative; got: {err}"
);
assert!(!tmp.path().join(".memstead").exists(), "no half-init");
assert_eq!(
std::fs::read_to_string(tmp.path().join("README.md")).unwrap(),
"# my project\n",
"the README is untouched",
);
}
#[test]
fn quickstart_refuses_conflicting_content_without_half_init() {
let tmp = TempDir::new().unwrap();
let root = tmp.path().to_path_buf();
std::fs::write(root.join("main.py"), "print()\n").unwrap();
let assert = memstead().arg("quickstart").arg(&root).assert().failure();
let err = stderr_of(assert);
assert!(err.contains("TARGET_NOT_EMPTY"), "typed code; got: {err}");
assert!(err.contains("main.py"), "names the conflict; got: {err}");
assert!(
err.contains("memstead quickstart"),
"names the exact alternative; got: {err}"
);
assert!(!root.join(".memstead").exists());
assert!(!root.join(".mcp.json").exists());
}
#[test]
fn quickstart_refuses_foreign_memstead_dir_and_ancestor_workspace() {
let tmp = TempDir::new().unwrap();
std::fs::create_dir_all(tmp.path().join(".memstead")).unwrap();
std::fs::write(tmp.path().join(".memstead").join("junk"), "x").unwrap();
let err = stderr_of(
memstead()
.arg("quickstart")
.arg(tmp.path())
.assert()
.failure(),
);
assert!(
err.contains("FOREIGN_MEMSTEAD_DIR"),
"typed code; got: {err}"
);
assert!(
err.contains("memstead quickstart"),
"carries next command; got: {err}"
);
let outer = TempDir::new().unwrap();
memstead()
.arg("quickstart")
.arg(outer.path())
.assert()
.success();
let inner = outer.path().join("inner");
std::fs::create_dir(&inner).unwrap();
let err = stderr_of(memstead().arg("quickstart").arg(&inner).assert().failure());
assert!(
err.contains("WORKSPACE_ALREADY_EXISTS_ABOVE"),
"typed code; got: {err}"
);
assert!(
err.contains("memstead overview"),
"viable next command; got: {err}"
);
assert!(
err.contains("memstead quickstart"),
"separate-graph alternative; got: {err}"
);
assert!(
!err.contains("mem init"),
"no dead-end suggestion; got: {err}"
);
assert!(
!inner.join(".memstead").exists(),
"no half-init in the nested target"
);
let err = stderr_of(
memstead()
.arg("quickstart")
.arg(outer.path())
.assert()
.failure(),
);
assert!(
err.contains("WORKSPACE_ALREADY_INITIALISED"),
"typed code; got: {err}"
);
assert!(
err.contains("memstead overview"),
"carries next command; got: {err}"
);
}
#[test]
fn quickstart_never_overwrites_existing_mcp_server_entry() {
let tmp = TempDir::new().unwrap();
let root = tmp.path().to_path_buf();
std::fs::write(
root.join(".mcp.json"),
serde_json::to_vec_pretty(&serde_json::json!({
"mcpServers": {
"memstead": { "command": "/custom/memstead-mcp", "args": ["--flag"] },
"other": { "command": "/bin/other" },
}
}))
.unwrap(),
)
.unwrap();
let assert = memstead()
.args(["quickstart", "--json"])
.arg(&root)
.assert()
.success();
let payload: serde_json::Value = serde_json::from_str(&stdout_of(assert)).unwrap();
assert!(
payload["agents"][0]["action"]
.as_str()
.unwrap()
.contains("left untouched"),
"report says the entry was left alone; got {payload}",
);
let mcp: serde_json::Value =
serde_json::from_slice(&std::fs::read(root.join(".mcp.json")).unwrap()).unwrap();
assert_eq!(
mcp["mcpServers"]["memstead"]["command"],
"/custom/memstead-mcp"
);
assert_eq!(mcp["mcpServers"]["memstead"]["args"][0], "--flag");
assert_eq!(mcp["mcpServers"]["other"]["command"], "/bin/other");
}
#[test]
fn quickstart_agent_flags_wire_cursor_gemini_codex() {
let tmp = TempDir::new().unwrap();
let root = tmp.path().to_path_buf();
let assert = memstead()
.args([
"quickstart",
"--json",
"--agent",
"cursor",
"--agent",
"gemini",
"--agent",
"codex",
])
.arg(&root)
.assert()
.success();
let payload: serde_json::Value = serde_json::from_str(&stdout_of(assert)).unwrap();
assert_eq!(payload["agents_defaulted"], false);
let cursor: serde_json::Value =
serde_json::from_slice(&std::fs::read(root.join(".cursor/mcp.json")).unwrap()).unwrap();
assert!(cursor["mcpServers"]["memstead"]["command"].is_string());
let gemini: serde_json::Value =
serde_json::from_slice(&std::fs::read(root.join(".gemini/settings.json")).unwrap())
.unwrap();
assert!(gemini["mcpServers"]["memstead"]["command"].is_string());
let codex_action = payload["agents"][2]["action"].as_str().unwrap();
assert!(
codex_action.contains("codex mcp add memstead --"),
"got: {codex_action}"
);
assert!(!root.join(".codex").exists());
assert!(!root.join(".mcp.json").exists());
}
#[test]
fn quickstart_underivable_name_refuses_with_flag_command_non_tty() {
let tmp = TempDir::new().unwrap();
let root = tmp.path().join("日本語");
std::fs::create_dir(&root).unwrap();
let err = stderr_of(memstead().arg("quickstart").arg(&root).assert().failure());
assert!(err.contains("--name"), "refusal names the flag; got: {err}");
assert!(
err.contains("memstead quickstart --name"),
"exact command; got: {err}"
);
assert!(!root.join(".memstead").exists(), "no half-init");
memstead()
.args(["quickstart", "--name", "nihongo"])
.arg(&root)
.assert()
.success();
assert!(root.join(".memstead").join("workspace.toml").is_file());
}
#[test]
fn schema_new_scaffold_validates_unmodified() {
let tmp = TempDir::new().unwrap();
let out = stdout_of(
memstead()
.current_dir(tmp.path())
.args(["schema", "new", "acme"])
.assert()
.success(),
);
assert!(out.contains("memstead schema validate acme"), "got: {out}");
#[cfg(feature = "mem-repo")]
assert!(out.contains("memstead schema install acme"), "got: {out}");
#[cfg(not(feature = "mem-repo"))]
assert!(
out.contains("memstead schema install ../acme"),
"got: {out}"
);
assert!(
out.contains("acme@0.1.0"),
"pin step names the version; got: {out}",
);
assert!(tmp.path().join("acme/schema.yaml").is_file());
assert!(tmp.path().join("acme/types/note.yaml").is_file());
memstead()
.current_dir(tmp.path())
.args(["schema", "validate", "acme"])
.assert()
.success();
}
#[cfg(feature = "mem-repo")]
#[test]
fn schema_new_follow_up_commands_end_in_pinned_mem_accepting_create() {
let tmp = TempDir::new().unwrap();
let ws = tmp.path().join("myws");
memstead()
.args(["init", "--name", "myws", "--schema", "default@1.0.0"])
.arg(&ws)
.assert()
.success();
let out = stdout_of(
memstead()
.current_dir(&ws)
.args(["schema", "new", "acme"])
.assert()
.success(),
);
assert!(
out.contains("memstead mem set-schema myws acme@0.1.0"),
"pin step names the workspace's mem; got: {out}",
);
assert!(
!out.contains("memstead delete"),
"no seed in an init workspace, so no delete step; got: {out}",
);
memstead()
.current_dir(&ws)
.args(["schema", "validate", "acme"])
.assert()
.success();
memstead()
.current_dir(&ws)
.args(["schema", "install", "acme"])
.assert()
.success();
let pin_out = stdout_of(
memstead()
.current_dir(&ws)
.args(["mem", "set-schema", "myws", "acme@0.1.0"])
.assert()
.success(),
);
assert!(
pin_out.contains("Switched"),
"empty mem switches atomically; got: {pin_out}"
);
memstead()
.current_dir(&ws)
.args([
"create",
"--type",
"note",
"--title",
"First note",
"--section",
"summary=It works.",
])
.assert()
.success();
}
#[cfg(feature = "mem-repo")]
#[test]
fn schema_new_follow_up_from_quickstart_workspace_ends_pinned() {
let tmp = TempDir::new().unwrap();
let ws = tmp.path().join("my-graph");
memstead().arg("quickstart").arg(&ws).assert().success();
let out = stdout_of(
memstead()
.current_dir(&ws)
.args(["schema", "new", "acme"])
.assert()
.success(),
);
let seed_id = "my-graph--welcome-to-memstead";
assert!(
out.contains(&format!("memstead delete {seed_id}")),
"follow-up includes the seed delete step; got: {out}",
);
assert!(
out.contains("memstead mem set-schema my-graph acme@0.1.0"),
"pin step names the quickstart mem; got: {out}",
);
memstead()
.current_dir(&ws)
.args(["schema", "validate", "acme"])
.assert()
.success();
memstead()
.current_dir(&ws)
.args(["schema", "install", "acme"])
.assert()
.success();
memstead()
.current_dir(&ws)
.args(["delete", seed_id])
.assert()
.success();
let pin_out = stdout_of(
memstead()
.current_dir(&ws)
.args(["mem", "set-schema", "my-graph", "acme@0.1.0"])
.assert()
.success(),
);
assert!(
pin_out.contains("Switched"),
"seedless mem switches atomically, no migration; got: {pin_out}",
);
memstead()
.current_dir(&ws)
.args([
"create",
"--type",
"note",
"--title",
"First note",
"--section",
"summary=It works.",
])
.assert()
.success();
}
#[cfg(feature = "mem-repo")]
#[test]
fn schema_install_resolves_retained_builtin_versions() {
let tmp = TempDir::new().unwrap();
let ws = tmp.path().join("retained");
memstead().arg("quickstart").arg(&ws).assert().success();
memstead()
.current_dir(&ws)
.args(["schema", "install", "planning@0.3.0"])
.assert()
.success();
let err = stderr_of(
memstead()
.current_dir(&ws)
.args(["schema", "install", "planning@9.9.9"])
.assert()
.failure(),
);
assert!(
err.contains("planning@9.9.9"),
"unregistered version refuses, naming the pin; got: {err}"
);
}
#[test]
fn quickstart_malformed_agent_config_refuses_before_any_write() {
let tmp = TempDir::new().unwrap();
std::fs::write(tmp.path().join(".mcp.json"), "{not json").unwrap();
let err = stderr_of(
memstead()
.arg("quickstart")
.arg(tmp.path())
.assert()
.failure(),
);
assert!(
err.contains("not valid JSON"),
"names the defect; got: {err}"
);
assert!(
err.contains("re-run: memstead quickstart"),
"carries the retry; got: {err}"
);
assert!(
!tmp.path().join(".memstead").exists(),
"nothing was created"
);
std::fs::remove_file(tmp.path().join(".mcp.json")).unwrap();
memstead()
.arg("quickstart")
.arg(tmp.path())
.assert()
.success();
let tmp = TempDir::new().unwrap();
std::fs::write(tmp.path().join(".mcp.json"), r#"{"mcpServers": []}"#).unwrap();
let err = stderr_of(
memstead()
.arg("quickstart")
.arg(tmp.path())
.assert()
.failure(),
);
assert!(err.contains("mcpServers"), "names the defect; got: {err}");
assert!(
err.contains("re-run: memstead quickstart"),
"carries the retry; got: {err}"
);
assert!(
!tmp.path().join(".memstead").exists(),
"nothing was created"
);
}
#[cfg(not(feature = "mem-repo"))]
#[test]
fn schema_new_lean_follow_up_ends_in_working_fresh_mem() {
let tmp = TempDir::new().unwrap();
let out = stdout_of(
memstead()
.current_dir(tmp.path())
.args(["schema", "new", "acme"])
.assert()
.success(),
);
assert!(
out.contains("memstead init --name acme-mem --schema acme@0.1.0"),
"lean follow-up routes through a fresh init; got: {out}",
);
assert!(
out.contains("memstead schema install ../acme"),
"install step targets the new workspace; got: {out}",
);
assert!(
!out.contains("mem set-schema"),
"lean never prints the full-only subcommand; got: {out}",
);
memstead()
.current_dir(tmp.path())
.args(["schema", "validate", "acme"])
.assert()
.success();
let fresh = tmp.path().join("acme-mem");
std::fs::create_dir(&fresh).unwrap();
memstead()
.current_dir(&fresh)
.args(["init", "--name", "acme-mem", "--schema", "acme@0.1.0"])
.assert()
.success();
memstead()
.current_dir(&fresh)
.args(["schema", "install", "../acme"])
.assert()
.success();
memstead()
.current_dir(&fresh)
.arg("overview")
.assert()
.success();
memstead()
.current_dir(&fresh)
.args([
"create",
"--type",
"note",
"--title",
"First note",
"--section",
"summary=It works.",
])
.assert()
.success();
}
#[cfg(not(feature = "mem-repo"))]
#[test]
fn schema_new_lean_follow_up_from_inside_workspace_lands_outside() {
let tmp = TempDir::new().unwrap();
let ws = tmp.path().join("my-graph");
memstead().arg("quickstart").arg(&ws).assert().success();
let out = stdout_of(
memstead()
.current_dir(&ws)
.args(["schema", "new", "acme"])
.assert()
.success(),
);
let quoted = |line_marker: &str| -> std::path::PathBuf {
let line = out
.lines()
.find(|l| l.contains(line_marker))
.unwrap_or_else(|| panic!("no step containing `{line_marker}`; got: {out}"));
let start = line
.find('"')
.unwrap_or_else(|| panic!("no quoted path in: {line}"));
let rest = &line[start + 1..];
let end = rest
.find('"')
.unwrap_or_else(|| panic!("unterminated quote in: {line}"));
std::path::PathBuf::from(&rest[..end])
};
let fresh = quoted("memstead init --name acme-mem");
let pkg = quoted("memstead schema install");
let ws_canon = std::fs::canonicalize(&ws).unwrap();
assert!(
!fresh.starts_with(&ws_canon) && !fresh.starts_with(&ws),
"fresh-mem dir {} must not nest inside the workspace {}",
fresh.display(),
ws.display(),
);
std::fs::create_dir_all(&fresh).unwrap();
memstead()
.current_dir(&fresh)
.args(["init", "--name", "acme-mem", "--schema", "acme@0.1.0"])
.assert()
.success();
memstead()
.current_dir(&fresh)
.args(["schema", "install"])
.arg(&pkg)
.assert()
.success();
memstead()
.current_dir(&fresh)
.arg("overview")
.assert()
.success();
memstead()
.current_dir(&fresh)
.args([
"create",
"--type",
"note",
"--title",
"First note",
"--section",
"summary=It works.",
])
.assert()
.success();
}
#[test]
fn schema_new_package_installs_into_folder_workspace() {
let tmp = TempDir::new().unwrap();
let ws = tmp.path().join("ws");
memstead()
.args(["init", "--name", "myws", "--schema", "default@1.0.0"])
.arg(&ws)
.assert()
.success();
memstead()
.current_dir(&ws)
.args(["schema", "new", "acme"])
.assert()
.success();
memstead()
.current_dir(&ws)
.args(["schema", "install", "acme"])
.assert()
.success();
assert!(
ws.join(".memstead/schemas/acme@0.1.0/schema.yaml")
.is_file()
);
assert!(
ws.join(".memstead/schemas/acme@0.1.0/types/note.yaml")
.is_file()
);
}
#[test]
fn schema_new_refusals_carry_next_commands() {
let tmp = TempDir::new().unwrap();
memstead()
.current_dir(tmp.path())
.args(["schema", "new", "acme"])
.assert()
.success();
let before = std::fs::read_to_string(tmp.path().join("acme/schema.yaml")).unwrap();
let err = stderr_of(
memstead()
.current_dir(tmp.path())
.args(["schema", "new", "acme"])
.assert()
.failure(),
);
assert!(
err.contains("SCHEMA_PACKAGE_EXISTS"),
"typed code; got: {err}"
);
assert!(
err.contains("memstead schema validate acme"),
"next command; got: {err}"
);
assert_eq!(
std::fs::read_to_string(tmp.path().join("acme/schema.yaml")).unwrap(),
before,
"the existing package is untouched",
);
let err = stderr_of(
memstead()
.current_dir(tmp.path())
.args(["schema", "new", "Acme Corp!"])
.assert()
.failure(),
);
assert!(err.contains("lowercase"), "states the rule; got: {err}");
assert!(
err.contains("memstead schema new acme-corp"),
"suggested correction as a runnable command; got: {err}",
);
std::fs::create_dir(tmp.path().join("busy")).unwrap();
std::fs::write(tmp.path().join("busy/x.txt"), "x").unwrap();
let err = stderr_of(
memstead()
.current_dir(tmp.path())
.args(["schema", "new", "busy"])
.assert()
.failure(),
);
assert!(err.contains("TARGET_NOT_EMPTY"), "typed code; got: {err}");
assert!(err.contains("x.txt"), "names the finding; got: {err}");
}
#[test]
fn generated_artifacts_speak_mem_vocabulary_only() {
let retired_noun = ["va", "ult"].concat();
let tmp = TempDir::new().unwrap();
memstead()
.current_dir(tmp.path())
.args(["schema", "new", "acme"])
.assert()
.success();
let scaffold = format!(
"{}{}",
std::fs::read_to_string(tmp.path().join("acme/schema.yaml")).unwrap(),
std::fs::read_to_string(tmp.path().join("acme/types/note.yaml")).unwrap(),
);
assert!(
!scaffold.to_lowercase().contains(&retired_noun),
"scaffold speaks mem only"
);
let root = tmp.path().join("qs");
let out = stdout_of(memstead().arg("quickstart").arg(&root).assert().success());
assert!(
!out.to_lowercase().contains(&retired_noun),
"quickstart report speaks mem only"
);
}
#[test]
fn every_refusal_on_these_paths_names_a_next_command() {
let tmp = TempDir::new().unwrap();
let dirty = tmp.path().join("dirty");
std::fs::create_dir(&dirty).unwrap();
std::fs::write(dirty.join("code.rs"), "x").unwrap();
let cases: Vec<String> = vec![
stderr_of(memstead().arg("quickstart").arg(&dirty).assert().failure()),
{
let weird = tmp.path().join("统一");
std::fs::create_dir(&weird).unwrap();
stderr_of(memstead().arg("quickstart").arg(&weird).assert().failure())
},
{
memstead()
.current_dir(tmp.path())
.args(["schema", "new", "acme"])
.assert()
.success();
stderr_of(
memstead()
.current_dir(tmp.path())
.args(["schema", "new", "acme"])
.assert()
.failure(),
)
},
stderr_of(
memstead()
.current_dir(tmp.path())
.args(["schema", "new", "BAD NAME"])
.assert()
.failure(),
),
];
for (i, err) in cases.iter().enumerate() {
assert!(
err.contains("memstead "),
"refusal #{i} must include an exact next command; got: {err}",
);
}
}
fn assert_filesystem_shape_disclosure(out: &str, ctx: &str) {
let mut needles = vec![
"filesystem-mem",
"cannot install mems from the registry",
"memstead mem-repo init",
];
if cfg!(feature = "mem-repo") {
needles.push("memstead install");
needles.push("UNSUPPORTED_WORKSPACE_SHAPE");
} else {
needles.push("this lean build does not carry them");
}
for needle in needles {
assert!(
out.contains(needle),
"{ctx}: shape disclosure must name `{needle}`; got:\n{out}",
);
}
}
#[test]
fn quickstart_receipt_discloses_the_shape_it_picked() {
let tmp = TempDir::new().unwrap();
let root = tmp.path().join("disclosed-graph");
let out = stdout_of(
memstead()
.args(["quickstart", "--agent", "claude-code"])
.arg(&root)
.assert()
.success(),
);
assert_filesystem_shape_disclosure(&out, "quickstart receipt");
}
#[test]
fn init_receipt_discloses_the_shape_it_picked() {
let tmp = TempDir::new().unwrap();
let root = tmp.path().join("strict-graph");
let out = stdout_of(
memstead()
.args([
"init",
"--name",
"strict-graph",
"--schema",
"default@1.3.0",
])
.arg(&root)
.assert()
.success(),
);
assert_filesystem_shape_disclosure(&out, "init receipt");
}
#[cfg(feature = "mem-repo")]
#[test]
fn mem_repo_init_discloses_its_shape_symmetrically() {
let tmp = TempDir::new().unwrap();
let out = stdout_of(
memstead()
.args(["mem-repo", "init"])
.arg(tmp.path())
.assert()
.success(),
);
for needle in ["mem-repo", "git", "memstead quickstart"] {
assert!(
out.contains(needle),
"mem-repo init receipt must name `{needle}`; got:\n{out}",
);
}
}
#[test]
fn json_receipts_carry_the_whole_disclosure_not_just_the_label() {
fn assert_disclosure(payload: &serde_json::Value, want_shape: &str, ctx: &str) {
let d = &payload["workspace_shape_disclosure"];
assert_eq!(d["shape"], want_shape, "{ctx}: shape; got {payload}");
for key in ["summary", "cannot", "other_shape", "other_shape_command"] {
let v = d[key].as_str().unwrap_or_default();
assert!(
!v.is_empty(),
"{ctx}: `{key}` must be present and non-empty; got {d}",
);
}
assert_ne!(
d["other_shape"], want_shape,
"{ctx}: the other shape must differ from this one; got {d}",
);
}
let tmp = TempDir::new().unwrap();
let assert = memstead()
.args(["quickstart", "--json", "--agent", "claude-code"])
.arg(tmp.path().join("json-qs"))
.assert()
.success();
let payload: serde_json::Value = serde_json::from_str(&stdout_of(assert)).unwrap();
assert_disclosure(&payload, "filesystem-mem", "quickstart --json");
let assert = memstead()
.args([
"init",
"--json",
"--name",
"json-init",
"--schema",
"default@1.3.0",
])
.arg(tmp.path().join("json-init"))
.assert()
.success();
let payload: serde_json::Value = serde_json::from_str(&stdout_of(assert)).unwrap();
assert_disclosure(&payload, "filesystem-mem", "init --json");
}
#[cfg(feature = "mem-repo")]
#[test]
fn mem_repo_init_json_carries_the_whole_disclosure() {
let tmp = TempDir::new().unwrap();
let assert = memstead()
.args(["mem-repo", "init", "--json"])
.arg(tmp.path())
.assert()
.success();
let payload: serde_json::Value = serde_json::from_str(&stdout_of(assert)).unwrap();
let d = &payload["workspace_shape_disclosure"];
assert_eq!(d["shape"], "mem-repo", "got {payload}");
assert_eq!(d["other_shape"], "filesystem-mem", "got {d}");
assert!(
d["other_shape_command"]
.as_str()
.unwrap_or_default()
.contains("memstead quickstart"),
"must name the other shape's command; got {d}",
);
}
#[cfg(feature = "mem-repo")]
#[test]
fn mem_repo_init_inside_git_repo_hints_layout_and_trackability() {
let tmp = TempDir::new().unwrap();
let repo = tmp.path().join("repo");
std::fs::create_dir_all(&repo).unwrap();
assert!(
std::process::Command::new("git")
.args(["init", "-q"])
.current_dir(&repo)
.status()
.unwrap()
.success()
);
let assert = memstead()
.args(["mem-repo", "init"])
.arg(&repo)
.assert()
.success();
let stderr = String::from_utf8_lossy(&assert.get_output().stderr).to_string();
assert!(
stderr.contains("common parent directory"),
"layout hint with the recipe expected on stderr, got:\n{stderr}"
);
assert!(
stderr.contains(".memstead/` is intentionally trackable"),
"trackability note expected next to the gitignore line, got:\n{stderr}"
);
let gitignore = std::fs::read_to_string(repo.join(".gitignore")).unwrap();
assert!(gitignore.contains("mem-repo/"), "got:\n{gitignore}");
let free = tmp.path().join("free-standing");
let assert = memstead()
.args(["mem-repo", "init"])
.arg(&free)
.assert()
.success();
let stderr = String::from_utf8_lossy(&assert.get_output().stderr).to_string();
assert!(
!stderr.contains("common parent directory") && !stderr.contains("intentionally trackable"),
"no hint noise where the shape is unaffected, got:\n{stderr}"
);
}
#[test]
fn every_verb_the_receipt_names_is_runnable_or_flagged_as_absent() {
let tmp = TempDir::new().unwrap();
let out = stdout_of(
memstead()
.args(["quickstart", "--agent", "claude-code"])
.arg(tmp.path().join("named-cmds"))
.assert()
.success(),
);
let help = stdout_of(memstead().arg("--help").assert().success());
let disowned =
out.contains("this lean build has no") || out.contains("this lean build does not carry");
for verb in ["install", "mem-repo", "quickstart", "overview", "delete"] {
if !out.contains(&format!("memstead {verb}")) {
continue;
}
let listed = help.lines().any(|l| l.trim_start().starts_with(verb));
assert!(
listed || disowned,
"the receipt names `memstead {verb}`, this build's help does not list it, and the \
receipt never says the build lacks it \u{2014} the reader would hit `unrecognized \
subcommand`.\n--- receipt ---\n{out}\n--- help ---\n{help}",
);
}
}
#[cfg(feature = "mem-repo")]
#[test]
fn mem_repo_only_subcommand_still_refuses_after_disclosure() {
let tmp = TempDir::new().unwrap();
let root = tmp.path().join("refusing-graph");
memstead()
.args(["quickstart", "--agent", "claude-code"])
.arg(&root)
.assert()
.success();
let assert = memstead()
.current_dir(&root)
.args(["install", "acme/notes", "--json"])
.assert()
.failure();
let body = stdout_of(assert);
let envelope: serde_json::Value =
serde_json::from_str(body.trim()).expect("--json refusal is JSON");
assert_eq!(
envelope["code"], "UNSUPPORTED_WORKSPACE_SHAPE",
"install must still refuse by shape; got: {envelope}",
);
let message = envelope["message"].as_str().unwrap_or_default();
assert!(
message.contains("mem-repo"),
"refusal must still name the recovering shape; got: {message}",
);
}
#[test]
fn quickstart_receipt_names_in_session_verification_and_the_restart() {
let tmp = TempDir::new().unwrap();
let root = tmp.path().join("verifiable-graph");
let assert = memstead()
.args(["quickstart", "--json", "--agent", "claude-code"])
.arg(&root)
.assert()
.success();
let payload: serde_json::Value =
serde_json::from_str(&stdout_of(assert)).expect("quickstart --json emits JSON");
let next = payload["next_action"].as_str().unwrap_or_default();
assert!(
next.contains("Restart") && next.contains("registers"),
"the restart must still be named for what it does; got: {next}",
);
let verify = payload["verify_now"]
.as_array()
.expect("receipt carries in-session verification steps");
let rendered = verify
.iter()
.map(|v| v["command"].as_str().unwrap_or_default())
.collect::<Vec<_>>()
.join("\n");
assert!(
rendered.contains("--version"),
"verification must exercise the binary the wiring points at; got:\n{rendered}",
);
assert!(
rendered.contains("memstead overview"),
"verification must name a read of the graph itself; got:\n{rendered}",
);
let wired = payload["mcp_command"].as_str().expect("mcp_command");
let status = std::process::Command::new(wired)
.arg("--version")
.status()
.expect("the wired memstead-mcp binary must be runnable");
assert!(status.success(), "`{wired} --version` must succeed");
let root2 = tmp.path().join("verifiable-graph-md");
let out = stdout_of(
memstead()
.args(["quickstart", "--agent", "claude-code"])
.arg(&root2)
.assert()
.success(),
);
assert!(
out.contains("--version") && out.contains("Restart"),
"markdown receipt must carry both the in-session check and the restart; got:\n{out}",
);
}
#[test]
fn the_receipts_printed_commands_run_verbatim_from_the_callers_cwd() {
fn commands_in_markdown(out: &str) -> Vec<String> {
out.split('`')
.skip(1)
.step_by(2)
.map(str::trim)
.filter(|s| {
s.contains(' ')
&& (s.starts_with("cd ") || s.starts_with("codex ") || s.contains("memstead"))
})
.filter(|s| !s.contains("quickstart") && !s.contains('<'))
.map(str::to_string)
.collect()
}
fn path_for(has_memstead: bool) -> String {
if has_memstead {
format!(
"{}:{}",
Path::new(env!("CARGO_BIN_EXE_memstead"))
.parent()
.unwrap()
.display(),
std::env::var("PATH").unwrap_or_default(),
)
} else {
"/usr/bin:/bin".to_string()
}
}
fn run(command: &str, cwd: &Path, path_has_memstead: bool) {
let path = path_for(path_has_memstead);
let out = std::process::Command::new("sh")
.arg("-c")
.arg(command)
.current_dir(cwd)
.env("PATH", path)
.output()
.expect("spawn sh");
assert!(
out.status.success(),
"the receipt printed `{command}`, which fails when run as printed \
(PATH carries memstead: {path_has_memstead}):\n--- stdout ---\n{}\n\
--- stderr ---\n{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr),
);
}
let cases = [
("My Graph", true, false),
("-dashed-graph", true, false),
("bob's graph", true, false),
("offpath-graph", false, false),
("awkward-binary-graph", false, true),
];
for (dir, on_path, awkward_binary) in cases {
let tmp = TempDir::new().unwrap();
let outer = tmp.path().join("outer");
std::fs::create_dir_all(&outer).unwrap();
let bin = if awkward_binary {
let dir = tmp.path().join("bob's bin dir");
std::fs::create_dir_all(&dir).unwrap();
for name in ["memstead", "memstead-mcp"] {
let src = Path::new(env!("CARGO_BIN_EXE_memstead"))
.parent()
.unwrap()
.join(name);
if src.is_file() {
std::fs::copy(&src, dir.join(name)).unwrap();
}
}
dir.join("memstead")
} else {
Path::new(env!("CARGO_BIN_EXE_memstead")).to_path_buf()
};
let assert = Command::new(&bin)
.current_dir(&outer)
.env("PATH", path_for(on_path))
.args(["quickstart", "--agent", "claude-code", "--"])
.arg(dir)
.assert()
.success();
let out = stdout_of(assert);
let disowned = out.contains("this lean build has no")
|| out.contains("this lean build does not carry");
for command in commands_in_markdown(&out) {
if disowned && command.contains("mem-repo") {
continue;
}
run(&command, &outer, on_path);
}
}
let tmp = TempDir::new().unwrap();
let outer = tmp.path().join("outer");
std::fs::create_dir_all(&outer).unwrap();
let assert = memstead()
.current_dir(&outer)
.args([
"quickstart",
"--json",
"--agent",
"claude-code",
"--agent",
"codex",
"--",
"My Graph",
])
.assert()
.success();
let payload: serde_json::Value = serde_json::from_str(&stdout_of(assert)).unwrap();
let steps = payload["verify_now"]
.as_array()
.expect("verify_now is an array of steps");
assert!(!steps.is_empty(), "got {payload}");
for s in steps {
let c = s["command"].as_str().unwrap_or_default();
assert!(
!c.contains('`') && !c.starts_with("- "),
"machine surface must carry a bare command, got: {c}",
);
run(c, &outer, true);
}
run(
payload["seed_entity_delete_command"]
.as_str()
.expect("seed delete command"),
&outer,
true,
);
let next = payload["next_action"].as_str().unwrap_or_default();
let (_, tail) = next
.rsplit_once("then try: ")
.unwrap_or_else(|| panic!("next_action names a follow-up command; got: {next}"));
run(tail, &outer, true);
let codex_action = payload["agents"]
.as_array()
.and_then(|a| a.iter().find(|w| w["target"] == "codex"))
.map(|w| w["action"].as_str().unwrap_or_default().to_string())
.expect("codex wiring action");
let codex_cmd = codex_action.split('`').nth(1).unwrap_or_else(|| {
panic!("codex action carries a backticked command; got: {codex_action}")
});
let out = std::process::Command::new("sh")
.arg("-c")
.arg(format!("set -- {codex_cmd}; echo $#"))
.output()
.expect("spawn sh");
let argc: usize = String::from_utf8_lossy(&out.stdout).trim().parse().unwrap();
assert_eq!(
argc, 6,
"`{codex_cmd}` must parse as exactly `codex mcp add memstead -- <path>` (6 words); \
an unquoted path with a space would split into more",
);
for key in ["workspace_root", "config_path"] {
let v = payload[key].as_str().unwrap_or_default();
assert!(
Path::new(v).is_absolute(),
"`{key}` must be absolute for a machine consumer, got: {v}",
);
}
}
#[test]
fn create_relation_lands_edges_on_a_filesystem_mem_workspace() {
let tmp = TempDir::new().unwrap();
let root = tmp.path().join("edge-graph");
memstead()
.args(["quickstart", "--agent", "claude-code"])
.arg(&root)
.assert()
.success();
memstead()
.current_dir(&root)
.args([
"create",
"--title",
"Edge Source",
"--type",
"concept",
"--section",
"definition=A concept that points at the seed entity.",
"--section",
"explanation=Its only job is to carry one inline relation, so the edge is \
observable after creation.",
"--relation",
"CONTRASTS_WITH:edge-graph--welcome-to-memstead",
])
.assert()
.success();
let out = stdout_of(
memstead()
.current_dir(&root)
.args(["entity", "edge-graph--edge-source", "--include-relations"])
.assert()
.success(),
);
assert!(
out.contains("welcome-to-memstead"),
"the inline relation must be readable after creation; got:\n{out}",
);
}
#[test]
fn create_help_no_longer_claims_a_mem_repo_only_relation_limit() {
let out = stdout_of(memstead().args(["create", "--help"]).assert().success());
assert!(
!out.contains("Mem-repo workspaces only"),
"create --help must not claim a lifted restriction; got:\n{out}",
);
}
#[test]
fn help_lists_quickstart_and_schema_new() {
let out = stdout_of(memstead().arg("--help").assert().success());
assert!(
out.contains("quickstart"),
"top-level help lists quickstart; got: {out}"
);
let out = stdout_of(memstead().args(["schema", "--help"]).assert().success());
assert!(out.contains("new"), "schema help lists new; got: {out}");
}
#[test]
fn scaffold_package_is_exactly_two_files() {
let tmp = TempDir::new().unwrap();
memstead()
.current_dir(tmp.path())
.args(["schema", "new", "acme"])
.assert()
.success();
let mut files: Vec<String> = walk(tmp.path().join("acme").as_path());
files.sort();
assert_eq!(
files,
vec!["schema.yaml".to_string(), "types/note.yaml".to_string()]
);
}
fn walk(dir: &Path) -> Vec<String> {
let mut out = Vec::new();
for entry in std::fs::read_dir(dir).unwrap() {
let entry = entry.unwrap();
let name = entry.file_name().to_string_lossy().to_string();
if entry.path().is_dir() {
for sub in walk(&entry.path()) {
out.push(format!("{name}/{sub}"));
}
} else {
out.push(name);
}
}
out
}
fn fixture_repo(parent: &Path, name: &str) -> std::path::PathBuf {
let repo = parent.join(name);
std::fs::create_dir_all(repo.join("src")).unwrap();
std::fs::create_dir_all(repo.join("docs")).unwrap();
std::fs::write(repo.join("src/main.rs"), b"fn main() {}\n").unwrap();
std::fs::write(repo.join("README.md"), b"# The App\n\nWhat it does.\n").unwrap();
std::fs::write(repo.join("docs/design.md"), b"# Design\n\nHow it works.\n").unwrap();
for args in [
vec!["init", "-q", "."],
vec!["add", "-A"],
vec![
"-c",
"user.email=fixture@example.com",
"-c",
"user.name=Fixture",
"commit",
"-qm",
"initial",
],
] {
let status = std::process::Command::new("git")
.args(&args)
.current_dir(&repo)
.status()
.expect("git must be available");
assert!(status.success(), "git {args:?} failed");
}
repo
}
fn replay(dir: &Path, command: &str) -> std::process::Output {
std::process::Command::new("sh")
.arg("-c")
.arg(command)
.current_dir(dir)
.output()
.expect("shell must run")
}
#[test]
fn quickstart_repo_mode_binds_the_repo_without_adopting_its_files() {
let tmp = TempDir::new().unwrap();
let repo = fixture_repo(tmp.path(), "the-app");
let assert = memstead()
.current_dir(&repo)
.args(["quickstart", "--json", "--repo", "."])
.assert()
.success();
let payload: serde_json::Value = serde_json::from_str(&stdout_of(assert)).unwrap();
assert_eq!(
std::fs::canonicalize(payload["workspace_root"].as_str().unwrap()).unwrap(),
std::fs::canonicalize(&repo).unwrap(),
);
assert_eq!(payload["name"], "the-app");
assert_eq!(payload["mem_folder"], "the-app");
assert!(repo.join(".memstead").join("workspace.toml").is_file());
assert!(
repo.join("the-app")
.join(".memstead")
.join("config.json")
.is_file()
);
assert_eq!(payload["binding"]["id"], "the-app/the-app");
assert_eq!(payload["binding"]["pointer"], ".");
let record = repo.join(payload["binding"]["record"].as_str().unwrap());
assert!(record.is_file(), "binding record must exist at {record:?}");
let binding: serde_json::Value =
serde_json::from_slice(&std::fs::read(&record).unwrap()).unwrap();
assert_eq!(binding["version"], 2);
assert_eq!(binding["destination_mem"], "the-app");
assert_eq!(binding["sources"][0]["type"], "codebase");
let deny: Vec<String> = serde_json::from_value(binding["deny_paths"].clone()).unwrap();
assert_eq!(
deny,
vec![
"**/.DS_Store",
"**/.git/**",
"**/node_modules/**",
"**/Thumbs.db"
],
"the record materialises the scaffold deny defaults as deletable entries",
);
let mem_files: Vec<String> = std::fs::read_dir(repo.join("the-app"))
.unwrap()
.map(|e| e.unwrap().file_name().to_string_lossy().to_string())
.filter(|n| n.ends_with(".md"))
.collect();
assert_eq!(mem_files, vec!["welcome-to-memstead.md"]);
let overview = stdout_of(
memstead()
.current_dir(&repo)
.arg("overview")
.assert()
.success(),
);
assert!(
overview.contains("_entity_count: 1"),
"the repo's files must not be adopted; overview:\n{overview}",
);
let status = std::process::Command::new("git")
.args(["status", "--porcelain"])
.current_dir(&repo)
.output()
.unwrap();
let mut added: Vec<String> = String::from_utf8(status.stdout)
.unwrap()
.lines()
.map(|l| l.trim().to_string())
.collect();
added.sort();
assert_eq!(
added,
vec!["?? .mcp.json", "?? .memstead/", "?? the-app/"],
"nothing beyond the receipt's own artifacts may appear in the tree",
);
let receipt_names: Vec<String> = serde_json::from_value(payload["brief"].clone()).unwrap();
let written = receipt_names
.iter()
.find(|l| l.starts_with("Written into your repository"))
.expect("the brief names what it wrote into the repo");
for named in [".memstead/", "the-app/", ".mcp.json"] {
assert!(
written.contains(named),
"brief must name `{named}`: {written}"
);
}
}
#[test]
fn quickstart_without_repo_flag_still_refuses_a_populated_repo() {
let tmp = TempDir::new().unwrap();
let repo = fixture_repo(tmp.path(), "untouched-app");
let err = stderr_of(
memstead()
.args(["quickstart"])
.arg(&repo)
.assert()
.failure(),
);
assert!(
err.contains("TARGET_NOT_EMPTY") && err.contains("README.md"),
"the plain path must still refuse the populated repo; got:\n{err}",
);
assert!(
err.contains("silently adopt them into the graph"),
"the refusal must still name the adoption risk; got:\n{err}",
);
assert!(
!repo.join(".memstead").exists(),
"a refused quickstart writes nothing",
);
}
#[test]
fn quickstart_repo_receipt_commands_replay_verbatim() {
let tmp = TempDir::new().unwrap();
let repo = fixture_repo(tmp.path(), "replay-app");
let assert = memstead()
.current_dir(&repo)
.args(["quickstart", "--json", "--repo", "."])
.assert()
.success();
let payload: serde_json::Value = serde_json::from_str(&stdout_of(assert)).unwrap();
let mut replayed = 0;
for check in payload["verify_now"].as_array().unwrap() {
let command = check["command"].as_str().unwrap();
if command.contains("memstead-mcp") {
continue;
}
let out = replay(&repo, command);
assert!(
out.status.success(),
"printed command must run verbatim: {command}\n{}",
String::from_utf8_lossy(&out.stderr),
);
replayed += 1;
}
assert!(replayed >= 2, "the receipt must print runnable checks");
let brief: Vec<String> = serde_json::from_value(payload["brief"].clone()).unwrap();
let growth = brief
.iter()
.find(|l| l.starts_with("Growth:"))
.expect("the brief states how the mem grows");
let command = growth
.split_once("Start with: `")
.expect("the growth line names a runnable command")
.1
.split('`')
.next()
.expect("the command is backtick-delimited");
assert!(
!command.contains('<') && !command.contains("..."),
"no printed command may carry a placeholder: {command}",
);
let out = replay(&repo, command);
assert!(
out.status.success(),
"the ingest brief must render: {command}\n{}",
String::from_utf8_lossy(&out.stderr),
);
}
#[test]
fn quickstart_repo_outside_the_workspace_warns_with_the_relocation_recipe() {
let tmp = TempDir::new().unwrap();
let repo = fixture_repo(tmp.path(), "beside-app");
let assert = memstead()
.current_dir(tmp.path())
.args(["quickstart", "--json", "graph", "--repo"])
.arg(&repo)
.assert()
.success();
let payload: serde_json::Value = serde_json::from_str(&stdout_of(assert)).unwrap();
assert_eq!(payload["binding"]["pointer"], "../beside-app");
assert!(payload["mem_folder"].as_str().unwrap() == ".");
let warnings: Vec<String> = serde_json::from_value(payload["warnings"].clone()).unwrap();
let layout = warnings
.iter()
.find(|w| w.contains("resolves outside the workspace root"))
.expect("the out-of-root layout must be named");
assert!(
layout.contains("root the workspace at the common parent"),
"the warning must carry the relocation recipe: {layout}",
);
assert!(
layout.contains("supported"),
"the shape is supported, not refused: {layout}",
);
let status = std::process::Command::new("git")
.args(["status", "--porcelain"])
.current_dir(&repo)
.output()
.unwrap();
assert!(
String::from_utf8(status.stdout).unwrap().trim().is_empty(),
"a workspace beside the repo writes nothing into it",
);
}
#[test]
fn quickstart_repo_mem_folder_collision_refuses_with_the_name_remedy() {
let tmp = TempDir::new().unwrap();
let repo = fixture_repo(tmp.path(), "collide-app");
std::fs::create_dir_all(repo.join("collide-app")).unwrap();
std::fs::write(repo.join("collide-app").join("notes.md"), b"# mine\n").unwrap();
let err = stderr_of(
memstead()
.current_dir(&repo)
.args(["quickstart", "--repo", "."])
.assert()
.failure(),
);
assert!(
err.contains("TARGET_NOT_EMPTY") && err.contains("--name"),
"the collision must refuse with the --name remedy; got:\n{err}",
);
assert!(
!repo.join(".memstead").exists(),
"a refused guided quickstart writes nothing",
);
assert_eq!(
std::fs::read(repo.join("collide-app").join("notes.md")).unwrap(),
b"# mine\n",
"the folder it refused to take is untouched",
);
}
#[test]
fn quickstart_repo_must_already_exist() {
let tmp = TempDir::new().unwrap();
let err = stderr_of(
memstead()
.current_dir(tmp.path())
.args(["quickstart", "graph", "--repo", "./typo"])
.assert()
.failure(),
);
assert!(
err.contains("INVALID_INPUT") && err.contains("--repo"),
"a missing repo must refuse naming the flag; got:\n{err}",
);
assert!(!tmp.path().join("graph").exists(), "nothing is created");
}
#[test]
fn quickstart_repo_scaffold_does_not_widen_the_dead_deny_exemption() {
let tmp = TempDir::new().unwrap();
let repo = fixture_repo(tmp.path(), "lint-app");
memstead()
.current_dir(&repo)
.args(["quickstart", "--json", "--repo", "."])
.assert()
.success();
let record = repo.join(".memstead/projections/lint-app/lint-app.json");
let mut binding: serde_json::Value =
serde_json::from_slice(&std::fs::read(&record).unwrap()).unwrap();
binding["deny_paths"]
.as_array_mut()
.unwrap()
.push(serde_json::json!("nowhere-near/**"));
std::fs::write(&record, serde_json::to_vec_pretty(&binding).unwrap()).unwrap();
let brief = stdout_of(
memstead()
.current_dir(&repo)
.args(["projection", "brief", "lint-app/lint-app"])
.assert()
.success(),
);
assert!(
brief.contains("nowhere-near/**"),
"a user-authored dead deny entry must still be reported; got:\n{brief}",
);
for scaffolded in ["**/node_modules/**", "**/Thumbs.db"] {
assert!(
!brief.contains(&format!("`{scaffolded}`")),
"the scaffold's own default must stay silent: {scaffolded}\n{brief}",
);
}
}
#[test]
fn quickstart_repo_workspace_inside_the_repo_keeps_the_mem_out_of_its_own_scope() {
let tmp = TempDir::new().unwrap();
let repo = fixture_repo(tmp.path(), "inside-app");
let assert = memstead()
.current_dir(&repo)
.args(["quickstart", "--json", "memgraph", "--repo", "."])
.assert()
.success();
let payload: serde_json::Value = serde_json::from_str(&stdout_of(assert)).unwrap();
assert_eq!(payload["mem_folder"], "memgraph");
assert_eq!(payload["binding"]["pointer"], "..");
let workspace = repo.join("memgraph");
let measured = memstead()
.current_dir(&workspace)
.args([
"projection",
"verify",
payload["binding"]["id"].as_str().unwrap(),
"--full",
"--include",
"uncovered_artifacts",
])
.assert()
.success();
let report = stdout_of(measured);
assert!(
!report.contains("welcome-to-memstead.md"),
"the mem's own entity must not enumerate as a source artifact; report:\n{report}",
);
for repo_file in ["../README.md", "../src/main.rs"] {
assert!(
report.contains(repo_file),
"the repository's files are the denominator; missing {repo_file}:\n{report}",
);
}
let brief: Vec<String> = serde_json::from_value(payload["brief"].clone()).unwrap();
let scope = brief
.iter()
.find(|l| l.starts_with("Scope:"))
.expect("the brief states the scope");
assert!(
scope.contains("the mem's own folder `memgraph/`"),
"the scope claim must name the excluded folder: {scope}",
);
}
#[test]
fn quickstart_repo_nested_workspace_names_what_the_repo_actually_gained() {
let tmp = TempDir::new().unwrap();
let repo = fixture_repo(tmp.path(), "nested-app");
let assert = memstead()
.current_dir(&repo)
.args(["quickstart", "--json", "wsdir", "--repo", "."])
.assert()
.success();
let payload: serde_json::Value = serde_json::from_str(&stdout_of(assert)).unwrap();
let brief: Vec<String> = serde_json::from_value(payload["brief"].clone()).unwrap();
let written = brief
.iter()
.find(|l| l.starts_with("Written into your repository"))
.expect("the brief states what the repository gained");
assert!(
written.contains("`wsdir/`"),
"the one new path must be named: {written}",
);
for workspace_relative in ["`.memstead/`", "`.mcp.json`"] {
assert!(
!written.contains(workspace_relative),
"a workspace-relative path must not be presented as repo-relative: {written}",
);
}
let status = std::process::Command::new("git")
.args(["status", "--porcelain"])
.current_dir(&repo)
.output()
.unwrap();
assert_eq!(
String::from_utf8(status.stdout).unwrap().trim(),
"?? wsdir/",
"the receipt's claim and the tracked tree must agree",
);
}
#[test]
fn quickstart_repo_names_a_modified_wiring_file_as_modified() {
let tmp = TempDir::new().unwrap();
let repo = fixture_repo(tmp.path(), "wired-app");
std::fs::write(
repo.join(".mcp.json"),
b"{\n \"mcpServers\": {\n \"other\": { \"command\": \"/bin/other\" }\n }\n}\n",
)
.unwrap();
for args in [
vec!["add", "-A"],
vec![
"-c",
"user.email=fixture@example.com",
"-c",
"user.name=Fixture",
"commit",
"-qm",
"wiring",
],
] {
std::process::Command::new("git")
.args(&args)
.current_dir(&repo)
.status()
.unwrap();
}
let assert = memstead()
.current_dir(&repo)
.args(["quickstart", "--json", "--repo", "."])
.assert()
.success();
let payload: serde_json::Value = serde_json::from_str(&stdout_of(assert)).unwrap();
let brief: Vec<String> = serde_json::from_value(payload["brief"].clone()).unwrap();
let written = brief
.iter()
.find(|l| l.starts_with("Written into your repository"))
.expect("the brief states what the repository gained");
assert!(
written.contains("`.mcp.json` (agent wiring added to it)"),
"a pre-existing file is modified, not added: {written}",
);
let status = std::process::Command::new("git")
.args(["status", "--porcelain"])
.current_dir(&repo)
.output()
.unwrap();
let out = String::from_utf8(status.stdout).unwrap();
assert!(
out.contains("M .mcp.json"),
"git must agree the file was modified: {out}",
);
}
#[test]
fn quickstart_repo_at_the_common_parent_is_reachable() {
let tmp = TempDir::new().unwrap();
let parent = tmp.path().join("parent");
std::fs::create_dir_all(&parent).unwrap();
fixture_repo(&parent, "repo-one");
let assert = memstead()
.current_dir(tmp.path())
.args([
"quickstart",
"--json",
"parent",
"--repo",
"parent/repo-one",
"--name",
"parent-ws",
])
.assert()
.success();
let payload: serde_json::Value = serde_json::from_str(&stdout_of(assert)).unwrap();
assert_eq!(payload["binding"]["pointer"], "repo-one");
let warnings: Vec<String> = serde_json::from_value(payload["warnings"].clone()).unwrap();
assert!(
!warnings.iter().any(|w| w.contains("resolves outside")),
"the recommended layout must not warn about itself: {warnings:?}",
);
let brief: Vec<String> = serde_json::from_value(payload["brief"].clone()).unwrap();
assert!(
!brief
.iter()
.any(|l| l.starts_with("Written into your repository")),
"no repository claim belongs here: {brief:?}",
);
}
#[test]
fn quickstart_repo_json_config_path_names_a_file_that_exists() {
let tmp = TempDir::new().unwrap();
let repo = fixture_repo(tmp.path(), "config-app");
for args in [
vec!["quickstart", "--json", "--repo", "."],
vec!["quickstart", "--json", "sub-ws", "--repo", "."],
] {
let fresh = fixture_repo(tmp.path(), &format!("c{}", args.len()));
let assert = memstead()
.current_dir(&fresh)
.args(&args)
.assert()
.success();
let payload: serde_json::Value = serde_json::from_str(&stdout_of(assert)).unwrap();
let config = std::path::PathBuf::from(payload["config_path"].as_str().unwrap());
assert!(
config.is_file(),
"config_path must name a file that exists ({args:?}): {config:?}",
);
let parsed: serde_json::Value =
serde_json::from_slice(&std::fs::read(&config).unwrap()).unwrap();
assert_eq!(parsed["schema"], payload["schema"]);
}
drop(repo);
}
#[test]
fn quickstart_repo_record_path_resolves_from_the_readers_cwd() {
let tmp = TempDir::new().unwrap();
let repo = fixture_repo(tmp.path(), "record-app");
let out = stdout_of(
memstead()
.current_dir(&repo)
.args([
"quickstart",
"--agent",
"claude-code",
"sub-ws",
"--repo",
".",
])
.assert()
.success(),
);
let edit_line = out
.lines()
.find(|l| l.contains("yours to edit"))
.expect("the brief names the record to edit");
let path = edit_line
.rsplit_once("yours to edit: `")
.expect("the line ends in a backticked path")
.1
.trim_end_matches('`');
assert!(
repo.join(path).is_file(),
"the record path must resolve from the reader's cwd: {path}",
);
}
#[test]
fn quickstart_repo_printed_mem_folder_resolves_from_the_readers_cwd() {
let tmp = TempDir::new().unwrap();
let repo = fixture_repo(tmp.path(), "framed-app");
let out = stdout_of(
memstead()
.current_dir(&repo)
.args([
"quickstart",
"--agent",
"claude-code",
"sub-ws",
"--repo",
".",
"--name",
"kb",
])
.assert()
.success(),
);
let folder_line = out
.lines()
.find(|l| l.starts_with("- Mem folder:"))
.expect("the receipt names the mem folder");
let printed = folder_line
.split('`')
.nth(1)
.expect("the folder is backticked")
.trim_end_matches('/');
assert!(
repo.join(printed).is_dir(),
"the printed mem folder must resolve from the reader's cwd: {printed}",
);
assert!(
repo.join(printed).join("welcome-to-memstead.md").is_file(),
"…and be the folder that actually holds the entities: {printed}",
);
assert!(
out.contains(&format!("plain `.md` files in `{printed}/`")),
"the disclosure must name the same resolvable folder; got:\n{out}",
);
}
#[test]
fn quickstart_repo_name_refusal_retry_keeps_the_repo_flag() {
let tmp = TempDir::new().unwrap();
let repo = fixture_repo(tmp.path(), "日本語");
let err = stderr_of(
memstead()
.current_dir(&repo)
.args(["quickstart", "--repo", "."])
.assert()
.failure(),
);
let retry = err
.split("pass one explicitly: ")
.nth(1)
.expect("the refusal carries a retry command")
.trim();
assert!(
retry.contains("--repo"),
"the retry must reproduce the guided invocation: {retry}",
);
let out = replay(&repo, retry);
assert!(
out.status.success(),
"the printed retry must run verbatim: {retry}\n{}",
String::from_utf8_lossy(&out.stderr),
);
}
#[test]
fn quickstart_repo_non_git_directory_claims_no_history() {
let tmp = TempDir::new().unwrap();
let plain = tmp.path().join("plain-dir");
std::fs::create_dir_all(plain.join("src")).unwrap();
std::fs::write(plain.join("src/a.rs"), b"fn a() {}\n").unwrap();
let ws = tmp.path().join("ws");
let assert = memstead()
.current_dir(tmp.path())
.args(["quickstart", "--json"])
.arg(&ws)
.arg("--repo")
.arg(&plain)
.assert()
.success();
let payload: serde_json::Value = serde_json::from_str(&stdout_of(assert)).unwrap();
let brief: Vec<String> = serde_json::from_value(payload["brief"].clone()).unwrap();
let not_yet = brief
.iter()
.find(|l| l.starts_with("Not yet:"))
.expect("the brief states what the mem does not hold");
assert!(
not_yet.contains("Its files are"),
"a non-git directory has files, not history: {not_yet}",
);
}
#[test]
fn quickstart_repo_printed_wiring_paths_resolve_from_the_readers_cwd() {
let tmp = TempDir::new().unwrap();
let repo = fixture_repo(tmp.path(), "wiring-app");
let out = stdout_of(
memstead()
.current_dir(&repo)
.args([
"quickstart",
"sub-ws",
"--repo",
".",
"--agent",
"claude-code",
"--agent",
"cursor",
"--agent",
"gemini",
])
.assert()
.success(),
);
let mut checked = 0;
for line in out.lines().filter(|l| l.contains("(server `memstead`)")) {
let printed = line
.split('`')
.nth(1)
.expect("the config file is backticked");
assert!(
repo.join(printed).is_file(),
"the printed wiring path must resolve from the reader's cwd: {printed}\n{out}",
);
checked += 1;
}
assert_eq!(
checked, 3,
"all three file-writing targets are named:\n{out}"
);
}
#[test]
fn quickstart_repo_json_paths_resolve_against_the_workspace_root() {
let tmp = TempDir::new().unwrap();
let repo = fixture_repo(tmp.path(), "frames-app");
let assert = memstead()
.current_dir(&repo)
.args(["quickstart", "--json", "sub-ws", "--repo", "."])
.assert()
.success();
let payload: serde_json::Value = serde_json::from_str(&stdout_of(assert)).unwrap();
let root = std::path::PathBuf::from(payload["workspace_root"].as_str().unwrap());
assert!(
root.join(payload["binding"]["record"].as_str().unwrap())
.is_file(),
"binding.record resolves against workspace_root",
);
assert!(
root.join(payload["mem_folder"].as_str().unwrap()).is_dir(),
"mem_folder resolves against workspace_root",
);
assert!(
root.join(
payload["agents"][0]["action"]
.as_str()
.unwrap()
.split('`')
.nth(1)
.unwrap()
)
.is_file(),
"the agent action's path resolves against workspace_root",
);
let brief: Vec<String> = serde_json::from_value(payload["brief"].clone()).unwrap();
let edit = brief
.iter()
.find(|l| l.contains("yours to edit"))
.expect("the brief names the record");
let path = edit
.rsplit_once("yours to edit: `")
.unwrap()
.1
.trim_end_matches('`');
assert!(
root.join(path).is_file(),
"the JSON brief's paths resolve against workspace_root too: {path}",
);
}
#[test]
fn quickstart_repo_every_printed_path_resolves_from_a_third_directory() {
let tmp = TempDir::new().unwrap();
let parent = tmp.path().join("parent");
std::fs::create_dir_all(&parent).unwrap();
let repo = fixture_repo(&parent, "third-app");
let elsewhere = tmp.path().join("elsewhere");
std::fs::create_dir_all(&elsewhere).unwrap();
let out = stdout_of(
memstead()
.current_dir(&elsewhere)
.args(["quickstart", "--agent", "claude-code"])
.arg("../parent/third-app/ws")
.arg("--repo")
.arg("../parent/third-app")
.arg("--name")
.arg("kb")
.assert()
.success(),
);
let printed = |prefix: &str| -> String {
let line = out
.lines()
.find(|l| l.starts_with(prefix))
.unwrap_or_else(|| panic!("receipt has no `{prefix}` line:\n{out}"));
line.split('`')
.nth(if prefix.starts_with("- Binding") {
3
} else {
1
})
.unwrap_or_else(|| panic!("no backticked path in: {line}"))
.trim_end_matches('/')
.to_string()
};
let mem = elsewhere.join(printed("- Mem folder:"));
assert!(
mem.join("welcome-to-memstead.md").is_file(),
"mem folder must resolve and hold the seed: {mem:?}",
);
let pointer = elsewhere.join(printed("- Binding:"));
assert!(
pointer.join("README.md").is_file() && pointer.join(".git").exists(),
"the pointer must resolve to the repository: {pointer:?}",
);
assert_eq!(
pointer.canonicalize().unwrap(),
repo.canonicalize().unwrap(),
"…and to THAT repository, not another directory at the same path",
);
let wiring = out
.lines()
.find(|l| l.contains("(server `memstead`)"))
.expect("the receipt names the wiring file");
let wiring = elsewhere.join(wiring.split('`').nth(1).unwrap());
assert!(wiring.is_file(), "wiring path must resolve: {wiring:?}");
let edit = out
.lines()
.find(|l| l.contains("yours to edit"))
.expect("the brief names the record");
let record = elsewhere.join(
edit.rsplit_once("yours to edit: `")
.unwrap()
.1
.trim_end_matches('`'),
);
assert!(record.is_file(), "record path must resolve: {record:?}");
}
#[test]
fn quickstart_repo_outside_layout_claims_only_engine_state() {
let tmp = TempDir::new().unwrap();
let repo = fixture_repo(tmp.path(), "outside-app");
let assert = memstead()
.current_dir(tmp.path())
.args(["quickstart", "--json", "graph", "--repo"])
.arg(&repo)
.assert()
.success();
let payload: serde_json::Value = serde_json::from_str(&stdout_of(assert)).unwrap();
assert_eq!(payload["mem_folder"], ".");
let brief: Vec<String> = serde_json::from_value(payload["brief"].clone()).unwrap();
let scope = brief
.iter()
.find(|l| l.starts_with("Scope:"))
.expect("the brief states the scope");
assert!(
scope.contains("engine state (`.memstead/`)") && !scope.contains("mem's own folder"),
"the collapsed layout claims only what it excludes: {scope}",
);
}
#[test]
fn quickstart_repo_receipt_discloses_the_shape_and_the_mem_folder() {
let tmp = TempDir::new().unwrap();
let repo = fixture_repo(tmp.path(), "disclose-app");
let out = stdout_of(
memstead()
.current_dir(&repo)
.args(["quickstart", "--agent", "claude-code", "--repo", "."])
.assert()
.success(),
);
assert_filesystem_shape_disclosure(&out, "guided quickstart receipt");
assert!(
out.contains("plain `.md` files in `disclose-app/`"),
"the disclosure must point at the mem's actual folder; got:\n{out}",
);
assert!(
!out.contains("plain `.md` files in this folder"),
"the collapsed-shape wording would be untrue here; got:\n{out}",
);
}
#[test]
fn llms_txt_export_works_on_a_filesystem_mem() {
let tmp = TempDir::new().unwrap();
let root = tmp.path().join("fs-graph");
memstead()
.args(["quickstart", "--json"])
.arg(&root)
.assert()
.success();
let doc = stdout_of(
memstead()
.current_dir(&root)
.args(["export", "--format", "llms-txt"])
.assert()
.success(),
);
assert!(doc.starts_with("# fs-graph"), "header names the mem: {doc}");
for field in [
"Mem: ",
"Subject: ",
"Schema: ",
"Entities: ",
"Provenance: ",
] {
assert!(doc.contains(field), "header carries `{field}`: {doc}");
}
assert!(
doc.contains("_Type: "),
"the seed entity carries a visible type line: {doc}"
);
assert!(doc.contains("\n---\n"), "entities are separated: {doc}");
assert!(!doc.contains("[["), "no raw wiki-link syntax: {doc}");
assert!(
!doc.contains("this deployment vouches"),
"a CLI export claims no deployment provenance: {doc}"
);
}