use std::sync::mpsc::{Receiver, RecvError, SyncSender};
use crate::chat::{BackgroundJob, ReadOutcome};
#[cfg(feature = "rich-tui")]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum PanelMode {
Inline(u16),
AlternateScreen,
}
#[derive(Debug)]
pub(crate) enum SurfaceRequest {
ReadLine {
prompt: String,
reply: SyncSender<anyhow::Result<ReadOutcome>>,
},
PresentClarification {
batch: String,
hint: String,
prompt: String,
color: bool,
verbose: bool,
reply: SyncSender<anyhow::Result<ReadOutcome>>,
},
Reload {
reply: SyncSender<anyhow::Result<()>>,
},
AddHistory(String),
SaveHistory,
SetRuntimeContext {
model: String,
endpoint: String,
gauge: Option<(u32, Option<u32>)>,
session: String,
},
SetBackgroundJobs(Vec<BackgroundJob>),
SetTabs(Vec<crate::tab_bar::TabCell>),
#[cfg(feature = "live-spill")]
SetSpillArchive(std::sync::Arc<crate::completed_spill::CompletedSpillArchive>),
TurnStarted {
cancel: std::sync::Arc<std::sync::atomic::AtomicBool>,
},
TurnEnded,
RunBang {
command: crate::OperatorCommand,
color: bool,
verbose: bool,
reply: SyncSender<anyhow::Result<bool>>,
},
Interact {
interaction: Box<newt_core::interaction_surface::SurfaceInteraction>,
reply: SyncSender<newt_core::HumanQuestionOutcome>,
},
#[cfg(feature = "rich-tui")]
Panel {
mode: PanelMode,
reply: SyncSender<Option<PanelWindow>>,
},
}
#[cfg(feature = "rich-tui")]
#[derive(Debug)]
pub(crate) struct PanelWindow {
out: std::fs::File,
area: std::cell::Cell<ratatui::layout::Rect>,
#[cfg(unix)]
signals: Option<SyncSender<PanelSignal>>,
}
#[cfg(all(feature = "rich-tui", unix))]
#[derive(Debug)]
pub(crate) enum PanelSignal {
Remeasure {
rows: Option<u16>,
reply: SyncSender<ratatui::layout::Rect>,
},
}
#[cfg(feature = "rich-tui")]
impl PanelWindow {
#[cfg(unix)]
pub(crate) fn new(
out: std::fs::File,
area: ratatui::layout::Rect,
signals: Option<SyncSender<PanelSignal>>,
) -> Self {
Self {
out,
area: std::cell::Cell::new(area),
signals,
}
}
#[cfg(not(unix))]
pub(crate) fn remeasure(&self, _rows: Option<u16>) {}
#[cfg(unix)]
pub(crate) fn remeasure(&self, rows: Option<u16>) {
let Some(signals) = &self.signals else {
return;
};
let (tx, rx) = std::sync::mpsc::sync_channel(1);
if signals
.send(PanelSignal::Remeasure { rows, reply: tx })
.is_ok()
{
if let Ok(area) = rx.recv() {
self.area.set(area);
}
}
}
pub(crate) fn area(&self) -> ratatui::layout::Rect {
self.area.get()
}
pub(crate) fn terminal(&self) -> std::io::Result<crate::inline_viewport::InlineTerm> {
crate::inline_viewport::cockpit_panel_terminal(self.out.try_clone()?, self.area.get())
}
pub(crate) fn output(&self) -> std::io::Result<std::fs::File> {
self.out.try_clone()
}
pub(crate) fn prompt_window(&self) -> std::io::Result<newt_core::tty::PromptWindow> {
Ok(newt_core::tty::Terminal::suspend_for_prompt_to(
self.out.try_clone()?,
newt_core::tty::TerminalTaker::PermissionAuthorization,
))
}
}
impl SurfaceRequest {
pub(crate) fn expects_reply(&self) -> bool {
#[cfg(feature = "rich-tui")]
if matches!(self, Self::Panel { .. }) {
return true;
}
matches!(
self,
Self::ReadLine { .. }
| Self::PresentClarification { .. }
| Self::Reload { .. }
| Self::Interact { .. }
| Self::RunBang { .. }
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum SurfaceError {
UiGone,
NoReply,
}
impl std::fmt::Display for SurfaceError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Self::UiGone => "the terminal thread is gone; this session cannot read input",
Self::NoReply => "the terminal thread dropped the request without answering",
})
}
}
impl std::error::Error for SurfaceError {}
pub(crate) struct RemoteSurface {
to_ui: SyncSender<SurfaceRequest>,
}
impl RemoteSurface {
pub(crate) fn new(to_ui: SyncSender<SurfaceRequest>) -> Self {
Self { to_ui }
}
fn notify(&self, request: SurfaceRequest) {
debug_assert!(
!request.expects_reply(),
"a request with a reply channel must go through `ask`"
);
let _ = self.to_ui.send(request);
}
fn ask<T>(
&self,
make: impl FnOnce(SyncSender<T>) -> SurfaceRequest,
rx: Receiver<T>,
tx: SyncSender<T>,
) -> Result<T, SurfaceError> {
let request = make(tx);
debug_assert!(request.expects_reply());
self.to_ui.send(request).map_err(|_| SurfaceError::UiGone)?;
rx.recv().map_err(|RecvError| SurfaceError::NoReply)
}
}
impl crate::chat::InputSurface for RemoteSurface {
fn read_line(&mut self, prompt: &str) -> anyhow::Result<ReadOutcome> {
let (tx, rx) = std::sync::mpsc::sync_channel(1);
self.ask(
|reply| SurfaceRequest::ReadLine {
prompt: prompt.to_string(),
reply,
},
rx,
tx,
)?
}
fn reload(&mut self) -> anyhow::Result<()> {
let (tx, rx) = std::sync::mpsc::sync_channel(1);
self.ask(|reply| SurfaceRequest::Reload { reply }, rx, tx)?
}
fn present_clarification(
&mut self,
batch: &str,
hint: &str,
prompt: &str,
color: bool,
verbose: bool,
) -> anyhow::Result<ReadOutcome> {
let (tx, rx) = std::sync::mpsc::sync_channel(1);
self.ask(
|reply| SurfaceRequest::PresentClarification {
batch: batch.to_string(),
hint: hint.to_string(),
prompt: prompt.to_string(),
color,
verbose,
reply,
},
rx,
tx,
)?
}
fn add_history(&mut self, entry: &str) {
self.notify(SurfaceRequest::AddHistory(entry.to_string()));
}
fn save_history(&mut self) {
self.notify(SurfaceRequest::SaveHistory);
}
fn set_runtime_context(
&mut self,
model: &str,
endpoint: &str,
gauge: Option<(u32, Option<u32>)>,
session: &str,
) {
self.notify(SurfaceRequest::SetRuntimeContext {
model: model.to_string(),
endpoint: endpoint.to_string(),
gauge,
session: session.to_string(),
});
}
fn set_background_jobs(&mut self, jobs: Vec<BackgroundJob>) {
self.notify(SurfaceRequest::SetBackgroundJobs(jobs));
}
fn set_tabs(&mut self, tabs: Vec<crate::tab_bar::TabCell>) {
self.notify(SurfaceRequest::SetTabs(tabs));
}
#[cfg(feature = "live-spill")]
fn set_spill_archive(
&mut self,
archive: std::sync::Arc<crate::completed_spill::CompletedSpillArchive>,
) {
self.notify(SurfaceRequest::SetSpillArchive(archive));
}
fn turn_started(&mut self, cancel: std::sync::Arc<std::sync::atomic::AtomicBool>) {
self.notify(SurfaceRequest::TurnStarted { cancel });
}
fn turn_ended(&mut self) {
self.notify(SurfaceRequest::TurnEnded);
}
fn run_bang_escape(&mut self, command: &str, color: bool, verbose: bool) -> anyhow::Result<()> {
let (tx, rx) = std::sync::mpsc::sync_channel(1);
self.ask(
|reply| SurfaceRequest::RunBang {
command: crate::OperatorCommand::Shell(command.to_string()),
color,
verbose,
reply,
},
rx,
tx,
)?
.map(|_| ())
}
fn run_login_argv(
&mut self,
argv: &[String],
color: bool,
verbose: bool,
) -> anyhow::Result<bool> {
let (tx, rx) = std::sync::mpsc::sync_channel(1);
self.ask(
|reply| SurfaceRequest::RunBang {
command: crate::OperatorCommand::Argv(argv.to_vec()),
color,
verbose,
reply,
},
rx,
tx,
)?
}
#[cfg(feature = "rich-tui")]
fn open_panel(&mut self, mode: PanelMode) -> Option<PanelWindow> {
let (tx, rx) = std::sync::mpsc::sync_channel(1);
self.ask(|reply| SurfaceRequest::Panel { mode, reply }, rx, tx)
.ok()
.flatten()
}
fn present_interaction(
&mut self,
interaction: &newt_core::interaction_surface::SurfaceInteraction,
) -> newt_core::HumanQuestionOutcome {
let (tx, rx) = std::sync::mpsc::sync_channel(1);
let interaction = Box::new(interaction.clone());
self.ask(
|reply| SurfaceRequest::Interact { interaction, reply },
rx,
tx,
)
.unwrap_or(newt_core::HumanQuestionOutcome::Unavailable)
}
}
pub(crate) fn pump_surface(
surface: &mut dyn crate::chat::InputSurface,
requests: &Receiver<SurfaceRequest>,
) {
for request in requests {
match request {
SurfaceRequest::ReadLine { prompt, reply } => {
let _ = reply.send(surface.read_line(&prompt));
}
SurfaceRequest::PresentClarification {
batch,
hint,
prompt,
color,
verbose,
reply,
} => {
let _ = reply
.send(surface.present_clarification(&batch, &hint, &prompt, color, verbose));
}
SurfaceRequest::Reload { reply } => {
let _ = reply.send(surface.reload());
}
SurfaceRequest::AddHistory(entry) => surface.add_history(&entry),
SurfaceRequest::SaveHistory => surface.save_history(),
SurfaceRequest::SetRuntimeContext {
model,
endpoint,
gauge,
session,
} => surface.set_runtime_context(&model, &endpoint, gauge, &session),
SurfaceRequest::SetBackgroundJobs(jobs) => surface.set_background_jobs(jobs),
SurfaceRequest::SetTabs(tabs) => surface.set_tabs(tabs),
#[cfg(feature = "live-spill")]
SurfaceRequest::SetSpillArchive(archive) => surface.set_spill_archive(archive),
SurfaceRequest::TurnStarted { cancel } => surface.turn_started(cancel),
SurfaceRequest::TurnEnded => surface.turn_ended(),
SurfaceRequest::RunBang {
command,
color,
verbose,
reply,
} => {
let result = match command {
crate::OperatorCommand::Shell(command) => surface
.run_bang_escape(&command, color, verbose)
.map(|()| true),
crate::OperatorCommand::Argv(argv) => {
surface.run_login_argv(&argv, color, verbose)
}
};
let _ = reply.send(result);
}
SurfaceRequest::Interact { interaction, reply } => {
let _ = reply.send(surface.present_interaction(&interaction));
}
#[cfg(feature = "rich-tui")]
SurfaceRequest::Panel { mode, reply } => {
let _ = reply.send(surface.open_panel(mode));
}
}
}
}
pub(crate) struct TurnBinding {
_session: newt_core::lifecycle::ScopedActiveSession,
_psyche: newt_core::psyche::TurnPsyche,
}
#[must_use]
pub(crate) fn bind_turn(session_id: &newt_core::lifecycle::SessionId) -> TurnBinding {
TurnBinding {
_session: newt_core::lifecycle::scoped_active_session(session_id),
_psyche: newt_core::psyche::capture_turn_psyche(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::chat::InputSurface;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::Arc;
fn serve_one(rx: &Receiver<SurfaceRequest>, answer: ReadOutcome) -> Option<SurfaceRequest> {
match rx.recv().ok()? {
SurfaceRequest::ReadLine { prompt, reply } => {
reply.send(Ok(answer)).ok()?;
Some(SurfaceRequest::AddHistory(prompt))
}
other => Some(other),
}
}
#[test]
fn a_session_parks_on_read_line_and_receives_the_ui_threads_answer() {
let (to_ui, from_session) = std::sync::mpsc::sync_channel(8);
let worker = std::thread::spawn(move || {
let mut surface = RemoteSurface::new(to_ui);
surface.read_line("› ").expect("served")
});
let echoed = serve_one(&from_session, ReadOutcome::Line("hello".into()));
assert!(matches!(
echoed,
Some(SurfaceRequest::AddHistory(p)) if p == "› "
));
match worker.join().expect("worker") {
ReadOutcome::Line(l) => assert_eq!(l, "hello"),
other => panic!("expected the served line, got {other:?}"),
}
}
#[test]
fn the_ui_side_stays_live_while_a_session_is_mid_turn() {
let (to_ui, from_session) = std::sync::mpsc::sync_channel(8);
let in_flight = Arc::new(AtomicBool::new(false));
let release = Arc::new(AtomicBool::new(false));
let worker = {
let in_flight = Arc::clone(&in_flight);
let release = Arc::clone(&release);
std::thread::spawn(move || {
let mut surface = RemoteSurface::new(to_ui);
in_flight.store(true, Ordering::Release);
surface.set_runtime_context("m", "http://h", Some((1, Some(2))), "s");
let outcome = surface.read_line("› ");
release.store(true, Ordering::Release);
outcome
})
};
let status = from_session.recv().expect("status");
assert!(
matches!(status, SurfaceRequest::SetRuntimeContext { .. }),
"the worker published state without touching the terminal"
);
assert!(
in_flight.load(Ordering::Acquire),
"the session is mid-turn right now"
);
assert!(
!release.load(Ordering::Acquire),
"and has NOT finished — so what follows happens DURING its turn"
);
serve_one(&from_session, ReadOutcome::Line("steer".into()));
let outcome = worker.join().expect("worker").expect("served");
assert!(matches!(outcome, ReadOutcome::Line(l) if l == "steer"));
}
#[test]
fn notifications_do_not_block_the_session() {
let (to_ui, from_session) = std::sync::mpsc::sync_channel(64);
let mut surface = RemoteSurface::new(to_ui);
for i in 0..32 {
surface.add_history(&format!("entry-{i}"));
}
surface.save_history();
drop(surface);
let served = from_session.iter().count();
assert_eq!(served, 33, "every notification was queued, none awaited");
}
#[test]
fn a_vanished_ui_thread_is_an_error_rather_than_a_hang() {
let (to_ui, from_session) = std::sync::mpsc::sync_channel(1);
drop(from_session);
let mut surface = RemoteSurface::new(to_ui);
let err = surface.read_line("› ").expect_err("must not park forever");
assert!(
err.to_string().contains("terminal thread is gone"),
"got: {err}"
);
}
#[test]
fn a_dropped_reply_is_distinguished_from_a_missing_ui() {
let (to_ui, from_session) = std::sync::mpsc::sync_channel(1);
let worker = std::thread::spawn(move || {
let mut surface = RemoteSurface::new(to_ui);
surface.read_line("› ")
});
drop(from_session.recv().expect("request"));
let err = worker.join().expect("worker").expect_err("no answer");
assert!(
err.to_string().contains("dropped the request"),
"got: {err}"
);
}
#[test]
fn only_the_value_returning_requests_expect_a_reply() {
let (tx, _rx) = std::sync::mpsc::sync_channel(1);
assert!(SurfaceRequest::Reload { reply: tx }.expects_reply());
let (itx, _irx) = std::sync::mpsc::sync_channel(1);
assert!(SurfaceRequest::Interact {
interaction: Box::new(an_interaction()),
reply: itx,
}
.expects_reply());
#[cfg(feature = "rich-tui")]
{
let (ptx, _prx) = std::sync::mpsc::sync_channel(1);
assert!(SurfaceRequest::Panel {
mode: PanelMode::Inline(10),
reply: ptx,
}
.expects_reply());
}
assert!(!SurfaceRequest::SaveHistory.expects_reply());
assert!(!SurfaceRequest::AddHistory("x".into()).expects_reply());
assert!(!SurfaceRequest::SetBackgroundJobs(Vec::new()).expects_reply());
}
#[test]
fn requests_preserve_session_order() {
let (to_ui, from_session) = std::sync::mpsc::sync_channel(64);
let mut surface = RemoteSurface::new(to_ui);
for i in 0..16 {
surface.add_history(&format!("h{i}"));
}
drop(surface);
let seen: Vec<String> = from_session
.iter()
.filter_map(|r| match r {
SurfaceRequest::AddHistory(h) => Some(h),
_ => None,
})
.collect();
let want: Vec<String> = (0..16).map(|i| format!("h{i}")).collect();
assert_eq!(seen, want);
}
#[test]
fn the_serving_side_is_the_one_doing_the_work() {
let (to_ui, from_session) = std::sync::mpsc::sync_channel(8);
let served = Arc::new(AtomicUsize::new(0));
let worker = std::thread::spawn(move || {
let mut surface = RemoteSurface::new(to_ui);
surface.read_line("› ")
});
let count = Arc::clone(&served);
if let Ok(SurfaceRequest::ReadLine { reply, .. }) = from_session.recv() {
count.fetch_add(1, Ordering::Release);
reply.send(Ok(ReadOutcome::Eof)).expect("reply");
}
worker.join().expect("worker").expect("served");
assert_eq!(served.load(Ordering::Acquire), 1);
}
fn a_cell(number: usize, active: bool) -> crate::tab_bar::TabCell {
crate::tab_bar::TabCell {
number,
label: format!("tab{number}"),
active,
degraded: false,
pending: false,
}
}
#[test]
fn a_tab_projection_reaches_the_terminal() {
let (to_ui, from_session) = std::sync::mpsc::sync_channel(4);
let mut surface = RemoteSurface::new(to_ui);
surface.set_tabs(vec![a_cell(1, true), a_cell(2, false)]);
drop(surface);
match from_session.recv().expect("the projection crossed") {
SurfaceRequest::SetTabs(cells) => {
assert_eq!(cells.len(), 2);
assert_eq!(cells[0], a_cell(1, true), "payload survives intact");
assert!(cells[1].label.ends_with('2'));
}
other => panic!("expected SetTabs, got {other:?}"),
}
}
#[test]
fn the_proxy_forwards_every_surface_method() {
let (to_ui, from_session) = std::sync::mpsc::sync_channel(16);
let served = std::thread::spawn(move || {
let mut recorder = CountingSurface::default();
pump_surface(&mut recorder, &from_session);
recorder
});
{
let mut surface = RemoteSurface::new(to_ui);
surface.set_runtime_context("m", "http://h", Some((1, Some(2))), "s");
surface.set_background_jobs(Vec::new());
surface.set_tabs(vec![a_cell(1, true)]);
#[cfg(feature = "live-spill")]
surface.set_spill_archive(Arc::new(
crate::completed_spill::CompletedSpillArchive::default(),
));
surface.add_history("entry");
surface.save_history();
surface.reload().expect("served");
surface.read_line("› ").expect("served");
surface
.present_clarification("1: pick a lane", "/discuss", "› ", false, false)
.expect("served");
let flag = || std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
surface.turn_started(flag());
surface.turn_ended();
surface
.run_bang_escape("true", false, false)
.expect("served");
assert!(!surface
.run_login_argv(&["literal-program".into(), "argument".into()], false, false)
.expect("served"));
surface.present_interaction(&an_interaction());
#[cfg(feature = "rich-tui")]
surface.open_panel(PanelMode::Inline(10));
}
let seen = served.join().expect("terminal thread");
assert_eq!(
seen.observed(),
CountingSurface::METHODS,
"every InputSurface method must cross the proxy; missing: {:?}",
seen.missing()
);
}
#[test]
fn the_definition_crosses_the_seam_rather_than_a_rendered_string() {
let (to_ui, from_session) = std::sync::mpsc::sync_channel(4);
let served = std::thread::spawn(move || {
let mut seen = None;
for request in &from_session {
if let SurfaceRequest::Interact { interaction, reply } = request {
seen = Some((*interaction).clone());
let _ = reply.send(newt_core::HumanQuestionOutcome::Answer("ok".into()));
}
}
seen
});
let outcome = {
let mut surface = RemoteSurface::new(to_ui);
surface.present_interaction(&an_interaction())
};
assert_eq!(
outcome,
newt_core::HumanQuestionOutcome::Answer("ok".into())
);
let arrived = served
.join()
.expect("terminal thread")
.expect("an Interact arrived");
assert_eq!(
arrived.definition.kind,
newt_interaction::InteractionKind::Prompt
);
assert_eq!(arrived.definition.markdown, "? which file");
assert_eq!(arrived.definition.controls.len(), 1);
assert!(matches!(
arrived.definition.controls[0].kind,
newt_interaction::ControlKind::Text
));
assert!(arrived.is_blocking());
assert!(arrived.wants_attention());
}
#[test]
fn the_production_session_gate_wires_the_surface_seam() {
let production = production_code(include_str!("chat.rs"));
assert!(
production.contains("ask_surface:Some(&ask_surface)"),
"the session's permission gate does not wire the surface seam — \
`ask_question` would take the `None` arm and acquire the \
PromptWindow on the session thread"
);
assert!(
production.contains("RemoteSurface::new(ask_to_ui.clone()).present_interaction"),
"the wired closure does not go through the proxy"
);
}
fn production_code(source: &str) -> String {
source
.lines()
.filter(|l| !l.trim_start().starts_with("//"))
.flat_map(str::split_whitespace)
.collect::<String>()
}
#[test]
fn the_wiring_scan_notices_an_unwired_gate() {
let unwired = production_code("let gate = PromptPermissionGate {\n ask_surface: None,\n};");
assert!(!unwired.contains("ask_surface:Some(&ask_surface)"));
let commented = production_code("// ask_surface: Some(&ask_surface) would go here");
assert!(
!commented.contains("ask_surface:Some(&ask_surface)"),
"a commented-out wiring counted as a real one"
);
let split = production_code(
"RemoteSurface::new(ask_to_ui.clone())\n .present_interaction(interaction)",
);
assert!(
split.contains("RemoteSurface::new(ask_to_ui.clone()).present_interaction"),
"the scan cannot see a rustfmt-split call chain"
);
}
fn an_interaction() -> newt_core::interaction_surface::SurfaceInteraction {
use newt_interaction::{Control, ControlId, ControlKind, InteractionKind, Requirement};
newt_core::interaction_surface::SurfaceInteraction::blocking(
newt_interaction::InteractionDefinition::new(
InteractionKind::Prompt,
"? which file",
vec![Control {
id: ControlId::new("answer").expect("valid control id"),
kind: ControlKind::Text,
label: String::new(),
requirement: Requirement::Required,
}],
),
)
}
#[derive(Default)]
struct CountingSurface {
read_line: usize,
present_clarification: usize,
add_history: usize,
save_history: usize,
reload: usize,
runtime_context: usize,
background_jobs: usize,
tabs: usize,
#[cfg(feature = "live-spill")]
spill_archive: usize,
turn_started: usize,
turn_ended: usize,
run_bang_escape: usize,
run_login_argv: usize,
present_interaction: usize,
#[cfg(feature = "rich-tui")]
open_panel: usize,
}
impl CountingSurface {
const METHODS: usize =
13 + cfg!(feature = "rich-tui") as usize + cfg!(feature = "live-spill") as usize;
fn each(&self) -> Vec<(&'static str, usize)> {
[
("read_line", self.read_line),
("present_clarification", self.present_clarification),
("add_history", self.add_history),
("save_history", self.save_history),
("reload", self.reload),
("set_runtime_context", self.runtime_context),
("set_background_jobs", self.background_jobs),
("set_tabs", self.tabs),
#[cfg(feature = "live-spill")]
("set_spill_archive", self.spill_archive),
("turn_started", self.turn_started),
("turn_ended", self.turn_ended),
("run_bang_escape", self.run_bang_escape),
("run_login_argv", self.run_login_argv),
("present_interaction", self.present_interaction),
#[cfg(feature = "rich-tui")]
("open_panel", self.open_panel),
]
.to_vec()
}
fn observed(&self) -> usize {
self.each().iter().filter(|(_, n)| *n > 0).count()
}
fn missing(&self) -> Vec<&'static str> {
self.each()
.iter()
.filter(|(_, n)| *n == 0)
.map(|(name, _)| *name)
.collect()
}
}
impl crate::chat::InputSurface for CountingSurface {
fn run_bang_escape(
&mut self,
_command: &str,
_color: bool,
_verbose: bool,
) -> anyhow::Result<()> {
self.run_bang_escape += 1;
Ok(())
}
fn run_login_argv(
&mut self,
argv: &[String],
_color: bool,
_verbose: bool,
) -> anyhow::Result<bool> {
assert_eq!(argv, &["literal-program", "argument"]);
self.run_login_argv += 1;
Ok(false)
}
#[cfg(feature = "rich-tui")]
fn open_panel(&mut self, _mode: PanelMode) -> Option<PanelWindow> {
self.open_panel += 1;
None
}
fn present_interaction(
&mut self,
_interaction: &newt_core::interaction_surface::SurfaceInteraction,
) -> newt_core::HumanQuestionOutcome {
self.present_interaction += 1;
newt_core::HumanQuestionOutcome::Answer("served".into())
}
fn read_line(&mut self, _prompt: &str) -> anyhow::Result<ReadOutcome> {
self.read_line += 1;
Ok(ReadOutcome::Eof)
}
fn present_clarification(
&mut self,
_batch: &str,
_hint: &str,
_prompt: &str,
_color: bool,
_verbose: bool,
) -> anyhow::Result<ReadOutcome> {
self.present_clarification += 1;
Ok(ReadOutcome::Eof)
}
fn add_history(&mut self, _entry: &str) {
self.add_history += 1;
}
fn save_history(&mut self) {
self.save_history += 1;
}
fn reload(&mut self) -> anyhow::Result<()> {
self.reload += 1;
Ok(())
}
fn set_runtime_context(
&mut self,
_m: &str,
_e: &str,
_g: Option<(u32, Option<u32>)>,
_s: &str,
) {
self.runtime_context += 1;
}
fn set_background_jobs(&mut self, _jobs: Vec<BackgroundJob>) {
self.background_jobs += 1;
}
#[cfg(feature = "live-spill")]
fn set_spill_archive(
&mut self,
_archive: Arc<crate::completed_spill::CompletedSpillArchive>,
) {
self.spill_archive += 1;
}
fn set_tabs(&mut self, _tabs: Vec<crate::tab_bar::TabCell>) {
self.tabs += 1;
}
fn turn_started(&mut self, _cancel: std::sync::Arc<std::sync::atomic::AtomicBool>) {
self.turn_started += 1;
}
fn turn_ended(&mut self) {
self.turn_ended += 1;
}
}
#[test]
fn a_session_serves_two_turns_and_shuts_down_without_leaking() {
use newt_core::initiative::{effective_initiative, set_cli_initiative, Initiative};
let _g = newt_core::test_guard::GlobalSettingsGuard::acquire();
let a = newt_core::lifecycle::new_session_id();
let b = newt_core::lifecycle::new_session_id();
set_cli_initiative(Initiative::Patient);
let (to_ui, from_session) = std::sync::mpsc::sync_channel(16);
let (release_tx, release_rx) = std::sync::mpsc::channel::<()>();
let turn_one_done = Arc::new(AtomicBool::new(false));
let observed = Arc::new(std::sync::Mutex::new(Vec::new()));
std::thread::scope(|scope| {
let session = {
let (a, b) = (a.clone(), b.clone());
let (done, seen) = (Arc::clone(&turn_one_done), Arc::clone(&observed));
scope.spawn(move || {
let mut surface = RemoteSurface::new(to_ui);
{
let _turn = bind_turn(&a);
surface.set_runtime_context("m1", "http://h", None, "s");
seen.lock().unwrap().push((
newt_core::lifecycle::active_session(),
effective_initiative(),
));
release_rx.recv().expect("terminal releases turn 1");
}
done.store(true, Ordering::Release);
{
let _turn = bind_turn(&b);
seen.lock().unwrap().push((
newt_core::lifecycle::active_session(),
effective_initiative(),
));
}
})
};
match from_session.recv().expect("turn 1 published status") {
SurfaceRequest::SetRuntimeContext { model, .. } => {
assert_eq!(model, "m1");
assert!(
!turn_one_done.load(Ordering::Acquire),
"the terminal served while turn 1 was still running — which is the entire point of the split"
);
}
other => panic!("expected the turn's status update, got {other:?}"),
}
set_cli_initiative(Initiative::Eager); release_tx.send(()).expect("session still listening");
let mut surface = RecordingSurface::default();
pump_surface(&mut surface, &from_session);
session.join().expect("session thread joined cleanly");
});
let seen = observed.lock().unwrap().clone();
assert_eq!(seen.len(), 2, "two turns ran");
assert_eq!(seen[0].0.as_deref(), Some(a.as_str()), "turn 1 is A's");
assert_eq!(seen[0].1, Initiative::Patient, "turn 1 held P1");
assert_eq!(
seen[1].0.as_deref(),
Some(b.as_str()),
"turn 2 is B's — a session-long binding answers A here"
);
assert_eq!(
seen[1].1,
Initiative::Eager,
"turn 2 sees P2 — a session-long capture answers P1 here"
);
}
#[derive(Default)]
struct RecordingSurface {
history: Vec<String>,
saves: usize,
}
impl crate::chat::InputSurface for RecordingSurface {
fn present_interaction(
&mut self,
_interaction: &newt_core::interaction_surface::SurfaceInteraction,
) -> newt_core::HumanQuestionOutcome {
newt_core::HumanQuestionOutcome::Unavailable
}
fn read_line(&mut self, _prompt: &str) -> anyhow::Result<ReadOutcome> {
Ok(ReadOutcome::Eof)
}
fn add_history(&mut self, entry: &str) {
self.history.push(entry.to_string());
}
fn save_history(&mut self) {
self.saves += 1;
}
fn reload(&mut self) -> anyhow::Result<()> {
Ok(())
}
}
#[test]
fn each_turn_is_attributed_to_the_tab_active_for_that_turn() {
let _g = newt_core::test_guard::GlobalSettingsGuard::acquire();
let a = newt_core::lifecycle::new_session_id();
let b = newt_core::lifecycle::new_session_id();
let observed = std::thread::spawn({
let (a, b) = (a.clone(), b.clone());
move || {
let mut seen = Vec::new();
{
let _turn = bind_turn(&a);
seen.push(newt_core::lifecycle::active_session());
}
{
let _turn = bind_turn(&b);
seen.push(newt_core::lifecycle::active_session());
}
seen.push(newt_core::lifecycle::active_session());
seen
}
})
.join()
.expect("session thread");
assert_eq!(observed[0].as_deref(), Some(a.as_str()), "turn 1 is A's");
assert_eq!(
observed[1].as_deref(),
Some(b.as_str()),
"turn 2 is B's — a session-long binding would still answer A here"
);
assert_eq!(
observed[2], None,
"between turns the thread holds no claim, so nothing inherits a \
stale binding"
);
}
#[test]
fn a_dial_moved_between_turns_lands_on_the_next_turn() {
use newt_core::initiative::{effective_initiative, set_cli_initiative, Initiative};
let _g = newt_core::test_guard::GlobalSettingsGuard::acquire();
set_cli_initiative(Initiative::Patient);
{
let _turn = bind_turn(&newt_core::lifecycle::new_session_id());
assert_eq!(
effective_initiative(),
Initiative::Patient,
"turn 1 sees P1"
);
set_cli_initiative(Initiative::Eager);
assert_eq!(
effective_initiative(),
Initiative::Patient,
"…and turn 1 stays internally stable despite it"
);
}
{
let _turn = bind_turn(&newt_core::lifecycle::new_session_id());
assert_eq!(
effective_initiative(),
Initiative::Eager,
"turn 2 sees P2 — a session-long capture would still answer P1"
);
}
}
#[test]
fn a_cognition_change_between_turns_also_lands_on_the_next_turn() {
use newt_core::cognition::{effective_cognition, set_cli_cognition, CognitionOverride};
use newt_core::role_profile::Cognition;
let _g = newt_core::test_guard::GlobalSettingsGuard::acquire();
set_cli_cognition(CognitionOverride::Set(Cognition::Rational));
{
let _turn = bind_turn(&newt_core::lifecycle::new_session_id());
assert_eq!(effective_cognition(), Some(Cognition::Rational));
set_cli_cognition(CognitionOverride::Set(Cognition::Meticulous));
assert_eq!(
effective_cognition(),
Some(Cognition::Rational),
"the running turn is stable"
);
}
{
let _turn = bind_turn(&newt_core::lifecycle::new_session_id());
assert_eq!(
effective_cognition(),
Some(Cognition::Meticulous),
"the next turn sees the new dial"
);
}
set_cli_cognition(CognitionOverride::Unset);
}
#[test]
fn concurrent_turns_on_two_threads_do_not_cross_attribution() {
let _g = newt_core::test_guard::GlobalSettingsGuard::acquire();
let a = newt_core::lifecycle::new_session_id();
let b = newt_core::lifecycle::new_session_id();
let seen = Arc::new(std::sync::Mutex::new(Vec::new()));
std::thread::scope(|scope| {
for id in [&a, &b] {
let seen = Arc::clone(&seen);
scope.spawn(move || {
let _turn = bind_turn(id);
seen.lock().unwrap().push((
id.as_str().to_string(),
newt_core::lifecycle::active_session(),
));
});
}
});
for (expected, actual) in seen.lock().unwrap().iter() {
assert_eq!(
actual.as_deref(),
Some(expected.as_str()),
"each turn resolved to its own session"
);
}
}
#[test]
fn a_panicking_turn_still_releases_its_binding() {
let _g = newt_core::test_guard::GlobalSettingsGuard::acquire();
newt_core::lifecycle::clear_active_session();
let id = newt_core::lifecycle::new_session_id();
let panicked = std::thread::spawn(move || {
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let _turn = bind_turn(&id);
panic!("turn blew up");
}));
assert!(result.is_err());
newt_core::lifecycle::active_session()
})
.join()
.expect("probe thread");
assert_eq!(
panicked, None,
"a failed turn releases its binding rather than leaking it"
);
}
}
#[cfg(all(test, unix, feature = "rich-tui"))]
mod panel_window_remeasure_tests {
use super::*;
use ratatui::layout::Rect;
use std::os::fd::FromRawFd;
fn pipe_file() -> std::fs::File {
let mut fds = [0; 2];
assert_eq!(unsafe { libc::pipe(fds.as_mut_ptr()) }, 0);
unsafe { libc::close(fds[0]) };
unsafe { std::fs::File::from_raw_fd(fds[1]) }
}
#[test]
fn a_remeasure_adopts_the_presenters_new_region() {
let (tx, rx) = std::sync::mpsc::sync_channel(1);
let window = PanelWindow::new(pipe_file(), Rect::new(0, 13, 100, 18), Some(tx));
let presenter = std::thread::spawn(move || {
let PanelSignal::Remeasure { rows, reply } = rx.recv().unwrap();
assert_eq!(rows, Some(8), "the requested height reaches the presenter");
reply.send(Rect::new(0, 4, 46, 8)).unwrap();
assert!(rx.recv().is_err(), "the window's drop must release");
});
window.remeasure(Some(8));
assert_eq!(window.area(), Rect::new(0, 4, 46, 8));
drop(window);
presenter.join().unwrap();
}
#[test]
fn with_nobody_answering_the_region_is_kept_not_stranded() {
let area = Rect::new(0, 13, 100, 18);
let unparked = PanelWindow::new(pipe_file(), area, None);
unparked.remeasure(None);
assert_eq!(unparked.area(), area);
let (tx, rx) = std::sync::mpsc::sync_channel(1);
let window = PanelWindow::new(pipe_file(), area, Some(tx));
let presenter = std::thread::spawn(move || drop(rx.recv().unwrap()));
window.remeasure(None);
assert_eq!(window.area(), area);
presenter.join().unwrap();
}
}