use std::io::Write;
use std::process::{Command, Stdio};
use std::sync::{Mutex, MutexGuard, OnceLock};
use tempfile::TempDir;
const MX: &str = env!("CARGO_BIN_EXE_mx");
fn test_lock() -> &'static Mutex<()> {
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
LOCK.get_or_init(|| Mutex::new(()))
}
struct Env {
dir: TempDir,
_guard: MutexGuard<'static, ()>,
}
fn setup() -> Env {
let guard = test_lock().lock().unwrap_or_else(|e| e.into_inner());
Env {
dir: TempDir::new().unwrap(),
_guard: guard,
}
}
fn isolate(cmd: &mut Command, home: &std::path::Path) {
cmd.env("MX_HOME", home)
.env("MX_CURRENT_AGENT", "test-agent")
.env("MX_SURREAL_MODE", "embedded")
.env_remove("MX_SURREAL_URL")
.env_remove("MX_SURREAL_NS")
.env_remove("MX_SURREAL_DB")
.env_remove("MX_SURREAL_USER")
.env_remove("MX_SURREAL_AUTH_LEVEL")
.env_remove("MX_SURREAL_ROOT");
}
fn mx(env: &Env, args: &[&str]) -> std::process::Output {
let mut cmd = Command::new(MX);
cmd.args(args);
isolate(&mut cmd, env.dir.path());
cmd.output().expect("failed to run mx")
}
fn mx_stdin(env: &Env, args: &[&str], stdin: &str) -> std::process::Output {
let mut cmd = Command::new(MX);
cmd.args(args);
isolate(&mut cmd, env.dir.path());
let mut child = cmd
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("failed to spawn mx");
child
.stdin
.as_mut()
.unwrap()
.write_all(stdin.as_bytes())
.unwrap();
drop(child.stdin.take());
child.wait_with_output().expect("failed to wait on mx")
}
fn stdout(out: &std::process::Output) -> String {
String::from_utf8_lossy(&out.stdout).to_string()
}
fn extract_json(text: &str) -> serde_json::Value {
let start = text
.find("{\n")
.unwrap_or_else(|| panic!("no JSON object found in stdout: {text:?}"));
serde_json::from_str(text[start..].trim())
.unwrap_or_else(|e| panic!("failed to parse extracted JSON ({e}): {:?}", &text[start..]))
}
fn stderr(out: &std::process::Output) -> String {
String::from_utf8_lossy(&out.stderr).to_string()
}
fn extract_json_array(text: &str) -> Vec<serde_json::Value> {
serde_json::from_str::<serde_json::Value>(text.trim())
.unwrap_or_else(|e| panic!("failed to parse JSON array ({e}): {text:?}"))
.as_array()
.unwrap_or_else(|| panic!("expected a JSON array: {text:?}"))
.clone()
}
#[test]
fn standard_path_recased_duplicate_is_skipped_exactly_one_entry() {
let env = setup();
let first = mx(
&env,
&[
"memory",
"add",
"--category",
"insight",
"--title",
"The External Plan",
"--content",
"Ship it, and move on.",
"--session-id",
"sess-1",
"--json",
"--no-embed",
"--no-auto-anchor",
],
);
assert!(
first.status.success(),
"first add must succeed: {}",
stderr(&first)
);
let first_json = extract_json(&stdout(&first));
let first_id = first_json["id"].as_str().unwrap().to_string();
let second = mx(
&env,
&[
"memory",
"add",
"--category",
"insight",
"--title",
"the external plan",
"--content",
"ship it and move on",
"--session-id",
"sess-1",
"--json",
"--no-embed",
"--no-auto-anchor",
],
);
assert!(
second.status.success(),
"a duplicate-skip must still exit 0: {}",
stderr(&second)
);
let second_json: serde_json::Value = serde_json::from_str(&stdout(&second)).unwrap();
assert_eq!(second_json["skipped"], serde_json::json!(true));
assert_eq!(
second_json["status"],
serde_json::json!("already_persisted")
);
assert_eq!(second_json["duplicate_of"], serde_json::json!(first_id));
assert!(
serde_json::from_str::<serde_json::Value>(stdout(&second).trim()).is_ok(),
"skip --json stdout must be pure JSON, got: {:?}",
stdout(&second)
);
let list = mx(&env, &["memory", "list", "--category", "insight", "--json"]);
assert!(list.status.success(), "list failed: {}", stderr(&list));
let entries = extract_json_array(&stdout(&list));
assert_eq!(
entries.len(),
1,
"exactly one entry should exist after a recased re-add via the standard path: {:?}",
entries
);
}
#[test]
fn evolved_re_add_different_title_and_body_produces_two_entries() {
let env = setup();
let first = mx(
&env,
&[
"memory",
"add",
"--category",
"insight",
"--title",
"Plan A",
"--content",
"Ship it and move on.",
"--session-id",
"sess-1",
"--json",
"--no-embed",
"--no-auto-anchor",
],
);
assert!(first.status.success());
let second = mx(
&env,
&[
"memory",
"add",
"--category",
"insight",
"--title",
"Plan B",
"--content",
"Hold off and reassess next week.",
"--session-id",
"sess-1",
"--json",
"--no-embed",
"--no-auto-anchor",
],
);
assert!(second.status.success());
let second_json = extract_json(&stdout(&second));
assert!(
second_json.get("skipped").is_none(),
"a genuinely different entry must not be skipped: {:?}",
second_json
);
}
#[test]
fn allow_duplicate_forces_the_second_write_through() {
let env = setup();
let first = mx(
&env,
&[
"memory",
"add",
"--category",
"insight",
"--title",
"Repeated Note",
"--content",
"Same content twice, on purpose.",
"--session-id",
"sess-1",
"--json",
"--no-embed",
"--no-auto-anchor",
],
);
assert!(first.status.success());
let second = mx(
&env,
&[
"memory",
"add",
"--category",
"insight",
"--title",
"Repeated Note",
"--content",
"Same content twice, on purpose.",
"--session-id",
"sess-1",
"--json",
"--no-embed",
"--no-auto-anchor",
"--allow-duplicate",
],
);
assert!(second.status.success());
let second_json = extract_json(&stdout(&second));
assert!(
second_json.get("skipped").is_none(),
"--allow-duplicate must force the write through: {:?}",
second_json
);
}
#[test]
fn session_id_none_writes_through_with_no_dedup() {
let env = setup();
for _ in 0..2 {
let out = mx(
&env,
&[
"memory",
"add",
"--category",
"insight",
"--title",
"No Session Note",
"--content",
"Written without a session id.",
"--json",
"--no-embed",
"--no-auto-anchor",
],
);
assert!(out.status.success());
let json = extract_json(&stdout(&out));
assert!(
json.get("skipped").is_none(),
"session_id=None must bypass dedup entirely (write through): {:?}",
json
);
assert_eq!(
json["dedup"],
serde_json::json!("bypassed_no_session"),
"json payload must surface the bypass signal"
);
assert!(
!stderr(&out).contains("dedup bypassed"),
"--json mode must not ALSO print the plain-mode stderr bypass note: {}",
stderr(&out)
);
}
}
#[test]
fn session_id_none_plain_mode_prints_bypass_note_on_stderr() {
let env = setup();
let out = mx(
&env,
&[
"memory",
"add",
"--category",
"insight",
"--title",
"No Session Note Plain",
"--content",
"Written without a session id, plain mode.",
"--no-embed",
"--no-auto-anchor",
],
);
assert!(out.status.success());
assert!(
stderr(&out).contains("dedup bypassed"),
"plain mode must still print the stderr bypass note: {}",
stderr(&out)
);
}
#[test]
fn skip_json_payload_carries_the_duplicate_id_under_the_id_key() {
let env = setup();
let first = mx(
&env,
&[
"memory",
"add",
"--category",
"insight",
"--title",
"Has An Id",
"--content",
"Content for the id-key regression test.",
"--session-id",
"sess-1",
"--json",
"--no-embed",
"--no-auto-anchor",
],
);
assert!(first.status.success());
let first_id = extract_json(&stdout(&first))["id"]
.as_str()
.unwrap()
.to_string();
let second = mx(
&env,
&[
"memory",
"add",
"--category",
"insight",
"--title",
"has an id",
"--content",
"content for the id-key regression test",
"--session-id",
"sess-1",
"--json",
"--no-embed",
"--no-auto-anchor",
],
);
assert!(second.status.success());
let second_json: serde_json::Value = serde_json::from_str(&stdout(&second)).unwrap();
assert_eq!(
second_json["id"],
serde_json::json!(first_id),
"skip payload's 'id' key must carry the duplicate's id, not be absent: {second_json:?}"
);
assert_eq!(second_json["duplicate_of"], serde_json::json!(first_id));
}
#[test]
fn fact_type_batch_path_idempotent_across_reruns() {
let env = setup();
let batch = "{\"type\": \"insight\", \"content\": \"Ship it, and move on.\", \"session\": \"sess-1\"}\n\
{\"type\": \"insight\", \"content\": \"ship it and move on\", \"session\": \"sess-1\"}\n";
let first = mx_stdin(&env, &["memory", "add-batch", "--no-embed"], batch);
assert!(
first.status.success(),
"batch with only a duplicate-skip (no hard failures) must still exit 0: {}",
stderr(&first)
);
let first_out = stdout(&first);
assert!(
first_out
.contains("Batch complete: 1 added, 1 already saved (no action needed), 0 failed."),
"first run must add exactly one entry and skip exactly one: {first_out}"
);
let list = mx(&env, &["memory", "list", "--category", "insight", "--json"]);
assert!(list.status.success(), "list failed: {}", stderr(&list));
assert_eq!(
extract_json_array(&stdout(&list)).len(),
1,
"exactly one entry should exist after the first batch run"
);
let second = mx_stdin(&env, &["memory", "add-batch", "--no-embed"], batch);
assert!(second.status.success());
let second_out = stdout(&second);
assert!(
second_out
.contains("Batch complete: 0 added, 2 already saved (no action needed), 0 failed."),
"re-running the identical batch must add zero new entries and skip both lines: {second_out}"
);
let list_again = mx(&env, &["memory", "list", "--category", "insight", "--json"]);
assert!(list_again.status.success());
assert_eq!(
extract_json_array(&stdout(&list_again)).len(),
1,
"row count must still be exactly one after the identical batch is re-run: {:?}",
stdout(&list_again)
);
}
#[test]
fn batch_skip_line_is_positional_and_carries_duplicate_of_id() {
let env = setup();
let batch = "{\"type\": \"insight\", \"content\": \"Ship it, and move on.\", \"session\": \"sess-1\"}\n\
{\"type\": \"insight\", \"content\": \"ship it and move on\", \"session\": \"sess-1\"}\n";
mx_stdin(&env, &["memory", "add-batch", "--no-embed"], batch);
let out = mx_stdin(&env, &["memory", "add-batch", "--no-embed"], batch);
let text = stdout(&out);
assert!(
text.contains("[1] Already saved: kn-"),
"line 1 must be a positional 'Already saved' line with a kn- id: {text}"
);
assert!(
text.contains("[2] Already saved: kn-"),
"line 2 must be a positional 'Already saved' line with a kn- id: {text}"
);
assert!(
!text.to_lowercase().contains("skipped:") && !text.to_lowercase().contains("duplicate:"),
"per-entry skip lines must never use bare 'skipped'/'duplicate' framing: {text}"
);
}
#[test]
fn standard_batch_path_also_dedups_via_add_one() {
let env = setup();
let batch = "{\"category\": \"insight\", \"title\": \"Batch Note\", \"content\": \"Same body twice.\", \"session_id\": \"sess-1\"}\n\
{\"category\": \"insight\", \"title\": \"batch note\", \"content\": \"same body twice.\", \"session_id\": \"sess-1\"}\n";
let out = mx_stdin(&env, &["memory", "add-batch", "--no-embed"], batch);
assert!(out.status.success());
let text = stdout(&out);
assert!(
text.contains("Batch complete: 1 added, 1 already saved (no action needed), 0 failed."),
"expected exactly one add and one skip: {text}"
);
let list = mx(&env, &["memory", "list", "--category", "insight", "--json"]);
assert!(list.status.success(), "list failed: {}", stderr(&list));
assert_eq!(
extract_json_array(&stdout(&list)).len(),
1,
"exactly one entry should exist after a recased duplicate via the standard batch path"
);
}
#[test]
fn single_add_fact_type_path_recased_duplicate_is_skipped_exactly_one_entry() {
let env = setup();
let first = mx(
&env,
&[
"memory",
"add",
"--type",
"insight",
"--content",
"Ship it, and move on.",
"--session",
"sess-1",
"--no-embed",
],
);
assert!(first.status.success());
assert!(
stdout(&first).contains("Added fact:"),
"first add must write through: {}",
stdout(&first)
);
let second = mx(
&env,
&[
"memory",
"add",
"--type",
"insight",
"--content",
"ship it and move on",
"--session",
"sess-1",
"--no-embed",
],
);
assert!(
second.status.success(),
"a duplicate-skip must exit 0: {}",
stderr(&second)
);
let second_out = stdout(&second);
assert!(
second_out.contains("Already saved:"),
"recased duplicate must be reported as an affirmative skip, not a second write: {second_out}"
);
assert!(
!second_out.contains("Added fact:"),
"recased duplicate must NOT write a second entry: {second_out}"
);
let list = mx(&env, &["memory", "list", "--category", "insight", "--json"]);
assert!(list.status.success(), "list failed: {}", stderr(&list));
let entries = extract_json_array(&stdout(&list));
assert_eq!(
entries.len(),
1,
"exactly one entry should exist after a recased re-add via the single-add fact-type path: {:?}",
entries
);
}
#[test]
fn single_add_fact_type_path_allow_duplicate_forces_the_write_through() {
let env = setup();
for _ in 0..2 {
let out = mx(
&env,
&[
"memory",
"add",
"--type",
"insight",
"--content",
"Ship it, and move on.",
"--session",
"sess-1",
"--no-embed",
"--allow-duplicate",
],
);
assert!(out.status.success());
let text = stdout(&out);
assert!(
text.contains("Added fact:"),
"--allow-duplicate must force every write through: {text}"
);
assert!(
!text.contains("Already saved:"),
"--allow-duplicate must bypass the dedup skip entirely: {text}"
);
}
}
#[test]
fn single_add_fact_type_path_no_session_emits_bypass_note_on_stderr() {
let env = setup();
let out = mx(
&env,
&[
"memory",
"add",
"--type",
"insight",
"--content",
"No session here.",
"--no-embed",
],
);
assert!(out.status.success());
assert!(
stderr(&out).contains("dedup bypassed"),
"the single-add fact-type path must surface the bypass note when --session is omitted: {}",
stderr(&out)
);
}
#[test]
fn batch_fact_type_path_no_session_emits_bypass_note_on_stderr() {
let env = setup();
let batch = "{\"type\": \"insight\", \"content\": \"No session on this line.\"}\n";
let out = mx_stdin(&env, &["memory", "add-batch", "--no-embed"], batch);
assert!(out.status.success());
assert!(
stderr(&out).contains("dedup bypassed"),
"the batch fact-type path must surface the bypass note when 'session' is absent: {}",
stderr(&out)
);
}
#[test]
fn standard_batch_path_no_session_id_emits_bypass_note_on_stderr() {
let env = setup();
let batch = "{\"category\": \"insight\", \"title\": \"No Session\", \"content\": \"No session_id on this line.\"}\n";
let out = mx_stdin(&env, &["memory", "add-batch", "--no-embed"], batch);
assert!(out.status.success());
assert!(
stderr(&out).contains("dedup bypassed"),
"the standard batch path must surface the bypass note when 'session_id' is absent: {}",
stderr(&out)
);
}
#[test]
fn batch_per_line_owner_claim_confirms_existence_of_matching_private_content() {
let env = setup();
let victim_write = mx(
&env,
&[
"memory",
"add",
"--category",
"insight",
"--title",
"Victim Secret",
"--content",
"Victim's private content.",
"--owner",
"victim",
"--private",
"--session-id",
"sess-1",
"--json",
"--no-embed",
"--no-auto-anchor",
],
);
assert!(victim_write.status.success());
let victim_id = extract_json(&stdout(&victim_write))["id"]
.as_str()
.unwrap()
.to_string();
let probe = "{\"category\": \"insight\", \"title\": \"Victim Secret\", \"content\": \"Victim's private content.\", \"private\": true, \"owner\": \"victim\", \"session_id\": \"sess-1\", \"source_agent\": \"attacker\"}\n";
let out = mx_stdin(&env, &["memory", "add-batch", "--no-embed"], probe);
assert!(out.status.success());
let text = stdout(&out);
assert!(
text.contains(&format!("Already saved: {}", victim_id)),
"a batch line claiming owner=victim with matching content confirms the private \
entry's existence via the skip -- accepted per PR #402's Honest Disclosures, \
extended to the batch per-line owner vector: {text}"
);
}