use serial_test::serial;
use std::process::{Command, Stdio};
use tempfile::TempDir;
mod common;
const MX: &str = env!("CARGO_BIN_EXE_mx");
const AGENT: &str = "agent-a";
fn mx(dir: &TempDir, args: &[&str]) -> std::process::Output {
let mut cmd = Command::new(MX);
common::isolate(&mut cmd, dir.path());
cmd.args(args)
.env("MX_CURRENT_AGENT", AGENT)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
let mut child = cmd.spawn().expect("failed to spawn mx");
drop(child.stdin.take());
child.wait_with_output().expect("failed to wait on mx")
}
fn stdout_of(out: &std::process::Output) -> String {
String::from_utf8_lossy(&out.stdout).into_owned()
}
fn stderr_of(out: &std::process::Output) -> String {
String::from_utf8_lossy(&out.stderr).into_owned()
}
fn json_of(out: &std::process::Output) -> serde_json::Value {
let text = stdout_of(out);
serde_json::from_str(text.trim())
.unwrap_or_else(|e| panic!("expected JSON on stdout, got {text:?} ({e})"))
}
fn add_bloom(dir: &TempDir, title: &str, body: &str, phrase: &str, tags: Option<&str>) {
let mut args = vec![
"memory",
"add",
"--category",
"bloom",
"--title",
title,
"--content",
body,
"--resonance",
"9",
"--resonance-type",
"foundational",
"--wake-phrase",
phrase,
"--no-embed",
"--no-auto-anchor",
];
if let Some(tags) = tags {
args.push("--tags");
args.push(tags);
}
let out = mx(dir, &args);
assert!(
out.status.success(),
"add {title:?} must succeed; stderr: {}",
stderr_of(&out)
);
}
fn activation_counts(dir: &TempDir) -> Vec<(String, i64)> {
let path = dir.path().join("export.jsonl");
let path_str = path.to_str().unwrap();
let out = mx(
dir,
&[
"memory", "export", "--format", "jsonl", "--output", path_str,
],
);
assert!(
out.status.success(),
"jsonl export must succeed; stderr: {}",
stderr_of(&out)
);
let text = std::fs::read_to_string(&path).expect("export file");
let mut counts: Vec<(String, i64)> = text
.lines()
.filter(|l| !l.trim().is_empty())
.map(|line| {
let v: serde_json::Value = serde_json::from_str(line).expect("jsonl line");
(
v["title"].as_str().unwrap_or_default().to_string(),
v["activation_count"].as_i64().unwrap_or_default(),
)
})
.collect();
counts.sort();
counts
}
#[test]
#[serial]
fn one_guess_ritual_end_to_end() {
let dir = TempDir::new().unwrap();
add_bloom(&dir, "Alpha Note", "Alpha body text.", "alpha cue", None);
add_bloom(&dir, "Beta Note", "Beta body text.", "beta cue", None);
add_bloom(
&dir,
"Shelved Note",
"Shelved body text.",
"shelved cue",
Some("archive"),
);
let out = mx(&dir, &["memory", "wake", "--skip"]);
assert!(!out.status.success(), "--skip must no longer parse");
let err = stderr_of(&out);
assert!(
err.contains("unexpected argument") || err.contains("--skip"),
"clap should reject --skip as unknown; stderr: {err}"
);
let out = mx(&dir, &["memory", "wake", "--min-resonance", "9"]);
assert!(
out.status.success(),
"plain wake must succeed; stderr: {}",
stderr_of(&out)
);
let listing = stdout_of(&out);
assert!(listing.contains("Alpha Note"), "{listing}");
assert!(
!listing.contains("Shelved Note"),
"an archive-tagged entry is out of the wake set by default: {listing}"
);
let out = mx(
&dir,
&[
"memory",
"wake",
"--min-resonance",
"9",
"--include-excluded",
],
);
assert!(
stdout_of(&out).contains("Shelved Note"),
"--include-excluded brings it back: {}",
stdout_of(&out)
);
let out = mx(
&dir,
&[
"memory",
"wake",
"--min-resonance",
"9",
"--begin",
"--wake",
"7",
"--model",
"test-model",
],
);
assert!(
out.status.success(),
"--begin must succeed; stderr: {}",
stderr_of(&out)
);
let begin = json_of(&out);
assert_eq!(begin["status"], "ritual_started");
assert_eq!(begin["excluded"]["archive"], 1);
assert_eq!(begin["progress"]["bloom_total"], 2);
let prompt_title = begin["prompt"]["title"].as_str().unwrap().to_string();
assert!(
!prompt_title.contains("Shelved"),
"the excluded bloom must not open the ritual: {prompt_title}"
);
let after_begin = activation_counts(&dir);
assert!(
after_begin.iter().any(|(_, c)| *c > 0),
"begin should have activated the cascade: {after_begin:?}"
);
let mut token = begin["session"].as_str().unwrap().to_string();
let mut bloom_id = begin["prompt"]["id"].as_str().unwrap().to_string();
let out = mx(
&dir,
&[
"memory",
"wake",
"--bloom-id",
&bloom_id,
"--respond",
"nothing like the cue",
"--session",
&token,
],
);
assert!(
out.status.success(),
"--respond must succeed; stderr: {}",
stderr_of(&out)
);
let first = json_of(&out);
assert_eq!(first["status"], "shown");
assert_eq!(first["bucket"], "revealed");
assert_eq!(first["match"]["kind"], "none");
assert!(
first["bloom"]["content"].as_str().unwrap().contains("body"),
"the bloom is shown after the one guess: {first}"
);
let after_respond = activation_counts(&dir);
assert_eq!(
after_begin, after_respond,
"a --respond call must not change any activation count"
);
token = first["session"].as_str().unwrap().to_string();
bloom_id = first["next"]["id"].as_str().unwrap().to_string();
let cue = if first["next"]["title"] == "Alpha Note" {
"alpha cue"
} else {
"beta cue"
};
let out = mx(
&dir,
&[
"memory",
"wake",
"--bloom-id",
&bloom_id,
"--respond",
cue,
"--session",
&token,
],
);
assert!(out.status.success(), "stderr: {}", stderr_of(&out));
let second = json_of(&out);
assert_eq!(second["bucket"], "unhinted");
assert_eq!(second["match"]["kind"], "exact");
let summary = &second["summary"];
assert_eq!(summary["chunks"], 2);
assert_eq!(summary["blooms"], 2);
assert_eq!(summary["buckets"]["unhinted"]["authored"], 1);
assert_eq!(summary["buckets"]["revealed"]["authored"], 1);
assert_eq!(
after_begin,
activation_counts(&dir),
"the second --respond must not change any activation count either"
);
let guess_text = "nothing like the cue";
let jsonl = dir.path().join("check.jsonl");
let out = mx(
&dir,
&[
"memory",
"export",
"--format",
"jsonl",
"--output",
jsonl.to_str().unwrap(),
],
);
assert!(out.status.success(), "stderr: {}", stderr_of(&out));
let jsonl_text = std::fs::read_to_string(&jsonl).unwrap();
assert!(
jsonl_text.contains("Alpha Note"),
"precondition: the export must contain entries at all"
);
assert!(
!jsonl_text.contains(guess_text) && !jsonl_text.contains("wake_guess"),
"jsonl export leaked the guess log"
);
let csv = dir.path().join("check.csv");
let out = mx(
&dir,
&[
"memory",
"export",
"--format",
"csv",
"--output",
csv.to_str().unwrap(),
],
);
assert!(out.status.success(), "stderr: {}", stderr_of(&out));
let csv_text = std::fs::read_to_string(&csv).unwrap();
assert!(
!csv_text.contains(guess_text) && !csv_text.contains("wake_guess"),
"csv export leaked the guess log"
);
let md_dir = dir.path().join("md-export");
let out = mx(
&dir,
&[
"memory",
"export",
"--format",
"md",
"--output",
md_dir.to_str().unwrap(),
],
);
assert!(out.status.success(), "stderr: {}", stderr_of(&out));
let mut md_text = String::new();
for entry in walk_files(&md_dir) {
md_text.push_str(&std::fs::read_to_string(entry).unwrap_or_default());
}
assert!(
md_text.contains("Alpha"),
"precondition: the md export must contain entries at all"
);
assert!(
!md_text.contains(guess_text) && !md_text.contains("wake_guess"),
"md export leaked the guess log"
);
}
#[test]
#[serial]
fn the_default_cascade_opens_a_ritual_and_honours_the_exclusion() {
let dir = TempDir::new().unwrap();
add_bloom(&dir, "Cascade Alpha", "Alpha body text.", "alpha cue", None);
add_bloom(&dir, "Cascade Beta", "Beta body text.", "beta cue", None);
add_bloom(
&dir,
"Cascade Shelved",
"Shelved body text.",
"shelved cue",
Some("wake-exclude"),
);
let out = mx(
&dir,
&["memory", "wake", "--begin", "--model", "test-model"],
);
assert!(
out.status.success(),
"the default cascade must open a ritual; stderr: {}",
stderr_of(&out)
);
let begin = json_of(&out);
assert_eq!(begin["status"], "ritual_started");
assert_eq!(
begin["progress"]["bloom_total"], 2,
"the excluded entry must not be in the default cascade: {begin}"
);
assert_eq!(begin["excluded"]["wake-exclude"], 1);
let out = mx(&dir, &["memory", "wake", "--begin", "--include-excluded"]);
assert!(out.status.success(), "stderr: {}", stderr_of(&out));
let begin = json_of(&out);
assert_eq!(
begin["progress"]["bloom_total"], 3,
"--include-excluded must restore the entry through --begin: {begin}"
);
assert_eq!(
begin["excluded"],
serde_json::json!({}),
"nothing is excluded when the override is on: {begin}"
);
}
#[test]
#[serial]
fn two_begins_over_the_same_data_produce_the_same_sequence() {
let dir = TempDir::new().unwrap();
for (title, cue) in [
("Order One", "one cue"),
("Order Two", "two cue"),
("Order Three", "three cue"),
] {
add_bloom(&dir, title, "Body text.", cue, None);
}
let sequence = |dir: &TempDir| -> Vec<String> {
let mut titles = Vec::new();
let out = mx(dir, &["memory", "wake", "--begin"]);
assert!(out.status.success(), "stderr: {}", stderr_of(&out));
let mut node = json_of(&out);
let mut token = node["session"].as_str().unwrap().to_string();
let mut id = node["prompt"]["id"].as_str().unwrap().to_string();
titles.push(node["prompt"]["title"].as_str().unwrap().to_string());
loop {
let out = mx(
dir,
&[
"memory",
"wake",
"--bloom-id",
&id,
"--respond",
"no match at all",
"--session",
&token,
],
);
assert!(out.status.success(), "stderr: {}", stderr_of(&out));
node = json_of(&out);
let Some(next) = node.get("next").filter(|n| !n.is_null()) else {
break;
};
titles.push(next["title"].as_str().unwrap().to_string());
id = next["id"].as_str().unwrap().to_string();
token = node["session"].as_str().unwrap().to_string();
}
titles
};
let first = sequence(&dir);
let second = sequence(&dir);
assert_eq!(first.len(), 3, "precondition: all three blooms walked");
assert_eq!(
first, second,
"the wake sequence is not stable across begins"
);
}
#[test]
#[serial]
fn a_bloom_shrunk_mid_ritual_reports_chunk_truncated_end_to_end() {
fn mx_chunked(dir: &TempDir, args: &[&str]) -> std::process::Output {
let mut cmd = Command::new(MX);
common::isolate(&mut cmd, dir.path());
cmd.args(args)
.env("MX_CURRENT_AGENT", AGENT)
.env("MX_WAKE_CHUNK_BYTES", "300")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
let mut child = cmd.spawn().expect("failed to spawn mx");
drop(child.stdin.take());
child.wait_with_output().expect("failed to wait on mx")
}
let dir = TempDir::new().unwrap();
let mut body = String::new();
for section in 1..=6 {
body.push_str(&format!(
"\n## Section {section}\n\nThis is section {section} of the fixture, \
long enough that the sections cross the chunking threshold.\n\n"
));
}
add_bloom(&dir, "Chunky Note", &body, "chunky cue", None);
add_bloom(&dir, "Tail Note", "Tail body text.", "tail cue", None);
let out = mx_chunked(&dir, &["memory", "wake", "--begin"]);
assert!(out.status.success(), "stderr: {}", stderr_of(&out));
let begin = json_of(&out);
let total = begin["progress"]["total"].as_u64().unwrap();
assert!(
total > 2,
"precondition: the fixture must chunk, got {total}"
);
let mut id = begin["prompt"]["id"].as_str().unwrap().to_string();
let mut title = begin["prompt"]["title"].as_str().unwrap().to_string();
let mut token = begin["session"].as_str().unwrap().to_string();
let mut chunky_id = String::new();
for _ in 0..total + 1 {
let out = mx_chunked(
&dir,
&[
"memory",
"wake",
"--bloom-id",
&id,
"--respond",
"no match",
"--session",
&token,
],
);
assert!(out.status.success(), "stderr: {}", stderr_of(&out));
let node = json_of(&out);
token = node["session"].as_str().unwrap().to_string();
if title.starts_with("Chunky") {
chunky_id = id.clone();
break;
}
let next = node.get("next").expect("more blooms to walk");
id = next["id"].as_str().unwrap().to_string();
title = next["title"].as_str().unwrap().to_string();
}
assert!(
!chunky_id.is_empty(),
"precondition: the chunked bloom must be reachable"
);
let out = mx_chunked(
&dir,
&["memory", "update", &chunky_id, "--content", "tiny now."],
);
assert!(out.status.success(), "update; stderr: {}", stderr_of(&out));
let out = mx_chunked(
&dir,
&[
"memory",
"wake",
"--bloom-id",
&chunky_id,
"--respond",
"ignored",
"--session",
&token,
],
);
assert!(out.status.success(), "stderr: {}", stderr_of(&out));
let trunc = json_of(&out);
assert_eq!(
trunc["status"], "chunk_truncated",
"a bloom shrunk under the cursor must report the truncation: {trunc}"
);
assert!(
trunc.get("bucket").is_none() && trunc.get("guess").is_none(),
"no guess was judged, so neither is reported: {trunc}"
);
assert!(
trunc.get("next").is_some() || trunc.get("summary").is_some(),
"a truncation must still hand back a next prompt or a summary: {trunc}"
);
}
#[test]
#[serial]
fn wake_set_flags_are_rejected_on_a_respond_call() {
let dir = TempDir::new().unwrap();
add_bloom(&dir, "Flag Alpha", "Alpha body text.", "alpha cue", None);
let out = mx(&dir, &["memory", "wake", "--begin"]);
assert!(out.status.success(), "stderr: {}", stderr_of(&out));
let begin = json_of(&out);
let token = begin["session"].as_str().unwrap().to_string();
let id = begin["prompt"]["id"].as_str().unwrap().to_string();
for flag in [
vec!["--include-excluded"],
vec!["--min-resonance", "9"],
vec!["--limit", "5"],
] {
let mut args = vec![
"memory",
"wake",
"--bloom-id",
&id,
"--respond",
"alpha cue",
"--session",
&token,
];
args.extend(flag.iter().copied());
let out = mx(&dir, &args);
assert!(
!out.status.success(),
"{flag:?} was accepted and silently ignored on a respond call; stdout: {}",
stdout_of(&out)
);
}
}
#[test]
#[serial]
fn an_all_excluded_wake_set_says_why_it_is_empty() {
let dir = TempDir::new().unwrap();
add_bloom(
&dir,
"Shelved One",
"Body text.",
"one cue",
Some("archive"),
);
add_bloom(
&dir,
"Shelved Two",
"Body text.",
"two cue",
Some("wake-exclude"),
);
let out = mx(&dir, &["memory", "wake", "--begin"]);
assert!(!out.status.success(), "an empty wake set must fail");
let check = mx(&dir, &["memory", "wake", "--begin", "--include-excluded"]);
assert!(
check.status.success(),
"precondition: the entries exist; stderr: {}",
stderr_of(&check)
);
let err = stderr_of(&out);
assert!(
err.contains("exclude") || err.contains("archive"),
"the error must name the exclusion that emptied the set, got: {err}"
);
}
#[test]
#[serial]
fn no_retired_vocabulary_survives_in_the_wake_help_text() {
let dir = TempDir::new().unwrap();
let mut seen = String::new();
for args in [
vec!["memory", "wake", "--help"],
vec!["memory", "add", "--help"],
vec!["memory", "update", "--help"],
vec!["memory", "--help"],
] {
seen.push_str(&stdout_of(&mx(&dir, &args)));
}
for retired in [
"remembered",
"needed_help",
"needed help",
"incorrect",
"prove",
"verification",
] {
assert!(
!seen.to_lowercase().contains(retired),
"help text still carries the retired word {retired:?}"
);
}
}
fn respond_args<'a>(bloom_id: &'a str, guess: &'a str, token: &'a str) -> Vec<&'a str> {
vec![
"memory",
"wake",
"--bloom-id",
bloom_id,
"--respond",
guess,
"--session",
token,
]
}
#[test]
#[serial]
fn failure_paths_end_to_end() {
let dir = TempDir::new().unwrap();
add_bloom(&dir, "Alpha Note", "Alpha body text.", "alpha cue", None);
add_bloom(&dir, "Beta Note", "Beta body text.", "beta cue", None);
let before = activation_counts(&dir);
let out = mx(&dir, &["memory", "wake", "--begin", "--wake", "0"]);
assert!(!out.status.success(), "--wake 0 must be refused");
assert!(stderr_of(&out).contains("positive"), "{}", stderr_of(&out));
assert_eq!(
before,
activation_counts(&dir),
"a refused --begin must not count as surfacing the entries"
);
let out = mx(
&dir,
&["memory", "wake", "--begin", "--wake", "7", "--model", "m-1"],
);
assert!(out.status.success(), "stderr: {}", stderr_of(&out));
let begin = json_of(&out);
assert_eq!(begin["wake"], 7);
assert_eq!(begin["model"], "m-1");
let token = begin["session"].as_str().unwrap().to_string();
let first_id = begin["prompt"]["id"].as_str().unwrap().to_string();
let out = mx(&dir, &respond_args("kn-not-this-one", "alpha cue", &token));
assert!(!out.status.success(), "a refused id must exit non-zero");
assert!(stdout_of(&out).trim().is_empty(), "nothing on stdout");
let refusal: serde_json::Value = serde_json::from_str(stderr_of(&out).trim())
.unwrap_or_else(|e| panic!("expected JSON on stderr ({e}): {}", stderr_of(&out)));
assert_eq!(refusal["status"], "error");
assert_eq!(refusal["error"], "invalid_bloom_id");
assert_eq!(refusal["expected_id"], first_id.as_str());
let hyphen_guess = "-ish, something about the cue";
let out = mx(&dir, &respond_args(&first_id, hyphen_guess, &token));
assert!(
out.status.success(),
"a guess starting with '-' must parse; stderr: {}",
stderr_of(&out)
);
let first = json_of(&out);
assert_eq!(first["guess"], hyphen_guess);
assert_eq!(first["wake"], 7, "every respond echoes the wake");
assert_eq!(first["model"], "m-1");
let out = mx(&dir, &respond_args(&first_id, "alpha cue", &token));
assert!(out.status.success(), "stderr: {}", stderr_of(&out));
let resumed = json_of(&out);
assert_eq!(resumed["replayed"], true);
assert_eq!(resumed["guess"], hyphen_guess, "the first guess counts");
assert_eq!(resumed["session"], first["session"]);
assert_eq!(resumed["next"], first["next"]);
let next_token = first["session"].as_str().unwrap();
let next_id = first["next"]["id"].as_str().unwrap();
let out = mx(&dir, &respond_args(next_id, "beta cue", next_token));
assert!(out.status.success(), "stderr: {}", stderr_of(&out));
let last = json_of(&out);
assert!(last["summary"].is_object());
let out = mx(&dir, &respond_args(&first_id, "alpha cue", &token));
assert!(!out.status.success());
assert!(
stderr_of(&out).contains("token from your last successful response"),
"{}",
stderr_of(&out)
);
let out = mx(&dir, &["memory", "wake", "--begin", "--wake", "7"]);
assert!(
!out.status.success(),
"a logged wake number must be refused"
);
assert!(
stderr_of(&out).contains("--force-wake"),
"{}",
stderr_of(&out)
);
let out = mx(
&dir,
&["memory", "wake", "--begin", "--wake", "7", "--force-wake"],
);
assert!(out.status.success(), "stderr: {}", stderr_of(&out));
let forced = json_of(&out);
assert_eq!(forced["wake"], 7);
assert!(
forced["warnings"][0]
.as_str()
.unwrap()
.contains("--force-wake"),
"{forced}"
);
let out = mx(&dir, &["memory", "wake", "--begin", "--force-wake"]);
assert!(!out.status.success(), "--force-wake requires --wake");
}
fn walk_order(dir: &TempDir) -> Vec<String> {
let out = mx(dir, &["memory", "wake", "--begin"]);
assert!(out.status.success(), "stderr: {}", stderr_of(&out));
let mut node = json_of(&out);
let mut token = node["session"].as_str().unwrap().to_string();
let mut id = node["prompt"]["id"].as_str().unwrap().to_string();
let mut ids = vec![id.clone()];
loop {
let out = mx(
dir,
&[
"memory",
"wake",
"--bloom-id",
&id,
"--respond",
"no match at all",
"--session",
&token,
],
);
assert!(out.status.success(), "stderr: {}", stderr_of(&out));
node = json_of(&out);
let Some(next) = node.get("next").filter(|n| !n.is_null()) else {
break;
};
id = next["id"].as_str().unwrap().to_string();
ids.push(id.clone());
token = node["session"].as_str().unwrap().to_string();
}
ids
}
fn seed_three(dir: &TempDir) {
add_bloom(dir, "Vanish One", "One body text.", "one cue", None);
add_bloom(dir, "Vanish Two", "Two body text.", "two cue", None);
add_bloom(dir, "Vanish Three", "Three body text.", "three cue", None);
}
#[test]
#[serial]
fn an_entry_deleted_under_the_cursor_reports_bloom_missing_end_to_end() {
let dir = TempDir::new().unwrap();
seed_three(&dir);
let order = walk_order(&dir);
assert_eq!(order.len(), 3, "precondition: three entries");
let out = mx(&dir, &["memory", "wake", "--begin"]);
assert!(out.status.success(), "stderr: {}", stderr_of(&out));
let begin = json_of(&out);
let mut token = begin["session"].as_str().unwrap().to_string();
let out = mx(
&dir,
&[
"memory",
"wake",
"--bloom-id",
&order[0],
"--respond",
"one cue",
"--session",
&token,
],
);
assert!(out.status.success(), "stderr: {}", stderr_of(&out));
let node = json_of(&out);
assert_eq!(node["next"]["id"], order[1].as_str());
token = node["session"].as_str().unwrap().to_string();
let out = mx(&dir, &["memory", "delete", &order[1]]);
assert!(out.status.success(), "delete; stderr: {}", stderr_of(&out));
let out = mx(
&dir,
&[
"memory",
"wake",
"--bloom-id",
&order[1],
"--respond",
"two cue",
"--session",
&token,
],
);
assert!(out.status.success(), "stderr: {}", stderr_of(&out));
let gone = json_of(&out);
assert_eq!(gone["status"], "bloom_missing", "{gone}");
assert!(
gone.get("bloom").is_none(),
"there is no entry left to show: {gone}"
);
assert!(gone.get("bucket").is_none(), "nothing was judged: {gone}");
assert_eq!(gone["next"]["id"], order[2].as_str());
let token = gone["session"].as_str().unwrap().to_string();
let out = mx(
&dir,
&[
"memory",
"wake",
"--bloom-id",
&order[2],
"--respond",
"three cue",
"--session",
&token,
],
);
assert!(out.status.success(), "stderr: {}", stderr_of(&out));
let last = json_of(&out);
assert_eq!(last["status"], "shown");
assert!(last.get("bloom").is_some(), "{last}");
assert!(last.get("summary").is_some(), "{last}");
assert_eq!(
last["summary"]["blooms"], 3,
"the deleted entry still counts toward the sequence length: {last}"
);
}
#[test]
#[serial]
fn an_entry_deleted_before_its_turn_is_stepped_over_end_to_end() {
let dir = TempDir::new().unwrap();
seed_three(&dir);
let order = walk_order(&dir);
let out = mx(&dir, &["memory", "wake", "--begin"]);
assert!(out.status.success(), "stderr: {}", stderr_of(&out));
let begin = json_of(&out);
assert_eq!(begin["progress"]["bloom_total"], 3);
let token = begin["session"].as_str().unwrap().to_string();
let out = mx(&dir, &["memory", "delete", &order[1]]);
assert!(out.status.success(), "delete; stderr: {}", stderr_of(&out));
let out = mx(
&dir,
&[
"memory",
"wake",
"--bloom-id",
&order[0],
"--respond",
"one cue",
"--session",
&token,
],
);
assert!(
out.status.success(),
"a deleted later entry must not fail the respond; stderr: {}",
stderr_of(&out)
);
let node = json_of(&out);
assert_eq!(node["status"], "shown");
assert_eq!(
node["next"]["id"],
order[2].as_str(),
"the deleted entry must be stepped over when choosing the next \
prompt: {node}"
);
}
fn mx_as(dir: &TempDir, agent: &str, args: &[&str]) -> std::process::Output {
let mut cmd = Command::new(MX);
common::isolate(&mut cmd, dir.path());
cmd.args(args)
.env("MX_CURRENT_AGENT", agent)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
let mut child = cmd.spawn().expect("failed to spawn mx");
drop(child.stdin.take());
child.wait_with_output().expect("failed to wait on mx")
}
fn add_bloom_as(dir: &TempDir, agent: &str, title: &str, phrase: &str) -> String {
let out = mx_as(
dir,
agent,
&[
"memory",
"add",
"--category",
"bloom",
"--title",
title,
"--content",
"Body text.",
"--resonance",
"9",
"--resonance-type",
"foundational",
"--wake-phrase",
phrase,
"--no-embed",
"--no-auto-anchor",
],
);
assert!(
out.status.success(),
"add {title:?} as {agent}; stderr: {}",
stderr_of(&out)
);
stdout_of(&out)
.lines()
.find_map(|l| {
l.strip_prefix("Added entry: ")
.map(|s| s.trim().to_string())
})
.unwrap_or_else(|| panic!("no id in add output: {}", stdout_of(&out)))
}
#[test]
#[serial]
#[ignore = "telling a deleted entry apart from one this caller can no longer \
read is follow-up work, not part of this change"]
fn an_entry_made_private_mid_ritual_is_not_reported_as_deleted() {
let dir = TempDir::new().unwrap();
add_bloom_as(&dir, AGENT, "Owned By A", "a cue");
let b_id = add_bloom_as(&dir, "agent-b", "Owned By B", "b cue");
let out = mx(&dir, &["memory", "wake", "--begin"]);
assert!(out.status.success(), "stderr: {}", stderr_of(&out));
let begin = json_of(&out);
assert_eq!(
begin["progress"]["bloom_total"], 2,
"precondition: both entries are in the wake set: {begin}"
);
let out = mx_as(
&dir,
"agent-b",
&[
"memory",
"update",
&b_id,
"--visibility",
"private",
"--owner",
"agent-b",
],
);
assert!(
out.status.success(),
"making the entry private; stderr: {}",
stderr_of(&out)
);
let mut token = begin["session"].as_str().unwrap().to_string();
let mut id = begin["prompt"]["id"].as_str().unwrap().to_string();
let summary = loop {
let out = mx(
&dir,
&[
"memory",
"wake",
"--bloom-id",
&id,
"--respond",
"a guess",
"--session",
&token,
],
);
assert!(out.status.success(), "stderr: {}", stderr_of(&out));
let node = json_of(&out);
token = node["session"].as_str().unwrap().to_string();
if let Some(summary) = node.get("summary").filter(|s| !s.is_null()) {
break summary.clone();
}
id = node["next"]["id"].as_str().unwrap().to_string();
};
let out = mx_as(
&dir,
"agent-b",
&["memory", "list", "--category", "bloom", "--include-private"],
);
assert!(out.status.success(), "stderr: {}", stderr_of(&out));
assert!(
stdout_of(&out).contains("Owned By B"),
"precondition: the entry still exists for its owner: {}",
stdout_of(&out)
);
assert_eq!(
summary["unjudged"], 0,
"an entry that still exists was counted as vanished because the \
caller could no longer read it: {summary}"
);
}
fn walk_files(dir: &std::path::Path) -> Vec<std::path::PathBuf> {
let mut out = Vec::new();
let Ok(entries) = std::fs::read_dir(dir) else {
return out;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
out.extend(walk_files(&path));
} else {
out.push(path);
}
}
out
}