#![cfg(feature = "mem-repo")]
use std::fs;
use std::path::Path;
use std::process::Command as StdCommand;
use assert_cmd::Command;
use memstead_git_branch::test_support::init_real_mem_repo_from_disk;
use tempfile::TempDir;
fn memstead() -> Command {
Command::cargo_bin("memstead").expect("memstead binary must be built by cargo")
}
fn write_mem_dir(root: &Path, name: &str) {
let dir = root.join(name);
let store = dir.join(".memstead");
fs::create_dir_all(&store).unwrap();
fs::write(store.join("config.json"), r#"{"schema": "default@1.0.0"}"#).unwrap();
fs::write(
dir.join("alpha.md"),
"---\ntype: spec\ncreated_date: 2026-01-01\nlast_modified: 2026-01-01\nlevel: M0\n---\n# Alpha\n\n## Identity\n\nThe alpha entity.\n\n## Purpose\n\nSeed.\n",
)
.unwrap();
}
fn seed() -> (TempDir, TempDir) {
let ws = TempDir::new().unwrap();
for m in ["alpha", "beta", "gamma"] {
write_mem_dir(ws.path(), m);
}
let dirs: Vec<_> = ["alpha", "beta", "gamma"]
.iter()
.map(|m| (ws.path().join(m), *m))
.collect();
let refs: Vec<(&Path, &str)> = dirs.iter().map(|(p, m)| (p.as_path(), *m)).collect();
init_real_mem_repo_from_disk(ws.path(), &refs);
let remote = TempDir::new().unwrap();
let status = StdCommand::new("git")
.args(["init", "-q", "--bare"])
.arg(remote.path())
.status()
.unwrap();
assert!(status.success());
memstead()
.current_dir(ws.path())
.args(["mem-repo", "remote-add", "origin", "--quiet"])
.arg(remote.path())
.assert()
.success();
(ws, remote)
}
fn create_entity(ws: &Path, mem: &str, title: &str) {
memstead()
.current_dir(ws)
.args([
"create",
"--quiet",
"--mem",
mem,
"--title",
title,
"--type",
"spec",
"--metadata",
"level=M0",
"--section",
"identity=x",
"--section",
"purpose=y",
])
.assert()
.success();
}
fn push_all(ws: &Path) -> std::process::Output {
memstead()
.current_dir(ws)
.args(["push", "--all", "--quiet"])
.output()
.unwrap()
}
fn moved_refs(stdout: &[u8]) -> Vec<String> {
String::from_utf8_lossy(stdout)
.lines()
.map(|l| l.split(' ').next().unwrap_or_default().to_string())
.collect()
}
#[test]
fn push_all_moves_exactly_the_lagging_refs_and_is_silent_when_in_sync() {
let (ws, _remote) = seed();
let out = push_all(ws.path());
assert!(
out.status.success(),
"{}",
String::from_utf8_lossy(&out.stderr)
);
assert_eq!(
moved_refs(&out.stdout),
vec![
"refs/heads/__MEMSTEAD",
"refs/heads/alpha",
"refs/heads/beta",
"refs/heads/gamma"
]
);
let out = push_all(ws.path());
assert!(out.status.success());
assert!(
out.stdout.is_empty(),
"{}",
String::from_utf8_lossy(&out.stdout)
);
create_entity(ws.path(), "alpha", "Second alpha");
create_entity(ws.path(), "beta", "Second beta");
memstead()
.current_dir(ws.path())
.args(["mem", "set-description", "alpha", "moved", "--quiet"])
.assert()
.success();
let out = push_all(ws.path());
assert!(
out.status.success(),
"{}",
String::from_utf8_lossy(&out.stderr)
);
assert_eq!(
moved_refs(&out.stdout),
vec![
"refs/heads/__MEMSTEAD",
"refs/heads/alpha",
"refs/heads/beta"
]
);
for line in String::from_utf8_lossy(&out.stdout).lines() {
let parts: Vec<&str> = line.split(' ').collect();
assert_eq!(parts.len(), 4, "line = {line}");
assert_eq!(parts[2], "->");
assert_ne!(parts[1], "<new>", "the ref existed on the remote already");
}
let out = push_all(ws.path());
assert!(out.status.success());
assert!(out.stdout.is_empty());
}
#[test]
fn push_all_refuses_a_non_fast_forward_ref_and_still_pushes_the_rest() {
let (ws, remote) = seed();
assert!(push_all(ws.path()).status.success());
let other = TempDir::new().unwrap();
let status = StdCommand::new("git")
.args(["clone", "-q", "--branch", "beta"])
.arg(remote.path())
.arg(other.path())
.status()
.unwrap();
assert!(status.success());
for args in [
vec![
"-c",
"user.email=t@t",
"-c",
"user.name=t",
"commit",
"-q",
"--allow-empty",
"-m",
"remote-only",
],
vec!["push", "-q", "origin", "beta"],
] {
let status = StdCommand::new("git")
.arg("-C")
.arg(other.path())
.args(&args)
.status()
.unwrap();
assert!(status.success(), "git {args:?}");
}
create_entity(ws.path(), "beta", "Local beta");
create_entity(ws.path(), "gamma", "Local gamma");
let out = push_all(ws.path());
assert_eq!(
out.status.code(),
Some(5),
"{}",
String::from_utf8_lossy(&out.stderr)
);
let moved = moved_refs(&out.stdout);
assert!(moved.contains(&"refs/heads/gamma".to_string()), "{moved:?}");
assert!(!moved.contains(&"refs/heads/beta".to_string()), "{moved:?}");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("NON_FAST_FORWARD"), "{stderr}");
assert!(stderr.contains("refs/heads/beta"), "{stderr}");
let out = memstead()
.current_dir(ws.path())
.args(["push", "--all", "--quiet", "--json"])
.output()
.unwrap();
assert_eq!(out.status.code(), Some(5));
let text = String::from_utf8_lossy(&out.stdout);
let envelope: serde_json::Value = serde_json::from_str(text.trim()).expect("one JSON document");
assert_eq!(envelope["code"], "NON_FAST_FORWARD");
assert_eq!(
envelope["details"]["refused"][0]["ref_name"],
"refs/heads/beta"
);
assert_eq!(envelope["details"]["refused"][0]["mem"], "beta");
memstead()
.current_dir(ws.path())
.args(["push", "--all", "--force", "--quiet"])
.assert()
.code(2)
.stderr(predicates::str::contains("cannot be used with"));
}