use std::collections::BTreeMap;
use std::process::{Command, Output};
use jiff::Timestamp;
use magi::ask::{QuestionStatus, Questions};
use magi::config::{AgentKind, AgentSpec, Config};
use magi::talk::{self, Talks};
struct Fixture {
tmp: tempfile::TempDir,
questions: Questions,
qid: String,
talk_id: String,
}
fn fixture() -> Fixture {
let tmp = tempfile::tempdir().expect("tempdir");
let talks = Talks::at(tmp.path().join("talks"));
let questions = Questions::at(tmp.path().join("questions"));
let cfg = Config {
agents: vec![AgentSpec {
id: "mock".to_owned(),
kind: AgentKind::Command,
model: None,
command: vec!["true".to_owned()],
extra_args: Vec::new(),
env: BTreeMap::new(),
prompt_delivery: None,
}],
..Config::default()
};
let mut t = talk::begin(&talks, &cfg, tmp.path().to_path_buf(), Some("mock")).expect("talk");
let mut q = magi::ask::Question::new(
"20260902-000000-beef".to_owned(),
magi::land::APPROVAL_NODE.to_owned(),
"land".to_owned(),
"Merge the pull request?".to_owned(),
"All checks are green.".to_owned(),
vec![magi::land::APPROVE.to_owned(), magi::land::HOLD.to_owned()],
);
questions.put(&mut q).expect("put question");
assert!(magi::consult::begin(&questions, &talks, &q, &t).expect("begin"));
let owner = |body: &str| talk::Turn {
breaks: Some(Vec::new()),
posted_by: None,
who: talk::Who::Operator,
body: body.into(),
at: Timestamp::now(),
attachments: Vec::new(),
usage: None,
};
let queued = talks.get(&t.id).expect("talk").pending;
t = talks.get(&t.id).expect("talk");
t.turns.push(owner(&queued));
t.turns.push(owner("merge it now"));
t.pending.clear();
t.pending_breaks = None;
talks.put(&mut t).expect("put talk");
Fixture {
qid: q.id.clone(),
talk_id: t.id.clone(),
questions,
tmp,
}
}
fn answer(f: &Fixture, run: &str, quote: Option<&str>) -> Output {
let mut cmd = Command::new(env!("CARGO_BIN_EXE_magi"));
cmd.arg("answer")
.arg(&f.qid)
.args(["--reply", magi::land::APPROVE])
.env("MAGI_HOME", f.tmp.path())
.env("MAGI_CONFIG_DIR", f.tmp.path().join("config"))
.env_remove("MAGI_RUN")
.env_remove("MAGI_NODE")
.env("MAGI_RUN", run)
.env("MAGI_NODE", magi::queue::CHAT_NODE);
if let Some(q) = quote {
cmd.args(["--quote", q]);
}
cmd.output().expect("spawn magi")
}
fn assert_untouched(f: &Fixture, out: &Output) {
let err = String::from_utf8_lossy(&out.stderr);
assert!(!out.status.success(), "should be refused: {err}");
let q = f.questions.get(&f.qid).expect("question");
assert!(q.status.open(), "question must stay open");
assert!(q.answer.is_none(), "nothing may be written");
}
#[test]
fn a_chat_merge_without_a_quote_is_refused() {
let f = fixture();
let out = answer(&f, &f.talk_id, None);
assert_untouched(&f, &out);
let err = String::from_utf8_lossy(&out.stderr).to_lowercase();
assert!(
err.contains("quote"),
"refusal should name the quote: {err}"
);
}
#[test]
fn a_chat_merge_with_words_the_owner_never_wrote_is_refused() {
let f = fixture();
let out = answer(&f, &f.talk_id, Some("ship it immediately"));
assert_untouched(&f, &out);
}
#[test]
fn a_chat_merge_from_a_different_chat_is_refused() {
let f = fixture();
let out = answer(&f, "20260902-000000-dead", Some("merge it now"));
assert_untouched(&f, &out);
}
#[test]
fn a_chat_merge_quoting_the_owner_is_accepted() {
let f = fixture();
let out = answer(&f, &f.talk_id, Some("merge it now"));
let err = String::from_utf8_lossy(&out.stderr);
assert!(out.status.success(), "should be accepted: {err}");
let q = f.questions.get(&f.qid).expect("question");
assert_eq!(q.status, QuestionStatus::Answered);
}
#[test]
fn ask_chat_busy_exits_unsuccessfully_without_changing_state() {
let f = fixture();
let talks = Talks::at(f.tmp.path().join("talks"));
let mut q = f.questions.get(&f.qid).expect("question");
q.consult = None;
f.questions.put(&mut q).expect("unconsulted question");
let mut task = magi::queue::Task::new(
"consult-task".to_owned(),
"Do it".to_owned(),
f.tmp.path().to_path_buf(),
magi::queue::Source::Agent {
run: f.talk_id.clone(),
node: magi::queue::CHAT_NODE.to_owned(),
},
);
task.start(q.run.clone());
magi::queue::Queue::at(f.tmp.path().join("queue"))
.put(&mut task)
.expect("chat task");
std::fs::write(
f.tmp.path().join("magi.toml"),
"[[agents]]\nid = \"mock\"\nkind = \"command\"\ncommand = [\"true\"]\n",
)
.expect("isolated config");
let question_before = std::fs::read(f.questions.path_of(&q.id)).unwrap();
let talk_before = std::fs::read(talks.path_of(&f.talk_id)).unwrap();
let _lease = talks
.claim_turn(&f.talk_id)
.unwrap()
.expect("held elsewhere");
let out = Command::new(env!("CARGO_BIN_EXE_magi"))
.args(["answer", &q.id, "--ask-chat"])
.env("MAGI_HOME", f.tmp.path())
.env("MAGI_CONFIG_DIR", f.tmp.path().join("config"))
.env_remove("MAGI_RUN")
.env_remove("MAGI_NODE")
.output()
.expect("run CLI starter");
assert!(!out.status.success());
let error = String::from_utf8_lossy(&out.stderr);
assert!(error.contains("a turn is already running"), "{error}");
assert_eq!(
std::fs::read(f.questions.path_of(&q.id)).unwrap(),
question_before
);
assert_eq!(
std::fs::read(talks.path_of(&f.talk_id)).unwrap(),
talk_before
);
}