use crate::parse::ParsedLine;
use crate::risk::RiskDirection;
use zero_engine_client::ExecuteSide;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ModeTarget {
Conversation,
Positions,
Decisions,
Heat,
Cockpit,
}
impl ModeTarget {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Conversation => "conversation",
Self::Positions => "positions",
Self::Decisions => "decisions",
Self::Heat => "heat",
Self::Cockpit => "cockpit",
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum OverlayTarget {
State,
Verdict(Box<zero_engine_client::Evaluation>),
}
impl OverlayTarget {
#[must_use]
pub const fn as_str(&self) -> &'static str {
match self {
Self::State => "state",
Self::Verdict(_) => "verdict",
}
}
}
impl Eq for OverlayTarget {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Command {
Help,
Quit,
Clear,
SwitchMode(ModeTarget),
Status,
Brief,
Risk,
HyperliquidStatus {
symbol: Option<String>,
},
HyperliquidAccount,
HyperliquidReconcile,
LiveCertify,
LiveCockpit,
LiveEvidence,
LiveReceipts,
LiveCanaryPolicy,
RuntimeParity,
Immune,
Quote {
symbol: Option<String>,
},
Regime {
coin: Option<String>,
},
Evaluate {
coin: Option<String>,
extras: Vec<String>,
},
Positions,
Pulse {
limit: Option<u32>,
},
Approaching,
Rejections {
coin: Option<String>,
limit: Option<u32>,
},
Kill,
FlattenAll,
PauseEntries,
ResumeEntries,
Break {
minutes: Option<u32>,
},
Execute,
ExecuteOrder {
coin: Option<String>,
side: Option<ExecuteSide>,
size: Option<String>,
error: Option<String>,
},
State,
Sessions {
limit: Option<u32>,
},
Resume {
needle: Option<String>,
},
Fork,
Heat,
Save {
label: Option<String>,
},
Replay {
needle: Option<String>,
},
Share {
needle: Option<String>,
},
Config {
action: ConfigAction,
},
Verbose {
action: VerboseAction,
},
StateOverride {
label: Option<StateOverrideLabel>,
},
Continue,
Close {
coin: Option<String>,
},
WrapOff,
CoachingReset,
DisclosureOverride {
confirmed: bool,
},
Rate {
trade_id: Option<String>,
rating: Option<u8>,
},
ZeroPrefix {
rest: String,
},
Auto {
action: AutoAction,
},
Headless {
action: HeadlessAction,
},
Unknown(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StateOverrideLabel {
Fresh,
Steady,
Elevated,
Tilt,
Fatigued,
Recovery,
}
impl StateOverrideLabel {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Fresh => "FRESH",
Self::Steady => "STEADY",
Self::Elevated => "ELEVATED",
Self::Tilt => "TILT",
Self::Fatigued => "FATIGUED",
Self::Recovery => "RECOVERY",
}
}
#[must_use]
pub fn parse(s: &str) -> Option<Self> {
match s.trim().to_ascii_uppercase().as_str() {
"FRESH" => Some(Self::Fresh),
"STEADY" => Some(Self::Steady),
"ELEVATED" => Some(Self::Elevated),
"TILT" => Some(Self::Tilt),
"FATIGUED" => Some(Self::Fatigued),
"RECOVERY" => Some(Self::Recovery),
_ => None,
}
}
}
pub const DISCLOSURE_OVERRIDE_CONFIRM: &str = "--i-know-what-i-am-doing";
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum VerboseAction {
On,
Off,
Toggle,
Unknown(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ConfigAction {
Show,
Doctor,
Missing,
Unknown(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AutoAction {
On,
Off,
Status,
Missing,
Unknown(String),
}
impl AutoAction {
#[must_use]
pub const fn is_risk_increasing(&self) -> bool {
matches!(self, Self::On)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum HeadlessAction {
Start,
Stop,
Status,
Missing,
Unknown(String),
}
impl Command {
#[must_use]
pub const fn risk(&self) -> RiskDirection {
match self {
Self::Help
| Self::Clear
| Self::SwitchMode(_)
| Self::Status
| Self::Brief
| Self::Risk
| Self::HyperliquidStatus { .. }
| Self::HyperliquidAccount
| Self::HyperliquidReconcile
| Self::LiveCertify
| Self::LiveCockpit
| Self::LiveEvidence
| Self::LiveReceipts
| Self::LiveCanaryPolicy
| Self::RuntimeParity
| Self::Immune
| Self::Quote { .. }
| Self::Regime { .. }
| Self::Evaluate { .. }
| Self::Positions
| Self::Pulse { .. }
| Self::Approaching
| Self::Rejections { .. }
| Self::State
| Self::Heat
| Self::Sessions { .. }
| Self::Resume { .. }
| Self::Fork
| Self::Save { .. }
| Self::Replay { .. }
| Self::Share { .. }
| Self::Config { .. }
| Self::Verbose { .. }
| Self::Continue
| Self::WrapOff
| Self::CoachingReset
| Self::Rate { .. }
| Self::ZeroPrefix { .. }
| Self::Headless { .. }
| Self::Unknown(_) => RiskDirection::Neutral,
Self::Quit
| Self::Kill
| Self::FlattenAll
| Self::PauseEntries
| Self::Break { .. }
| Self::Close { .. } => RiskDirection::Reduces,
Self::Execute
| Self::ResumeEntries
| Self::StateOverride { .. }
| Self::DisclosureOverride { .. } => RiskDirection::Increases,
Self::ExecuteOrder {
coin,
side,
size,
error,
} => {
if coin.is_some() && side.is_some() && size.is_some() && error.is_none() {
RiskDirection::Increases
} else {
RiskDirection::Neutral
}
}
Self::Auto { action } => {
if action.is_risk_increasing() {
RiskDirection::Increases
} else {
RiskDirection::Neutral
}
}
}
}
#[must_use]
pub const fn name(&self) -> &'static str {
match self {
Self::Help => "/help",
Self::Quit => "/quit",
Self::Clear => "/clear",
Self::SwitchMode(ModeTarget::Conversation) => "/conv",
Self::SwitchMode(ModeTarget::Positions) => "/positions (mode)",
Self::SwitchMode(ModeTarget::Decisions) => "/decisions",
Self::SwitchMode(ModeTarget::Heat) => "/heat-mode",
Self::SwitchMode(ModeTarget::Cockpit) => "/cockpit-mode",
Self::Heat => "/heat",
Self::Status => "/status",
Self::Brief => "/brief",
Self::Risk => "/risk",
Self::HyperliquidStatus { .. } => "/hl-status",
Self::HyperliquidAccount => "/hl-account",
Self::HyperliquidReconcile => "/hl-reconcile",
Self::LiveCertify => "/live-certify",
Self::LiveCockpit => "/live-cockpit",
Self::LiveEvidence => "/live-evidence",
Self::LiveReceipts => "/live-receipts",
Self::LiveCanaryPolicy => "/live-canary",
Self::RuntimeParity => "/runtime-parity",
Self::Immune => "/immune",
Self::Quote { .. } => "/quote",
Self::Regime { .. } => "/regime",
Self::Evaluate { .. } => "/evaluate",
Self::Positions => "/pos",
Self::Pulse { .. } => "/pulse",
Self::Approaching => "/approaching",
Self::Rejections { .. } => "/rejections",
Self::Kill => "/kill",
Self::FlattenAll => "/flatten-all",
Self::PauseEntries => "/pause-entries",
Self::ResumeEntries => "/resume-entries",
Self::Break { .. } => "/break",
Self::Execute | Self::ExecuteOrder { .. } => "/execute",
Self::State => "/state",
Self::Sessions { .. } => "/sessions",
Self::Resume { .. } => "/resume",
Self::Fork => "/fork",
Self::Save { .. } => "/save",
Self::Replay { .. } => "/replay",
Self::Share { .. } => "/share",
Self::Config { .. } => "/config",
Self::Verbose { .. } => "/verbose",
Self::StateOverride { .. } => "/state-override",
Self::Continue => "/continue",
Self::Close { .. } => "/close",
Self::WrapOff => "/wrap-off",
Self::CoachingReset => "/coaching reset",
Self::DisclosureOverride { .. } => "/disclosure-override",
Self::Rate { .. } => "/rate",
Self::ZeroPrefix { .. } => "(zero-prefix)",
Self::Auto { .. } => "/auto",
Self::Headless { .. } => "/headless",
Self::Unknown(_) => "(unknown)",
}
}
#[must_use]
pub const fn default_pulse_limit() -> u32 {
20
}
#[must_use]
pub const fn default_rejections_limit() -> u32 {
20
}
#[must_use]
pub const fn default_sessions_limit() -> u32 {
20
}
#[must_use]
pub const fn max_sessions_limit() -> u32 {
50
}
}
pub const COMMAND_CATALOG: &[CommandInfo] = &[
CommandInfo {
name: "/help",
summary: "list commands",
risk: RiskDirection::Neutral,
},
CommandInfo {
name: "/status",
summary: "operator + engine snapshot",
risk: RiskDirection::Neutral,
},
CommandInfo {
name: "/brief",
summary: "one-line situation readout",
risk: RiskDirection::Neutral,
},
CommandInfo {
name: "/risk",
summary: "risk posture",
risk: RiskDirection::Neutral,
},
CommandInfo {
name: "/hl-status",
summary: "read-only Hyperliquid info status",
risk: RiskDirection::Neutral,
},
CommandInfo {
name: "/hl-account",
summary: "read-only Hyperliquid account truth",
risk: RiskDirection::Neutral,
},
CommandInfo {
name: "/hl-reconcile",
summary: "Hyperliquid account reconciliation",
risk: RiskDirection::Neutral,
},
CommandInfo {
name: "/live-certify",
summary: "dry-run live certification harness",
risk: RiskDirection::Neutral,
},
CommandInfo {
name: "/live-cockpit",
summary: "live readiness cockpit",
risk: RiskDirection::Neutral,
},
CommandInfo {
name: "/live-evidence",
summary: "hash-only live evidence bundle",
risk: RiskDirection::Neutral,
},
CommandInfo {
name: "/live-receipts",
summary: "public-safe execution receipts",
risk: RiskDirection::Neutral,
},
CommandInfo {
name: "/live-canary",
summary: "canary readiness and proof policy",
risk: RiskDirection::Neutral,
},
CommandInfo {
name: "/runtime-parity",
summary: "production-parity OODA report",
risk: RiskDirection::Neutral,
},
CommandInfo {
name: "/immune",
summary: "immune breaker state",
risk: RiskDirection::Neutral,
},
CommandInfo {
name: "/quote",
summary: "active paper quote source",
risk: RiskDirection::Neutral,
},
CommandInfo {
name: "/heat",
summary: "composite heat (risk + circuit)",
risk: RiskDirection::Neutral,
},
CommandInfo {
name: "/regime",
summary: "market regime (optional coin)",
risk: RiskDirection::Neutral,
},
CommandInfo {
name: "/evaluate",
summary: "gate verdict for a coin (overlay)",
risk: RiskDirection::Neutral,
},
CommandInfo {
name: "/pos",
summary: "open positions",
risk: RiskDirection::Neutral,
},
CommandInfo {
name: "/pulse",
summary: "recent engine events",
risk: RiskDirection::Neutral,
},
CommandInfo {
name: "/approaching",
summary: "coins near a gate",
risk: RiskDirection::Neutral,
},
CommandInfo {
name: "/rejections",
summary: "recent gate rejections",
risk: RiskDirection::Neutral,
},
CommandInfo {
name: "/state",
summary: "operator-state overlay",
risk: RiskDirection::Neutral,
},
CommandInfo {
name: "/sessions",
summary: "list recent sessions",
risk: RiskDirection::Neutral,
},
CommandInfo {
name: "/resume",
summary: "replay a past session into the log",
risk: RiskDirection::Neutral,
},
CommandInfo {
name: "/fork",
summary: "start a new session, linked to this one",
risk: RiskDirection::Neutral,
},
CommandInfo {
name: "/save",
summary: "label the current session",
risk: RiskDirection::Neutral,
},
CommandInfo {
name: "/replay",
summary: "show a past session without switching",
risk: RiskDirection::Neutral,
},
CommandInfo {
name: "/share",
summary: "dump a session as copyable JSON",
risk: RiskDirection::Neutral,
},
CommandInfo {
name: "/config show",
summary: "show resolved config values",
risk: RiskDirection::Neutral,
},
CommandInfo {
name: "/config doctor",
summary: "self-diagnose config + secrets",
risk: RiskDirection::Neutral,
},
CommandInfo {
name: "/verbose",
summary: "toggle rich log timestamps",
risk: RiskDirection::Neutral,
},
CommandInfo {
name: "/continue",
summary: "acknowledge coaching notice",
risk: RiskDirection::Neutral,
},
CommandInfo {
name: "/rate",
summary: "attach conviction rating to a past trade",
risk: RiskDirection::Neutral,
},
CommandInfo {
name: "/coaching reset",
summary: "clear coaching notice buffer",
risk: RiskDirection::Neutral,
},
CommandInfo {
name: "/wrap-off",
summary: "skip the daily wrap (this session only)",
risk: RiskDirection::Neutral,
},
CommandInfo {
name: "/clear",
summary: "clear conversation log",
risk: RiskDirection::Neutral,
},
CommandInfo {
name: "/pause-entries",
summary: "block new positions",
risk: RiskDirection::Reduces,
},
CommandInfo {
name: "/break",
summary: "operator-initiated pause",
risk: RiskDirection::Reduces,
},
CommandInfo {
name: "/close",
summary: "close one position (per-coin)",
risk: RiskDirection::Reduces,
},
CommandInfo {
name: "/flatten-all",
summary: "close all positions",
risk: RiskDirection::Reduces,
},
CommandInfo {
name: "/kill",
summary: "hard stop — close + halt",
risk: RiskDirection::Reduces,
},
CommandInfo {
name: "/quit",
summary: "exit the CLI",
risk: RiskDirection::Reduces,
},
CommandInfo {
name: "/state-override",
summary: "declare operator-state label (gated)",
risk: RiskDirection::Increases,
},
CommandInfo {
name: "/disclosure-override",
summary: "bypass progressive disclosure (gated)",
risk: RiskDirection::Increases,
},
CommandInfo {
name: "/execute",
summary: "place a new order: /execute BTC buy 0.001 (gated)",
risk: RiskDirection::Increases,
},
CommandInfo {
name: "/auto",
summary: "toggle auto-accept (on: gated, off/status: neutral)",
risk: RiskDirection::Increases,
},
CommandInfo {
name: "/headless",
summary: "start/stop/status the supervisor daemon",
risk: RiskDirection::Neutral,
},
];
#[derive(Debug, Clone, Copy)]
pub struct CommandInfo {
pub name: &'static str,
pub summary: &'static str,
pub risk: RiskDirection,
}
#[must_use]
#[allow(clippy::too_many_lines)]
pub fn resolve(line: &ParsedLine) -> Option<Command> {
if line.is_empty() {
return None;
}
let head = line.canonical_head();
let cmd = match head.as_str() {
"help" | "?" => Command::Help,
"quit" | "exit" | "q" => Command::Quit,
"clear" | "cls" => Command::Clear,
"conv" | "conversation" => Command::SwitchMode(ModeTarget::Conversation),
"pos-mode" | "positions-mode" => Command::SwitchMode(ModeTarget::Positions),
"decisions" => Command::SwitchMode(ModeTarget::Decisions),
"heat" => Command::Heat,
"heat-mode" | "heatmode" => Command::SwitchMode(ModeTarget::Heat),
"cockpit-mode" | "live-mode" | "live-board" => Command::SwitchMode(ModeTarget::Cockpit),
"status" => Command::Status,
"brief" => Command::Brief,
"risk" => Command::Risk,
"hl-status" | "hl" | "hyperliquid" => Command::HyperliquidStatus {
symbol: line.args.first().cloned(),
},
"hl-account" | "hyperliquid-account" => Command::HyperliquidAccount,
"hl-reconcile" | "reconcile" | "hyperliquid-reconcile" => Command::HyperliquidReconcile,
"live-certify" | "certify-live" | "live-certification" => Command::LiveCertify,
"live-cockpit" | "cockpit" | "live" => Command::LiveCockpit,
"live-evidence" | "evidence" | "canary-evidence" => Command::LiveEvidence,
"live-receipts" | "receipts" | "execution-receipts" | "live-execution-receipts" => {
Command::LiveReceipts
}
"live-canary" | "live-canary-policy" | "canary" | "canary-policy" => {
Command::LiveCanaryPolicy
}
"runtime-parity" | "parity" | "ooda-parity" | "production-parity" => Command::RuntimeParity,
"immune" | "breakers" | "circuit-breakers" => Command::Immune,
"quote" | "price" => Command::Quote {
symbol: line.args.first().cloned(),
},
"regime" => Command::Regime {
coin: line.args.first().cloned(),
},
"evaluate" | "eval" => Command::Evaluate {
coin: line.args.first().cloned(),
extras: line.args.iter().skip(1).cloned().collect(),
},
"positions" | "pos" => Command::Positions,
"pulse" => Command::Pulse {
limit: line.args.first().and_then(|s| s.parse::<u32>().ok()),
},
"approaching" | "near" => Command::Approaching,
"rejections" | "rej" => {
let mut coin: Option<String> = None;
let mut limit: Option<u32> = None;
for a in &line.args {
if let Ok(n) = a.parse::<u32>() {
if limit.is_none() {
limit = Some(n);
}
} else if coin.is_none() {
coin = Some(a.clone());
}
}
Command::Rejections { coin, limit }
}
"kill" => Command::Kill,
"flatten-all" | "flatten" => Command::FlattenAll,
"pause-entries" | "pause" => Command::PauseEntries,
"resume-entries" | "live-resume" => Command::ResumeEntries,
"break" => Command::Break {
minutes: line.args.first().and_then(|s| s.parse::<u32>().ok()),
},
"execute" | "exec" | "e" => resolve_execute(&line.args),
"state" => Command::State,
"sessions" | "ls-sessions" => Command::Sessions {
limit: line.args.first().and_then(|s| s.parse::<u32>().ok()),
},
"resume" => Command::Resume {
needle: line.args.first().cloned(),
},
"fork" => Command::Fork,
"save" => Command::Save {
label: line.args.first().cloned(),
},
"replay" => Command::Replay {
needle: line.args.first().cloned(),
},
"share" | "export" => Command::Share {
needle: line.args.first().cloned(),
},
"state-override" | "stateoverride" => {
let label = line.args.first().and_then(|s| StateOverrideLabel::parse(s));
Command::StateOverride { label }
}
"continue" | "cont" => Command::Continue,
"rate" => {
let mut trade_id: Option<String> = None;
let mut rating: Option<u8> = None;
for a in &line.args {
if rating.is_none()
&& let Ok(n) = a.parse::<u8>()
&& (1..=10).contains(&n)
{
rating = Some(n);
continue;
}
if trade_id.is_none() {
trade_id = Some(a.clone());
}
}
Command::Rate { trade_id, rating }
}
"close" => Command::Close {
coin: line.args.first().cloned(),
},
"wrap-off" | "wrapoff" => Command::WrapOff,
"coaching" => match line.args.first().map(|s| s.to_ascii_lowercase()).as_deref() {
Some("reset") => Command::CoachingReset,
Some(other) => Command::Unknown(format!("coaching {other}")),
None => Command::Unknown("coaching".to_owned()),
},
"coaching-reset" => Command::CoachingReset,
"disclosure-override" | "disclosureoverride" => {
let confirmed = line.args.iter().any(|a| {
a == DISCLOSURE_OVERRIDE_CONFIRM
|| a.trim_start_matches('-')
== DISCLOSURE_OVERRIDE_CONFIRM.trim_start_matches('-')
});
Command::DisclosureOverride { confirmed }
}
"verbose" => {
let action = match line.args.first().map(|s| s.to_ascii_lowercase()).as_deref() {
None | Some("toggle") => VerboseAction::Toggle,
Some("on" | "1" | "true") => VerboseAction::On,
Some("off" | "0" | "false") => VerboseAction::Off,
Some(other) => VerboseAction::Unknown(other.to_owned()),
};
Command::Verbose { action }
}
"config" => {
let action = match line.args.first().map(String::as_str) {
None => ConfigAction::Missing,
Some("show" | "view" | "list" | "ls") => ConfigAction::Show,
Some("doctor" | "diag" | "diagnose" | "check") => ConfigAction::Doctor,
Some(other) => ConfigAction::Unknown(other.to_owned()),
};
Command::Config { action }
}
"auto" => {
let action = match line.args.first().map(|s| s.to_ascii_lowercase()).as_deref() {
None => AutoAction::Missing,
Some("on" | "1" | "true") => AutoAction::On,
Some("off" | "0" | "false") => AutoAction::Off,
Some("status" | "stat" | "show") => AutoAction::Status,
Some(other) => AutoAction::Unknown(other.to_owned()),
};
Command::Auto { action }
}
"headless" => {
let action = match line.args.first().map(|s| s.to_ascii_lowercase()).as_deref() {
None => HeadlessAction::Missing,
Some("start" | "up") => HeadlessAction::Start,
Some("stop" | "down") => HeadlessAction::Stop,
Some("status" | "stat" | "show") => HeadlessAction::Status,
Some(other) => HeadlessAction::Unknown(other.to_owned()),
};
Command::Headless { action }
}
"doctor" | "diag" | "diagnose" | "check" => Command::Config {
action: ConfigAction::Doctor,
},
"zero" => Command::ZeroPrefix {
rest: line.args.join(" "),
},
_ => Command::Unknown(head),
};
Some(cmd)
}
fn resolve_execute(args: &[String]) -> Command {
if args.is_empty() {
return Command::Execute;
}
let coin = args.first().map(|coin| coin.to_ascii_uppercase());
let direction = args
.get(1)
.and_then(|raw| match raw.to_ascii_lowercase().as_str() {
"buy" | "long" | "bid" => Some(ExecuteSide::Buy),
"sell" | "short" | "ask" => Some(ExecuteSide::Sell),
_ => None,
});
let quantity = args.get(2).cloned();
let error = if args.len() > 3 {
Some("too many arguments".to_string())
} else if args.is_empty() {
Some("missing coin, side, and size".to_string())
} else if coin.is_none() {
Some("missing coin".to_string())
} else if args.get(1).is_none() {
Some("missing side".to_string())
} else if direction.is_none() {
Some("side must be buy or sell".to_string())
} else if args.get(2).is_none() {
Some("missing size".to_string())
} else if quantity
.as_deref()
.and_then(|value| value.parse::<f64>().ok())
.is_none_or(|value| !value.is_finite() || value <= 0.0)
{
Some("size must be a positive number".to_string())
} else {
None
};
Command::ExecuteOrder {
coin,
side: direction,
size: quantity,
error,
}
}
#[cfg(test)]
mod tests {
use super::{
Command, ConfigAction, DISCLOSURE_OVERRIDE_CONFIRM, ModeTarget, StateOverrideLabel,
VerboseAction, resolve,
};
use crate::parse::parse_line;
use crate::risk::RiskDirection;
use zero_engine_client::ExecuteSide;
fn r(line: &str) -> Option<Command> {
resolve(&parse_line(line))
}
#[test]
fn empty_input_returns_none() {
assert_eq!(r(""), None);
assert_eq!(r(" "), None);
}
#[test]
fn common_commands_resolve() {
assert_eq!(r("/help"), Some(Command::Help));
assert_eq!(r("?"), Some(Command::Help));
assert_eq!(r("/quit"), Some(Command::Quit));
assert_eq!(r("q"), Some(Command::Quit));
assert_eq!(r("/status"), Some(Command::Status));
assert_eq!(r("/brief"), Some(Command::Brief));
assert_eq!(r("/risk"), Some(Command::Risk));
}
#[test]
fn regime_takes_optional_coin() {
assert_eq!(r("/regime"), Some(Command::Regime { coin: None }));
assert_eq!(
r("/regime BTC"),
Some(Command::Regime {
coin: Some("BTC".into())
})
);
}
#[test]
fn break_parses_minutes() {
assert_eq!(r("/break"), Some(Command::Break { minutes: None }));
assert_eq!(r("/break 15"), Some(Command::Break { minutes: Some(15) }));
}
#[test]
fn mode_switches() {
assert_eq!(
r("/conv"),
Some(Command::SwitchMode(ModeTarget::Conversation))
);
assert_eq!(
r("/decisions"),
Some(Command::SwitchMode(ModeTarget::Decisions))
);
assert_eq!(r("/heat-mode"), Some(Command::SwitchMode(ModeTarget::Heat)));
assert_eq!(
r("/cockpit-mode"),
Some(Command::SwitchMode(ModeTarget::Cockpit))
);
}
#[test]
fn heat_resolves_to_inline_readout() {
assert_eq!(r("/heat"), Some(Command::Heat));
assert_eq!(r("/heat something"), Some(Command::Heat));
}
#[test]
fn heat_is_neutral_risk() {
assert_eq!(Command::Heat.risk(), RiskDirection::Neutral);
}
#[test]
fn evaluate_takes_optional_coin() {
assert_eq!(
r("/evaluate"),
Some(Command::Evaluate {
coin: None,
extras: vec![]
})
);
assert_eq!(
r("/evaluate BTC"),
Some(Command::Evaluate {
coin: Some("BTC".into()),
extras: vec![]
})
);
assert_eq!(
r("/eval eth"),
Some(Command::Evaluate {
coin: Some("eth".into()),
extras: vec![]
})
);
}
#[test]
fn evaluate_preserves_extra_args_for_warning() {
assert_eq!(
r("/evaluate sol short"),
Some(Command::Evaluate {
coin: Some("sol".into()),
extras: vec!["short".into()],
})
);
assert_eq!(
r("/evaluate BTC long now please"),
Some(Command::Evaluate {
coin: Some("BTC".into()),
extras: vec!["long".into(), "now".into(), "please".into()],
})
);
}
#[test]
fn evaluate_is_neutral_risk() {
assert_eq!(
Command::Evaluate {
coin: Some("BTC".into()),
extras: vec![],
}
.risk(),
RiskDirection::Neutral
);
}
#[test]
fn pulse_parses_optional_limit() {
assert_eq!(r("/pulse"), Some(Command::Pulse { limit: None }));
assert_eq!(r("/pulse 50"), Some(Command::Pulse { limit: Some(50) }));
assert_eq!(r("/pulse BTC"), Some(Command::Pulse { limit: None }));
}
#[test]
fn approaching_takes_no_args() {
assert_eq!(r("/approaching"), Some(Command::Approaching));
assert_eq!(r("/near"), Some(Command::Approaching));
assert_eq!(r("/approaching ignored"), Some(Command::Approaching));
}
#[test]
fn rejections_parses_coin_and_limit_in_any_order() {
assert_eq!(
r("/rejections"),
Some(Command::Rejections {
coin: None,
limit: None
})
);
assert_eq!(
r("/rejections BTC"),
Some(Command::Rejections {
coin: Some("BTC".into()),
limit: None
})
);
assert_eq!(
r("/rejections 50"),
Some(Command::Rejections {
coin: None,
limit: Some(50)
})
);
assert_eq!(
r("/rejections BTC 50"),
Some(Command::Rejections {
coin: Some("BTC".into()),
limit: Some(50)
})
);
assert_eq!(
r("/rejections 50 BTC"),
Some(Command::Rejections {
coin: Some("BTC".into()),
limit: Some(50)
})
);
assert_eq!(
r("/rej"),
Some(Command::Rejections {
coin: None,
limit: None
})
);
}
#[test]
fn new_read_commands_are_neutral() {
assert_eq!(
Command::HyperliquidStatus { symbol: None }.risk(),
RiskDirection::Neutral
);
assert_eq!(Command::HyperliquidAccount.risk(), RiskDirection::Neutral);
assert_eq!(Command::HyperliquidReconcile.risk(), RiskDirection::Neutral);
assert_eq!(Command::LiveCertify.risk(), RiskDirection::Neutral);
assert_eq!(Command::LiveCockpit.risk(), RiskDirection::Neutral);
assert_eq!(Command::LiveEvidence.risk(), RiskDirection::Neutral);
assert_eq!(Command::LiveReceipts.risk(), RiskDirection::Neutral);
assert_eq!(Command::LiveCanaryPolicy.risk(), RiskDirection::Neutral);
assert_eq!(Command::RuntimeParity.risk(), RiskDirection::Neutral);
assert_eq!(Command::Immune.risk(), RiskDirection::Neutral);
assert_eq!(
Command::Quote { symbol: None }.risk(),
RiskDirection::Neutral
);
assert_eq!(
Command::Pulse { limit: None }.risk(),
RiskDirection::Neutral
);
assert_eq!(Command::Approaching.risk(), RiskDirection::Neutral);
assert_eq!(
Command::Rejections {
coin: None,
limit: None
}
.risk(),
RiskDirection::Neutral
);
}
#[test]
fn hyperliquid_status_takes_optional_symbol() {
assert_eq!(
r("/hl-status"),
Some(Command::HyperliquidStatus { symbol: None })
);
assert_eq!(
r("/hl BTC"),
Some(Command::HyperliquidStatus {
symbol: Some("BTC".into())
})
);
assert_eq!(
r("/hyperliquid ETH"),
Some(Command::HyperliquidStatus {
symbol: Some("ETH".into())
})
);
}
#[test]
fn hyperliquid_account_commands_parse() {
assert_eq!(r("/hl-account"), Some(Command::HyperliquidAccount));
assert_eq!(r("/hl-reconcile"), Some(Command::HyperliquidReconcile));
assert_eq!(r("/reconcile"), Some(Command::HyperliquidReconcile));
assert_eq!(r("/live-certify"), Some(Command::LiveCertify));
assert_eq!(r("/certify-live"), Some(Command::LiveCertify));
assert_eq!(r("/live-cockpit"), Some(Command::LiveCockpit));
assert_eq!(r("/cockpit"), Some(Command::LiveCockpit));
assert_eq!(r("/live-evidence"), Some(Command::LiveEvidence));
assert_eq!(r("/canary-evidence"), Some(Command::LiveEvidence));
assert_eq!(r("/live-receipts"), Some(Command::LiveReceipts));
assert_eq!(r("/receipts"), Some(Command::LiveReceipts));
assert_eq!(r("/live-canary"), Some(Command::LiveCanaryPolicy));
assert_eq!(r("/canary-policy"), Some(Command::LiveCanaryPolicy));
assert_eq!(r("/runtime-parity"), Some(Command::RuntimeParity));
assert_eq!(r("/parity"), Some(Command::RuntimeParity));
assert_eq!(r("/production-parity"), Some(Command::RuntimeParity));
assert_eq!(r("/immune"), Some(Command::Immune));
assert_eq!(r("/breakers"), Some(Command::Immune));
assert_eq!(r("/resume-entries"), Some(Command::ResumeEntries));
assert_eq!(r("/live-resume"), Some(Command::ResumeEntries));
}
#[test]
fn execute_order_parses_real_order_shape() {
assert_eq!(
r("/execute btc buy 0.001"),
Some(Command::ExecuteOrder {
coin: Some("BTC".to_string()),
side: Some(ExecuteSide::Buy),
size: Some("0.001".to_string()),
error: None,
})
);
assert_eq!(
r("/exec ETH short 1.5"),
Some(Command::ExecuteOrder {
coin: Some("ETH".to_string()),
side: Some(ExecuteSide::Sell),
size: Some("1.5".to_string()),
error: None,
})
);
}
#[test]
fn execute_usage_shapes_are_neutral_until_valid() {
let missing = r("/execute BTC").expect("command");
assert_eq!(missing.risk(), RiskDirection::Neutral);
let bad_size = r("/execute BTC buy nope").expect("command");
assert_eq!(bad_size.risk(), RiskDirection::Neutral);
let valid = r("/execute BTC buy 0.001").expect("command");
assert_eq!(valid.risk(), RiskDirection::Increases);
}
#[test]
fn quote_requires_symbol_at_dispatch() {
assert_eq!(r("/quote"), Some(Command::Quote { symbol: None }));
assert_eq!(
r("/quote BTC"),
Some(Command::Quote {
symbol: Some("BTC".into())
})
);
assert_eq!(
r("/price eth"),
Some(Command::Quote {
symbol: Some("eth".into())
})
);
}
#[test]
fn sessions_parses_optional_limit() {
assert_eq!(r("/sessions"), Some(Command::Sessions { limit: None }));
assert_eq!(r("/sessions 5"), Some(Command::Sessions { limit: Some(5) }));
assert_eq!(r("/ls-sessions"), Some(Command::Sessions { limit: None }));
assert_eq!(r("/sessions BTC"), Some(Command::Sessions { limit: None }));
}
#[test]
fn resume_takes_optional_needle() {
assert_eq!(r("/resume"), Some(Command::Resume { needle: None }));
assert_eq!(
r("/resume 01H"),
Some(Command::Resume {
needle: Some("01H".into())
})
);
assert_eq!(
r("/resume scratch"),
Some(Command::Resume {
needle: Some("scratch".into())
})
);
}
#[test]
fn fork_takes_no_args_and_ignores_extras() {
assert_eq!(r("/fork"), Some(Command::Fork));
assert_eq!(r("/fork ignored"), Some(Command::Fork));
}
#[test]
fn save_parses_label() {
assert_eq!(r("/save"), Some(Command::Save { label: None }));
assert_eq!(
r("/save pre-cpi"),
Some(Command::Save {
label: Some("pre-cpi".into())
})
);
}
#[test]
fn session_cohort_is_neutral_risk() {
assert_eq!(
Command::Sessions { limit: None }.risk(),
RiskDirection::Neutral
);
assert_eq!(
Command::Resume { needle: None }.risk(),
RiskDirection::Neutral
);
assert_eq!(Command::Fork.risk(), RiskDirection::Neutral);
assert_eq!(Command::Save { label: None }.risk(), RiskDirection::Neutral);
assert_eq!(
Command::Replay { needle: None }.risk(),
RiskDirection::Neutral
);
assert_eq!(
Command::Share { needle: None }.risk(),
RiskDirection::Neutral
);
}
#[test]
fn replay_takes_optional_needle() {
assert_eq!(r("/replay"), Some(Command::Replay { needle: None }));
assert_eq!(
r("/replay 01HOLD"),
Some(Command::Replay {
needle: Some("01HOLD".into())
})
);
assert_eq!(
r("/replay pre-cpi"),
Some(Command::Replay {
needle: Some("pre-cpi".into())
})
);
}
#[test]
fn share_takes_optional_needle_and_export_alias() {
assert_eq!(r("/share"), Some(Command::Share { needle: None }));
assert_eq!(
r("/share 01HOLD"),
Some(Command::Share {
needle: Some("01HOLD".into())
})
);
assert_eq!(
r("/export 01HOLD"),
Some(Command::Share {
needle: Some("01HOLD".into())
})
);
}
#[test]
fn config_subcommand_parses_known_actions_and_aliases() {
assert_eq!(
r("/config show"),
Some(Command::Config {
action: ConfigAction::Show
})
);
assert_eq!(
r("/config view"),
Some(Command::Config {
action: ConfigAction::Show
})
);
assert_eq!(
r("/config ls"),
Some(Command::Config {
action: ConfigAction::Show
})
);
assert_eq!(
r("/config doctor"),
Some(Command::Config {
action: ConfigAction::Doctor
})
);
assert_eq!(
r("/config check"),
Some(Command::Config {
action: ConfigAction::Doctor
})
);
}
#[test]
fn zero_prefix_is_intercepted_with_typed_tail() {
match r("zero doctor") {
Some(Command::ZeroPrefix { rest }) => assert_eq!(rest, "doctor"),
other => panic!("expected ZeroPrefix, got {other:?}"),
}
match r("zero --version") {
Some(Command::ZeroPrefix { rest }) => assert_eq!(rest, "--version"),
other => panic!("expected ZeroPrefix, got {other:?}"),
}
match r("zero init --force") {
Some(Command::ZeroPrefix { rest }) => assert_eq!(rest, "init --force"),
other => panic!("expected ZeroPrefix, got {other:?}"),
}
match r("zero") {
Some(Command::ZeroPrefix { rest }) => assert_eq!(rest, ""),
other => panic!("expected ZeroPrefix, got {other:?}"),
}
}
#[test]
fn slash_zero_also_triggers_prefix_hint() {
match r("/zero doctor") {
Some(Command::ZeroPrefix { rest }) => assert_eq!(rest, "doctor"),
other => panic!("expected ZeroPrefix, got {other:?}"),
}
}
#[test]
fn zero_prefix_is_neutral_risk() {
let cmd = Command::ZeroPrefix {
rest: String::new(),
};
assert_eq!(cmd.risk(), RiskDirection::Neutral);
}
#[test]
fn doctor_top_level_alias_resolves_to_config_doctor() {
for input in ["/doctor", "doctor", "/diag", "/diagnose", "/check"] {
assert_eq!(
r(input),
Some(Command::Config {
action: ConfigAction::Doctor
}),
"input {input:?} did not alias to /config doctor",
);
}
}
#[test]
fn config_bare_invocation_is_missing_action() {
assert_eq!(
r("/config"),
Some(Command::Config {
action: ConfigAction::Missing
})
);
}
#[test]
fn config_unknown_action_preserved_for_hint() {
assert_eq!(
r("/config secrets"),
Some(Command::Config {
action: ConfigAction::Unknown("secrets".into())
})
);
}
#[test]
fn config_is_neutral_risk() {
assert_eq!(
Command::Config {
action: ConfigAction::Show
}
.risk(),
RiskDirection::Neutral
);
assert_eq!(
Command::Config {
action: ConfigAction::Doctor
}
.risk(),
RiskDirection::Neutral
);
}
#[test]
fn verbose_parses_on_off_toggle() {
assert_eq!(
r("/verbose"),
Some(Command::Verbose {
action: VerboseAction::Toggle
})
);
assert_eq!(
r("/verbose toggle"),
Some(Command::Verbose {
action: VerboseAction::Toggle
})
);
assert_eq!(
r("/verbose on"),
Some(Command::Verbose {
action: VerboseAction::On
})
);
assert_eq!(
r("/verbose ON"),
Some(Command::Verbose {
action: VerboseAction::On
})
);
assert_eq!(
r("/verbose off"),
Some(Command::Verbose {
action: VerboseAction::Off
})
);
assert_eq!(
r("/verbose true"),
Some(Command::Verbose {
action: VerboseAction::On
})
);
assert_eq!(
r("/verbose 0"),
Some(Command::Verbose {
action: VerboseAction::Off
})
);
}
#[test]
fn verbose_preserves_unknown_token_for_usage_hint() {
assert_eq!(
r("/verbose maybe"),
Some(Command::Verbose {
action: VerboseAction::Unknown("maybe".into())
})
);
}
#[test]
fn verbose_is_neutral_risk() {
assert_eq!(
Command::Verbose {
action: VerboseAction::Toggle
}
.risk(),
RiskDirection::Neutral
);
}
#[test]
fn state_override_parses_canonical_labels() {
assert_eq!(
r("/state-override STEADY"),
Some(Command::StateOverride {
label: Some(StateOverrideLabel::Steady),
})
);
assert_eq!(
r("/state-override steady"),
Some(Command::StateOverride {
label: Some(StateOverrideLabel::Steady),
})
);
assert_eq!(
r("/state-override Tilt"),
Some(Command::StateOverride {
label: Some(StateOverrideLabel::Tilt),
})
);
assert_eq!(
r("/state-override blue"),
Some(Command::StateOverride { label: None })
);
assert_eq!(
r("/state-override"),
Some(Command::StateOverride { label: None })
);
}
#[test]
fn state_override_is_increases_risk() {
assert_eq!(
Command::StateOverride {
label: Some(StateOverrideLabel::Steady)
}
.risk(),
RiskDirection::Increases
);
}
#[test]
fn continue_parses_with_alias() {
assert_eq!(r("/continue"), Some(Command::Continue));
assert_eq!(r("/cont"), Some(Command::Continue));
assert_eq!(Command::Continue.risk(), RiskDirection::Neutral);
}
#[test]
fn rate_parses_id_and_rating_in_either_order() {
assert_eq!(
r("/rate t-001 8"),
Some(Command::Rate {
trade_id: Some("t-001".into()),
rating: Some(8),
})
);
assert_eq!(
r("/rate 8 t-001"),
Some(Command::Rate {
trade_id: Some("t-001".into()),
rating: Some(8),
})
);
assert_eq!(
r("/rate t 1"),
Some(Command::Rate {
trade_id: Some("t".into()),
rating: Some(1),
})
);
assert_eq!(
r("/rate t 10"),
Some(Command::Rate {
trade_id: Some("t".into()),
rating: Some(10),
})
);
}
#[test]
fn rate_rejects_out_of_range_and_missing_arguments() {
assert_eq!(
r("/rate"),
Some(Command::Rate {
trade_id: None,
rating: None,
})
);
assert_eq!(
r("/rate 0"),
Some(Command::Rate {
trade_id: Some("0".into()),
rating: None,
})
);
assert_eq!(
r("/rate 11"),
Some(Command::Rate {
trade_id: Some("11".into()),
rating: None,
})
);
assert_eq!(
r("/rate t-001"),
Some(Command::Rate {
trade_id: Some("t-001".into()),
rating: None,
})
);
}
#[test]
fn rate_is_neutral_risk() {
assert_eq!(
Command::Rate {
trade_id: Some("t".into()),
rating: Some(5),
}
.risk(),
RiskDirection::Neutral,
"/rate is a self-report about a past trade, not a position change",
);
}
#[test]
fn close_takes_optional_coin() {
assert_eq!(r("/close"), Some(Command::Close { coin: None }));
assert_eq!(
r("/close BTC"),
Some(Command::Close {
coin: Some("BTC".into())
})
);
assert_eq!(Command::Close { coin: None }.risk(), RiskDirection::Reduces);
}
#[test]
fn wrap_off_parses_with_alias_and_is_neutral() {
assert_eq!(r("/wrap-off"), Some(Command::WrapOff));
assert_eq!(r("/wrapoff"), Some(Command::WrapOff));
assert_eq!(Command::WrapOff.risk(), RiskDirection::Neutral);
}
#[test]
fn coaching_reset_parses_two_token_and_dash_forms() {
assert_eq!(r("/coaching reset"), Some(Command::CoachingReset));
assert_eq!(r("/coaching RESET"), Some(Command::CoachingReset));
assert_eq!(r("/coaching-reset"), Some(Command::CoachingReset));
assert!(matches!(r("/coaching"), Some(Command::Unknown(_))));
assert!(matches!(r("/coaching wut"), Some(Command::Unknown(_))));
assert_eq!(Command::CoachingReset.risk(), RiskDirection::Neutral);
}
#[test]
fn disclosure_override_requires_exact_phrase() {
assert_eq!(
r("/disclosure-override"),
Some(Command::DisclosureOverride { confirmed: false })
);
let exact = format!("/disclosure-override {DISCLOSURE_OVERRIDE_CONFIRM}");
assert_eq!(
r(&exact),
Some(Command::DisclosureOverride { confirmed: true })
);
assert_eq!(
r("/disclosure-override i-know-what-i-am-doing"),
Some(Command::DisclosureOverride { confirmed: true })
);
assert_eq!(
r("/disclosure-override yolo"),
Some(Command::DisclosureOverride { confirmed: false })
);
}
#[test]
fn disclosure_override_is_increases_risk_regardless_of_confirm() {
assert_eq!(
Command::DisclosureOverride { confirmed: true }.risk(),
RiskDirection::Increases
);
assert_eq!(
Command::DisclosureOverride { confirmed: false }.risk(),
RiskDirection::Increases
);
}
#[test]
fn risk_classification_holds() {
assert_eq!(Command::Help.risk(), RiskDirection::Neutral);
assert_eq!(Command::Status.risk(), RiskDirection::Neutral);
assert_eq!(Command::Quit.risk(), RiskDirection::Reduces);
assert_eq!(Command::Kill.risk(), RiskDirection::Reduces);
assert_eq!(Command::FlattenAll.risk(), RiskDirection::Reduces);
assert_eq!(Command::PauseEntries.risk(), RiskDirection::Reduces);
assert_eq!(Command::ResumeEntries.risk(), RiskDirection::Increases);
assert_eq!(
Command::Break { minutes: None }.risk(),
RiskDirection::Reduces
);
}
}