use std::sync::Arc;
use zero_commands::{
AutoAction, AutoMode, AutoRequest, AutoSource, Command, DispatchContext, FrictionDecision,
HeadlessAction, MockAutoSource, MockSupervisorSource, OutputLine, RiskDirection, StaticLabel,
SupervisorAction, SupervisorError, SupervisorReply, SupervisorSource, SupervisorState,
dispatch, parse_line, resolve,
};
use zero_engine_client::EngineState;
use zero_operator_state::Label;
fn ctx_at(label: Label) -> DispatchContext {
DispatchContext::new(None, EngineState::shared()).with_state(Arc::new(StaticLabel(label)))
}
fn ctx_tilt_with_auto(src: Arc<dyn AutoSource>) -> DispatchContext {
ctx_at(Label::Tilt).with_auto(src)
}
fn ctx_steady_with_auto(src: Arc<dyn AutoSource>) -> DispatchContext {
ctx_at(Label::Steady).with_auto(src)
}
fn ctx_with_supervisor(src: Arc<dyn SupervisorSource>) -> DispatchContext {
DispatchContext::new(None, EngineState::shared()).with_supervisor(src)
}
#[test]
fn parser_auto_resolves_on_off_status_and_missing_and_unknown() {
let r = |s: &str| resolve(&parse_line(s)).unwrap();
assert_eq!(
r("/auto on"),
Command::Auto {
action: AutoAction::On
}
);
assert_eq!(
r("/auto OFF"), Command::Auto {
action: AutoAction::Off
}
);
assert_eq!(
r("/auto status"),
Command::Auto {
action: AutoAction::Status
}
);
assert_eq!(
r("/auto true"),
Command::Auto {
action: AutoAction::On
}
);
assert_eq!(
r("/auto 0"),
Command::Auto {
action: AutoAction::Off
}
);
assert_eq!(
r("/auto show"),
Command::Auto {
action: AutoAction::Status
}
);
assert_eq!(
r("/auto"),
Command::Auto {
action: AutoAction::Missing
}
);
assert_eq!(
r("/auto wiggle"),
Command::Auto {
action: AutoAction::Unknown("wiggle".into())
}
);
}
#[test]
fn parser_headless_resolves_start_stop_status_and_missing_and_unknown() {
let r = |s: &str| resolve(&parse_line(s)).unwrap();
assert_eq!(
r("/headless start"),
Command::Headless {
action: HeadlessAction::Start
}
);
assert_eq!(
r("/headless stop"),
Command::Headless {
action: HeadlessAction::Stop
}
);
assert_eq!(
r("/headless STATUS"), Command::Headless {
action: HeadlessAction::Status
}
);
assert_eq!(
r("/headless up"),
Command::Headless {
action: HeadlessAction::Start
}
);
assert_eq!(
r("/headless down"),
Command::Headless {
action: HeadlessAction::Stop
}
);
assert_eq!(
r("/headless"),
Command::Headless {
action: HeadlessAction::Missing
}
);
assert_eq!(
r("/headless restart"),
Command::Headless {
action: HeadlessAction::Unknown("restart".into())
}
);
}
#[test]
fn auto_on_classifies_as_risk_increases_off_and_status_as_neutral() {
let on = Command::Auto {
action: AutoAction::On,
};
let off = Command::Auto {
action: AutoAction::Off,
};
let status = Command::Auto {
action: AutoAction::Status,
};
let missing = Command::Auto {
action: AutoAction::Missing,
};
let unknown = Command::Auto {
action: AutoAction::Unknown("foo".into()),
};
assert_eq!(on.risk(), RiskDirection::Increases);
assert_eq!(off.risk(), RiskDirection::Neutral);
assert_eq!(status.risk(), RiskDirection::Neutral);
assert_eq!(missing.risk(), RiskDirection::Neutral);
assert_eq!(unknown.risk(), RiskDirection::Neutral);
}
#[tokio::test]
async fn auto_on_at_tilt_triggers_typed_confirm_and_carries_pending() {
let src = Arc::new(MockAutoSource::new(AutoMode::Off));
let ctx = ctx_tilt_with_auto(src.clone());
let out = dispatch(&ctx, "/auto on").await.unwrap().unwrap();
assert_eq!(out.risk, Some(RiskDirection::Increases));
assert!(
matches!(out.friction, Some(FrictionDecision::TypedConfirm { .. })),
"expected TypedConfirm at TILT, got {:?}",
out.friction
);
assert_eq!(
out.pending_command,
Some(Command::Auto {
action: AutoAction::On
}),
);
assert_eq!(
src.current(),
AutoMode::Off,
"adapter must not flip until the operator honors friction",
);
}
#[tokio::test]
async fn auto_off_and_status_are_never_gated_even_at_tilt() {
let src = Arc::new(MockAutoSource::new(AutoMode::On));
let ctx = ctx_tilt_with_auto(src.clone());
let off = dispatch(&ctx, "/auto off").await.unwrap().unwrap();
assert_eq!(off.risk, Some(RiskDirection::Neutral));
assert!(matches!(off.friction, Some(FrictionDecision::Proceed)));
assert!(off.pending_command.is_none());
assert_eq!(src.current(), AutoMode::Off, "adapter ran at TILT");
let status = dispatch(&ctx, "/auto status").await.unwrap().unwrap();
assert!(matches!(status.friction, Some(FrictionDecision::Proceed)));
assert_eq!(status.risk, Some(RiskDirection::Neutral));
}
#[tokio::test]
async fn headless_is_never_gated_regardless_of_label() {
let src = Arc::new(MockSupervisorSource::new(false));
let ctx = ctx_at(Label::Tilt).with_supervisor(src.clone());
for line in ["/headless start", "/headless status", "/headless stop"] {
let out = dispatch(&ctx, line).await.unwrap().unwrap();
assert_eq!(out.risk, Some(RiskDirection::Neutral), "line = {line}");
assert!(
matches!(out.friction, Some(FrictionDecision::Proceed)),
"line = {line}, got {:?}",
out.friction
);
assert!(out.pending_command.is_none());
}
}
#[tokio::test]
async fn auto_on_at_steady_flips_adapter_and_renders_changed_line() {
let src = Arc::new(MockAutoSource::new(AutoMode::Off));
let ctx = ctx_steady_with_auto(src.clone());
let out = dispatch(&ctx, "/auto on").await.unwrap().unwrap();
assert!(matches!(out.friction, Some(FrictionDecision::Proceed)));
assert_eq!(src.current(), AutoMode::On);
let OutputLine::Command(line) = &out.lines[0] else {
panic!("expected Command, got {:?}", out.lines);
};
assert!(line.contains("mode=on"), "line = {line:?}");
assert!(line.contains("changed"), "line = {line:?}");
}
#[tokio::test]
async fn auto_on_when_already_on_surfaces_warn_not_alert() {
let src = Arc::new(MockAutoSource::new(AutoMode::On));
let ctx = ctx_steady_with_auto(src.clone());
let out = dispatch(&ctx, "/auto on").await.unwrap().unwrap();
let OutputLine::Warn(line) = &out.lines[0] else {
panic!("expected Warn, got {:?}", out.lines);
};
assert!(line.contains("already on"), "line = {line:?}");
}
#[tokio::test]
async fn auto_without_adapter_surfaces_unavailable_alert() {
let ctx = ctx_at(Label::Steady);
let out = dispatch(&ctx, "/auto on").await.unwrap().unwrap();
assert_eq!(out.risk, Some(RiskDirection::Increases));
let OutputLine::Alert(line) = &out.lines[0] else {
panic!("expected Alert, got {:?}", out.lines);
};
assert!(line.contains("unavailable"), "line = {line:?}");
}
#[tokio::test]
async fn auto_missing_and_unknown_surface_usage_hints() {
let src = Arc::new(MockAutoSource::new(AutoMode::Off));
let ctx = ctx_steady_with_auto(src.clone());
let missing = dispatch(&ctx, "/auto").await.unwrap().unwrap();
let OutputLine::System(m) = &missing.lines[0] else {
panic!("expected System, got {:?}", missing.lines);
};
assert!(m.contains("usage"), "missing line = {m:?}");
assert!(m.contains("on | off | status"), "missing line = {m:?}");
let unknown = dispatch(&ctx, "/auto wiggle").await.unwrap().unwrap();
let OutputLine::Warn(u) = &unknown.lines[0] else {
panic!("expected Warn, got {:?}", unknown.lines);
};
assert!(u.contains("wiggle"), "unknown line = {u:?}");
assert_eq!(src.current(), AutoMode::Off);
}
#[tokio::test]
async fn headless_start_then_status_reports_running_with_socket() {
let src = Arc::new(MockSupervisorSource::new(false));
let ctx = ctx_with_supervisor(src.clone());
let started = dispatch(&ctx, "/headless start").await.unwrap().unwrap();
assert!(matches!(started.friction, Some(FrictionDecision::Proceed)));
let OutputLine::Command(s) = &started.lines[0] else {
panic!("expected Command, got {:?}", started.lines);
};
assert!(s.contains("start"), "line = {s:?}");
assert!(s.contains("running"), "line = {s:?}");
assert!(s.contains("changed"), "line = {s:?}");
assert!(s.contains("socket=<operator-socket>"), "line = {s:?}");
assert!(src.is_running());
let status = dispatch(&ctx, "/headless status").await.unwrap().unwrap();
let OutputLine::Command(line) = &status.lines[0] else {
panic!("expected Command, got {:?}", status.lines);
};
assert!(line.contains("status"), "line = {line:?}");
assert!(line.contains("running"), "line = {line:?}");
assert!(
!line.contains("changed"),
"status must not claim `changed`, got: {line:?}"
);
}
#[tokio::test]
async fn headless_without_adapter_surfaces_unavailable_alert() {
let ctx = ctx_at(Label::Steady);
let out = dispatch(&ctx, "/headless start").await.unwrap().unwrap();
let OutputLine::Alert(line) = &out.lines[0] else {
panic!("expected Alert, got {:?}", out.lines);
};
assert!(line.contains("unavailable"), "line = {line:?}");
}
#[tokio::test]
async fn headless_refused_surfaces_warn_not_alert() {
struct AlwaysRefuse;
impl SupervisorSource for AlwaysRefuse {
fn act(&self, _action: SupervisorAction) -> Result<SupervisorReply, SupervisorError> {
Err(SupervisorError::Refused("already stopping".into()))
}
fn tear_down_socket(&self) -> Result<bool, SupervisorError> {
Ok(false)
}
}
let ctx = ctx_with_supervisor(Arc::new(AlwaysRefuse));
let out = dispatch(&ctx, "/headless stop").await.unwrap().unwrap();
let OutputLine::Warn(line) = &out.lines[0] else {
panic!("expected Warn, got {:?}", out.lines);
};
assert!(line.contains("refused"), "line = {line:?}");
assert!(line.contains("already stopping"), "line = {line:?}");
}
#[tokio::test]
async fn kill_with_running_supervisor_tears_down_socket_and_tags_line() {
let src = Arc::new(MockSupervisorSource::new(true));
let ctx = ctx_with_supervisor(src.clone());
assert!(src.is_running());
let out = dispatch(&ctx, "/kill").await.unwrap().unwrap();
assert_eq!(out.risk, Some(RiskDirection::Reduces));
assert!(matches!(out.friction, Some(FrictionDecision::Proceed)));
assert!(
out.lines.iter().any(|line| matches!(
line,
OutputLine::Alert(s) if s.contains("engine client unavailable")
)),
"live kill honesty line missing: {:?}",
out.lines,
);
assert!(
out.lines.iter().any(|line| matches!(
line,
OutputLine::Alert(s) if s.contains("headless supervisor") && s.contains("operator-local socket")
)),
"headless tear-down line missing: {:?}",
out.lines,
);
assert!(!src.is_running(), "daemon must be stopped");
assert!(src.socket_torn_down(), "socket must have been torn down");
}
#[tokio::test]
async fn kill_without_supervisor_surfaces_missing_engine_client() {
let ctx = ctx_at(Label::Steady);
let out = dispatch(&ctx, "/kill").await.unwrap().unwrap();
let OutputLine::Alert(line) = &out.lines[0] else {
panic!("expected Alert, got {:?}", out.lines);
};
assert!(
line.contains("engine client unavailable"),
"line = {line:?}"
);
assert!(line.contains("live kill not posted"), "line = {line:?}");
}
#[tokio::test]
async fn kill_with_stopped_supervisor_does_not_tag_line() {
let src = Arc::new(MockSupervisorSource::new(false));
let ctx = ctx_with_supervisor(src.clone());
let out = dispatch(&ctx, "/kill").await.unwrap().unwrap();
let OutputLine::Alert(line) = &out.lines[0] else {
panic!("expected Alert, got {:?}", out.lines);
};
assert!(
!line.contains("headless"),
"line must not tag when daemon was already stopped, got: {line:?}"
);
assert!(
line.contains("engine client unavailable"),
"line = {line:?}"
);
}
#[test]
fn auto_request_enum_covers_every_actionable_variant() {
let all = [AutoRequest::On, AutoRequest::Off, AutoRequest::Status];
assert_eq!(all.len(), 3);
let src = MockAutoSource::new(AutoMode::Off);
for r in all {
let reply = src.act(r).unwrap();
match r {
AutoRequest::On => assert_eq!(reply.mode, AutoMode::On),
AutoRequest::Off => assert_eq!(reply.mode, AutoMode::Off),
AutoRequest::Status => { }
}
}
}
#[test]
fn supervisor_action_enum_covers_every_actionable_variant() {
let src = MockSupervisorSource::new(false);
for action in [
SupervisorAction::Start,
SupervisorAction::Status,
SupervisorAction::Stop,
] {
let reply = src.act(action).unwrap();
match action {
SupervisorAction::Start => assert_eq!(reply.state, SupervisorState::Running),
SupervisorAction::Stop => assert_eq!(reply.state, SupervisorState::Stopped),
SupervisorAction::Status => { }
}
}
}