use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use chrono::{DateTime, Utc};
use mecha_core::doctor::Remedy;
use mecha_core::outbox::{OutboxItem, OutboxStore};
use mecha_core::trigger::{RunRecord, Trigger, TriggerStore};
use serde::{Deserialize, Serialize};
pub mod ids {
pub const OUTBOX_SEND: &str = "slack_outbox_send";
pub const OUTBOX_SEND_CONFIRM: &str = "slack_outbox_send_confirm";
pub const OUTBOX_REJECT: &str = "slack_outbox_reject";
pub const RESTART_UNIT: &str = "slack_action_restart_unit";
pub const TRIGGER_RUN: &str = "slack_action_trigger_run";
pub const TRIGGER_CANCEL: &str = "slack_action_trigger_cancel";
pub const TRIGGER_ENABLE: &str = "slack_action_trigger_enable";
pub const TRIGGER_DISABLE: &str = "slack_action_trigger_disable";
pub const MAIL_IMPORT: &str = "slack_action_mail_import";
pub const FRONTDOOR_CLOSE_SUBMIT: &str = "slack_frontdoor_close_submit";
pub const FRONTDOOR_NEEDS_INFO_SUBMIT: &str = "slack_frontdoor_needs_info_submit";
}
pub const MODAL_TEXT_MAX: usize = 500;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum Action {
OutboxSend { id: String },
OutboxReject { id: String },
RestartUnit { unit: String },
TriggerRun { name: String },
TriggerCancel { name: String },
TriggerEnable { name: String },
TriggerDisable { name: String },
MailImport { provider: String },
FrontdoorClose { seq: i64, reason: String },
FrontdoorNeedsInfo { seq: i64, question: String },
}
impl Action {
pub fn argv(&self) -> Vec<String> {
match self {
Action::OutboxSend { id } => vec![
"mecha".into(),
"outbox".into(),
"send".into(),
id.clone(),
"-y".into(),
],
Action::OutboxReject { id } => vec![
"mecha".into(),
"outbox".into(),
"reject".into(),
id.clone(),
"--reason".into(),
"rejected from Slack".into(),
],
Action::RestartUnit { unit } => vec![
"systemctl".into(),
"--user".into(),
"restart".into(),
unit.clone(),
],
Action::TriggerRun { name } => {
vec!["mecha".into(), "trigger".into(), "run".into(), name.clone()]
}
Action::TriggerCancel { name } => vec![
"mecha".into(),
"trigger".into(),
"cancel".into(),
name.clone(),
],
Action::TriggerEnable { name } => vec![
"mecha".into(),
"trigger".into(),
"enable".into(),
name.clone(),
],
Action::TriggerDisable { name } => vec![
"mecha".into(),
"trigger".into(),
"disable".into(),
name.clone(),
],
Action::MailImport { provider } => vec![
"mecha-mail".into(),
"import".into(),
provider.clone(),
"--provider".into(),
provider.clone(),
],
Action::FrontdoorClose { seq, reason } => vec![
"mecha".into(),
"frontdoor".into(),
"close".into(),
seq.to_string(),
"--reason".into(),
reason.clone(),
],
Action::FrontdoorNeedsInfo { seq, question } => vec![
"mecha".into(),
"frontdoor".into(),
"needs-info".into(),
seq.to_string(),
"--note".into(),
question.clone(),
],
}
}
pub fn from_remedy(remedy: &Remedy) -> Option<Action> {
if remedy.needs_terminal {
return None;
}
let argv: Vec<&str> = remedy.argv.iter().map(String::as_str).collect();
match argv.as_slice() {
["systemctl", "--user", "restart", unit] if is_mecha_unit(unit) => {
Some(Action::RestartUnit {
unit: (*unit).to_string(),
})
}
["mecha", "trigger", "run", name] if is_trigger_name(name) => {
Some(Action::TriggerRun {
name: (*name).to_string(),
})
}
["mecha-mail", "import", name, "--provider", provider]
if name == provider && is_mail_provider(provider) =>
{
Some(Action::MailImport {
provider: (*provider).to_string(),
})
}
_ => None,
}
}
pub fn from_payload(action_id: &str, value: &str) -> Option<Action> {
match action_id {
ids::OUTBOX_SEND | ids::OUTBOX_SEND_CONFIRM if is_outbox_id(value) => {
Some(Action::OutboxSend {
id: value.to_string(),
})
}
ids::OUTBOX_REJECT if is_outbox_id(value) => Some(Action::OutboxReject {
id: value.to_string(),
}),
ids::RESTART_UNIT if is_mecha_unit(value) => Some(Action::RestartUnit {
unit: value.to_string(),
}),
ids::TRIGGER_RUN if is_trigger_name(value) => Some(Action::TriggerRun {
name: value.to_string(),
}),
ids::TRIGGER_CANCEL if is_trigger_name(value) => Some(Action::TriggerCancel {
name: value.to_string(),
}),
ids::TRIGGER_ENABLE if is_trigger_name(value) => Some(Action::TriggerEnable {
name: value.to_string(),
}),
ids::TRIGGER_DISABLE if is_trigger_name(value) => Some(Action::TriggerDisable {
name: value.to_string(),
}),
ids::MAIL_IMPORT if is_mail_provider(value) => Some(Action::MailImport {
provider: value.to_string(),
}),
_ => None,
}
}
pub fn from_submission(callback_id: &str, seq: &str, text: &str) -> Option<Action> {
let seq: i64 = seq.parse().ok().filter(|s| *s > 0)?;
let text = text.trim();
if text.is_empty() || text.chars().count() > MODAL_TEXT_MAX {
return None;
}
match callback_id {
ids::FRONTDOOR_CLOSE_SUBMIT => Some(Action::FrontdoorClose {
seq,
reason: text.to_string(),
}),
ids::FRONTDOOR_NEEDS_INFO_SUBMIT => Some(Action::FrontdoorNeedsInfo {
seq,
question: text.to_string(),
}),
_ => None,
}
}
pub fn action_id(&self) -> &'static str {
match self {
Action::OutboxSend { .. } => ids::OUTBOX_SEND,
Action::OutboxReject { .. } => ids::OUTBOX_REJECT,
Action::RestartUnit { .. } => ids::RESTART_UNIT,
Action::TriggerRun { .. } => ids::TRIGGER_RUN,
Action::TriggerCancel { .. } => ids::TRIGGER_CANCEL,
Action::TriggerEnable { .. } => ids::TRIGGER_ENABLE,
Action::TriggerDisable { .. } => ids::TRIGGER_DISABLE,
Action::MailImport { .. } => ids::MAIL_IMPORT,
Action::FrontdoorClose { .. } => ids::FRONTDOOR_CLOSE_SUBMIT,
Action::FrontdoorNeedsInfo { .. } => ids::FRONTDOOR_NEEDS_INFO_SUBMIT,
}
}
pub fn value(&self) -> String {
match self {
Action::OutboxSend { id } | Action::OutboxReject { id } => id.clone(),
Action::RestartUnit { unit } => unit.clone(),
Action::TriggerRun { name }
| Action::TriggerCancel { name }
| Action::TriggerEnable { name }
| Action::TriggerDisable { name } => name.clone(),
Action::MailImport { provider } => provider.clone(),
Action::FrontdoorClose { seq, .. } | Action::FrontdoorNeedsInfo { seq, .. } => {
seq.to_string()
}
}
}
pub fn describe(&self) -> String {
match self {
Action::OutboxSend { id } => format!("releasing draft `{id}`"),
Action::OutboxReject { id } => format!("rejecting draft `{id}`"),
Action::RestartUnit { unit } => format!("restarting {unit}"),
Action::TriggerRun { name } => format!("running trigger `{name}`"),
Action::TriggerCancel { name } => format!("cancelling trigger `{name}`"),
Action::TriggerEnable { name } => format!("enabling trigger `{name}`"),
Action::TriggerDisable { name } => format!("disabling trigger `{name}`"),
Action::MailImport { provider } => {
format!("importing the legacy {provider} mail login")
}
Action::FrontdoorClose { seq, .. } => format!("closing request {seq}"),
Action::FrontdoorNeedsInfo { seq, .. } => {
format!("parking request {seq} for more information")
}
}
}
}
fn is_mecha_unit(unit: &str) -> bool {
unit.strip_prefix("mecha-")
.and_then(|rest| rest.strip_suffix(".service"))
.is_some_and(|mid| {
!mid.is_empty() && mid.chars().all(|c| c.is_ascii_lowercase() || c == '-')
})
}
fn is_trigger_name(name: &str) -> bool {
Trigger::valid_name(name).is_ok()
}
fn is_mail_provider(provider: &str) -> bool {
matches!(provider, "google" | "outlook")
}
fn is_outbox_id(id: &str) -> bool {
!id.is_empty()
&& id.len() <= 80
&& id
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
}
pub fn fingerprint(text: &str) -> String {
let mut h: u64 = 0xcbf2_9ce4_8422_2325;
for b in text.as_bytes() {
h ^= u64::from(*b);
h = h.wrapping_mul(0x100_0000_01b3);
}
format!("{h:016x}")
}
pub fn confirm_value(id: &str, args: &str) -> String {
format!("{id}#{}", fingerprint(args))
}
pub fn parse_confirm_value(value: &str) -> Option<(&str, Option<&str>)> {
match value.split_once('#') {
Some((id, fp)) => is_outbox_id(id).then_some((id, Some(fp))),
None => is_outbox_id(value).then_some((value, None)),
}
}
pub fn new_tap_id() -> String {
static SEQ: AtomicU64 = AtomicU64::new(0);
format!(
"{}-{}",
Utc::now().format("%Y%m%dT%H%M%S"),
SEQ.fetch_add(1, Ordering::Relaxed)
)
}
pub struct ActionLedger {
path: Option<PathBuf>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum LedgerRow {
Dispatch {
tap_id: String,
at: DateTime<Utc>,
user_id: String,
action: Action,
argv: Vec<String>,
surface: String,
},
Outcome {
tap_id: String,
at: DateTime<Utc>,
status: String,
line: String,
},
}
impl ActionLedger {
pub fn open_default() -> Self {
let path = mecha_core::work::mecha_home()
.ok()
.map(|home| home.join("slack").join("actions.jsonl"));
Self { path }
}
#[cfg(test)]
pub fn at(path: impl Into<PathBuf>) -> Self {
Self {
path: Some(path.into()),
}
}
pub fn dispatched(&self, tap_id: &str, user_id: &str, action: &Action, surface: &str) {
self.append(&LedgerRow::Dispatch {
tap_id: tap_id.to_string(),
at: Utc::now(),
user_id: user_id.to_string(),
action: action.clone(),
argv: action.argv(),
surface: surface.to_string(),
});
}
pub fn resolved(&self, tap_id: &str, status: &str, line: &str) {
self.append(&LedgerRow::Outcome {
tap_id: tap_id.to_string(),
at: Utc::now(),
status: status.to_string(),
line: line.to_string(),
});
}
fn append(&self, row: &LedgerRow) {
let Some(path) = &self.path else {
tracing::warn!("no mecha home — a tap went unledgered");
return;
};
let write = || -> std::io::Result<()> {
use std::io::Write;
if let Some(dir) = path.parent() {
mecha_slack::store::create_private_dir(dir)?;
}
let fresh = !path.exists();
let mut file = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(path)?;
if fresh {
mecha_slack::store::set_owner_only(path)?;
}
let line = serde_json::to_string(row).map_err(std::io::Error::other)?;
writeln!(file, "{line}")
};
if let Err(e) = write() {
tracing::warn!("could not append to the action ledger: {e}");
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Outcome {
pub status: String,
pub line: String,
}
impl Outcome {
fn of(status: &str, line: impl Into<String>) -> Self {
Self {
status: status.to_string(),
line: line.into(),
}
}
}
pub struct Executor {
pub outbox_root: PathBuf,
}
impl Executor {
pub async fn run(&self, action: &Action) -> Outcome {
if let Action::RestartUnit { unit } = action {
if let Some(line) =
crate::commands::doctor::recovered_before_restart(unit, unit_is_failed(unit).await)
{
return Outcome::of("skipped", line);
}
let _ = self.spawn(action).await;
return restart_outcome(unit, unit_is_failed(unit).await);
}
let started = Utc::now();
let child_note = self.spawn(action).await;
let action = action.clone();
let outbox_root = self.outbox_root.clone();
tokio::task::spawn_blocking(move || {
store_outcome(&outbox_root, &action, started, child_note.as_deref())
})
.await
.unwrap_or_else(|e| {
Outcome::of(
"unknown",
format!("the outcome could not be read back: {e}"),
)
})
}
async fn spawn(&self, action: &Action) -> Option<String> {
let argv = action.argv();
let (program, rest) = argv.split_first().expect("argv() is never empty");
let program: PathBuf = if program == "mecha" {
std::env::current_exe().unwrap_or_else(|_| "mecha".into())
} else if let Some(sibling) = std::env::current_exe()
.ok()
.and_then(|exe| Some(exe.parent()?.join(program)))
.filter(|p| program.starts_with("mecha-") && p.is_file())
{
sibling
} else {
program.into()
};
match tokio::process::Command::new(program)
.args(rest)
.stdin(std::process::Stdio::null())
.output()
.await
{
Ok(out) if out.status.success() => None,
Ok(out) => String::from_utf8_lossy(&out.stderr)
.lines()
.next()
.map(str::to_string),
Err(e) => Some(e.to_string()),
}
}
pub(crate) async fn item(&self, id: &str) -> Option<OutboxItem> {
let root = self.outbox_root.clone();
let id = id.to_string();
tokio::task::spawn_blocking(move || item_in(&root, &id))
.await
.ok()
.flatten()
}
}
fn item_in(root: &Path, id: &str) -> Option<OutboxItem> {
OutboxStore::open(root).ok()?.item_exact(id).ok().flatten()
}
fn store_outcome(
outbox_root: &Path,
action: &Action,
started: DateTime<Utc>,
child_note: Option<&str>,
) -> Outcome {
match action {
Action::OutboxSend { id } => {
draft_outcome(true, id, item_in(outbox_root, id).as_ref(), child_note)
}
Action::OutboxReject { id } => {
draft_outcome(false, id, item_in(outbox_root, id).as_ref(), child_note)
}
Action::RestartUnit { unit } => Outcome::of(
"unknown",
format!("{unit} — the restart outcome is read in run()"),
),
Action::TriggerRun { name } => {
trigger_run_outcome(name, latest_trigger_row(name, started).as_ref(), child_note)
}
Action::TriggerCancel { name } => cancel_outcome(name),
Action::TriggerEnable { name } => {
toggle_outcome(name, true, trigger_enabled(name), child_note)
}
Action::TriggerDisable { name } => {
toggle_outcome(name, false, trigger_enabled(name), child_note)
}
Action::MailImport { provider } => {
import_outcome(provider, registry_credentials_exist(provider), child_note)
}
Action::FrontdoorClose { seq, .. } => mark_outcome(
*seq,
mecha_core::frontdoor::CLOSED,
request_state(*seq).as_deref(),
child_note,
),
Action::FrontdoorNeedsInfo { seq, .. } => mark_outcome(
*seq,
mecha_core::frontdoor::NEEDS_INFO,
request_state(*seq).as_deref(),
child_note,
),
}
}
pub fn draft_outcome(
send: bool,
id: &str,
item: Option<&OutboxItem>,
child_note: Option<&str>,
) -> Outcome {
let Some(item) = item else {
return Outcome::of(
"unknown",
format!("draft `{id}` — outcome unknown; check `mecha outbox show {id}`"),
);
};
match item.status.as_str() {
"sent" => Outcome::of("sent", format!("Draft `{id}` sent")),
"rejected" => Outcome::of("rejected", format!("Draft `{id}` rejected")),
"pending" => match &item.error {
Some(error) => Outcome::of(
"failed",
format!("Draft `{id}` release failed: {error} — the draft is still pending"),
),
None => Outcome::of(
"failed",
match child_note {
Some(note) => {
format!("Draft `{id}` unchanged — {note}")
}
None => format!(
"Draft `{id}` unchanged — still pending; check `mecha outbox show {id}`"
),
},
),
},
other => Outcome::of(
other,
format!(
"Draft `{id}` is `{other}`{}",
if send {
" — nothing was sent by this tap"
} else {
""
}
),
),
}
}
pub fn restart_outcome(unit: &str, still_failed: bool) -> Outcome {
if still_failed {
Outcome::of(
"failed-again",
format!(
"Restarted {unit}, and it failed again — the fix is upstream \
(journalctl --user -u {unit} -n 20)"
),
)
} else {
Outcome::of("restarted", format!("Restarted {unit}, and it is running"))
}
}
pub fn trigger_run_outcome(
name: &str,
row: Option<&RunRecord>,
child_note: Option<&str>,
) -> Outcome {
match row {
Some(row) => {
let status = row.status.as_str();
let line = match &row.error {
Some(error) => format!("Trigger `{name}` ran: {status} — {error}"),
None => format!("Trigger `{name}` ran: {status}"),
};
Outcome::of(status, line)
}
None => Outcome::of(
"unknown",
match child_note {
Some(note) => format!("Trigger `{name}` recorded no run — {note}"),
None => {
format!("Trigger `{name}` recorded no run — see `mecha trigger runs {name}`")
}
},
),
}
}
fn cancel_outcome(name: &str) -> Outcome {
let Some(store) = TriggerStore::open_existing_default() else {
return Outcome::of("unknown", format!("no trigger store — `{name}` unknown"));
};
match store.running(name) {
Some(_) if store.cancel_requested(name) => Outcome::of(
"cancelling",
format!("Asked `{name}` to stop — it ends at its next safe point, partial turn kept"),
),
Some(_) => Outcome::of(
"unknown",
format!("`{name}` is still running and no cancel was recorded — try again"),
),
None => Outcome::of("stopped", format!("`{name}` is not running")),
}
}
fn trigger_enabled(name: &str) -> Option<bool> {
let store = TriggerStore::open_existing_default()?;
store.get(name).ok().map(|t| t.enabled)
}
pub fn toggle_outcome(
name: &str,
want_enabled: bool,
enabled_now: Option<bool>,
child_note: Option<&str>,
) -> Outcome {
let (verb, undo) = if want_enabled {
("enabled", "disable")
} else {
("disabled", "enable")
};
match enabled_now {
Some(state) if state == want_enabled => Outcome::of(
verb,
format!("Trigger `{name}` is {verb} — `mecha trigger {undo} {name}` undoes it"),
),
Some(_) => Outcome::of(
"failed",
match child_note {
Some(note) => format!("Trigger `{name}` is unchanged — {note}"),
None => format!("Trigger `{name}` is unchanged — see `mecha trigger show {name}`"),
},
),
None => Outcome::of(
"unknown",
format!("Trigger `{name}` could not be re-read — see `mecha trigger show {name}`"),
),
}
}
fn registry_credentials_exist(provider: &str) -> bool {
mecha_core::work::mecha_home()
.map(|home| {
home.join("mail")
.join(provider)
.join("oauth.json")
.is_file()
})
.unwrap_or(false)
}
pub fn import_outcome(
provider: &str,
credentials_present: bool,
child_note: Option<&str>,
) -> Outcome {
if credentials_present {
Outcome::of(
"imported",
format!(
"Imported the legacy {provider} login into the unified registry as \
`{provider}`. The import moves the login, not its health — \
re-authenticate at a terminal: `mecha-mail auth {provider} \
--provider {provider}`"
),
)
} else {
Outcome::of(
"failed",
match child_note {
Some(note) => format!("The {provider} import made no account — {note}"),
None => {
format!("The {provider} import made no account — see `mecha-mail accounts`")
}
},
)
}
}
fn request_state(seq: i64) -> Option<String> {
let store = mecha_core::frontdoor::Frontdoor::open_default().ok()?;
store.record(seq).ok().map(|r| r.state)
}
pub fn mark_outcome(
seq: i64,
want: &str,
state_now: Option<&str>,
child_note: Option<&str>,
) -> Outcome {
match state_now {
Some(state) if state == want => match want {
mecha_core::frontdoor::NEEDS_INFO => Outcome::of(
want,
format!("Request {seq} is parked as `needs_info` — it waits on the requester now"),
),
_ => Outcome::of(want, format!("Request {seq} is `{want}`")),
},
Some(state) => Outcome::of(
"failed",
match child_note {
Some(note) => format!("Request {seq} is still `{state}` — {note}"),
None => {
format!("Request {seq} is still `{state}` — see `mecha frontdoor show {seq}`")
}
},
),
None => Outcome::of(
"unknown",
format!("Request {seq} could not be re-read — see `mecha frontdoor list`"),
),
}
}
async fn unit_is_failed(unit: &str) -> bool {
let unit = unit.to_string();
tokio::task::spawn_blocking(move || crate::commands::doctor::unit_is_failed(&unit))
.await
.unwrap_or(false)
}
fn row_is_this_taps(row: &RunRecord, name: &str, since: DateTime<Utc>) -> bool {
row.trigger == name && row.manual && row.started_at >= since
}
fn latest_trigger_row(name: &str, since: DateTime<Utc>) -> Option<RunRecord> {
latest_trigger_row_in(&TriggerStore::open_existing_default()?, name, since)
}
fn latest_trigger_row_in(
store: &TriggerStore,
name: &str,
since: DateTime<Utc>,
) -> Option<RunRecord> {
let mut found = None;
let _ = store.scan_runs_rev(|row| {
if row_is_this_taps(&row, name, since) {
found = Some(row);
false
} else {
true
}
});
found
}
#[cfg(test)]
mod tests {
use super::*;
use mecha_core::trigger::RunStatus;
fn remedy(argv: &[&str], needs_terminal: bool) -> Remedy {
Remedy {
description: "a remedy".into(),
argv: argv.iter().map(|s| s.to_string()).collect(),
needs_terminal,
}
}
#[test]
fn from_remedy_recognises_exactly_the_three_shapes() {
assert_eq!(
Action::from_remedy(&remedy(
&["systemctl", "--user", "restart", "mecha-triggers.service"],
false
)),
Some(Action::RestartUnit {
unit: "mecha-triggers.service".into()
})
);
assert_eq!(
Action::from_remedy(&remedy(&["mecha", "trigger", "run", "briefing"], false)),
Some(Action::TriggerRun {
name: "briefing".into()
})
);
assert_eq!(
Action::from_remedy(&remedy(
&["mecha-mail", "import", "google", "--provider", "google"],
false
)),
Some(Action::MailImport {
provider: "google".into()
})
);
}
#[test]
fn from_remedy_refuses_every_terminal_bound_and_unrecognised_shape() {
assert_eq!(
Action::from_remedy(&remedy(
&["systemctl", "--user", "restart", "mecha-triggers.service"],
true
)),
None
);
for argv in [
vec!["mecha-mail", "auth", "personal", "--provider", "google"],
vec!["mecha-mail", "import", "aol", "--provider", "aol"],
vec!["mecha-mail", "import", "personal", "--provider", "google"],
vec!["mecha-mail", "import", "google", "--provider", "outlook"],
vec![
"mecha-mail",
"import",
"google",
"--provider",
"google",
"--force",
],
vec!["mecha", "outbox", "review"],
vec!["mecha", "frontdoor", "list"],
vec!["systemctl", "--user", "restart", "nginx.service"],
vec!["systemctl", "--user", "restart", "mecha-x.service", "--now"],
vec!["systemctl", "--user", "restart", "mecha-X.SERVICE"],
vec!["systemctl", "--user", "restart", "mecha-.service"],
vec!["systemctl", "restart", "mecha-triggers.service"],
vec!["mecha", "trigger", "run", "briefing", "--force"],
vec!["mecha", "trigger", "delete", "briefing"],
vec!["mecha", "trigger", "run", "../escape"],
vec![],
] {
let r = remedy(&argv, false);
assert_eq!(Action::from_remedy(&r), None, "{argv:?} must not execute");
}
}
#[test]
fn argv_is_total_and_its_verbs_are_literals() {
let samples = [
Action::OutboxSend {
id: "abc-123".into(),
},
Action::OutboxReject {
id: "abc-123".into(),
},
Action::RestartUnit {
unit: "mecha-triggers.service".into(),
},
Action::TriggerRun {
name: "briefing".into(),
},
Action::TriggerCancel {
name: "briefing".into(),
},
Action::TriggerEnable {
name: "briefing".into(),
},
Action::TriggerDisable {
name: "briefing".into(),
},
Action::MailImport {
provider: "google".into(),
},
Action::FrontdoorClose {
seq: 5,
reason: "spam".into(),
},
Action::FrontdoorNeedsInfo {
seq: 5,
question: "which Tuesday?".into(),
},
];
for action in &samples {
let argv = action.argv();
assert!(!argv.is_empty());
assert!(
argv[0] == "mecha" || argv[0] == "systemctl" || argv[0] == "mecha-mail",
"{argv:?} spawns an unexpected program"
);
assert!(
argv.iter().any(|a| *a == action.value()),
"the object id rides as its own argument: {argv:?}"
);
}
}
#[test]
fn owner_typed_modal_text_rides_as_a_single_argv_element() {
let hostile = r#"done"; rm -rf ~; echo "--yes $(cat /etc/passwd)"#;
let close = Action::FrontdoorClose {
seq: 9,
reason: hostile.into(),
};
let argv = close.argv();
assert_eq!(
argv,
vec![
"mecha".to_string(),
"frontdoor".into(),
"close".into(),
"9".into(),
"--reason".into(),
hostile.into(),
],
"the text is one element, bytes intact, position fixed"
);
assert_eq!(
argv.iter().filter(|a| a.contains("rm -rf")).count(),
1,
"the hostile text exists only inside the one reason argument"
);
let park = Action::FrontdoorNeedsInfo {
seq: 9,
question: "which Tuesday — this week's, or next?".into(),
};
let argv = park.argv();
assert_eq!(argv[4], "--note");
assert_eq!(argv[5], "which Tuesday — this week's, or next?");
assert_eq!(argv.len(), 6);
}
#[test]
fn a_payload_round_trips_through_its_fixed_verb_and_carries_the_id_only() {
for action in [
Action::OutboxSend {
id: "abc-123".into(),
},
Action::OutboxReject {
id: "abc-123".into(),
},
Action::RestartUnit {
unit: "mecha-triggers.service".into(),
},
Action::TriggerRun {
name: "briefing".into(),
},
Action::TriggerCancel {
name: "briefing".into(),
},
Action::TriggerEnable {
name: "briefing".into(),
},
Action::TriggerDisable {
name: "briefing".into(),
},
Action::MailImport {
provider: "outlook".into(),
},
] {
let back = Action::from_payload(action.action_id(), &action.value());
assert_eq!(back, Some(action));
}
assert_eq!(
Action::from_payload(ids::OUTBOX_SEND_CONFIRM, "abc-123"),
Some(Action::OutboxSend {
id: "abc-123".into()
})
);
}
#[test]
fn a_payload_with_an_unknown_verb_or_a_hostile_value_is_refused() {
assert_eq!(Action::from_payload("slack_stop", "D1-1.0"), None);
assert_eq!(Action::from_payload("slack_action_run_command", "ls"), None);
for hostile in [
"mecha-x.service; rm -rf /",
"../../etc/passwd",
"mecha-X.SERVICE",
"nginx.service",
"",
] {
assert_eq!(
Action::from_payload(ids::RESTART_UNIT, hostile),
None,
"{hostile}"
);
}
for hostile in ["../escape", "a b", "UPPER", ""] {
assert_eq!(
Action::from_payload(ids::TRIGGER_RUN, hostile),
None,
"{hostile}"
);
assert_eq!(
Action::from_payload(ids::TRIGGER_CANCEL, hostile),
None,
"{hostile}"
);
assert_eq!(
Action::from_payload(ids::TRIGGER_ENABLE, hostile),
None,
"{hostile}"
);
assert_eq!(
Action::from_payload(ids::TRIGGER_DISABLE, hostile),
None,
"{hostile}"
);
}
for hostile in ["", "a b", "x/../y", &"x".repeat(200)] {
assert_eq!(
Action::from_payload(ids::OUTBOX_SEND, hostile),
None,
"{hostile}"
);
}
for hostile in ["aol", "google; rm -rf /", "GOOGLE", "google outlook", ""] {
assert_eq!(
Action::from_payload(ids::MAIL_IMPORT, hostile),
None,
"{hostile}"
);
}
assert_eq!(
Action::from_payload(ids::FRONTDOOR_CLOSE_SUBMIT, "5"),
None,
"a button cannot close a request — the reason comes from a modal"
);
assert_eq!(
Action::from_payload(ids::FRONTDOOR_NEEDS_INFO_SUBMIT, "5"),
None
);
}
#[test]
fn a_submission_round_trips_and_carries_the_owner_text_typed() {
let close = Action::from_submission(
ids::FRONTDOOR_CLOSE_SUBMIT,
"5",
" spam, politely declined ",
);
assert_eq!(
close,
Some(Action::FrontdoorClose {
seq: 5,
reason: "spam, politely declined".into()
}),
"trimmed, typed, and nothing else changed"
);
let park =
Action::from_submission(ids::FRONTDOOR_NEEDS_INFO_SUBMIT, "12", "which Tuesday?");
assert_eq!(
park,
Some(Action::FrontdoorNeedsInfo {
seq: 12,
question: "which Tuesday?".into()
})
);
}
#[test]
fn a_submission_with_a_bad_seq_an_empty_text_or_an_over_cap_text_is_refused() {
for bad_seq in ["", "abc", "-1", "0", "5; rm -rf /", "5 6", "1e3"] {
assert_eq!(
Action::from_submission(ids::FRONTDOOR_CLOSE_SUBMIT, bad_seq, "a reason"),
None,
"{bad_seq:?}"
);
}
for empty in ["", " ", "\n\t"] {
assert_eq!(
Action::from_submission(ids::FRONTDOOR_CLOSE_SUBMIT, "5", empty),
None,
"{empty:?}"
);
}
let over = "x".repeat(MODAL_TEXT_MAX + 1);
assert_eq!(
Action::from_submission(ids::FRONTDOOR_CLOSE_SUBMIT, "5", &over),
None
);
let at_cap = "x".repeat(MODAL_TEXT_MAX);
assert!(Action::from_submission(ids::FRONTDOOR_CLOSE_SUBMIT, "5", &at_cap).is_some());
assert_eq!(
Action::from_submission("slack_outbox_send", "5", "a reason"),
None,
"a message verb is not a modal verb"
);
assert_eq!(
Action::from_submission("anything_else", "5", "a reason"),
None
);
}
#[test]
fn the_ledger_writes_a_dispatch_row_and_an_outcome_row_that_share_a_tap_id() {
let dir = std::env::temp_dir().join(format!(
"mecha-action-ledger-{}-{:?}",
std::process::id(),
std::thread::current().id()
));
let _ = std::fs::remove_dir_all(&dir);
let path = dir.join("actions.jsonl");
let ledger = ActionLedger::at(&path);
let tap = new_tap_id();
let action = Action::RestartUnit {
unit: "mecha-triggers.service".into(),
};
ledger.dispatched(&tap, "U_OWNER", &action, "doctor");
ledger.resolved(&tap, "restarted", "Restarted mecha-triggers.service");
let text = std::fs::read_to_string(&path).unwrap();
let rows: Vec<LedgerRow> = text
.lines()
.map(|l| serde_json::from_str(l).unwrap())
.collect();
assert_eq!(rows.len(), 2);
match &rows[0] {
LedgerRow::Dispatch {
tap_id,
user_id,
action: recorded,
argv,
surface,
..
} => {
assert_eq!(tap_id, &tap);
assert_eq!(user_id, "U_OWNER");
assert_eq!(recorded, &action);
assert_eq!(argv, &action.argv(), "the argv is derived, and recorded");
assert_eq!(surface, "doctor");
}
other => panic!("expected a dispatch row, got {other:?}"),
}
match &rows[1] {
LedgerRow::Outcome { tap_id, status, .. } => {
assert_eq!(tap_id, &tap, "the rows share the tap id");
assert_eq!(status, "restarted");
}
other => panic!("expected an outcome row, got {other:?}"),
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mode = std::fs::metadata(&path).unwrap().permissions().mode() & 0o777;
assert_eq!(mode, 0o600, "the ledger names drafts and who pressed what");
}
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn a_ledger_that_cannot_write_never_blocks_the_action() {
let ledger = ActionLedger::at("/proc/no-such-dir/actions.jsonl");
let action = Action::TriggerRun {
name: "briefing".into(),
};
ledger.dispatched("t-1", "U_OWNER", &action, "doctor");
ledger.resolved("t-1", "ok", "ran");
}
#[test]
fn tap_ids_never_collide_within_a_process() {
let a = new_tap_id();
let b = new_tap_id();
assert_ne!(a, b);
}
fn item_with(status: &str, error: Option<&str>) -> OutboxItem {
use mecha_core::agent::Taint;
use mecha_core::outbox::OutboxKind;
OutboxItem {
id: "abc-123".into(),
status: status.into(),
tool: "mail__send".into(),
kind: OutboxKind::Message,
args_before: serde_json::json!({}),
args: serde_json::json!({}),
summary: "mail__send".into(),
session_id: None,
workspace: None,
taint: Taint::default(),
created_at: "2026-08-14T00:00:00Z".into(),
resolved_at: None,
reason: None,
error: error.map(String::from),
}
}
#[test]
fn a_drafts_outcome_is_the_items_status_never_the_childs_exit() {
let sent = draft_outcome(
true,
"abc-123",
Some(&item_with("sent", None)),
Some("child was killed"),
);
assert_eq!(sent.status, "sent");
assert!(sent.line.contains("sent"), "{}", sent.line);
assert!(
!sent.line.contains("killed"),
"a sent mail is sent, whatever the child said: {}",
sent.line
);
let failed = draft_outcome(
true,
"abc-123",
Some(&item_with("pending", Some("smtp said no"))),
None,
);
assert_eq!(failed.status, "failed");
assert!(failed.line.contains("smtp said no"), "{}", failed.line);
assert!(failed.line.contains("still pending"), "{}", failed.line);
let rejected = draft_outcome(false, "abc-123", Some(&item_with("rejected", None)), None);
assert_eq!(rejected.status, "rejected");
let unknown = draft_outcome(true, "abc-123", None, None);
assert_eq!(unknown.status, "unknown");
assert!(
unknown.line.contains("mecha outbox show abc-123"),
"{}",
unknown.line
);
}
#[test]
fn a_restart_reports_the_units_state_not_the_commands_exit() {
let ok = restart_outcome("mecha-triggers.service", false);
assert_eq!(ok.status, "restarted");
assert!(ok.line.contains("running"), "{}", ok.line);
let refailed = restart_outcome("mecha-triggers.service", true);
assert_eq!(refailed.status, "failed-again");
assert!(refailed.line.contains("upstream"), "{}", refailed.line);
assert!(
refailed
.line
.contains("journalctl --user -u mecha-triggers.service"),
"{}",
refailed.line
);
}
#[test]
fn a_trigger_runs_outcome_is_the_ledger_row_and_a_skip_is_an_answer() {
let mut row = RunRecord::started("briefing", None, true);
row.status = RunStatus::Error;
row.error = Some("provider said no".into());
let out = trigger_run_outcome("briefing", Some(&row), None);
assert_eq!(out.status, "error");
assert!(out.line.contains("provider said no"), "{}", out.line);
let mut skipped = RunRecord::started("briefing", None, true);
skipped.status = RunStatus::SkippedOverlap;
let out = trigger_run_outcome("briefing", Some(&skipped), None);
assert_eq!(out.status, "skipped (overlap)");
let none = trigger_run_outcome("briefing", None, None);
assert_eq!(none.status, "unknown");
assert!(
none.line.contains("mecha trigger runs briefing"),
"{}",
none.line
);
}
#[test]
fn an_enable_or_disable_reports_the_flag_as_it_now_stands() {
let on = toggle_outcome("briefing", true, Some(true), Some("child was killed"));
assert_eq!(on.status, "enabled");
assert!(
on.line.contains("mecha trigger disable briefing"),
"{}",
on.line
);
assert!(!on.line.contains("killed"), "{}", on.line);
let off = toggle_outcome("briefing", false, Some(false), None);
assert_eq!(off.status, "disabled");
assert!(
off.line.contains("mecha trigger enable briefing"),
"{}",
off.line
);
let unchanged = toggle_outcome("briefing", false, Some(true), Some("store locked"));
assert_eq!(unchanged.status, "failed");
assert!(
unchanged.line.contains("store locked"),
"{}",
unchanged.line
);
let unknown = toggle_outcome("briefing", true, None, None);
assert_eq!(unknown.status, "unknown");
assert!(
unknown.line.contains("mecha trigger show briefing"),
"{}",
unknown.line
);
}
#[test]
fn an_import_reports_the_registry_and_names_the_reauth_it_cannot_do() {
let ok = import_outcome("google", true, None);
assert_eq!(ok.status, "imported");
assert!(
ok.line.contains("mecha-mail auth google --provider google"),
"{}",
ok.line
);
let failed = import_outcome("outlook", false, Some("already has credentials"));
assert_eq!(failed.status, "failed");
assert!(
failed.line.contains("already has credentials"),
"{}",
failed.line
);
}
#[test]
fn a_daemon_fired_run_is_never_attributed_to_a_tap() {
let since = Utc::now();
let late = since + chrono::Duration::seconds(2);
let mut daemon = RunRecord::started("briefing", Some(since), false);
daemon.started_at = late;
assert!(
!row_is_this_taps(&daemon, "briefing", since),
"a scheduled row is the daemon's evidence, not the tap's"
);
let mut manual = RunRecord::started("briefing", None, true);
manual.started_at = late;
assert!(row_is_this_taps(&manual, "briefing", since));
let mut earlier = RunRecord::started("briefing", None, true);
earlier.started_at = since - chrono::Duration::seconds(2);
assert!(!row_is_this_taps(&earlier, "briefing", since));
let mut other = RunRecord::started("nightly", None, true);
other.started_at = late;
assert!(!row_is_this_taps(&other, "briefing", since));
}
#[test]
fn a_taps_row_is_found_from_the_ledger_tail_past_a_torn_old_line() {
use std::io::Write;
let dir = std::env::temp_dir().join(format!(
"mecha-action-tail-{}-{:?}",
std::process::id(),
std::thread::current().id()
));
let _ = std::fs::remove_dir_all(&dir);
let store = mecha_core::trigger::TriggerStore::open(&dir).unwrap();
{
let mut file = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(store.ledger_path())
.unwrap();
file.write_all(b"{\"trigger\": \"briefing\xff\xfe\n")
.unwrap();
}
let since = Utc::now();
let mut before = RunRecord::started("briefing", None, true);
before.started_at = since - chrono::Duration::seconds(30);
store.append_run(&before).unwrap();
let mut mine = RunRecord::started("briefing", None, true);
mine.started_at = since + chrono::Duration::seconds(2);
mine.status = RunStatus::Ok;
store.append_run(&mine).unwrap();
let row = latest_trigger_row_in(&store, "briefing", since).expect("the tap's row");
assert_eq!(row.started_at, mine.started_at);
assert!(row.manual);
assert!(store.runs().is_err());
assert!(latest_trigger_row_in(&store, "nightly", since).is_none());
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn a_confirm_value_carries_the_id_and_a_fingerprint_of_the_shown_bytes() {
let args = "{\n \"to\": \"a@x.org\"\n}";
let value = confirm_value("abc-123", args);
let (id, fp) = parse_confirm_value(&value).expect("round trips");
assert_eq!(id, "abc-123");
assert_eq!(fp, Some(fingerprint(args).as_str()));
assert_ne!(
fingerprint(args),
fingerprint("{\n \"to\": \"b@x.org\"\n}")
);
assert_eq!(fingerprint(""), "cbf29ce484222325");
assert_eq!(parse_confirm_value("abc-123"), Some(("abc-123", None)));
assert_eq!(parse_confirm_value("a b#deadbeef"), None);
assert_eq!(parse_confirm_value("../x#00"), None);
assert_eq!(parse_confirm_value(""), None);
}
#[test]
fn a_frontdoor_mark_reports_the_requests_state_never_the_childs_exit() {
let closed = mark_outcome(5, "closed", Some("closed"), Some("child was killed"));
assert_eq!(closed.status, "closed");
assert!(!closed.line.contains("killed"), "{}", closed.line);
let parked = mark_outcome(5, "needs_info", Some("needs_info"), None);
assert_eq!(parked.status, "needs_info");
assert!(parked.line.contains("requester"), "{}", parked.line);
let stuck = mark_outcome(
5,
"closed",
Some("extracted"),
Some("no request with seq 5"),
);
assert_eq!(stuck.status, "failed");
assert!(stuck.line.contains("extracted"), "{}", stuck.line);
assert!(
stuck.line.contains("no request with seq 5"),
"{}",
stuck.line
);
let unknown = mark_outcome(5, "closed", None, None);
assert_eq!(unknown.status, "unknown");
assert!(
unknown.line.contains("mecha frontdoor list"),
"{}",
unknown.line
);
}
}