mod agent;
mod app;
mod banner;
mod broker;
mod buffer;
mod config;
mod layout;
mod llm_log;
mod mode;
mod palette;
mod pane;
#[cfg(feature = "memory")]
mod retrieval;
#[cfg(not(feature = "memory"))]
#[path = "retrieval_stub.rs"]
mod retrieval;
mod project;
mod session;
mod tab;
mod terminal;
mod prompts;
mod tiers;
mod worklog;
mod tuning;
mod ui;
use anyhow::Result;
use app::{App, InputEvent};
use crossterm::{
event::{
DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture,
Event, KeyboardEnhancementFlags, PopKeyboardEnhancementFlags,
PushKeyboardEnhancementFlags,
},
execute,
terminal::{
disable_raw_mode, enable_raw_mode, supports_keyboard_enhancement, EnterAlternateScreen,
LeaveAlternateScreen,
},
};
use ratatui::{backend::CrosstermBackend, Terminal};
use std::{env, io};
const HELP: &str = "\
mars — mission control for your terminal
USAGE
mars new session, opens a terminal (like tmux)
mars FILE new session, editing FILE
mars -s [FILE] quick standalone edit, no daemon (scripts)
mars <COMMAND> [ARGS]
By default every `mars` is a session: detachable, survives disconnects,
auto-named (a number first, then an AI label). See below to manage them.
SESSIONS (work survives closed windows and disconnects)
mars new <name> [FILE] start an explicitly-named session
(aliases: session, --session)
mars attach [name] reattach — most recent session if unnamed
(aliases: a, resume, --resume)
mars ls list sessions and their attach state
(aliases: list, --list)
mars rename <old> <new> rename a running session
mars kill <name> end + delete a session (autosaves first)
mars killall end ALL sessions, then start fresh
(alias: --killall)
Inside a session: quitting (C-x C-c) just DETACHES — the session lives on;
\"Kill session\" in the menu (or mars kill) ends it for good
Closing the terminal window just detaches — nothing is lost.
Reattach greets you with a \"while away\" line if anything happened;
C-x g opens the full Away Digest (timeline + durations).
AGENT (BETA — an assistant, not an authority; review what it proposes)
mars ask \"<question>\" one-shot answer from the LLM agent
Keys (paid-first): ANTHROPIC_API_KEY, OPENAI_API_KEY, GROQ_API_KEY,
GEMINI_API_KEY, or MARS_LLM_KEY + MARS_LLM_URL (any
OpenAI-compatible endpoint, e.g. local Ollama).
MARS_LLM_MODEL overrides the model for any provider.
LLM DEBUG (calibrate prompts / right-size models per call)
mars --llm-debug <cmd> log every LLM call (prompt, model, tokens,
latency) to ~/.mars/logs/ (or MARS_LLM_DEBUG=1;
export it so session daemons inherit it all day)
mars llm-stats [--raw] profile the log: per task×model, ranked by
token use — avg in/out tokens, total, latency
mars translate \"<english>\" headless: English → one shell command (logs it)
--memory none|history|docs|full retrieval variant: history = your own commands
for translate; docs = Mars's own docs for ask
REMOTE (BETA — the agent works on every box; the key never leaves home)
mars ssh <host> [ssh args] ssh in with the auth socket forwarded; the
remote agent asks home, no key on the box.
Auto-starts the key broker if it isn't running.
mars keyd (optional) start the broker explicitly, in a
shell where your API key is set
INSIDE THE EDITOR
Ctrl+Space mission control (search) ! run a shell command
? ask the agent C-t space warp (tabs/panes)
C-x C-f Navigator (browse & jump) C-x C-s save C-g cancel anything
MORE
mars help this text (aliases: -h, --help)
mars version version (aliases: -V, --version)
mars reset restore default keybindings + tuning (backs up old)
mars --selfcheck run the built-in test suite
Config: ~/.config/mars/keys.json (bindings), tuning.json (behavior knobs)
Session logs: ~/.local/state/mars/<name>.log";
fn main() -> Result<()> {
session::sanitize_tty();
let raw_args: Vec<String> = env::args().skip(1).collect();
if raw_args.iter().any(|a| a == "--llm-debug") {
std::env::set_var("MARS_LLM_DEBUG", "1");
}
if let Some(i) = raw_args.iter().position(|a| a == "--memory") {
if let Some(mode) = raw_args.get(i + 1) {
std::env::set_var("MARS_MEMORY", mode);
}
}
let mut args = raw_args
.into_iter()
.filter(|a| a != "--llm-debug")
.scan(false, |skip, a| {
if *skip {
*skip = false;
return Some(None);
}
if a == "--memory" {
*skip = true;
return Some(None);
}
Some(Some(a))
})
.flatten();
let first = args.next();
let _llm_session = llm_log::SessionGuard::start();
match first.as_deref() {
Some("help") | Some("--help") | Some("-h") => {
println!("{HELP}");
return Ok(());
}
Some("version") | Some("--version") | Some("-V") => {
banner::print_banner();
println!("\n mars {}", env!("CARGO_PKG_VERSION"));
return Ok(());
}
Some("--selfcheck") => return selfcheck(),
Some("llm-stats") => {
let raw = args.any(|a| a == "--raw");
return llm_log::stats(raw);
}
Some("ask") | Some("--ask") => {
let question: String = args.collect::<Vec<_>>().join(" ");
return ask_cli(question);
}
Some("translate") => {
let request: String = args.collect::<Vec<_>>().join(" ");
return translate_cli(request);
}
Some("--server") => {
let name = args.next().ok_or_else(|| anyhow::anyhow!("--server needs a name"))?;
eprintln!("[mars] session '{name}' starting (pid {})", std::process::id());
let result = session::server_main(&name, args.next());
match &result {
Ok(_) => eprintln!("[mars] session '{name}' ended cleanly"),
Err(e) => eprintln!("[mars] session '{name}' died: {e}"),
}
return result;
}
Some("new") | Some("session") | Some("--session") => {
let name = args
.next()
.ok_or_else(|| anyhow::anyhow!("usage: mars new <name> [file]"))?;
return session::session_main(&name, args.next());
}
Some("attach") | Some("a") | Some("resume") | Some("--resume") => {
return session::resume_main(args.next());
}
Some("ls") | Some("list") | Some("--list") => {
let interactive = !args.any(|a| a == "--no-prompt");
return session::list_main(interactive);
}
Some("keyd") => return broker::keyd_main(),
Some("ssh") => {
let host = args
.next()
.ok_or_else(|| anyhow::anyhow!("usage: mars ssh <host> [ssh args…] (needs: mars keyd)"))?;
return broker::ssh_main(host, args.collect());
}
Some("kill") | Some("--kill") => {
let name = args
.next()
.ok_or_else(|| anyhow::anyhow!("usage: mars kill <name> (see: mars ls)"))?;
return session::kill_main(&name);
}
Some("killall") | Some("--killall") => {
if let Ok(session) = std::env::var("MARS_SESSION") {
anyhow::bail!(
"you're inside Mars session '{session}' — killall would cut its own branch.\n \
Detach first (C-x C-c), then run: mars --killall"
);
}
session::killall_main()?;
let name = session::next_auto_name()?;
return session::session_main(&name, args.next());
}
Some("rename") | Some("--rename") => {
let (old, new) = (args.next(), args.next());
let (Some(old), Some(new)) = (old, new) else {
anyhow::bail!("usage: mars rename <old> <new> (see: mars ls)");
};
return session::rename_main(&old, &new);
}
Some("reset") | Some("--reset") => {
let path = config::reset_keys()?;
tuning::reset();
println!(
"Restored default keybindings and tuning.\n {}\n (your previous files were \
backed up alongside as *.bak)",
path.display()
);
return Ok(());
}
Some("-s") | Some("--standalone") => {} Some(s) if s.starts_with('-') => {
eprintln!("unknown option: {s}\n");
eprintln!("{HELP}");
std::process::exit(2);
}
_ => {}
}
let standalone = matches!(first.as_deref(), Some("-s") | Some("--standalone"));
let file = if standalone { args.next() } else { first };
if !standalone {
if let Ok(session) = std::env::var("MARS_SESSION") {
match &file {
Some(f) => match session::open_in_session(&session, f) {
Ok(()) => {
println!("opened '{f}' in Mars session '{session}' (new tab)");
return Ok(());
}
Err(_) => {}
},
None => {
eprintln!(
"You're already inside Mars session '{session}'.\n \
mars <file> open a file here (new tab)\n \
mars new <name> start a separate session"
);
return Ok(());
}
}
}
let name = session::next_auto_name()?;
return session::session_main(&name, file);
}
session::install_panic_restore();
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture, EnableBracketedPaste)?;
let enhanced = supports_keyboard_enhancement().unwrap_or(false);
if enhanced {
execute!(
stdout,
PushKeyboardEnhancementFlags(KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES)
)?;
}
let backend = CrosstermBackend::new(io::stdout());
let mut terminal = Terminal::new(backend)?;
let (tx, rx) = std::sync::mpsc::channel::<InputEvent>();
std::thread::spawn(move || loop {
match crossterm::event::read() {
Ok(Event::Key(k)) => { if tx.send(InputEvent::Key(k)).is_err() { break; } }
Ok(Event::Mouse(m)) => { if tx.send(InputEvent::Mouse(m)).is_err() { break; } }
Ok(Event::Paste(s)) => { if tx.send(InputEvent::Paste(s)).is_err() { break; } }
Ok(_) => {} Err(_) => break,
}
});
let had_file = file.is_some();
let mut app = App::new(file)?;
if !had_file {
app.open_terminal(); }
let result = app.run(&mut terminal, &rx);
disable_raw_mode()?;
if enhanced {
let _ = execute!(terminal.backend_mut(), PopKeyboardEnhancementFlags);
}
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture,
DisableBracketedPaste
)?;
terminal.show_cursor()?;
result
}
fn translate_cli(request: String) -> Result<()> {
if request.trim().is_empty() {
anyhow::bail!("usage: mars translate \"<english request>\" [--memory none|history|docs|full]");
}
let cfg = agent::AgentConfig::from_env();
if !cfg.is_configured() {
anyhow::bail!("no API key: export ANTHROPIC_API_KEY, OPENAI_API_KEY, GROQ_API_KEY, \
GEMINI_API_KEY, or MARS_LLM_KEY (+ MARS_LLM_URL for a custom endpoint)");
}
let (command, _call_id) = agent::translate_once(&cfg, &request, "")?;
println!("{command}");
Ok(())
}
fn ask_cli(question: String) -> Result<()> {
if question.trim().is_empty() {
anyhow::bail!("usage: mars --ask \"<question>\"");
}
let cfg = agent::AgentConfig::from_env();
if !cfg.is_configured() {
anyhow::bail!("no API key: export ANTHROPIC_API_KEY, OPENAI_API_KEY, GROQ_API_KEY, \
GEMINI_API_KEY, or MARS_LLM_KEY (+ MARS_LLM_URL for a custom endpoint)");
}
println!("provider: {} model: {}", cfg.provider, cfg.model);
let (tx, rx) = std::sync::mpsc::channel();
agent::ask(
cfg,
question,
palette::registry_context(),
String::new(), Vec::new(),
tx,
);
loop {
match rx.recv_timeout(std::time::Duration::from_secs(60))? {
agent::AgentEvent::Answer { text, directive } => {
println!("{}", text);
match directive {
Some(agent::AgentDirective::Run(name)) => println!("[would run: {name}]"),
Some(agent::AgentDirective::Type(cmd)) => {
println!("[would type into terminal: {cmd}]")
}
Some(agent::AgentDirective::Open(loc)) => println!("[would open: {loc}]"),
Some(agent::AgentDirective::Need(_)) => {}
None => {}
}
return Ok(());
}
agent::AgentEvent::AnswerStart | agent::AgentEvent::AnswerDelta { .. } => continue,
agent::AgentEvent::AutoName { .. }
| agent::AgentEvent::SessionName { .. }
| agent::AgentEvent::WatchSummary { .. }
| agent::AgentEvent::Mission { .. }
| agent::AgentEvent::BgDone
| agent::AgentEvent::ShellTranslation { .. } => return Ok(()),
agent::AgentEvent::Error(e) => anyhow::bail!("agent error: {}", e),
}
}
}
fn selfcheck() -> Result<()> {
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use ratatui::backend::TestBackend;
for key in [
"GEMINI_API_KEY", "GOOGLE_API_KEY", "GROQ_API_KEY",
"ANTHROPIC_API_KEY", "OPENAI_API_KEY",
"MARS_LLM_KEY", "MARS_LLM_URL", "ARES_LLM_KEY", "ARES_LLM_URL",
"MARS_AUTH_SOCK", "MARS_LLM_DEBUG",
] {
std::env::remove_var(key);
}
fn k(code: KeyCode) -> KeyEvent {
KeyEvent::new(code, KeyModifiers::NONE)
}
fn kc(code: KeyCode) -> KeyEvent {
KeyEvent::new(code, KeyModifiers::CONTROL)
}
fn typ(app: &mut App, s: &str) -> Result<()> {
for c in s.chars() { app.handle_key(k(KeyCode::Char(c)))?; }
Ok(())
}
fn screen_text(term: &Terminal<TestBackend>) -> String {
term.backend().buffer().content().iter().map(|c| c.symbol()).collect()
}
std::env::set_var("MARS_NO_SYSTEM_CLIPBOARD", "1");
let cfg_dir = std::env::temp_dir().join(format!("mars-selfcheck-{}", std::process::id()));
std::fs::create_dir_all(&cfg_dir)?;
std::env::set_var("XDG_CONFIG_HOME", &cfg_dir);
let mut app = App::new(None)?;
let mut term = Terminal::new(TestBackend::new(120, 40))?;
term.draw(|f| ui::render(f, &mut app))?;
let t1 = screen_text(&term);
assert!(t1.contains("EDIT"), "status bar missing EDIT mode");
assert!(t1.contains("scratch"), "scratch buffer not shown");
assert!(t1.contains("control for your terminal"), "MARS splash missing on first render");
println!("[selfcheck] render + splash ............ PASS");
typ(&mut app, "hello world")?;
term.draw(|f| ui::render(f, &mut app))?;
let t2 = screen_text(&term);
assert!(t2.contains("hello world"), "typing did not insert");
assert!(!t2.contains("control for your terminal"), "splash did not dismiss on keypress");
assert!(app.mode == mode::Mode::Edit, "typing changed mode");
println!("[selfcheck] non-modal insert ........... PASS");
{
let mut app = App::new(None)?;
app.needs_redraw = false; app.tick();
assert!(!app.needs_redraw, "idle tick asked for a redraw (would flush over SSH every tick)");
app.agent_tx.send(agent::AgentEvent::BgDone)?; app.tick();
assert!(app.needs_redraw, "an agent event did not request a redraw");
app.needs_redraw = false;
app.agent_pending = true; app.tick();
assert!(app.needs_redraw, "the thinking spinner did not request a redraw");
}
println!("[selfcheck] idle render gating ........ PASS");
app.handle_key(k(KeyCode::Enter))?;
typ(&mut app, "KILLME")?;
app.handle_key(kc(KeyCode::Char('a')))?; app.handle_key(kc(KeyCode::Char('k')))?; assert_eq!(app.kill_ring.last().map(String::as_str), Some("KILLME"), "kill-ring empty");
term.draw(|f| ui::render(f, &mut app))?;
assert!(!screen_text(&term).contains("KILLME"), "C-k did not remove text");
app.handle_key(kc(KeyCode::Char('y')))?; term.draw(|f| ui::render(f, &mut app))?;
assert!(screen_text(&term).contains("KILLME"), "C-y did not restore text");
println!("[selfcheck] kill/yank (C-k/C-y) ........ PASS");
let raw = KeyEvent::new(KeyCode::Char('<'), KeyModifiers::ALT | KeyModifiers::SHIFT);
assert_eq!(
config::chord_of(&raw),
config::parse_key("M-<").unwrap(),
"chord_of failed to normalize ALT|SHIFT+'<'"
);
typ(&mut app, "abc")?;
app.handle_key(kc(KeyCode::Char('_')))?; term.draw(|f| ui::render(f, &mut app))?;
assert!(!screen_text(&term).contains("abc"), "C-_ did not undo");
println!("[selfcheck] chord normalize + C-_ ...... PASS");
app.handle_key(raw)?; assert_eq!((app.focused_pane().cursor_row, app.focused_pane().cursor_col), (0, 0));
for _ in 0..5 {
app.handle_key(KeyEvent::new(KeyCode::Right, KeyModifiers::SHIFT))?;
}
typ(&mut app, "X")?;
term.draw(|f| ui::render(f, &mut app))?;
let t5 = screen_text(&term);
assert!(t5.contains("X world"), "typing did not replace the selection");
assert!(!t5.contains("hello world"), "selection text survived replacement");
println!("[selfcheck] select + replace-on-type ... PASS");
let origin = (app.focused_pane().cursor_row, app.focused_pane().cursor_col);
app.handle_key(kc(KeyCode::Char('s')))?; assert!(app.mode == mode::Mode::Prompt, "C-s did not open isearch");
typ(&mut app, "world")?;
assert_eq!(
(app.focused_pane().cursor_row, app.focused_pane().cursor_col),
(0, 2),
"isearch did not jump to the live match"
);
assert!(!app.search_hl.is_empty(), "isearch matches not highlighted");
app.handle_key(kc(KeyCode::Char('g')))?; assert_eq!(
(app.focused_pane().cursor_row, app.focused_pane().cursor_col),
origin,
"C-g did not restore the origin"
);
assert!(app.search_hl.is_empty(), "highlights not cleared after cancel");
println!("[selfcheck] live isearch (C-s/C-g) ..... PASS");
{
let mut app = App::new(None)?;
typ(&mut app, "aa bb aa cc aa")?; app.handle_key(kc(KeyCode::Char('a')))?; app.handle_key(kc(KeyCode::Char('s')))?; typ(&mut app, "aa")?;
assert_eq!(app.isearch_status(), Some((1, 3)), "counter should read 1/3 at first match");
app.handle_key(k(KeyCode::Tab))?;
assert!(app.search_pick && app.search_labels.len() == 3, "Tab did not label matches");
assert_eq!(app.search_labels[1].2, 's', "second label should be 's'");
app.handle_key(k(KeyCode::Char('s')))?; assert_eq!(app.focused_pane().cursor_col, 6, "label jump did not land on the 2nd match");
assert!(matches!(app.mode, mode::Mode::Edit), "label pick did not accept the search");
app.handle_key(kc(KeyCode::Char('s')))?; typ(&mut app, "cc")?; assert_eq!(app.focused_pane().cursor_col, 9, "isearch did not land on cc");
app.handle_key(kc(KeyCode::Char('a')))?; assert!(matches!(app.mode, mode::Mode::Edit), "land-on-key did not exit search");
assert_eq!(app.focused_pane().cursor_col, 0, "C-a did not apply after committing search");
}
println!("[selfcheck] search-as-teleport ......... PASS");
{
assert_eq!(
app::extract_code_block("here you go:\n```rust\nfn a() {}\n```\ndone"),
Some("fn a() {}".to_string()),
"code block not extracted"
);
assert_eq!(app::extract_code_block("no fences here"), None);
let mut app = App::new(None)?;
typ(&mut app, "old_code")?;
app.handle_key(kc(KeyCode::Char('a')))?; app.handle_key(KeyEvent::new(KeyCode::End, KeyModifiers::SHIFT))?; assert!(app.selection_range().is_some(), "selection not set");
app.refactor_target = app.selection_range();
app.refactor_replacement = Some("new_code".to_string());
app.apply_refactor();
term.draw(|f| ui::render(f, &mut app))?;
let t = screen_text(&term);
assert!(t.contains("new_code") && !t.contains("old_code"), "refactor not applied");
app.handle_key(kc(KeyCode::Char('_')))?; term.draw(|f| ui::render(f, &mut app))?;
assert!(screen_text(&term).contains("old_code"), "one undo did not revert the refactor");
}
println!("[selfcheck] selection refactor (undo) .. PASS");
{
let mut app = App::new(None)?;
typ(&mut app, "foo.bar(baz)")?;
app.handle_key(kc(KeyCode::Char('a')))?; let col = |a: &App| a.focused_pane().cursor_col;
app.move_token_forward(); assert_eq!(col(&app), 3, "token→ should stop at '.'");
app.move_token_forward(); assert_eq!(col(&app), 4, "token→ should stop at 'bar'");
app.move_token_forward(); assert_eq!(col(&app), 7, "token→ should stop at '('");
app.move_token_backward(); assert_eq!(col(&app), 4, "token← should return to 'bar'");
app.move_token_forward(); app.match_bracket(); assert_eq!(col(&app), 11, "match_bracket → ')'");
app.match_bracket(); assert_eq!(col(&app), 7, "match_bracket → '('");
let mut app = App::new(None)?;
typ(&mut app, "fn one() {")?; app.handle_key(k(KeyCode::Enter))?;
typ(&mut app, " body")?; app.handle_key(k(KeyCode::Enter))?;
typ(&mut app, "fn two() {")?; app.jump_symbol(false); assert_eq!(app.focused_pane().cursor_row, 0, "symbol← to fn one");
app.jump_symbol(true); assert_eq!(app.focused_pane().cursor_row, 2, "symbol→ to fn two");
}
println!("[selfcheck] token/bracket/symbol jumps . PASS");
{
let mut app = App::new(None)?;
typ(&mut app, "hello")?;
term.draw(|f| ui::render(f, &mut app))?;
assert!(screen_text(&term).contains("hello"), "typing did not appear");
app.handle_key(kc(KeyCode::Char('_')))?; term.draw(|f| ui::render(f, &mut app))?;
assert!(!screen_text(&term).contains("hello"), "undo did not reverse a typed run");
let mut app = App::new(None)?;
typ(&mut app, "AAA")?;
app.handle_key(kc(KeyCode::Char('a')))?; typ(&mut app, "BBB")?; app.handle_key(kc(KeyCode::Char('_')))?; term.draw(|f| ui::render(f, &mut app))?;
let t = screen_text(&term);
assert!(t.contains("AAA") && !t.contains("BBB"), "undo did not stop at the run boundary");
}
println!("[selfcheck] undo (coalesced runs) ..... PASS");
{
let mut app = App::new(None)?;
typ(&mut app, "one")?;
app.handle_key(kc(KeyCode::Char('a')))?; typ(&mut app, "two")?; let text = |a: &App| -> String {
match a.focused_pane().content {
pane::PaneContent::Editor(id) => a.buffers[&id].rope.to_string(),
_ => String::new(),
}
};
assert!(text(&app).contains("one"), "setup: text not present before undo");
app.run_action(palette::Action::UndoMode);
assert!(matches!(app.mode, mode::Mode::Undo), "UndoMode did not enter undo mode");
app.handle_key(k(KeyCode::Home))?; assert!(text(&app).trim().is_empty(), "Home did not undo to the start: {:?}", text(&app));
app.handle_key(k(KeyCode::End))?; assert!(text(&app).contains("one"), "End did not redo forward");
app.handle_key(k(KeyCode::Esc))?;
assert!(matches!(app.mode, mode::Mode::Edit), "Esc did not exit undo mode");
}
println!("[selfcheck] undo time-travel mode ..... PASS");
{
let mut app = App::new(None)?;
typ(&mut app, "ab")?;
app.handle_key(k(KeyCode::Enter))?;
typ(&mut app, "cd")?; app.handle_key(k(KeyCode::Up))?; app.handle_key(k(KeyCode::End))?; let pos = |a: &App| (a.focused_pane().cursor_row, a.focused_pane().cursor_col);
assert_eq!(pos(&app), (0, 2), "End did not reach line end");
app.handle_key(k(KeyCode::Right))?;
assert_eq!(pos(&app), (1, 0), "→ at line end did not wrap to the next line");
app.handle_key(k(KeyCode::Left))?;
assert_eq!(pos(&app), (0, 2), "← at line start did not wrap to the previous line");
}
println!("[selfcheck] cross-line arrow wrap ..... PASS");
{
let mut app = App::new(None)?;
typ(&mut app, " x")?; app.handle_key(k(KeyCode::Enter))?;
assert_eq!(app.focused_pane().cursor_col, 4, "Enter did not auto-indent to match");
}
println!("[selfcheck] auto-indent on newline .... PASS");
let text_of = |a: &App| -> String {
match a.focused_pane().content {
pane::PaneContent::Editor(id) => a.buffers[&id].rope.to_string(),
_ => String::new(),
}
};
{
let mut app = App::new(None)?;
typ(&mut app, "aa")?; app.handle_key(k(KeyCode::Enter))?; typ(&mut app, "bb")?;
app.handle_key(kc(KeyCode::Char('x')))?; app.handle_key(k(KeyCode::Char('h')))?; app.handle_key(k(KeyCode::Tab))?;
assert_eq!(text_of(&app), " aa\n bb", "Tab did not indent the block");
app.handle_key(KeyEvent::new(KeyCode::BackTab, KeyModifiers::SHIFT))?;
assert_eq!(text_of(&app), "aa\nbb", "Shift-Tab did not dedent the block");
}
println!("[selfcheck] indent/dedent selection ... PASS");
{
let mut app = App::new(None)?;
typ(&mut app, "foo bar foo")?;
app.run_action(palette::Action::QueryReplace);
assert!(app.mode == mode::Mode::Prompt, "query-replace did not prompt");
typ(&mut app, "foo")?; app.handle_key(k(KeyCode::Enter))?; typ(&mut app, "XYZ")?; app.handle_key(k(KeyCode::Enter))?; assert!(app.mode == mode::Mode::Prompt, "query-replace stepping prompt missing");
app.handle_key(k(KeyCode::Char('y')))?; app.handle_key(k(KeyCode::Char('!')))?; assert_eq!(text_of(&app), "XYZ bar XYZ", "query-replace did not replace all matches");
assert!(app.mode == mode::Mode::Edit, "query-replace did not finish");
}
println!("[selfcheck] query-replace (y/n/!/q) ... PASS");
app.handle_key(kc(KeyCode::Char('x')))?;
assert_eq!(app.pending_prefix.len(), 1, "C-x did not arm a prefix");
app.frame_tick += 30; term.draw(|f| ui::render(f, &mut app))?;
assert!(screen_text(&term).contains("C-x -"), "which-key panel missing");
app.handle_key(kc(KeyCode::Char('s')))?; assert!(app.mode == mode::Mode::Prompt, "pathless save did not prompt Save-As");
term.draw(|f| ui::render(f, &mut app))?;
assert!(screen_text(&term).contains("Save as:"), "Save-As prompt not shown");
app.handle_key(kc(KeyCode::Char('g')))?; println!("[selfcheck] which-key + Save-As ........ PASS");
app.handle_key(kc(KeyCode::Char('x')))?;
app.handle_key(kc(KeyCode::Char('c')))?;
assert!(!app.should_quit, "quit discarded unsaved work without asking");
assert!(app.mode == mode::Mode::Prompt, "no quit confirmation prompt");
app.handle_key(k(KeyCode::Char('q')))?; assert!(app.should_quit, "confirmed quit did not quit");
app.should_quit = false;
println!("[selfcheck] dirty-quit guard ........... PASS");
let mut app = App::new(None)?;
app.handle_key(kc(KeyCode::Char(' ')))?;
term.draw(|f| ui::render(f, &mut app))?;
assert!(screen_text(&term).contains("CMD"), "Ctrl+Space did not open the bar");
typ(&mut app, "split")?;
term.draw(|f| ui::render(f, &mut app))?;
let t9 = screen_text(&term);
assert!(t9.contains("Split"), "fuzzy search lost Split");
assert!(t9.contains("C-x 3"), "dropdown row missing its live (universal) keybinding");
assert!(!t9.contains("C--"), "dropdown taught a kitty-only chord (honesty breach)");
app.handle_key(k(KeyCode::Esc))?;
println!("[selfcheck] bar fuzzy + live binding ... PASS");
for _ in 0..3 {
app.handle_key(kc(KeyCode::Char(' ')))?;
typ(&mut app, "undo")?;
app.handle_key(k(KeyCode::Enter))?;
}
assert!(
app.status_msg.as_deref().unwrap_or("").contains("next time"),
"no graduation nudge after 3 bar uses"
);
println!("[selfcheck] graduation nudge ........... PASS");
app.handle_key(kc(KeyCode::Char(' ')))?;
app.handle_key(k(KeyCode::Char('!')))?;
term.draw(|f| ui::render(f, &mut app))?;
assert!(screen_text(&term).contains("SH !"), "! did not enter shell mode");
typ(&mut app, "echo ares_shell_ok")?;
app.handle_key(k(KeyCode::Enter))?;
assert!(app.mode == mode::Mode::Terminal, "shell command did not attach terminal");
std::thread::sleep(std::time::Duration::from_millis(900));
let tid = match app.focused_pane().content {
pane::PaneContent::Terminal(id) => id,
_ => panic!("focused pane is not a terminal"),
};
assert!(
app.terms[&tid].screen().contents().contains("ares_shell_ok"),
"shell command output not found in PTY"
);
println!("[selfcheck] bar `!` → shell ............ PASS");
app.handle_key(kc(KeyCode::Char(' ')))?;
assert!(app.mode == mode::Mode::Bar, "Ctrl+Space dead inside terminal");
app.handle_key(k(KeyCode::Esc))?;
assert!(app.mode == mode::Mode::Terminal, "bar did not return to terminal");
app.handle_key(kc(KeyCode::Char('g')))?; assert!(app.mode == mode::Mode::Edit, "C-g did not detach");
println!("[selfcheck] bar from terminal .......... PASS");
app.handle_key(kc(KeyCode::Char(' ')))?;
app.handle_key(k(KeyCode::Tab))?; typ(&mut app, "hi")?;
app.handle_key(k(KeyCode::Enter))?;
if agent::AgentConfig::from_env().is_configured() {
assert!(app.agent_pending, "expected a pending request with key set");
println!("[selfcheck] ask agent (key set) ........ PASS");
} else {
let ans = app.agent_answer.clone().unwrap_or_default();
assert!(ans.contains("API key"), "expected no-key notice, got: {ans:?}");
println!("[selfcheck] ask agent (no key) ......... PASS");
}
app.handle_key(k(KeyCode::Esc))?;
let mut app = App::new(None)?;
typ(&mut app, "one")?;
app.handle_key(kc(KeyCode::Char('x')))?;
app.handle_key(k(KeyCode::Char('2')))?; app.new_scratch(); app.handle_key(kc(KeyCode::Char('x')))?;
app.handle_key(k(KeyCode::Char('k')))?; app.handle_key(kc(KeyCode::Char('x')))?;
app.handle_key(k(KeyCode::Char('o')))?; let _ = app.focused_buf(); assert_eq!(app.buffers.len(), 1, "kill_buffer left extra buffers");
println!("[selfcheck] kill_buffer retarget ....... PASS");
let (tx, rx) = std::sync::mpsc::channel();
let mut sh = terminal::spawn(0, 24, 80, 1000, None, None, tx)?;
sh.send_bytes(b"echo ares_pty_ok\n");
std::thread::sleep(std::time::Duration::from_millis(700));
while rx.try_recv().is_ok() {}
assert!(sh.screen().contents().contains("ares_pty_ok"), "terminal echo not found");
println!("[selfcheck] terminal PTY echo .......... PASS");
{
sh.send_bytes(b"printf 'COPYME123\\n'\n");
std::thread::sleep(std::time::Duration::from_millis(600));
while rx.try_recv().is_ok() {}
let screen = sh.screen();
let (rows, cols) = screen.size();
let mut out_row = None;
for r in 0..rows {
let mut line = String::new();
for c in 0..cols {
line.push_str(&screen.cell(r, c).map(|x| x.contents()).unwrap_or_default());
}
if line.trim() == "COPYME123" { out_row = Some(r); break; }
}
let r = out_row.expect("printf output row not found on screen");
let text = app::selection_text_from_screen(&screen, (r, 0), (r, 8), cols - 1);
assert_eq!(text, "COPYME123", "terminal selection extraction wrong: {text:?}");
}
println!("[selfcheck] terminal mouse-copy ....... PASS");
{
let mut app = App::new(None)?;
let tabs_before = app.tabs.len();
let f = std::env::temp_dir().join(format!("mars-nest-{}.txt", std::process::id()));
std::fs::write(&f, "nested_open_content")?;
app.open_file_in_new_tab(f.to_str().unwrap());
assert_eq!(app.tabs.len(), tabs_before + 1, "open did not add a tab");
assert_eq!(app.active_tab, app.tabs.len() - 1, "did not switch to the new tab");
assert!(app.mode == mode::Mode::Edit, "not in edit mode after nested open");
let shows = match app.focused_pane().content {
pane::PaneContent::Editor(id) => app.buffers[&id].rope.to_string().contains("nested_open_content"),
_ => false,
};
assert!(shows, "new tab's focused pane is not the opened file");
let _ = std::fs::remove_file(&f);
}
println!("[selfcheck] nested open (new tab) ..... PASS");
sh.send_bytes(b"seq 1 100\n");
std::thread::sleep(std::time::Duration::from_millis(700));
while rx.try_recv().is_ok() {}
let live = sh.screen().contents();
assert!(live.contains("100"), "seq output missing");
assert!(!live.contains("\n3\n"), "expected early lines scrolled out of view");
sh.scroll_view(80); assert!(sh.view_offset() > 0, "view offset did not move");
let back = sh.screen().contents();
assert_ne!(live, back, "scrollback view identical to live view");
assert!(back.contains("\n3\n") || back.contains("seq 1 100"),
"history not visible after scrolling back");
sh.scroll_to_live();
assert_eq!(sh.view_offset(), 0, "snap-back failed");
assert_eq!(sh.screen().contents(), live, "live view changed after snap-back");
let tail = sh.history_tail(60);
assert!(tail.contains("100"), "history_tail missing recent output");
assert!(tail.lines().count() > 24, "history_tail did not exceed one screen");
assert_eq!(sh.view_offset(), 0, "history_tail left the view scrolled back");
println!("[selfcheck] terminal scrollback ........ PASS");
let mut app = App::new(None)?;
app.handle_key(kc(KeyCode::Char(' ')))?;
app.handle_key(k(KeyCode::Char('!')))?;
typ(&mut app, "exit")?;
app.handle_key(k(KeyCode::Enter))?; let mut dead = false;
for _ in 0..40 {
std::thread::sleep(std::time::Duration::from_millis(50));
app.tick(); if app.terms.values().any(|t| t.exited) { dead = true; break; }
}
assert!(dead, "shell exit not detected");
term.draw(|f| ui::render(f, &mut app))?;
assert!(
screen_text(&term).contains("process exited"),
"dead-shell notice not rendered"
);
app.handle_key(k(KeyCode::Enter))?; assert!(app.terms.is_empty(), "exited terminal not cleaned up");
assert!(
matches!(app.focused_pane().content, pane::PaneContent::Editor(_)),
"pane not recycled to an editor"
);
assert!(app.mode == mode::Mode::Edit, "mode not restored after dismissal");
println!("[selfcheck] dead-shell lifecycle ....... PASS");
let autosave_file = cfg_dir.join("autosave_probe.txt");
std::fs::write(&autosave_file, "original")?;
let mut app = App::new(Some(autosave_file.to_string_lossy().to_string()))?;
app.tuning.autosave_secs = 1;
app.handle_key(kc(KeyCode::Char('e')))?; typ(&mut app, " EDITED")?;
let ticks = (1000 / app.tuning.poll_interval_ms).max(1) + 2;
for _ in 0..ticks {
app.tick();
}
assert!(
std::fs::read_to_string(&autosave_file)?.contains("EDITED"),
"autosave did not write the buffer to disk"
);
println!("[selfcheck] autosave (crash safety) .... PASS");
fn alt(code: KeyCode) -> KeyEvent {
KeyEvent::new(code, KeyModifiers::ALT)
}
let mut app = App::new(None)?;
app.handle_key(kc(KeyCode::Char('t')))?; assert!(app.mode == mode::Mode::Tab, "C-t did not enter travel mode");
app.handle_key(k(KeyCode::Char('t')))?; assert_eq!(app.tabs.len(), 2, "C-t t did not open a new tab");
assert_eq!(app.active_tab, 1);
assert!(app.mode == mode::Mode::Edit, "new tab did not exit travel mode");
app.handle_key(KeyEvent::new(KeyCode::Char('{'), KeyModifiers::ALT | KeyModifiers::SHIFT))?;
assert_eq!(app.active_tab, 0, "M-{{ did not switch to the previous tab");
app.handle_key(KeyEvent::new(KeyCode::Char('}'), KeyModifiers::ALT | KeyModifiers::SHIFT))?;
assert_eq!(app.active_tab, 1, "M-}} did not switch to the next tab");
app.handle_key(alt(KeyCode::Char('1')))?;
assert_eq!(app.active_tab, 0, "M-1 did not jump to tab 1");
println!("[selfcheck] tab movement ............... PASS");
let mut app = App::new(None)?;
app.handle_key(kc(KeyCode::Char('\\')))?; assert_eq!(app.tab().layout.count(), 2, "C-\\ did not split");
let right = app.focused_pane_id();
term.draw(|f| ui::render(f, &mut app))?; app.handle_key(kc(KeyCode::Left))?; let left = app.focused_pane_id();
assert_ne!(left, right, "C-Left did not move focus");
app.handle_key(kc(KeyCode::Right))?; assert_eq!(app.focused_pane_id(), right, "C-Right did not move focus back");
app.handle_key(alt(KeyCode::Char('o')))?; assert_eq!(app.focused_pane_id(), left, "M-o did not cycle panes");
let buf1 = app.new_scratch();
app.panes.get_mut(&left).unwrap().content = pane::PaneContent::Editor(buf1);
app.handle_key(kc(KeyCode::Char('x')))?;
app.handle_key(k(KeyCode::Char('x')))?; assert_eq!(app.focused_pane_id(), right, "swap did not follow the moved content");
assert!(
matches!(app.focused_pane().content, pane::PaneContent::Editor(id) if id == buf1),
"pane content did not move on swap"
);
println!("[selfcheck] pane movement + swap ....... PASS");
let mut app = App::new(None)?;
typ(&mut app, "l1")?;
app.handle_key(k(KeyCode::Enter))?;
typ(&mut app, "l2")?;
app.handle_key(k(KeyCode::Enter))?;
typ(&mut app, "l3")?;
app.handle_key(alt(KeyCode::Char('g')))?; assert!(app.mode == mode::Mode::Prompt, "M-g did not prompt");
typ(&mut app, "1")?;
app.handle_key(k(KeyCode::Enter))?;
assert_eq!(app.focused_pane().cursor_row, 0, "goto-line did not jump");
println!("[selfcheck] goto line (M-g) ............ PASS");
app.paste_text("PASTED");
term.draw(|f| ui::render(f, &mut app))?;
assert!(screen_text(&term).contains("PASTED"), "paste_text did not insert");
let cv = config::parse_key("C-v").unwrap();
assert_eq!(
app.keys.lookup(&[cv]),
Some(palette::Action::Paste),
"C-v is not bound to Paste"
);
println!("[selfcheck] paste (C-v + bracketed) .... PASS");
let mut app = App::new(None)?;
typ(&mut app, "hello world")?;
app.handle_key(kc(KeyCode::Char('a')))?; for _ in 0..5 {
app.handle_key(KeyEvent::new(KeyCode::Right, KeyModifiers::SHIFT))?;
}
app.handle_key(kc(KeyCode::Char('c')))?; assert_eq!(app.kill_ring.last().map(String::as_str), Some("hello"), "C-c did not copy");
app.handle_key(kc(KeyCode::Char('e')))?; app.handle_key(kc(KeyCode::Char('v')))?; term.draw(|f| ui::render(f, &mut app))?;
assert!(screen_text(&term).contains("hello worldhello"), "C-v did not paste the copy");
app.handle_key(kc(KeyCode::Char('c')))?; assert_eq!(
app.kill_ring.last().map(String::as_str),
Some("hello worldhello"),
"C-c without selection did not copy the line"
);
println!("[selfcheck] Ctrl+C / Ctrl+V ............ PASS");
let mut app = App::new(None)?;
app.handle_key(kc(KeyCode::Char('t')))?; term.draw(|f| ui::render(f, &mut app))?;
let t21 = screen_text(&term);
assert!(t21.contains("space warp"), "space-warp cheat panel missing");
assert!(t21.contains("split right"), "cheat panel missing split hint");
app.handle_key(k(KeyCode::Char('-')))?; assert_eq!(app.tab().layout.count(), 2, "travel '-' did not split");
assert!(app.mode == mode::Mode::Edit, "split did not exit travel mode");
app.handle_key(kc(KeyCode::Char('t')))?;
app.handle_key(k(KeyCode::Char('o')))?; assert!(app.mode == mode::Mode::Tab, "navigation should stay in travel mode");
app.handle_key(k(KeyCode::Char('|')))?; assert_eq!(app.tab().layout.count(), 3, "travel '|' did not split");
app.handle_key(kc(KeyCode::Char('t')))?;
app.handle_key(k(KeyCode::Esc))?; assert!(app.mode == mode::Mode::Edit, "Esc did not leave travel mode");
println!("[selfcheck] C-t travel mode ............ PASS");
for (seq, action) in [
("C-{", palette::Action::PrevTab),
("C-}", palette::Action::NextTab),
("C--", palette::Action::SplitHorizontal),
("C-|", palette::Action::SplitVertical),
("M--", palette::Action::SplitHorizontal),
] {
let chord = config::parse_key(seq).unwrap();
assert_eq!(app.keys.lookup(&[chord]), Some(action), "{seq} not bound");
}
println!("[selfcheck] modern chords (kitty) ...... PASS");
let mut app = App::new(None)?;
app.handle_key(kc(KeyCode::Char('\\')))?; let right = app.focused_pane_id();
term.draw(|f| ui::render(f, &mut app))?;
app.handle_key(kc(KeyCode::Left))?; let left = app.focused_pane_id();
assert_ne!(left, right, "Ctrl+Left did not move focus");
app.handle_key(kc(KeyCode::Char('o')))?; assert_eq!(app.focused_pane_id(), right, "C-o did not cycle panes");
println!("[selfcheck] pane nav sans Meta ......... PASS");
let mut app = App::new(None)?;
app.handle_key(kc(KeyCode::Char(' ')))?;
app.handle_key(k(KeyCode::Char('!')))?;
typ(&mut app, "true")?;
app.handle_key(k(KeyCode::Enter))?; assert!(app.mode == mode::Mode::Terminal);
app.handle_key(kc(KeyCode::Char('t')))?; assert!(app.mode == mode::Mode::Tab, "C-t dead inside terminal");
app.handle_key(k(KeyCode::Char('t')))?; assert_eq!(app.tabs.len(), 2);
assert!(app.mode == mode::Mode::Edit);
app.handle_key(KeyEvent::new(KeyCode::Char('{'), KeyModifiers::ALT | KeyModifiers::SHIFT))?;
assert_eq!(app.active_tab, 0, "M-{{ did not switch tab from editor");
app.handle_key(k(KeyCode::Enter))?; assert!(app.mode == mode::Mode::Terminal);
app.handle_key(KeyEvent::new(KeyCode::Char('}'), KeyModifiers::ALT | KeyModifiers::SHIFT))?;
assert_eq!(app.active_tab, 1, "M-}} dead inside terminal");
println!("[selfcheck] terminal chrome layer ...... PASS");
let cmd_c = config::parse_key("cmd-c").unwrap();
assert!(cmd_c.modifiers.contains(KeyModifiers::SUPER), "cmd- prefix not SUPER");
assert_eq!(app.keys.lookup(&[cmd_c]), Some(palette::Action::CopyRegion));
assert_eq!(
app.keys.lookup(&[config::parse_key("cmd-v").unwrap()]),
Some(palette::Action::Paste)
);
println!("[selfcheck] cmd-c / cmd-v bindings ..... PASS");
let tuning_path = cfg_dir.join("mars").join("tuning.json");
let written = std::fs::read_to_string(&tuning_path)?;
assert!(written.contains("description"), "tuning defaults lack descriptions");
assert!(written.contains("which_key_delay_ms"), "tuning defaults missing knobs");
std::fs::write(
&tuning_path,
r#"{ "max_panes": { "value": 2, "description": "test override" } }"#,
)?;
let mut app = App::new(None)?;
assert_eq!(app.tuning.max_panes, 2, "tuning override not applied");
app.handle_key(kc(KeyCode::Char('\\')))?; app.handle_key(kc(KeyCode::Char('\\')))?; assert_eq!(app.tab().layout.count(), 2, "max_panes override not enforced");
assert!(
app.status_msg.as_deref().unwrap_or("").contains("Max 2"),
"cap message should reflect the tuned value"
);
std::fs::remove_file(&tuning_path)?; println!("[selfcheck] tuning knobs + override .... PASS");
let mut app = App::new(None)?;
app.handle_key(k(KeyCode::Char('G')))?; typ(&mut app, "UT")?;
term.draw(|f| ui::render(f, &mut app))?;
let t26 = screen_text(&term);
assert!(t26.contains("GUT"), "typed text missing");
assert!(!t26.contains(" 1│"), "number gutter rendered despite line_numbers=false");
assert!(t26.contains("▸"), "pointer gutter marker missing on the cursor line");
assert!(t26.contains("Ln 1, Col 4"), "status bar missing line/col readout");
app.tuning.line_numbers = true;
term.draw(|f| ui::render(f, &mut app))?;
assert!(screen_text(&term).contains(" 1│"), "line_numbers knob did not restore numbers");
println!("[selfcheck] pointer gutter + Ln/Col ... PASS");
let proj = cfg_dir.join(format!("proj-{}", std::process::id()));
{
use std::io::Write as _;
std::fs::create_dir_all(proj.join("src"))?;
std::fs::create_dir_all(proj.join("target"))?; for f in ["src/main.rs", "src/session.rs", "README.md"] {
let p = proj.join(f);
std::fs::create_dir_all(p.parent().unwrap())?;
std::fs::File::create(&p)?.write_all(b"x")?;
}
std::fs::File::create(proj.join("target/junk.rs"))?.write_all(b"x")?;
let idx = project::Index::build(proj.clone(), 10_000, &["target".to_string()]);
assert!(idx.files.iter().any(|f| f == "src/session.rs"), "index missing a real file");
assert!(!idx.files.iter().any(|f| f.contains("target")), "index did not skip target/");
assert!(project::Index::build(proj.clone(), 2, &["target".to_string()]).files.len() <= 2,
"index did not honor the cap");
}
println!("[selfcheck] project index (skip/cap) .. PASS");
let mut app = App::new(None)?;
app.set_project_index_for_test(
proj.clone(),
vec!["src/main.rs".into(), "src/session.rs".into(), "README.md".into()],
);
app.handle_key(kc(KeyCode::Char(' ')))?; app.handle_key(k(KeyCode::Char('@')))?; assert!(matches!(app.mode, mode::Mode::Tree) && app.tree_open, "@ did not open the tree");
assert!(app.tree_rows.iter().any(|r| r.label == "src" && r.is_dir), "tree missing src/ folder");
assert!(!app.tree_rows.iter().any(|r| r.label == "target"), "tree showed the ignored dir");
app.handle_key(k(KeyCode::Down))?; app.handle_key(k(KeyCode::Enter))?; assert!(app.tree_rows.iter().any(|r| r.label == "session.rs" && r.depth == 1),
"expanding src/ did not reveal its children");
term.draw(|f| ui::render(f, &mut app))?;
assert!(screen_text(&term).contains("src"), "tree sidebar did not render");
typ(&mut app, "sesn")?;
assert_eq!(app.tree_rows.first().map(|r| r.label.as_str()), Some("src/session.rs"),
"filter did not shortlist session.rs on top");
let bufs_before = app.buffers.len();
app.handle_key(k(KeyCode::Right))?;
assert!(matches!(app.mode, mode::Mode::Tree), "→ on a file left the tree (should preview)");
app.handle_key(k(KeyCode::Right))?;
assert_eq!(app.buffers.len(), bufs_before + 1, "preview duplicated the buffer");
app.handle_key(k(KeyCode::Enter))?;
assert!(matches!(app.mode, mode::Mode::Edit), "Enter on a tree file did not focus the editor");
assert!(app.tree_open, "sidebar should stay open after opening a file");
println!("[selfcheck] file tree (preview/open) .. PASS");
let mut app = App::new(None)?;
app.set_project_index_for_test(proj.clone(), vec!["README.md".into()]);
app.handle_key(kc(KeyCode::Char('x')))?;
app.handle_key(k(KeyCode::Char('d')))?; assert!(app.tree_open && matches!(app.mode, mode::Mode::Tree), "C-x d did not open the tree");
let root_before = app.file_tree.as_ref().map(|t| t.root.clone()).unwrap();
app.handle_key(k(KeyCode::Enter))?; let root_after = app.file_tree.as_ref().map(|t| t.root.clone()).unwrap();
assert_eq!(root_after, root_before.parent().unwrap(), "../ did not re-root to the parent");
app.handle_key(k(KeyCode::Esc))?; assert!(!app.tree_open && matches!(app.mode, mode::Mode::Edit), "Esc did not close the tree");
app.handle_key(kc(KeyCode::Char('x')))?;
app.handle_key(k(KeyCode::Char('d')))?; let reopened_root = app.file_tree.as_ref().map(|t| t.root.clone()).unwrap();
assert_eq!(reopened_root, root_before, "tree did not reset to the project root on reopen");
assert!(app.tree_open && matches!(app.mode, mode::Mode::Tree), "C-x d did not reopen the tree");
println!("[selfcheck] file tree (reset/../) ..... PASS");
let mut app = App::new(None)?;
app.agent_history.push(("user".into(), "first question".into()));
let long: String = (1..=30).map(|i| format!("L{i}")).collect::<Vec<_>>().join("\n");
app.agent_history.push(("assistant".into(), long));
app.handle_key(kc(KeyCode::Char(' ')))?;
app.handle_key(k(KeyCode::Tab))?; term.draw(|f| ui::render(f, &mut app))?;
let t27 = screen_text(&term);
assert!(t27.contains("L30") && t27.contains("L23"),
"panel lost the tail of a long answer");
assert!(!t27.contains("L15"), "ask panel escaped the ask_panel_max_pct cap");
assert!(t27.contains("(Up to scroll)"), "scroll-up marker missing");
for _ in 0..25 {
app.handle_key(k(KeyCode::Up))?;
}
term.draw(|f| ui::render(f, &mut app))?;
let t27b = screen_text(&term);
assert!(t27b.contains("first question"), "scroll-up did not reach the first turn");
assert!(t27b.contains("more"), "scroll indicator missing");
app.handle_key(kc(KeyCode::Char('l')))?; assert!(app.agent_history.is_empty(), "C-l did not clear the conversation");
app.handle_key(k(KeyCode::Esc))?;
println!("[selfcheck] ask transcript + scroll .... PASS");
let msgs = agent::build_messages(
"reg", "screen",
&[("user".into(), "q1".into()), ("assistant".into(), "a1".into())],
"q2",
);
assert_eq!(msgs.len(), 4, "system + 2 history + question expected");
assert!(msgs[1]["content"].as_str().unwrap_or("").contains("q1"));
assert!(msgs[0]["content"].as_str().unwrap_or("").contains("screen"));
let (d1, dir1) = agent::parse_directive("use ls.\nTYPE: ls -la");
assert_eq!(d1, "use ls.");
assert_eq!(dir1, Some(agent::AgentDirective::Type("ls -la".into())));
let (_, dir2) = agent::parse_directive("split it\nRUN: SplitVertical");
assert_eq!(dir2, Some(agent::AgentDirective::Run("SplitVertical".into())));
assert_eq!(agent::parse_directive("plain answer").1, None);
println!("[selfcheck] agent history + directives . PASS");
let mut app = App::new(None)?;
app.handle_key(kc(KeyCode::Char(' ')))?;
app.handle_key(k(KeyCode::Char('!')))?;
typ(&mut app, "true")?;
app.handle_key(k(KeyCode::Enter))?; app.handle_key(kc(KeyCode::Char(' ')))?; app.handle_key(kc(KeyCode::Char(' ')))?; app.handle_key(k(KeyCode::Tab))?; app.agent_directive = Some(agent::AgentDirective::Type("echo mars_type_ok".into()));
app.handle_key(k(KeyCode::Enter))?; assert!(app.mode == mode::Mode::Terminal, "TYPE did not land in the terminal");
let mut typed = false;
for _ in 0..40 {
std::thread::sleep(std::time::Duration::from_millis(50));
app.tick();
if let pane::PaneContent::Terminal(tid) = app.focused_pane().content {
if app.terms[&tid].screen().contents().contains("mars_type_ok") {
typed = true;
break;
}
}
}
assert!(typed, "TYPE'd command never reached the PTY");
println!("[selfcheck] TYPE → terminal ............ PASS");
let mut app = App::new(None)?;
app.handle_key(kc(KeyCode::Char('t')))?; app.handle_key(k(KeyCode::Char('r')))?; assert!(app.mode == mode::Mode::Prompt, "travel r did not prompt");
app.handle_key(k(KeyCode::Backspace))?; typ(&mut app, "build")?;
app.handle_key(k(KeyCode::Enter))?;
assert_eq!(app.tab().name, "build", "tab rename failed");
let tid0 = app.tab().id;
app.agent_tx.send(agent::AgentEvent::AutoName { tab_id: tid0, name: "x".into() })?;
app.tick();
assert_eq!(app.tab().name, "build", "auto-name overrode a manual rename");
app.run_action(palette::Action::RenamePane);
typ(&mut app, "logs")?;
app.handle_key(k(KeyCode::Enter))?;
term.draw(|f| ui::render(f, &mut app))?;
assert!(screen_text(&term).contains(" logs "), "pane title not rendered");
let mut app = App::new(None)?;
let tid1 = app.tab().id;
app.agent_tx.send(agent::AgentEvent::AutoName { tab_id: tid1, name: "auto-label".into() })?;
app.tick();
assert_eq!(app.tab().name, "auto-label", "auto-name not applied");
println!("[selfcheck] renames + auto-name ........ PASS");
use agent::AgentDirective;
assert_eq!(
agent::parse_directive("Line 42 is the bug.\nOPEN: src/main.rs:42").1,
Some(AgentDirective::Open("src/main.rs:42".into()))
);
assert_eq!(
agent::parse_directive("try this\n`TYPE: git status`").1,
Some(AgentDirective::Type("git status".into())),
"parser should tolerate backtick-wrapped directives"
);
let (disp, dir) = agent::parse_directive("Here's the fix.\nRUN: SplitVertical\nHope that helps!");
assert_eq!(dir, Some(AgentDirective::Run("SplitVertical".into())));
assert!(!disp.contains("SplitVertical"), "directive line should be stripped from display");
assert_eq!(agent::parse_directive("just prose").1, None);
assert_eq!(agent::retry_secs("quota exceeded. Please retry in 14.89197552s."), Some(15));
assert_eq!(agent::retry_secs("no hint here"), None);
println!("[selfcheck] directive parse (OPEN/lenient) PASS");
let probe = cfg_dir.join("open_probe.txt");
std::fs::write(&probe, "a\nb\nc\nd\ne\nf\n")?;
let mut app = App::new(None)?;
app.handle_key(kc(KeyCode::Char(' ')))?; app.handle_key(k(KeyCode::Tab))?; app.agent_directive = Some(AgentDirective::Open(format!("{}:4", probe.to_string_lossy())));
app.handle_key(k(KeyCode::Enter))?; assert_eq!(app.focused_pane().cursor_row, 3, "OPEN did not land on line 4");
assert!(app.focused_buf().name.contains("open_probe"), "OPEN did not open the file");
println!("[selfcheck] OPEN directive lands ....... PASS");
let mut app = App::new(None)?;
typ(&mut app, "some code")?;
app.run_action(palette::Action::ExplainThis);
assert!(app.mode == mode::Mode::Bar, "ExplainThis did not open the bar");
assert!(
matches!(app.palette.as_ref().map(|p| &p.bar_mode), Some(palette::BarMode::Ask)),
"ExplainThis is not in Ask mode"
);
assert!(
app.palette.as_ref().map(|p| p.query.contains("Explain")).unwrap_or(false),
"ExplainThis did not pre-fill a question"
);
assert!(
app.agent_answer.as_deref().unwrap_or("").contains("API key"),
"ExplainThis did not auto-submit"
);
println!("[selfcheck] explain-this / failure .... PASS");
let mut app = App::new(None)?;
app.handle_key(kc(KeyCode::Char('\\')))?; term.draw(|f| ui::render(f, &mut app))?;
let two_rects = app.pane_rects.clone();
assert_eq!(two_rects.len(), 2, "expected 2 panes");
app.handle_key(kc(KeyCode::Char('t')))?; app.handle_key(k(KeyCode::Char('>')))?; app.handle_key(k(KeyCode::Char('>')))?;
term.draw(|f| ui::render(f, &mut app))?;
let grown = app.pane_rects.clone();
let focused = app.focused_pane_id();
let w_before = two_rects.iter().find(|(id, _)| *id == focused).unwrap().1.width;
let w_after = grown.iter().find(|(id, _)| *id == focused).unwrap().1.width;
assert!(w_after > w_before, "resize did not grow the focused pane");
app.handle_key(k(KeyCode::Char('z')))?; term.draw(|f| ui::render(f, &mut app))?;
assert_eq!(app.pane_rects.len(), 1, "zoom did not collapse to one pane");
app.handle_key(k(KeyCode::Char('z')))?; term.draw(|f| ui::render(f, &mut app))?;
assert_eq!(app.pane_rects.len(), 2, "unzoom did not restore both panes");
app.handle_key(k(KeyCode::Esc))?; println!("[selfcheck] pane resize + zoom ........ PASS");
let mut app = App::new(None)?;
app.handle_key(kc(KeyCode::Char(' ')))?;
app.handle_key(k(KeyCode::Char('!')))?; typ(&mut app, "true")?;
app.handle_key(k(KeyCode::Enter))?; assert!(app.mode == mode::Mode::Terminal, "not in a terminal");
app.handle_key(kc(KeyCode::Char(' ')))?; assert!(
matches!(app.palette.as_ref().map(|p| &p.bar_mode), Some(palette::BarMode::Command)),
"Ctrl+Space in terminal did not open the unified (Command) composer"
);
typ(&mut app, "split")?;
assert!(
app.palette.as_ref().map(|p| p.navigated).unwrap_or(false),
"typing did not pre-select the top match"
);
let panes_before = app.tab().layout.pane_ids().len();
app.handle_key(k(KeyCode::Enter))?;
assert!(
app.tab().layout.pane_ids().len() > panes_before,
"Enter did not fire the pre-selected top match"
);
app.handle_key(kc(KeyCode::Char(' ')))?;
typ(&mut app, "qqq")?;
let no_match = app
.palette
.as_ref()
.map(|p| p.visible_items(&app.frecency).is_empty())
.unwrap_or(false);
assert!(no_match, "'qqq' unexpectedly matched a registry row");
app.handle_key(k(KeyCode::Enter))?;
assert!(
app.mode == mode::Mode::Terminal && app.palette.is_none(),
"no-match Enter did not fall through to the shell"
);
app.handle_key(kc(KeyCode::Char(' ')))?;
app.handle_key(k(KeyCode::Char('!')))?;
assert!(
matches!(app.palette.as_ref().map(|p| &p.bar_mode), Some(palette::BarMode::Shell)),
"`!` did not force pure-shell mode"
);
typ(&mut app, "echo composer_ok")?;
app.handle_key(k(KeyCode::Enter))?; assert!(app.mode == mode::Mode::Terminal, "shell composer Enter did not run the command");
println!("[selfcheck] terminal composer (unified) . PASS");
{
let mut app = App::new(None)?;
app.handle_key(kc(KeyCode::Char(' ')))?;
app.handle_key(k(KeyCode::Char('!')))?;
typ(&mut app, "true")?;
app.handle_key(k(KeyCode::Enter))?; app.handle_key(kc(KeyCode::Char(' ')))?; app.handle_key(k(KeyCode::Enter))?; assert!(
app.palette.is_some() && matches!(app.mode, mode::Mode::Bar),
"empty-query Enter should be a no-op, not fire a row"
);
app.handle_key(k(KeyCode::Char('?')))?;
assert!(
matches!(app.palette.as_ref().map(|p| &p.bar_mode), Some(palette::BarMode::Ask)),
"`?` did not switch to ask mode"
);
app.handle_key(kc(KeyCode::Char('g')))?;
app.handle_key(kc(KeyCode::Char(' ')))?;
app.handle_key(k(KeyCode::Char('@')))?;
assert!(app.tree_open, "`@` did not open the navigator");
assert_eq!(palette::bar_quick_key(&palette::Action::ToggleFileTree), Some('@'));
assert_eq!(palette::bar_quick_key(&palette::Action::AskAgent), Some('?'));
assert!(
palette::bar_quick_legend().iter().any(|(key, _)| *key == "!"),
"quick-key legend lost `!` shell"
);
let mut app = App::new(None)?;
app.handle_key(kc(KeyCode::Char(' ')))?;
typ(&mut app, "qqq")?;
app.handle_key(k(KeyCode::Enter))?;
assert!(
matches!(app.palette.as_ref().map(|p| &p.bar_mode), Some(palette::BarMode::Ask)),
"editor no-match Enter did not fall through to an ask"
);
assert!(
app.agent_answer.as_deref().unwrap_or("").starts_with('⚠'),
"hermetic ask fallback should surface the no-key notice"
);
}
println!("[selfcheck] bar quick keys + fallbacks . PASS");
{
let mut app = App::new(None)?;
typ(&mut app, "ab")?;
let buf_id = match app.focused_pane().content {
pane::PaneContent::Editor(id) => id,
_ => panic!("scratch pane is not an editor"),
};
std::env::set_var("MARS_LLM_KEY", "selfcheck");
std::env::set_var("MARS_LLM_URL", "http://127.0.0.1:9/v1/chat/completions");
app.handle_key(kc(KeyCode::Char(' ')))?;
app.handle_key(k(KeyCode::Char('?')))?;
typ(&mut app, "write a limerick about potatoes")?;
app.handle_key(k(KeyCode::Enter))?;
assert_eq!(
app.refactor_target,
Some((buf_id, 2, 2)),
"no-selection ask did not target an empty range at the cursor"
);
std::env::remove_var("MARS_LLM_KEY");
std::env::remove_var("MARS_LLM_URL");
app.refactor_target = Some((buf_id, 1, 1));
app.refactor_replacement = Some("XY".into());
term.draw(|f| ui::render(f, &mut app))?;
assert!(
screen_text(&term).contains("insert at the cursor"),
"confirm chip did not say insert for an empty target range"
);
app.apply_refactor();
let text = app.buffers.get(&buf_id).map(|b| b.rope.to_string()).unwrap_or_default();
assert_eq!(text, "aXYb", "empty-range refactor did not insert at point");
app.run_action(palette::Action::Undo);
let text = app.buffers.get(&buf_id).map(|b| b.rope.to_string()).unwrap_or_default();
assert_eq!(text, "ab", "cursor insertion was not one reversible undo step");
}
println!("[selfcheck] cursor-point generation .... PASS");
{
let mut app = App::new(None)?;
app.handle_key(kc(KeyCode::Char(' ')))?;
app.handle_key(k(KeyCode::Char('!')))?;
typ(&mut app, "true")?;
app.handle_key(k(KeyCode::Enter))?; term.draw(|f| ui::render(f, &mut app))?; let tid = match app.focused_pane().content {
pane::PaneContent::Terminal(id) => id,
_ => panic!("focused pane is not a terminal"),
};
app.handle_key(kc(KeyCode::Char(' ')))?; term.draw(|f| ui::render(f, &mut app))?;
let t = screen_text(&term);
assert!(
t.contains("run a command…"),
"overlay missing though it does not overlap the dropdown"
);
assert!(
t.contains("! shell") && t.contains("? ask") && t.contains("@ files"),
"quick-key legend missing from the empty-query bar line"
);
app.handle_key(kc(KeyCode::Char('g')))?; typ(&mut app, "seq 1 200")?;
app.handle_key(k(KeyCode::Enter))?;
for _ in 0..30 {
app.tick();
if app.terms.get(&tid).map(|t| t.screen().cursor_position().0 >= 25).unwrap_or(false) {
break;
}
std::thread::sleep(std::time::Duration::from_millis(100));
}
assert!(
app.terms.get(&tid).map(|t| t.screen().cursor_position().0 >= 25).unwrap_or(false),
"seq did not push the terminal cursor into the dropdown rows"
);
app.handle_key(kc(KeyCode::Char(' ')))?;
term.draw(|f| ui::render(f, &mut app))?;
let t = screen_text(&term);
assert!(
!t.contains("run a command…"),
"overlay drew on top of the dropdown instead of yielding"
);
assert!(t.contains("Navigator"), "dropdown missing while the overlay yielded");
app.handle_key(kc(KeyCode::Char('g')))?;
}
println!("[selfcheck] overlay yields to dropdown . PASS");
{
use crossterm::event::{MouseEvent, MouseEventKind};
let mut app = App::new(None)?;
app.handle_key(kc(KeyCode::Char(' ')))?;
app.handle_key(k(KeyCode::Char('?')))?; for i in 0..40 {
app.agent_history.push(("user".into(), format!("question number {i}")));
app.agent_history.push(("assistant".into(), format!("answer number {i}")));
}
term.draw(|f| ui::render(f, &mut app))?;
let t = screen_text(&term);
assert!(t.contains("answer number 39"), "panel not pinned to the newest turn");
assert!(!t.contains("question number 0"), "80-turn transcript rendered unbounded");
assert!(t.contains("more (Up to scroll)"), "scroll-up marker missing");
let turns = t.matches("you ›").count() + t.matches("mars ›").count();
assert!(
(2..=13).contains(&turns),
"ask panel height escaped the 30% cap ({turns} turns visible)"
);
let wheel = |up: bool| MouseEvent {
kind: if up { MouseEventKind::ScrollUp } else { MouseEventKind::ScrollDown },
column: 5,
row: 5,
modifiers: KeyModifiers::NONE,
};
app.handle_mouse(wheel(true));
assert_eq!(app.ask_scroll, app.tuning.wheel_scroll_lines, "wheel did not scroll the ask panel");
for _ in 0..20 { app.handle_mouse(wheel(true)); }
term.draw(|f| ui::render(f, &mut app))?;
let t = screen_text(&term);
assert!(
t.contains("more (Down to scroll)"),
"scrolled-up panel lost its way back down"
);
app.handle_mouse(wheel(false));
assert!(app.ask_scroll < 21 * app.tuning.wheel_scroll_lines, "wheel down did not scroll back");
}
println!("[selfcheck] ask panel bounded+scrolls .. PASS");
let mut app = App::new(None)?;
app.handle_key(kc(KeyCode::Char(' ')))?;
app.handle_key(k(KeyCode::Char('!')))?;
typ(&mut app, "true")?;
app.handle_key(k(KeyCode::Enter))?; let tid = match app.focused_pane().content {
pane::PaneContent::Terminal(id) => id,
_ => panic!("focused pane is not a terminal"),
};
app.run_action(palette::Action::WatchPane);
assert!(app.watches.get(&tid).map(|w| w.watched).unwrap_or(false), "pane not marked watched");
app.agent_tx.send(agent::AgentEvent::WatchSummary { term_id: tid, verdict: "failed: linker error".into() })?;
app.tick();
assert_eq!(app.notices.len(), 1, "verdict did not queue a notice");
assert!(matches!(app.notices[0].kind, app::NoticeKind::Failure), "verdict not classified as failure");
term.draw(|f| ui::render(f, &mut app))?;
assert!(screen_text(&term).contains("linker error"), "notice not rendered");
app.mode = mode::Mode::Edit; app.handle_key(k(KeyCode::Esc))?;
assert!(app.notices.is_empty(), "Esc did not dismiss the notice");
app.bg_busy = true;
app.agent_tx.send(agent::AgentEvent::BgDone)?;
app.tick();
assert!(!app.bg_busy, "BgDone did not release the bg_busy gate");
println!("[selfcheck] watch pane + notice (W6) ... PASS");
{
let mut app = App::new(None)?;
app.tuning.watch_quiet_secs = 0;
app.watches.insert(4242, app::WatchState { watched: true, last_output_tick: 0, ..Default::default() });
app.tick();
assert!(app.watches.get(&4242).map(|w| w.triggered).unwrap_or(false),
"quiet timer did not fire maybe_fire_watches");
}
println!("[selfcheck] watch quiet-timer fires .... PASS");
{
let mut app = App::new(None)?;
app.on_detach();
app.on_attach();
assert!(app.notices.is_empty(), "briefing appeared when nothing changed");
app.on_detach();
app.watches.insert(7, app::WatchState { watched: true, run_started_tick: 1, ..Default::default() });
for _ in 0..20 { app.frame_tick += 1; } app.agent_tx.send(agent::AgentEvent::WatchSummary { term_id: 7, verdict: "failed: tests red".into() })?;
app.tick();
app.on_attach();
assert_eq!(app.notices.len(), 1, "expected exactly one briefing (W6 dupe not subsumed?)");
let n = &app.notices[0];
assert!(n.text.contains("while away") && n.text.contains("tests red"),
"headline missing duration/verdict: {}", n.text);
assert!(matches!(n.kind, app::NoticeKind::Failure), "failing briefing not a Failure");
app.run_action(palette::Action::AwayDigest);
let d = app.agent_history.last().map(|(_, t)| t.clone()).unwrap_or_default();
assert!(d.contains("needs you") && d.contains("tests red") && d.contains("ran "),
"digest sections/duration missing:\n{d}");
}
println!("[selfcheck] away digest (W7+) ......... PASS");
{
assert_eq!(
agent::parse_directive("looking…\nNEED: scrollback").1,
Some(agent::AgentDirective::Need(agent::NeedKind::Scrollback)),
"NEED: scrollback did not parse"
);
assert_eq!(
agent::parse_directive("NEED: tab api").1,
Some(agent::AgentDirective::Need(agent::NeedKind::Tab("api".into()))),
"NEED: tab did not parse"
);
let mut app = App::new(None)?;
let base = app.agent_history.len();
let need = || agent::AgentEvent::Answer {
text: "need more".into(),
directive: Some(agent::AgentDirective::Need(agent::NeedKind::Scrollback)),
};
app.agent_tx.send(need())?;
app.tick(); assert_eq!(app.agent_history.len(), base, "first NEED should not reach the transcript");
app.agent_tx.send(need())?;
app.tick(); assert_eq!(app.agent_history.len(), base + 1, "capped NEED should surface");
}
println!("[selfcheck] NEED: expansion (W4/W5) ... PASS");
{
use std::io::{BufRead, BufReader};
use std::os::unix::net::UnixStream;
let sname = format!("selfcheck-{}", std::process::id());
let spath = session::socket_path(&sname)?;
let sname2 = sname.clone();
let server = std::thread::spawn(move || session::server_main(&sname2, None));
let mut up = false;
for _ in 0..100 {
std::thread::sleep(std::time::Duration::from_millis(30));
if UnixStream::connect(&spath).is_ok() { up = true; break; }
}
assert!(up, "session server did not come up");
struct TestClient {
writer: UnixStream,
reader: BufReader<UnixStream>,
screen: vt100::Parser,
}
impl TestClient {
fn connect(path: &std::path::Path, version: &str) -> Result<Self> {
let stream = UnixStream::connect(path)?;
let reader = BufReader::new(stream.try_clone()?);
let mut me = TestClient { writer: stream, reader, screen: vt100::Parser::new(30, 100, 0) };
session::write_frame(&mut me.writer, &session::ClientFrame::Hello {
cols: 100, rows: 30, version: version.to_string(),
})?;
Ok(me)
}
fn key(&mut self, key: KeyEvent) -> Result<()> {
session::write_frame(&mut self.writer, &session::ClientFrame::Key(key))
.map_err(Into::into)
}
fn text(&mut self, s: &str) -> Result<()> {
for c in s.chars() {
self.key(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE))?;
}
Ok(())
}
fn read_until(&mut self, needle: &str, secs: u64) -> Result<(bool, Option<String>)> {
use base64::{engine::general_purpose::STANDARD as B64, Engine as _};
self.reader.get_ref().set_read_timeout(Some(std::time::Duration::from_millis(200)))?;
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(secs);
while std::time::Instant::now() < deadline {
let mut line = String::new();
match self.reader.read_line(&mut line) {
Ok(0) => break,
Ok(_) => match serde_json::from_str::<session::ServerFrame>(line.trim()) {
Ok(session::ServerFrame::Output { b64 }) => {
if let Ok(bytes) = B64.decode(b64) {
self.screen.process(&bytes);
}
if self.screen.screen().contents().contains(needle) {
return Ok((true, None));
}
}
Ok(session::ServerFrame::Exit { message }) => {
return Ok((self.screen.screen().contents().contains(needle), Some(message)));
}
Ok(session::ServerFrame::Status { .. }) => {}
Err(_) => {}
},
Err(_) => {} }
}
Ok((self.screen.screen().contents().contains(needle), None))
}
}
let mut c1 = TestClient::connect(&spath, env!("CARGO_PKG_VERSION"))?;
c1.text("sessionmarker")?;
let (found, _) = c1.read_until("sessionmarker", 5)?;
assert!(found, "marker not rendered to first client");
let mut c_bad = TestClient::connect(&spath, "0.0.0-bogus")?;
let (_, exit) = c_bad.read_until("\u{0}never\u{0}", 3)?;
assert!(
exit.map(|m| m.contains("version mismatch")).unwrap_or(false),
"version mismatch not refused"
);
let mut c2 = TestClient::connect(&spath, env!("CARGO_PKG_VERSION"))?;
let (_, c1_exit) = c1.read_until("\u{0}never\u{0}", 3)?;
assert!(c1_exit.is_some(), "old client not notified on takeover");
let (found2, _) = c2.read_until("sessionmarker", 5)?;
assert!(found2, "state lost across reattach");
c2.key(KeyEvent::new(KeyCode::Char(' '), KeyModifiers::CONTROL))?;
c2.key(KeyEvent::new(KeyCode::Char('!'), KeyModifiers::NONE))?;
c2.text("echo daemon_pty_ok")?;
c2.key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE))?;
let (pty_ok, _) = c2.read_until("daemon_pty_ok", 8)?;
assert!(pty_ok, "shell output not rendered in session");
drop(c2); std::thread::sleep(std::time::Duration::from_millis(150));
let mut c3 = TestClient::connect(&spath, env!("CARGO_PKG_VERSION"))?;
let (pty_survived, _) = c3.read_until("daemon_pty_ok", 5)?;
assert!(pty_survived, "PTY did not survive the disconnect");
assert!(
session::list_sessions()?
.iter()
.any(|(n, alive, attached)| n == &sname && *alive && *attached),
"ls missing the live+attached session"
);
let renamed = format!("{sname}-renamed");
let rpath = session::socket_path(&renamed)?;
{
let ctl = UnixStream::connect(&spath)?;
let mut w = ctl.try_clone()?;
session::write_frame(&mut w, &session::ClientFrame::Rename { to: renamed.clone() })?;
}
let mut moved = false;
for _ in 0..40 {
std::thread::sleep(std::time::Duration::from_millis(50));
if rpath.exists() && !spath.exists() { moved = true; break; }
}
assert!(moved, "session rename did not move the socket");
assert!(
session::list_sessions()?.iter().any(|(n, alive, _)| n == &renamed && *alive),
"renamed session missing from ls"
);
c3.text("post-rename")?;
let (still_alive, _) = c3.read_until("post-rename", 5)?;
assert!(still_alive, "attached client broke across the rename");
c3.key(KeyEvent::new(KeyCode::Char('g'), KeyModifiers::CONTROL))?; c3.key(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::CONTROL))?;
c3.key(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL))?;
let (_, quit_exit) = c3.read_until("\u{0}never\u{0}", 5)?;
assert!(
quit_exit.map(|m| m.contains("detached")).unwrap_or(false),
"quit did not detach"
);
assert!(rpath.exists(), "quit killed the session instead of detaching");
session::kill_main(&renamed)?;
server.join().expect("server thread panicked")?;
assert!(!rpath.exists(), "socket not removed after kill");
println!("[selfcheck] session daemon ............ PASS");
let kname = format!("selfcheck-kill-{}", std::process::id());
let kpath = session::socket_path(&kname)?;
let kname2 = kname.clone();
let kserver = std::thread::spawn(move || session::server_main(&kname2, None));
let mut up = false;
for _ in 0..100 {
std::thread::sleep(std::time::Duration::from_millis(30));
if UnixStream::connect(&kpath).is_ok() { up = true; break; }
}
assert!(up, "kill-test server did not come up");
assert!(
session::list_sessions()?
.iter()
.any(|(n, alive, attached)| n == &kname && *alive && !*attached),
"fresh session should be alive and detached"
);
session::kill_main(&kname)?;
kserver.join().expect("kill-test server panicked")?;
assert!(!kpath.exists(), "socket not removed after kill");
println!("[selfcheck] session status + kill ..... PASS");
{
let mut app = App::new(None)?;
app.session_name = Some("some-session".into());
app.run_action(palette::Action::Quit);
assert!(app.detach_requested, "in-session Quit did not request a detach");
assert!(!app.should_quit, "in-session Quit ended the session");
app.detach_requested = false;
app.run_action(palette::Action::KillSession);
assert!(app.should_quit, "KillSession did not end a clean session");
assert!(
palette::Action::KillSession.is_destructive(),
"KillSession must be confirm-gated for agent directives"
);
let saved_tmp = std::env::var("TMPDIR").ok();
let iso = std::env::temp_dir().join(format!("mars-killall-{}", std::process::id()));
std::fs::create_dir_all(&iso)?;
std::env::set_var("TMPDIR", &iso);
let names: Vec<String> =
(0..2).map(|i| format!("selfcheck-ka{i}-{}", std::process::id())).collect();
let mut servers = Vec::new();
for n in &names {
let n2 = n.clone();
servers.push(std::thread::spawn(move || session::server_main(&n2, None)));
}
for n in &names {
let p = session::socket_path(n)?;
let mut up = false;
for _ in 0..100 {
std::thread::sleep(std::time::Duration::from_millis(30));
if UnixStream::connect(&p).is_ok() { up = true; break; }
}
assert!(up, "killall-test server '{n}' did not come up");
}
session::killall_main()?;
for s in servers { s.join().expect("killall-test server panicked")?; }
for n in &names {
assert!(!session::socket_path(n)?.exists(), "killall left the socket for '{n}'");
}
match saved_tmp {
Some(v) => std::env::set_var("TMPDIR", v),
None => std::env::remove_var("TMPDIR"),
}
}
println!("[selfcheck] quit=detach + killall ..... PASS");
}
assert!(
session::next_auto_name()?.parse::<u32>().is_ok(),
"auto session name should be numeric"
);
{
let mut app = App::new(None)?;
app.session_name = Some("0".into()); app.agent_tx.send(agent::AgentEvent::SessionName { name: "mars-dev".into() })?;
app.tick();
assert_eq!(app.rename_session_to.as_deref(), Some("mars-dev"), "numeric session not renamed");
let mut app = App::new(None)?;
app.session_name = Some("work".into()); app.agent_tx.send(agent::AgentEvent::SessionName { name: "auto".into() })?;
app.tick();
assert!(app.rename_session_to.is_none(), "explicit session name overridden by AI");
}
println!("[selfcheck] session auto-naming ....... PASS");
{
let mig_dir = std::env::temp_dir().join(format!("mars-migrate-{}", std::process::id()));
let ares_dir = mig_dir.join("ares");
std::fs::create_dir_all(&ares_dir)?;
std::fs::write(
ares_dir.join("keys.json"),
r#"{ "edit": {}, "bar_open": ["ctrl-space", "M-x"] }"#,
)?;
std::fs::write(
ares_dir.join("tuning.json"),
r#"{ "max_panes": { "value": 3, "description": "migrated" } }"#,
)?;
std::env::set_var("XDG_CONFIG_HOME", &mig_dir);
let app = App::new(None)?;
assert_eq!(app.tuning.max_panes, 3, "ares→mars migration did not carry tuning");
assert!(mig_dir.join("mars").join("keys.json").exists(), "keys.json not migrated");
std::env::set_var("XDG_CONFIG_HOME", &cfg_dir); let _ = std::fs::remove_dir_all(&mig_dir);
println!("[selfcheck] ares→mars migration ....... PASS");
}
for v in ["MARS_LLM_KEY", "MARS_LLM_URL", "MARS_LLM_MODEL",
"ARES_LLM_KEY", "ARES_LLM_URL", "ARES_LLM_MODEL", "MARS_AUTH_SOCK",
"GROQ_API_KEY", "ANTHROPIC_API_KEY", "OPENAI_API_KEY"] {
std::env::remove_var(v);
}
std::env::set_var("GEMINI_API_KEY", "test-key");
let cfg = agent::AgentConfig::from_env();
assert!(cfg.is_configured(), "GEMINI_API_KEY not detected");
assert_eq!(cfg.provider, "gemini");
assert!(cfg.url.contains("generativelanguage"), "wrong Gemini endpoint: {}", cfg.url);
assert!(cfg.model.starts_with("gemini"), "wrong Gemini model: {}", cfg.model);
std::env::set_var("OPENAI_API_KEY", "test-key");
let cfg = agent::AgentConfig::from_env();
assert_eq!(cfg.provider, "openai", "OPENAI_API_KEY should beat GEMINI (paid-first)");
assert!(cfg.url.contains("api.openai.com"), "wrong OpenAI endpoint: {}", cfg.url);
assert!(cfg.model.starts_with("gpt-"), "wrong OpenAI default model: {}", cfg.model);
std::env::set_var("ANTHROPIC_API_KEY", "test-key");
let cfg = agent::AgentConfig::from_env();
assert_eq!(cfg.provider, "anthropic", "ANTHROPIC should beat OPENAI (paid-first order)");
assert!(cfg.url.contains("api.anthropic.com"), "wrong Anthropic endpoint: {}", cfg.url);
assert!(cfg.model.contains("claude"), "wrong Claude default model: {}", cfg.model);
std::env::set_var("MARS_LLM_KEY", "test-key");
assert_eq!(agent::AgentConfig::from_env().provider, "custom", "MARS_LLM_KEY must win");
for v in ["GEMINI_API_KEY", "OPENAI_API_KEY", "ANTHROPIC_API_KEY", "MARS_LLM_KEY"] {
std::env::remove_var(v);
}
println!("[selfcheck] provider detection ........ PASS");
{
for v in ["MARS_LLM_MODEL", "ARES_LLM_MODEL", "MARS_LLM_URL", "ARES_LLM_URL",
"MARS_LLM_KEY", "ARES_LLM_KEY"] {
std::env::remove_var(v);
}
assert_eq!(
tiers::model_above("groq", "translate").as_deref(),
Some("llama-3.3-70b-versatile"),
"mid-tier translate must escalate to groq high"
);
assert_eq!(
tiers::model_above("openai", "auto_name").as_deref(),
Some("gpt-4o"),
"escalation must walk past a tier repointed to the same model"
);
assert_eq!(tiers::model_above("groq", "ask"), None, "top tier must not escalate");
assert_eq!(tiers::model_above("groq", "no_such_task"), None);
std::env::set_var("MARS_LLM_MODEL", "pinned");
assert_eq!(tiers::model_above("groq", "translate"), None, "pin disables escalation");
std::env::remove_var("MARS_LLM_MODEL");
assert_eq!(tiers::model_for("groq", "ask_escalated", "escalated-model"), "escalated-model");
std::env::set_var("GROQ_API_KEY", "test-key");
std::env::set_var("GEMINI_API_KEY", "test-key");
let alts = agent::rotation_candidates("groq");
assert_eq!(alts.len(), 1, "expected exactly one alternate");
assert_eq!(alts[0].provider, "gemini");
assert_eq!(agent::rotation_candidates("gemini")[0].provider, "groq");
std::env::set_var("MARS_LLM_MODEL", "pinned");
assert!(agent::rotation_candidates("groq").is_empty(), "pin disables rotation");
std::env::remove_var("MARS_LLM_MODEL");
std::env::set_var("MARS_LLM_KEY", "test-key");
assert!(agent::rotation_candidates("custom").is_empty(), "custom key never rotates away");
for v in ["GROQ_API_KEY", "GEMINI_API_KEY", "MARS_LLM_KEY"] {
std::env::remove_var(v);
}
let e = anyhow::Error::new(agent::RateLimited("throttled".into()));
assert!(e.downcast_ref::<agent::RateLimited>().is_some());
println!("[selfcheck] cascade rotate+escalate .. PASS");
}
#[cfg(feature = "memory")]
{
use retrieval::redact;
let r = redact("export ANTHROPIC_API_KEY=sk-ant-api03-abcdefghij0123456789XYZ");
assert!(r.contains("[REDACTED]") && !r.contains("sk-ant"), "provider key survived: {r}");
assert_eq!(redact("a risk-free plan"), "a risk-free plan", "prose false positive");
assert_eq!(redact("sk-12"), "sk-12", "short token wrongly redacted");
let r = redact("mysql -u root --password=hunter2 db");
assert!(r.contains("--password=[REDACTED]") && !r.contains("hunter2"), "{r}");
let r = redact("curl -H 'Authorization: Bearer abc123def456' api");
assert!(r.contains("Bearer [REDACTED]") && !r.contains("abc123"), "{r}");
let r = redact("git clone https://bob:s3cret@github.com/x.git");
assert!(r.contains("bob:[REDACTED]@github.com") && !r.contains("s3cret"), "{r}");
let dl = std::env::temp_dir().join(format!("mars-denylist-{}", std::process::id()));
std::fs::write(&dl, "# comment\nmy-secret-host.internal\n")?;
std::env::set_var("MARS_DENYLIST", &dl);
let r = redact("ssh my-secret-host.internal");
assert!(!r.contains("my-secret-host") && r.contains("[REDACTED]"), "{r}");
assert_eq!(redact("# comment"), "# comment", "denylist comment line applied");
std::env::remove_var("MARS_DENYLIST");
let _ = std::fs::remove_file(&dl);
let mem = |req: &str, cmd: &str, ts: u64, cwd: &str| retrieval::CommandMemory {
request: req.into(), command: cmd.into(), ts, session: String::new(), cwd: cwd.into(),
};
let now = 1_800_000_000u64;
let records = vec![
mem("run the tests", "npm test", now - 90 * 86_400, "/other"),
mem("run the tests", "cargo test", now - 3_600, "/proj"),
mem("deploy the site", "make deploy", now, "/proj"),
];
let top = retrieval::rank_memories(&records, "run the tests", 2, "/proj", now, 0.25, 0.15, 14.0);
assert_eq!(top[0], 1, "same-cwd + recent must win the lexical tie");
assert_eq!(top[1], 0);
assert!(!top.contains(&2), "lexically-irrelevant record resurrected by boost");
let bare = vec![
mem("run the tests", "npm test", 0, ""),
mem("run the tests", "cargo test", 0, ""),
];
let top = retrieval::rank_memories(&bare, "run the tests", 2, "/proj", now, 0.25, 0.15, 14.0);
assert_eq!(top[0], 0, "metadata-free records must keep pure lexical order");
std::env::remove_var("MARS_MEMORY");
assert_eq!(retrieval::fewshot_for("run the tests"), "", "fewshot must gate on mode");
assert!(retrieval::docs_context_for("how do I").is_none(), "docs must gate on mode");
std::env::set_var("MARS_MEMORY", "docs");
assert!(
retrieval::docs_context_for("how do I turn on memory retrieval").is_some(),
"docs mode must retrieve from the always-present reference corpus"
);
let cm = std::env::temp_dir().join(format!("mars-cm-{}", std::process::id()));
std::fs::write(&cm, "{\"request\":\"ship it\",\"command\":\"cargo publish\"}\n")?;
std::env::set_var("MARS_CMD_MEMORY", &cm);
std::env::set_var("MARS_MEMORY", "history");
assert!(
retrieval::fewshot_for("ship it").contains("cargo publish"),
"history mode must surface the seeded pair"
);
for v in ["MARS_MEMORY", "MARS_CMD_MEMORY"] {
std::env::remove_var(v);
}
let _ = std::fs::remove_file(&cm);
println!("[selfcheck] memory hygiene ........... PASS");
}
{
let chunks = ["Hel", "lo <thi", "nk>secret reasoning</th", "ink> world"];
let mut raw = String::new();
let mut emitted = 0usize;
let mut seen = String::new();
for c in chunks {
raw.push_str(c);
let vis = agent::stream_visible(&raw);
assert!(vis.len() >= emitted, "visible prefix retracted at {c:?}");
assert!(vis.starts_with(&seen), "emitted text not a stable prefix");
if vis.len() > emitted {
seen.push_str(&vis[emitted..]);
emitted = vis.len();
}
assert!(!seen.contains("secret"), "reasoning leaked mid-stream: {seen}");
}
assert_eq!(seen, "Hello world", "final streamed text wrong: {seen:?}");
let mut app = App::new(None)?;
app.agent_pending = true;
app.agent_tx.send(agent::AgentEvent::AnswerStart)?;
app.agent_tx.send(agent::AgentEvent::AnswerDelta { text: "streaming ".into() })?;
app.agent_tx.send(agent::AgentEvent::AnswerDelta { text: "tokens".into() })?;
app.tick();
assert_eq!(app.agent_partial.as_deref(), Some("streaming tokens"));
app.agent_tx.send(agent::AgentEvent::AnswerStart)?;
app.agent_tx.send(agent::AgentEvent::AnswerDelta { text: "better".into() })?;
app.agent_tx.send(agent::AgentEvent::Answer { text: "better answer".into(), directive: None })?;
app.tick();
assert!(app.agent_partial.is_none(), "final Answer did not clear the partial");
assert!(!app.agent_pending, "final Answer left the spinner on");
assert_eq!(
app.agent_history.last().map(|(r, t)| (r.as_str(), t.as_str())),
Some(("assistant", "better answer")),
"streamed turn did not land in history"
);
println!("[selfcheck] streaming ask ............ PASS");
}
{
for (name, p, holders) in [
("ask_system", prompts::ASK_SYSTEM, vec!["{registry}", "{screen}"]),
("translate_system", prompts::TRANSLATE_SYSTEM, vec!["{reasoning_cap}", "{examples_block}"]),
("translate_reasoning_cap", prompts::TRANSLATE_REASONING_CAP, vec![]),
("translate_examples", prompts::TRANSLATE_EXAMPLES, vec!["{examples}"]),
("watch_system", prompts::WATCH_SYSTEM, vec!["{hint}"]),
("watch_hint_exit", prompts::WATCH_HINT_EXIT, vec![]),
("watch_hint_quiet", prompts::WATCH_HINT_QUIET, vec![]),
("mission_system", prompts::MISSION_SYSTEM, vec![]),
("auto_name_system", prompts::AUTO_NAME_SYSTEM, vec![]),
("name_session_system", prompts::NAME_SESSION_SYSTEM, vec![]),
#[cfg(feature = "memory")]
("docs_context_preamble", prompts::DOCS_CONTEXT_PREAMBLE, vec!["{body}"]),
("cursor_insert", prompts::CURSOR_INSERT, vec!["{file}", "{line}"]),
("explain_this", prompts::EXPLAIN_THIS, vec![]),
("explain_failure", prompts::EXPLAIN_FAILURE, vec![]),
] {
assert!(!p.trim().is_empty(), "prompt template {name}.md is empty");
for h in holders {
assert!(p.contains(h), "prompt template {name}.md lost placeholder {h}");
}
}
assert_eq!(tiers::model_for("groq", "auto_name", "x"), "llama-3.1-8b-instant");
assert_eq!(tiers::model_for("groq", "name_session", "x"), "llama-3.1-8b-instant");
assert_eq!(tiers::model_for("groq", "mission", "x"), "llama-3.1-8b-instant");
println!("[selfcheck] prompt templates ......... PASS");
}
{
let wl = std::env::temp_dir().join(format!("mars-worklog-{}", std::process::id()));
let _ = std::fs::remove_file(&wl);
std::env::set_var("MARS_WORKLOG", &wl);
for (i, v) in ["done: build green", "failed: 3 tests red", "done: tests green"]
.iter()
.enumerate()
{
worklog::record(&worklog::WorkEntry {
ts: 1000 + i as u64,
session: "train".into(),
tab: "build".into(),
verdict: v.to_string(),
failed: v.starts_with("failed"),
dur_secs: Some(60),
});
}
worklog::record(&worklog::WorkEntry {
ts: 2000, session: "other".into(), tab: "t".into(),
verdict: "done: unrelated".into(), failed: false, dur_secs: None,
});
let r = worklog::recent("train", 2);
assert_eq!(r.len(), 2, "recent() limit not applied");
assert_eq!(r[0].verdict, "failed: 3 tests red", "recent() order/session filter wrong");
assert!(r.iter().all(|e| e.session == "train"), "session filter leaked");
worklog::save_mission("train", "fixing the red tests", 1234);
assert_eq!(
worklog::load_mission("train"),
Some(("fixing the red tests".to_string(), 1234)),
"mission round-trip failed"
);
assert_eq!(worklog::load_mission("other"), None, "mission leaked across sessions");
assert_eq!(
session::session_summary("train"),
"fixing the red tests",
"summary should be the mission"
);
let s = session::session_summary("other");
assert!(
s.starts_with("last: done: unrelated (") && s.ends_with(')'),
"summary should fall back to the last verdict: {s}"
);
assert_eq!(session::session_summary("nowhere"), "", "no journal → empty summary");
assert_eq!(
session::wrap_text("fixing the red tests in the training run", 16),
vec!["fixing the red", "tests in the", "training run"],
"word wrap broke"
);
assert!(
session::wrap_text("supercalifragilisticexpialidocious", 10)
.iter()
.all(|l| l.chars().count() <= 10),
"overlong word not hard-split to width"
);
assert!(session::wrap_text("", 20).is_empty(), "empty summary should wrap to no lines");
assert!(
session::wrap_text("short", 20) == vec!["short"],
"short summary should stay one line"
);
std::env::remove_var("MARS_WORKLOG");
let _ = std::fs::remove_file(&wl);
let _ = std::fs::remove_file(std::env::temp_dir().join(format!("mars-worklog-{}", std::process::id())).with_file_name("mission.json"));
let mut app = App::new(None)?;
app.notices.push(app::Notice { text: "failed: run A".into(), kind: app::NoticeKind::Failure });
app.notices.push(app::Notice { text: "done: run B".into(), kind: app::NoticeKind::Info });
app.run_action(palette::Action::ExpandNotices);
assert!(app.notices.is_empty(), "expand-all did not clear the notice queue");
let digest = &app.agent_history.last().expect("no digest turn").1;
assert!(
digest.contains("failed: run A") && digest.contains("done: run B"),
"digest missing notices: {digest}"
);
let wl2 = std::env::temp_dir().join(format!("mars-worklog2-{}", std::process::id()));
let _ = std::fs::remove_file(&wl2);
std::env::set_var("MARS_WORKLOG", &wl2);
worklog::record(&worklog::WorkEntry {
ts: 1000, session: "standalone".into(), tab: "train".into(),
verdict: "failed: OOM at step 40".into(), failed: true, dur_secs: Some(300),
});
worklog::save_mission("standalone", "debugging the OOM in the training run", 1000);
let mut app = App::new(None)?;
let turns_before = app.agent_history.len();
app.on_detach();
app.on_attach();
let brief = &app.agent_history.last().expect("no briefing turn").1;
assert!(app.agent_history.len() > turns_before, "reattach pushed no briefing");
assert!(
brief.contains("Where you left off")
&& brief.contains("debugging the OOM")
&& brief.contains("failed: OOM at step 40"),
"briefing missing mission or journal lines: {brief}"
);
std::env::remove_var("MARS_WORKLOG");
let _ = std::fs::remove_file(&wl2);
let _ = std::fs::remove_file(wl2.with_file_name("mission.json"));
println!("[selfcheck] worklog + mission + expand PASS");
}
{
assert!(palette::Action::from_name("OpenCommandMemory").is_some());
assert!(palette::Action::from_name("OpenDenylist").is_some());
let clear = palette::Action::from_name("ClearCommandMemory").expect("action");
assert!(clear.is_destructive(), "memory wipe must be confirmation-gated");
println!("[selfcheck] memory actions ........... PASS");
}
#[cfg(not(feature = "memory"))]
{
std::env::set_var("MARS_MEMORY", "full");
assert_eq!(retrieval::MemoryMode::from_env().as_str(), "none", "stub mode must be inert");
assert!(retrieval::fewshot_for("x").is_empty());
assert!(retrieval::docs_context_for("x").is_none());
assert!(retrieval::command_memory_path().is_none());
assert!(retrieval::denylist_path().is_none());
assert!(retrieval::load_command_records().is_empty());
retrieval::remember_command("a", "b");
std::env::remove_var("MARS_MEMORY");
println!("[selfcheck] memory stub (feature off) PASS");
}
{
use std::io::{BufRead, BufReader};
for v in ["GROQ_API_KEY", "GEMINI_API_KEY", "GOOGLE_API_KEY",
"ANTHROPIC_API_KEY", "OPENAI_API_KEY",
"MARS_LLM_KEY", "ARES_LLM_KEY", "MARS_LLM_MODEL", "ARES_LLM_MODEL"] {
std::env::remove_var(v);
}
let dir = std::env::temp_dir().join(format!("mars-broker-sc-{}", std::process::id()));
std::fs::create_dir_all(&dir)?;
let sock = dir.join("auth.sock");
let sock_s = sock.to_string_lossy().to_string();
let _ = std::fs::remove_file(&sock);
let listener = std::os::unix::net::UnixListener::bind(&sock)?;
std::thread::spawn(move || {
for conn in listener.incoming() {
let Ok(stream) = conn else { break };
let mut reader = BufReader::new(stream.try_clone().unwrap());
let mut line = String::new();
if reader.read_line(&mut line).unwrap_or(0) == 0 {
continue; }
let mut w = stream;
let _ = session::write_frame(
&mut w,
&broker::BrokerResponse::Chat { text: "broker-ok".into() },
);
}
});
std::env::set_var("MARS_AUTH_SOCK", &sock_s);
let c = agent::AgentConfig::from_env();
assert_eq!(c.provider, "broker", "MARS_AUTH_SOCK not detected as broker");
assert!(c.is_configured(), "live broker socket should be configured");
std::env::set_var("MARS_LLM_KEY", "explicit");
assert_ne!(agent::AgentConfig::from_env().provider, "broker",
"explicit key should outrank the broker socket");
std::env::remove_var("MARS_LLM_KEY");
let dead = agent::AgentConfig {
url: String::new(), key: String::new(), model: String::new(),
provider: "broker", max_tokens: 512, temperature: 0.3,
broker_sock: Some("/tmp/mars-nope-does-not-exist.sock".into()),
};
assert!(!dead.is_configured(), "dead broker socket reported configured");
let out = agent::chat(&c, vec![], "test").unwrap_or_default();
assert_eq!(out, "broker-ok", "broker proxy did not return the reply: {out:?}");
std::env::remove_var("MARS_AUTH_SOCK");
let _ = std::fs::remove_file(&sock);
}
println!("[selfcheck] ssh broker (proxy/detect) . PASS");
{
let hosts = vec!["gpubox".to_string(), "prod-7".to_string()];
assert_eq!(broker::resolve_target(&hosts, "2"), Some("prod-7".into()), "ordinal");
assert_eq!(broker::resolve_target(&hosts, "gpubox"), Some("gpubox".into()), "exact name");
assert_eq!(broker::resolve_target(&hosts, "prod"), Some("prod-7".into()), "unique prefix");
assert_eq!(broker::resolve_target(&hosts, ""), None, "empty skips");
assert_eq!(broker::resolve_target(&hosts, "9"), None, "out-of-range ordinal");
let saved = std::env::var("HOME").ok();
let tmp = std::env::temp_dir().join(format!("mars-fleet-sc-{}", std::process::id()));
std::fs::create_dir_all(&tmp)?;
std::env::set_var("HOME", &tmp);
broker::fleet_record("prod-7", None);
broker::fleet_record("gpubox", None); broker::fleet_record("prod-7", None); let f = broker::fleet_load();
assert_eq!(f.len(), 2, "fleet upsert duplicated a host");
assert_eq!(f[0].host, "prod-7", "fleet not ordered most-recent-first");
broker::fleet_status("gpubox", Some("train".into()), "agent active");
let f = broker::fleet_load();
let g = f.iter().find(|e| e.host == "gpubox").expect("status push dropped the host");
assert_eq!(g.last_status.as_deref(), Some("agent active"));
assert_eq!(g.session.as_deref(), Some("train"));
assert!(
f.iter().all(|e| g.as_of >= e.as_of),
"status push did not refresh recency"
);
let entries = session::all_sessions()?;
let g = entries
.iter()
.find(|e| e.name == "gpubox")
.expect("remote host missing from all_sessions");
assert!(g.remote && g.as_of.is_some(), "remote entry lost its provenance");
assert!(
g.status.contains("agent active") && g.status.contains("session train"),
"pushed status not plumbed into ls: {}",
g.status
);
assert!(g.summary.is_empty(), "remotes have no LLM summary column");
assert_eq!(g.connect, "mars ssh gpubox");
assert!(
entries.iter().all(|e| e.remote || e.as_of.is_none()),
"a local session carried a stale as_of"
);
let names: Vec<String> = entries.iter().map(|e| e.name.clone()).collect();
assert_eq!(
broker::resolve_target(&names, "gpub").as_deref(),
Some("gpubox"),
"unified resolver lost prefix matching"
);
match saved {
Some(h) => std::env::set_var("HOME", h),
None => std::env::remove_var("HOME"),
}
let _ = std::fs::remove_dir_all(&tmp);
}
println!("[selfcheck] fleet cache + ls resolver . PASS");
assert!(broker::INSTALL_SH.starts_with("#!/bin/sh"), "install.sh lost its shebang");
assert!(broker::INSTALL_SH.contains("sh.rustup.rs") && broker::INSTALL_SH.contains("mars-terminal"),
"embedded install.sh missing its core steps");
assert!(broker::INSTALL_SH.contains("MINGW"), "embedded install.sh lost the Windows guard");
println!("[selfcheck] embedded installer ........ PASS");
let mut app = App::new(None)?;
app.new_tab(); app.open_terminal();
let tid = match app.focused_pane().content {
pane::PaneContent::Terminal(id) => id,
_ => panic!("open_terminal did not attach a terminal"),
};
app.run_action(palette::Action::WatchPane); assert!(app.terms.contains_key(&tid) && app.watches.contains_key(&tid), "terminal/watch not registered");
app.run_action(palette::Action::CloseTab);
assert!(app.mode == mode::Mode::Prompt, "close with a live terminal did not confirm");
assert!(app.terms.contains_key(&tid), "terminal reaped before confirmation");
app.handle_key(k(KeyCode::Char('n')))?; assert_eq!(app.tabs.len(), 2, "declined close still removed the tab");
assert!(app.terms.contains_key(&tid), "declined close still reaped the terminal");
app.run_action(palette::Action::CloseTab);
app.handle_key(k(KeyCode::Char('y')))?; assert_eq!(app.tabs.len(), 1, "confirmed close did not remove the tab");
assert!(!app.terms.contains_key(&tid), "confirmed close ORPHANED the terminal (not reaped)");
assert!(!app.watches.contains_key(&tid), "watch state not cleaned on reap");
println!("[selfcheck] close gate reaps PTYs ..... PASS");
let mut app = App::new(None)?;
app.new_tab(); app.handle_key(kc(KeyCode::Char('t')))?; assert!(app.mode == mode::Mode::Tab, "C-t did not enter space warp");
app.handle_key(k(KeyCode::Char('d')))?; assert!(app.mode == mode::Mode::Prompt, "warp 'd' did not confirm (motor-slip guard)");
app.handle_key(k(KeyCode::Char('n')))?;
assert_eq!(app.tabs.len(), 2, "declined warp 'd' still closed the tab");
println!("[selfcheck] warp keys motor-slip gate . PASS");
let mut app = App::new(None)?;
app.handle_key(kc(KeyCode::Char(' ')))?; assert!(app.mode == mode::Mode::Bar, "bar did not open");
app.handle_key(kc(KeyCode::Char('g')))?;
assert!(app.mode != mode::Mode::Bar && app.palette.is_none(), "C-g did not cancel Command bar");
app.handle_key(kc(KeyCode::Char(' ')))?;
app.handle_key(k(KeyCode::Char('!')))?; assert!(matches!(app.palette.as_ref().map(|p| &p.bar_mode), Some(palette::BarMode::Shell)), "! did not reach shell");
app.handle_key(kc(KeyCode::Char('g')))?;
assert!(app.palette.is_none(), "C-g did not cancel shell submode");
println!("[selfcheck] C-g cancels the bar ....... PASS");
{
use crossterm::event::{MouseButton, MouseEvent, MouseEventKind};
let mut app = App::new(None)?;
app.open_terminal();
let tid = match app.focused_pane().content {
pane::PaneContent::Terminal(id) => id,
_ => panic!("no terminal"),
};
let before = app.kill_ring.len();
app.term_sel = Some(app::TermSel { tid, ox: 0, oy: 0, vw: 80, vh: 24, anchor: (2, 3), end: (2, 3) });
app.handle_mouse(MouseEvent {
kind: MouseEventKind::Up(MouseButton::Left),
column: 3, row: 2, modifiers: KeyModifiers::NONE,
});
assert_eq!(app.kill_ring.len(), before, "plain click copied to the kill ring");
assert!(app.term_sel.is_none(), "term_sel not cleared on release");
}
println!("[selfcheck] click-no-drag no clobber .. PASS");
{
use crossterm::event::{MouseEvent, MouseEventKind};
fn wheel(up: bool) -> MouseEvent {
MouseEvent {
kind: if up { MouseEventKind::ScrollUp } else { MouseEventKind::ScrollDown },
column: 5, row: 5, modifiers: KeyModifiers::NONE,
}
}
fn term_with(app: &mut App, setup: &[u8]) -> usize {
app.open_terminal();
let tid = match app.focused_pane().content {
pane::PaneContent::Terminal(id) => id,
_ => panic!("no terminal"),
};
std::thread::sleep(std::time::Duration::from_millis(400));
app.tick();
app.terms.get_mut(&tid).unwrap().send_bytes(setup);
std::thread::sleep(std::time::Duration::from_millis(500));
app.tick();
tid
}
fn wait_for(app: &mut App, tid: usize, needle: &str) -> bool {
for _ in 0..30 {
app.tick();
if app.terms.get(&tid).unwrap().screen().contents().contains(needle) {
return true;
}
std::thread::sleep(std::time::Duration::from_millis(100));
}
false
}
let mut app = App::new(None)?;
let tid = term_with(&mut app, b"printf '\\033[?1049h'; cat\n");
assert!(app.terms.get(&tid).unwrap().screen().alternate_screen(), "alt screen not entered");
app.handle_mouse(wheel(true));
assert!(wait_for(&mut app, tid, "^[[A^[[A^[[A"),
"alt-screen wheel-up did not become arrow keys");
assert_eq!(app.terms.get(&tid).unwrap().view_offset(), 0);
let mut app = App::new(None)?;
let tid = term_with(&mut app, b"printf '\\033[?1049h\\033[?1h'; cat\n");
app.handle_mouse(wheel(true));
assert!(wait_for(&mut app, tid, "^[OA^[OA^[OA"),
"DECCKM wheel-up did not send application-cursor arrows");
let mut app = App::new(None)?;
let tid = term_with(&mut app, b"printf '\\033[?1002h\\033[?1006h'; cat\n");
assert!(
app.terms.get(&tid).unwrap().screen().mouse_protocol_mode()
!= vt100::MouseProtocolMode::None,
"mouse mode not entered"
);
app.handle_mouse(wheel(true));
assert!(wait_for(&mut app, tid, "[<64;1;1M"), "SGR wheel-up not forwarded");
app.handle_mouse(wheel(false));
assert!(wait_for(&mut app, tid, "[<65;1;1M"), "SGR wheel-down not forwarded");
let mut app = App::new(None)?;
let tid = term_with(&mut app, b"seq 1 100\n");
app.handle_mouse(wheel(true));
assert!(app.terms.get(&tid).unwrap().view_offset() > 0,
"plain-shell wheel-up no longer scrolls mars scrollback");
app.handle_mouse(wheel(false));
println!("[selfcheck] terminal wheel dispatch .. PASS");
}
{
let app = App::new(None)?;
let b = |a| app.keys.binding_for(&a).unwrap_or_default();
assert_eq!(b(palette::Action::Save), "C-x C-s", "Save should teach the universal chord, not ⌘-s");
assert_eq!(b(palette::Action::SelectAll), "C-x h", "SelectAll should not teach ⌘-a");
assert_eq!(b(palette::Action::SplitVertical), "C-x 3", "SplitVertical should not teach C-\\/C-|");
assert_eq!(b(palette::Action::Search), "C-s", "Search should teach canonical C-s, not the C-r alias");
for a in [palette::Action::Save, palette::Action::SelectAll, palette::Action::CopyRegion, palette::Action::Paste] {
assert!(!b(a).contains('⌘'), "binding_for taught a kitty-only ⌘ chord");
}
println!("[selfcheck] tiered binding_for ........ PASS");
}
{
let mut app = App::new(None)?;
app.open_terminal();
assert!(app.mode == mode::Mode::Terminal, "not focused on the terminal");
app.notices.push(app::Notice {
text: "build failed".into(),
kind: app::NoticeKind::Failure,
});
app.handle_key(k(KeyCode::Esc))?;
assert!(app.notices.is_empty(), "Esc did not dismiss the notice from a terminal");
assert!(app.mode == mode::Mode::Terminal, "notice-dismiss should keep terminal focus");
println!("[selfcheck] terminal Esc dismisses .... PASS");
}
let mut app = App::new(None)?;
app.handle_key(kc(KeyCode::Char('t')))?; assert!(app.mode == mode::Mode::Tab, "C-t did not enter space warp");
app.handle_key(kc(KeyCode::Char(' ')))?;
assert!(app.mode == mode::Mode::Bar, "Ctrl+Space dead in space warp");
app.handle_key(k(KeyCode::Esc))?;
app.handle_key(kc(KeyCode::Char('u')))?; assert!(app.mode == mode::Mode::Undo, "C-u did not enter time-travel");
app.handle_key(kc(KeyCode::Char(' ')))?;
assert!(app.mode == mode::Mode::Bar, "Ctrl+Space dead in time-travel");
app.handle_key(k(KeyCode::Esc))?;
app.toggle_file_tree();
assert!(app.mode == mode::Mode::Tree, "file tree did not open");
app.handle_key(kc(KeyCode::Char(' ')))?;
assert!(app.mode == mode::Mode::Bar, "Ctrl+Space dead in the file tree");
println!("[selfcheck] bar opens from any mode .. PASS");
{
use std::collections::HashMap;
let frec: HashMap<String, u32> = HashMap::new();
let mut p = palette::Palette::root();
p.query = "space warp".to_string();
assert!(
p.visible_items(&frec).iter().any(|r| matches!(r.kind, palette::ItemKind::Run(palette::Action::TabMode))),
"TabMode not searchable in the command bar"
);
p.query = "kill buffer".to_string();
assert!(
p.visible_items(&frec).iter().any(|r| matches!(r.kind, palette::ItemKind::Run(palette::Action::KillBuffer))),
"KillBuffer not searchable in the command bar"
);
println!("[selfcheck] orphan actions now in menu PASS");
}
{
let sc_dir = std::env::temp_dir().join(format!("mars-llmtest-{}", std::process::id()));
std::fs::create_dir_all(&sc_dir)?;
std::env::set_var("MARS_LLM_LOG_DIR", &sc_dir);
assert!(llm_log::log_path().starts_with(&sc_dir), "log path not isolated to temp dir!");
let _ = std::fs::remove_file(llm_log::log_path()); std::env::set_var("MARS_LLM_DEBUG", "1");
let input = vec![serde_json::json!({"role": "user", "content": "hi"})];
llm_log::record(&llm_log::CallRecord {
call_id: 1, task: "ask", provider: "groq", model: "qwen/qwen3-32b", retrieval: "none",
prompt_tokens: 100, completion_tokens: 20, latency_ms: 500,
ok: true, error: None, input: &input, output: "hello",
});
let logged = std::fs::read_to_string(llm_log::log_path())?;
assert!(logged.contains("\"task\":\"ask\"") && logged.contains("qwen/qwen3-32b"), "call not logged");
assert!(logged.contains("\"total_tokens\":120"), "token total not computed");
assert!(logged.contains("\"call_id\":1") && logged.contains("\"session_id\""), "call_id/session_id not logged");
llm_log::session_start();
llm_log::record_outcome(1, Some("git status"), false, false);
let outc = std::fs::read_to_string(llm_log::outcomes_path())?;
assert!(outc.contains("\"call_id\":1") && outc.contains("git status"), "outcome not logged");
assert!(std::fs::read_to_string(llm_log::log_path())?.contains("session_start"), "session event not logged");
llm_log::stats(false)?; std::env::remove_var("MARS_LLM_DEBUG");
let before = std::fs::metadata(llm_log::log_path())?.len();
llm_log::record(&llm_log::CallRecord {
call_id: 2, task: "ask", provider: "groq", model: "m", retrieval: "none",
prompt_tokens: 1, completion_tokens: 1, latency_ms: 1,
ok: true, error: None, input: &input, output: "x",
});
assert_eq!(std::fs::metadata(llm_log::log_path())?.len(), before, "record() wrote while disabled");
let _ = std::fs::remove_file(llm_log::log_path());
let _ = std::fs::remove_file(llm_log::outcomes_path());
let _ = std::fs::remove_dir_all(&sc_dir);
std::env::remove_var("MARS_LLM_LOG_DIR");
println!("[selfcheck] llm debug log + stats ..... PASS");
}
#[cfg(feature = "memory")]
{
let docs = vec![
"git status shows the working tree and staged changes".to_string(),
"docker compose up starts the containers".to_string(),
"list files in a directory with ls -la".to_string(),
];
let top = retrieval::rank("how do I check the git working tree", &docs, 1);
assert_eq!(top.first().copied(), Some(0), "BM25 did not rank the git doc first");
std::env::set_var("MARS_MEMORY", "history");
assert!(retrieval::MemoryMode::from_env().includes_history(), "MARS_MEMORY=history not parsed");
std::env::set_var("MARS_MEMORY", "docs");
assert!(retrieval::MemoryMode::from_env().includes_docs(), "MARS_MEMORY=docs not parsed");
std::env::remove_var("MARS_MEMORY");
assert_eq!(retrieval::MemoryMode::from_env().as_str(), "none");
println!("[selfcheck] retrieval (bm25 + mode) ... PASS");
}
println!("\nALL SELFCHECKS PASSED ✓");
Ok(())
}