This commit is contained in:
@@ -1,3 +1,70 @@
|
||||
//! Deterministic state and fixtures shared by the GPUI and Floem spikes.
|
||||
//!
|
||||
//! UI candidates translate framework input into [`ShellAction`] and render the
|
||||
//! resulting [`ShellModel`]. This makes their interaction traces comparable.
|
||||
|
||||
use std::fmt;
|
||||
|
||||
pub const GRID_ROWS: usize = 2;
|
||||
pub const GRID_COLUMNS: usize = 3;
|
||||
pub const DEFAULT_TERMINAL_LINE_LIMIT: usize = 64;
|
||||
|
||||
/// Stable identity used in traces and accessibility identifiers.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||
#[repr(u8)]
|
||||
pub enum PaneId {
|
||||
CodexRuntime = 0,
|
||||
ClaudeUi = 1,
|
||||
PiDocs = 2,
|
||||
Architecture = 3,
|
||||
AcpPreview = 4,
|
||||
RuntimeReview = 5,
|
||||
}
|
||||
|
||||
impl PaneId {
|
||||
pub const ALL: [Self; GRID_ROWS * GRID_COLUMNS] = [
|
||||
Self::CodexRuntime,
|
||||
Self::ClaudeUi,
|
||||
Self::PiDocs,
|
||||
Self::Architecture,
|
||||
Self::AcpPreview,
|
||||
Self::RuntimeReview,
|
||||
];
|
||||
|
||||
#[must_use]
|
||||
pub const fn index(self) -> usize {
|
||||
self as usize
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn stable_name(self) -> &'static str {
|
||||
match self {
|
||||
Self::CodexRuntime => "codex-runtime",
|
||||
Self::ClaudeUi => "claude-ui",
|
||||
Self::PiDocs => "pi-docs",
|
||||
Self::Architecture => "architecture",
|
||||
Self::AcpPreview => "acp-preview",
|
||||
Self::RuntimeReview => "runtime-review",
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn row(self) -> usize {
|
||||
self.index() / GRID_COLUMNS
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn column(self) -> usize {
|
||||
self.index() % GRID_COLUMNS
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for PaneId {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter.write_str(self.stable_name())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum SurfaceKind {
|
||||
Terminal,
|
||||
@@ -6,21 +73,45 @@ pub enum SurfaceKind {
|
||||
Review,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum PaneStatus {
|
||||
Working,
|
||||
NeedsInput,
|
||||
Streaming,
|
||||
Ready,
|
||||
}
|
||||
|
||||
impl PaneStatus {
|
||||
#[must_use]
|
||||
pub const fn label(self) -> &'static str {
|
||||
match self {
|
||||
Self::Working => "WORKING",
|
||||
Self::NeedsInput => "NEEDS INPUT",
|
||||
Self::Streaming => "STREAMING",
|
||||
Self::Ready => "READY",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct PaneFixture {
|
||||
pub id: PaneId,
|
||||
pub title: &'static str,
|
||||
pub badge: &'static str,
|
||||
pub target: &'static str,
|
||||
pub kind: SurfaceKind,
|
||||
pub status: PaneStatus,
|
||||
pub lines: [&'static str; 4],
|
||||
}
|
||||
|
||||
pub const PANES: [PaneFixture; 6] = [
|
||||
PaneFixture {
|
||||
id: PaneId::CodexRuntime,
|
||||
title: "Codex · runtime",
|
||||
badge: "WORKING",
|
||||
target: "metal · Tailscale SSH",
|
||||
kind: SurfaceKind::Terminal,
|
||||
status: PaneStatus::Working,
|
||||
lines: [
|
||||
"$ cargo nextest run -p lumbridge-runtime",
|
||||
"PASS remote::reconnect_replays_output",
|
||||
@@ -29,10 +120,12 @@ pub const PANES: [PaneFixture; 6] = [
|
||||
],
|
||||
},
|
||||
PaneFixture {
|
||||
id: PaneId::ClaudeUi,
|
||||
title: "Claude Code · UI",
|
||||
badge: "NEEDS INPUT",
|
||||
target: "MacBook Air · local",
|
||||
kind: SurfaceKind::Terminal,
|
||||
status: PaneStatus::NeedsInput,
|
||||
lines: [
|
||||
"$ bacon clippy",
|
||||
"finished in 0.42s",
|
||||
@@ -41,10 +134,12 @@ pub const PANES: [PaneFixture; 6] = [
|
||||
],
|
||||
},
|
||||
PaneFixture {
|
||||
id: PaneId::PiDocs,
|
||||
title: "Pi · docs",
|
||||
badge: "STREAMING",
|
||||
target: "amd-server · OpenSSH",
|
||||
kind: SurfaceKind::Terminal,
|
||||
status: PaneStatus::Streaming,
|
||||
lines: [
|
||||
"$ cargo watch --why",
|
||||
"ACP session resumed at event 1842",
|
||||
@@ -53,10 +148,12 @@ pub const PANES: [PaneFixture; 6] = [
|
||||
],
|
||||
},
|
||||
PaneFixture {
|
||||
id: PaneId::Architecture,
|
||||
title: "Architecture.md",
|
||||
badge: "MARKDOWN",
|
||||
target: "lumbridge-code · worktree",
|
||||
kind: SurfaceKind::Markdown,
|
||||
status: PaneStatus::Ready,
|
||||
lines: [
|
||||
"## Remote session path",
|
||||
"The destination runtime owns the PTY.",
|
||||
@@ -65,10 +162,12 @@ pub const PANES: [PaneFixture; 6] = [
|
||||
],
|
||||
},
|
||||
PaneFixture {
|
||||
id: PaneId::AcpPreview,
|
||||
title: "Preview · ACP docs",
|
||||
badge: "BROWSER",
|
||||
target: "isolated system web engine",
|
||||
kind: SurfaceKind::Browser,
|
||||
status: PaneStatus::Ready,
|
||||
lines: [
|
||||
"https://agentclientprotocol.com",
|
||||
"Content process: sandboxed",
|
||||
@@ -77,10 +176,12 @@ pub const PANES: [PaneFixture; 6] = [
|
||||
],
|
||||
},
|
||||
PaneFixture {
|
||||
id: PaneId::RuntimeReview,
|
||||
title: "Changes · lumbridge-runtime",
|
||||
badge: "REVIEW",
|
||||
target: "metal · worktree remote-runtime",
|
||||
kind: SurfaceKind::Review,
|
||||
status: PaneStatus::Ready,
|
||||
lines: [
|
||||
"+ 184 remote transport",
|
||||
"+ 96 SQLite migrations",
|
||||
@@ -97,26 +198,538 @@ pub const WORKSPACES: [&str; 5] = [
|
||||
"ACP adapters",
|
||||
"Usage telemetry",
|
||||
];
|
||||
|
||||
pub const FOOTER_LEFT: &str = "6 panes · 3 remote · 1 needs input";
|
||||
pub const FOOTER_CENTER: &str = "Codex · ChatGPT subscription · 62% window remaining";
|
||||
pub const FOOTER_RIGHT: &str = "burn 8.4%/hr · resets in 2h 14m";
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct PaneState {
|
||||
fixture: PaneFixture,
|
||||
status: PaneStatus,
|
||||
status_before_input: Option<PaneStatus>,
|
||||
lines: Vec<String>,
|
||||
synthetic_line_sequence: u64,
|
||||
}
|
||||
|
||||
impl PaneState {
|
||||
#[must_use]
|
||||
pub const fn id(&self) -> PaneId {
|
||||
self.fixture.id
|
||||
}
|
||||
#[must_use]
|
||||
pub const fn fixture(&self) -> &PaneFixture {
|
||||
&self.fixture
|
||||
}
|
||||
#[must_use]
|
||||
pub const fn kind(&self) -> SurfaceKind {
|
||||
self.fixture.kind
|
||||
}
|
||||
#[must_use]
|
||||
pub const fn status(&self) -> PaneStatus {
|
||||
self.status
|
||||
}
|
||||
#[must_use]
|
||||
pub const fn needs_input(&self) -> bool {
|
||||
matches!(self.status, PaneStatus::NeedsInput)
|
||||
}
|
||||
#[must_use]
|
||||
pub fn lines(&self) -> &[String] {
|
||||
&self.lines
|
||||
}
|
||||
#[must_use]
|
||||
pub const fn synthetic_line_sequence(&self) -> u64 {
|
||||
self.synthetic_line_sequence
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
||||
pub struct CommandPaletteState {
|
||||
open: bool,
|
||||
query: String,
|
||||
}
|
||||
|
||||
impl CommandPaletteState {
|
||||
#[must_use]
|
||||
pub const fn is_open(&self) -> bool {
|
||||
self.open
|
||||
}
|
||||
#[must_use]
|
||||
pub fn query(&self) -> &str {
|
||||
&self.query
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum FocusDirection {
|
||||
Left,
|
||||
Right,
|
||||
Up,
|
||||
Down,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum ShellAction {
|
||||
MoveFocus(FocusDirection),
|
||||
SelectPane(PaneId),
|
||||
SetNeedsInput { pane: PaneId, needs_input: bool },
|
||||
OpenCommandPalette,
|
||||
CloseCommandPalette,
|
||||
SetCommandPaletteQuery(String),
|
||||
SyntheticStreamTick,
|
||||
}
|
||||
|
||||
/// Counters prove both candidates replayed the same workload. Framework
|
||||
/// adapters may use the event sequence and revision for explicitly labelled
|
||||
/// timing stages; these counters do not imply pixels reached the display.
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
pub struct MeasurementCounters {
|
||||
pub events_dispatched: u64,
|
||||
pub state_changes: u64,
|
||||
pub focus_moves: u64,
|
||||
pub blocked_focus_moves: u64,
|
||||
pub direct_selections: u64,
|
||||
pub needs_input_changes: u64,
|
||||
pub palette_changes: u64,
|
||||
pub synthetic_ticks: u64,
|
||||
/// Total surfaces invalidated by the deterministic six-surface workload.
|
||||
pub surface_updates: u64,
|
||||
pub terminal_lines_appended: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct ActionOutcome {
|
||||
pub event_sequence: u64,
|
||||
pub revision: u64,
|
||||
pub changed: bool,
|
||||
pub selected_pane: PaneId,
|
||||
pub terminal_lines_appended: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct InvalidTerminalLineLimit;
|
||||
|
||||
impl fmt::Display for InvalidTerminalLineLimit {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter.write_str("terminal line limit must be greater than zero")
|
||||
}
|
||||
}
|
||||
impl std::error::Error for InvalidTerminalLineLimit {}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct ShellModel {
|
||||
panes: [PaneState; GRID_ROWS * GRID_COLUMNS],
|
||||
selected_pane: PaneId,
|
||||
command_palette: CommandPaletteState,
|
||||
terminal_line_limit: usize,
|
||||
synthetic_tick: u64,
|
||||
revision: u64,
|
||||
counters: MeasurementCounters,
|
||||
}
|
||||
|
||||
impl Default for ShellModel {
|
||||
fn default() -> Self {
|
||||
Self::with_terminal_line_limit(DEFAULT_TERMINAL_LINE_LIMIT)
|
||||
.expect("the default terminal line limit is non-zero")
|
||||
}
|
||||
}
|
||||
|
||||
impl ShellModel {
|
||||
pub fn with_terminal_line_limit(limit: usize) -> Result<Self, InvalidTerminalLineLimit> {
|
||||
if limit == 0 {
|
||||
return Err(InvalidTerminalLineLimit);
|
||||
}
|
||||
let panes = PANES.map(|fixture| {
|
||||
let mut lines = fixture.lines.map(String::from).to_vec();
|
||||
if fixture.kind == SurfaceKind::Terminal && lines.len() > limit {
|
||||
lines.drain(..lines.len() - limit);
|
||||
}
|
||||
PaneState {
|
||||
fixture,
|
||||
status: fixture.status,
|
||||
status_before_input: (fixture.status == PaneStatus::NeedsInput)
|
||||
.then_some(PaneStatus::Working),
|
||||
lines,
|
||||
synthetic_line_sequence: 0,
|
||||
}
|
||||
});
|
||||
Ok(Self {
|
||||
panes,
|
||||
selected_pane: PaneId::CodexRuntime,
|
||||
command_palette: CommandPaletteState::default(),
|
||||
terminal_line_limit: limit,
|
||||
synthetic_tick: 0,
|
||||
revision: 0,
|
||||
counters: MeasurementCounters::default(),
|
||||
})
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn panes(&self) -> &[PaneState; GRID_ROWS * GRID_COLUMNS] {
|
||||
&self.panes
|
||||
}
|
||||
#[must_use]
|
||||
pub fn pane(&self, id: PaneId) -> &PaneState {
|
||||
&self.panes[id.index()]
|
||||
}
|
||||
#[must_use]
|
||||
pub const fn selected_pane(&self) -> PaneId {
|
||||
self.selected_pane
|
||||
}
|
||||
#[must_use]
|
||||
pub const fn command_palette(&self) -> &CommandPaletteState {
|
||||
&self.command_palette
|
||||
}
|
||||
#[must_use]
|
||||
pub const fn terminal_line_limit(&self) -> usize {
|
||||
self.terminal_line_limit
|
||||
}
|
||||
#[must_use]
|
||||
pub const fn synthetic_tick(&self) -> u64 {
|
||||
self.synthetic_tick
|
||||
}
|
||||
#[must_use]
|
||||
pub const fn revision(&self) -> u64 {
|
||||
self.revision
|
||||
}
|
||||
#[must_use]
|
||||
pub const fn counters(&self) -> MeasurementCounters {
|
||||
self.counters
|
||||
}
|
||||
|
||||
pub fn dispatch(&mut self, action: ShellAction) -> ActionOutcome {
|
||||
self.counters.events_dispatched += 1;
|
||||
let mut changed = false;
|
||||
let mut appended = 0;
|
||||
match action {
|
||||
ShellAction::MoveFocus(direction) => {
|
||||
if let Some(next) = focus_neighbor(self.selected_pane, direction) {
|
||||
self.selected_pane = next;
|
||||
self.counters.focus_moves += 1;
|
||||
changed = true;
|
||||
} else {
|
||||
self.counters.blocked_focus_moves += 1;
|
||||
}
|
||||
}
|
||||
ShellAction::SelectPane(pane) => {
|
||||
if self.selected_pane != pane {
|
||||
self.selected_pane = pane;
|
||||
self.counters.direct_selections += 1;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
ShellAction::SetNeedsInput { pane, needs_input } => {
|
||||
let pane = &mut self.panes[pane.index()];
|
||||
if needs_input && !pane.needs_input() {
|
||||
pane.status_before_input = Some(pane.status);
|
||||
pane.status = PaneStatus::NeedsInput;
|
||||
self.counters.needs_input_changes += 1;
|
||||
changed = true;
|
||||
} else if !needs_input && pane.needs_input() {
|
||||
pane.status = pane.status_before_input.take().unwrap_or(PaneStatus::Ready);
|
||||
self.counters.needs_input_changes += 1;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
ShellAction::OpenCommandPalette => {
|
||||
if !self.command_palette.open {
|
||||
self.command_palette.open = true;
|
||||
self.command_palette.query.clear();
|
||||
self.counters.palette_changes += 1;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
ShellAction::CloseCommandPalette => {
|
||||
if self.command_palette.open {
|
||||
self.command_palette.open = false;
|
||||
self.command_palette.query.clear();
|
||||
self.counters.palette_changes += 1;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
ShellAction::SetCommandPaletteQuery(query) => {
|
||||
if self.command_palette.open && self.command_palette.query != query {
|
||||
self.command_palette.query = query;
|
||||
self.counters.palette_changes += 1;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
ShellAction::SyntheticStreamTick => {
|
||||
self.synthetic_tick += 1;
|
||||
self.counters.synthetic_ticks += 1;
|
||||
for pane in &mut self.panes {
|
||||
pane.synthetic_line_sequence += 1;
|
||||
if pane.kind() != SurfaceKind::Terminal {
|
||||
continue;
|
||||
}
|
||||
pane.lines.push(format!(
|
||||
"[tick {:06}] {} · synthetic line {:06}",
|
||||
self.synthetic_tick,
|
||||
pane.id(),
|
||||
pane.synthetic_line_sequence
|
||||
));
|
||||
if pane.lines.len() > self.terminal_line_limit {
|
||||
pane.lines.remove(0);
|
||||
}
|
||||
appended += 1;
|
||||
}
|
||||
self.counters.surface_updates += self.panes.len() as u64;
|
||||
self.counters.terminal_lines_appended += appended as u64;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if changed {
|
||||
self.revision += 1;
|
||||
self.counters.state_changes += 1;
|
||||
}
|
||||
ActionOutcome {
|
||||
event_sequence: self.counters.events_dispatched,
|
||||
revision: self.revision,
|
||||
changed,
|
||||
selected_pane: self.selected_pane,
|
||||
terminal_lines_appended: appended,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn focus_neighbor(pane: PaneId, direction: FocusDirection) -> Option<PaneId> {
|
||||
let row = pane.row();
|
||||
let column = pane.column();
|
||||
let next = match direction {
|
||||
FocusDirection::Left if column > 0 => Some(pane.index() - 1),
|
||||
FocusDirection::Right if column + 1 < GRID_COLUMNS => Some(pane.index() + 1),
|
||||
FocusDirection::Up if row > 0 => Some(pane.index() - GRID_COLUMNS),
|
||||
FocusDirection::Down if row + 1 < GRID_ROWS => Some(pane.index() + GRID_COLUMNS),
|
||||
_ => None,
|
||||
};
|
||||
match next {
|
||||
Some(index) => Some(PaneId::ALL[index]),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{PANES, SurfaceKind};
|
||||
use super::{
|
||||
FocusDirection, GRID_COLUMNS, GRID_ROWS, PANES, PaneId, PaneStatus, ShellAction,
|
||||
ShellModel, SurfaceKind, focus_neighbor,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn comparison_fixture_has_six_mixed_surfaces() {
|
||||
assert_eq!(PANES.len(), 6);
|
||||
fn fixture_has_stable_ids_and_mixed_surfaces() {
|
||||
assert_eq!(PANES.len(), GRID_ROWS * GRID_COLUMNS);
|
||||
assert_eq!(PANES.map(|pane| pane.id), PaneId::ALL);
|
||||
assert_eq!(PaneId::CodexRuntime.stable_name(), "codex-runtime");
|
||||
assert_eq!(PaneId::RuntimeReview.to_string(), "runtime-review");
|
||||
assert!(PANES.iter().any(|pane| pane.kind == SurfaceKind::Markdown));
|
||||
assert!(PANES.iter().any(|pane| pane.kind == SurfaceKind::Browser));
|
||||
assert_eq!(
|
||||
PANES
|
||||
.iter()
|
||||
.filter(|pane| pane.kind == SurfaceKind::Terminal)
|
||||
.filter(|p| p.kind == SurfaceKind::Terminal)
|
||||
.count(),
|
||||
3
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn initial_state_has_one_explicit_needs_input_pane() {
|
||||
let model = ShellModel::default();
|
||||
assert_eq!(model.selected_pane(), PaneId::CodexRuntime);
|
||||
assert_eq!(model.revision(), 0);
|
||||
assert!(!model.command_palette().is_open());
|
||||
assert_eq!(
|
||||
model
|
||||
.panes()
|
||||
.iter()
|
||||
.filter(|pane| pane.needs_input())
|
||||
.map(|pane| pane.id())
|
||||
.collect::<Vec<_>>(),
|
||||
vec![PaneId::ClaudeUi]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn neighbors_match_two_by_three_grid_without_wrapping() {
|
||||
assert_eq!(
|
||||
focus_neighbor(PaneId::CodexRuntime, FocusDirection::Right),
|
||||
Some(PaneId::ClaudeUi)
|
||||
);
|
||||
assert_eq!(
|
||||
focus_neighbor(PaneId::ClaudeUi, FocusDirection::Down),
|
||||
Some(PaneId::AcpPreview)
|
||||
);
|
||||
assert_eq!(
|
||||
focus_neighbor(PaneId::RuntimeReview, FocusDirection::Up),
|
||||
Some(PaneId::PiDocs)
|
||||
);
|
||||
assert_eq!(
|
||||
focus_neighbor(PaneId::Architecture, FocusDirection::Left),
|
||||
None
|
||||
);
|
||||
assert_eq!(focus_neighbor(PaneId::PiDocs, FocusDirection::Right), None);
|
||||
assert_eq!(
|
||||
focus_neighbor(PaneId::CodexRuntime, FocusDirection::Up),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn focus_updates_selection_revision_and_blocked_counter() {
|
||||
let mut model = ShellModel::default();
|
||||
let blocked = model.dispatch(ShellAction::MoveFocus(FocusDirection::Left));
|
||||
assert!(!blocked.changed);
|
||||
assert_eq!((blocked.event_sequence, blocked.revision), (1, 0));
|
||||
let moved = model.dispatch(ShellAction::MoveFocus(FocusDirection::Right));
|
||||
assert!(moved.changed);
|
||||
assert_eq!(moved.selected_pane, PaneId::ClaudeUi);
|
||||
assert_eq!(moved.revision, 1);
|
||||
model.dispatch(ShellAction::MoveFocus(FocusDirection::Down));
|
||||
assert_eq!(model.selected_pane(), PaneId::AcpPreview);
|
||||
assert_eq!(model.counters().focus_moves, 2);
|
||||
assert_eq!(model.counters().blocked_focus_moves, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn direct_selection_is_idempotent_and_counted_separately() {
|
||||
let mut model = ShellModel::default();
|
||||
assert!(
|
||||
!model
|
||||
.dispatch(ShellAction::SelectPane(PaneId::CodexRuntime))
|
||||
.changed
|
||||
);
|
||||
assert!(
|
||||
model
|
||||
.dispatch(ShellAction::SelectPane(PaneId::RuntimeReview))
|
||||
.changed
|
||||
);
|
||||
assert_eq!(model.selected_pane(), PaneId::RuntimeReview);
|
||||
assert_eq!(model.counters().direct_selections, 1);
|
||||
assert_eq!(model.counters().events_dispatched, 2);
|
||||
assert_eq!(model.counters().state_changes, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn needs_input_restores_previous_status() {
|
||||
let mut model = ShellModel::default();
|
||||
model.dispatch(ShellAction::SetNeedsInput {
|
||||
pane: PaneId::PiDocs,
|
||||
needs_input: true,
|
||||
});
|
||||
assert_eq!(model.pane(PaneId::PiDocs).status(), PaneStatus::NeedsInput);
|
||||
assert!(
|
||||
!model
|
||||
.dispatch(ShellAction::SetNeedsInput {
|
||||
pane: PaneId::PiDocs,
|
||||
needs_input: true,
|
||||
})
|
||||
.changed
|
||||
);
|
||||
model.dispatch(ShellAction::SetNeedsInput {
|
||||
pane: PaneId::PiDocs,
|
||||
needs_input: false,
|
||||
});
|
||||
assert_eq!(model.pane(PaneId::PiDocs).status(), PaneStatus::Streaming);
|
||||
model.dispatch(ShellAction::SetNeedsInput {
|
||||
pane: PaneId::ClaudeUi,
|
||||
needs_input: false,
|
||||
});
|
||||
assert_eq!(model.pane(PaneId::ClaudeUi).status(), PaneStatus::Working);
|
||||
assert_eq!(model.counters().needs_input_changes, 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn command_palette_has_explicit_lifecycle() {
|
||||
let mut model = ShellModel::default();
|
||||
assert!(
|
||||
!model
|
||||
.dispatch(ShellAction::SetCommandPaletteQuery("pane".into()))
|
||||
.changed
|
||||
);
|
||||
model.dispatch(ShellAction::OpenCommandPalette);
|
||||
model.dispatch(ShellAction::SetCommandPaletteQuery("pane: next".into()));
|
||||
assert!(model.command_palette().is_open());
|
||||
assert_eq!(model.command_palette().query(), "pane: next");
|
||||
model.dispatch(ShellAction::CloseCommandPalette);
|
||||
assert!(!model.command_palette().is_open());
|
||||
assert_eq!(model.command_palette().query(), "");
|
||||
assert_eq!(model.counters().palette_changes, 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ticks_append_predictable_lines_to_terminals_only() {
|
||||
let mut model = ShellModel::default();
|
||||
let markdown = model.pane(PaneId::Architecture).lines().to_vec();
|
||||
let outcome = model.dispatch(ShellAction::SyntheticStreamTick);
|
||||
assert_eq!(outcome.terminal_lines_appended, 3);
|
||||
assert_eq!(model.synthetic_tick(), 1);
|
||||
assert_eq!(
|
||||
model.pane(PaneId::CodexRuntime).lines().last().unwrap(),
|
||||
"[tick 000001] codex-runtime · synthetic line 000001"
|
||||
);
|
||||
assert_eq!(model.pane(PaneId::Architecture).lines(), markdown);
|
||||
assert!(
|
||||
model
|
||||
.panes()
|
||||
.iter()
|
||||
.all(|pane| pane.synthetic_line_sequence() == 1)
|
||||
);
|
||||
assert_eq!(model.counters().synthetic_ticks, 1);
|
||||
assert_eq!(model.counters().surface_updates, 6);
|
||||
assert_eq!(model.counters().terminal_lines_appended, 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn terminal_scrollback_is_bounded_below_fixture_size() {
|
||||
let mut model = ShellModel::with_terminal_line_limit(2).unwrap();
|
||||
assert_eq!(model.pane(PaneId::CodexRuntime).lines().len(), 2);
|
||||
for _ in 0..10 {
|
||||
model.dispatch(ShellAction::SyntheticStreamTick);
|
||||
}
|
||||
for pane in model
|
||||
.panes()
|
||||
.iter()
|
||||
.filter(|pane| pane.kind() == SurfaceKind::Terminal)
|
||||
{
|
||||
assert_eq!(pane.lines().len(), 2);
|
||||
assert_eq!(pane.synthetic_line_sequence(), 10);
|
||||
assert!(pane.lines()[0].starts_with("[tick 000009]"));
|
||||
assert!(pane.lines()[1].starts_with("[tick 000010]"));
|
||||
}
|
||||
assert!(ShellModel::with_terminal_line_limit(0).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn identical_action_replays_produce_identical_models_and_counters() {
|
||||
let actions = [
|
||||
ShellAction::MoveFocus(FocusDirection::Right),
|
||||
ShellAction::MoveFocus(FocusDirection::Down),
|
||||
ShellAction::OpenCommandPalette,
|
||||
ShellAction::SetCommandPaletteQuery("workspace".into()),
|
||||
ShellAction::CloseCommandPalette,
|
||||
ShellAction::SetNeedsInput {
|
||||
pane: PaneId::CodexRuntime,
|
||||
needs_input: true,
|
||||
},
|
||||
ShellAction::SyntheticStreamTick,
|
||||
ShellAction::SyntheticStreamTick,
|
||||
];
|
||||
let mut gpui = ShellModel::default();
|
||||
let mut floem = ShellModel::default();
|
||||
let gpui_outcomes = actions
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|action| gpui.dispatch(action))
|
||||
.collect::<Vec<_>>();
|
||||
let floem_outcomes = actions
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|action| floem.dispatch(action))
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(gpui_outcomes, floem_outcomes);
|
||||
assert_eq!(gpui, floem);
|
||||
assert_eq!(gpui.counters().events_dispatched, actions.len() as u64);
|
||||
assert_eq!(gpui.counters().surface_updates, 12);
|
||||
assert_eq!(gpui.counters().terminal_lines_appended, 6);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user