This commit is contained in:
@@ -81,6 +81,12 @@ pub enum PaneStatus {
|
||||
Ready,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum OutputSource {
|
||||
Deterministic,
|
||||
External,
|
||||
}
|
||||
|
||||
impl PaneStatus {
|
||||
#[must_use]
|
||||
pub const fn label(self) -> &'static str {
|
||||
@@ -209,6 +215,7 @@ pub struct PaneState {
|
||||
status_before_input: Option<PaneStatus>,
|
||||
lines: Vec<String>,
|
||||
synthetic_line_sequence: u64,
|
||||
output_source: OutputSource,
|
||||
}
|
||||
|
||||
impl PaneState {
|
||||
@@ -240,6 +247,10 @@ impl PaneState {
|
||||
pub const fn synthetic_line_sequence(&self) -> u64 {
|
||||
self.synthetic_line_sequence
|
||||
}
|
||||
#[must_use]
|
||||
pub const fn output_source(&self) -> OutputSource {
|
||||
self.output_source
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
||||
@@ -276,6 +287,7 @@ pub enum ShellAction {
|
||||
CloseCommandPalette,
|
||||
SetCommandPaletteQuery(String),
|
||||
SyntheticStreamTick,
|
||||
AppendExternalOutput { pane: PaneId, lines: Vec<String> },
|
||||
}
|
||||
|
||||
/// Counters prove both candidates replayed the same workload. Framework
|
||||
@@ -294,6 +306,8 @@ pub struct MeasurementCounters {
|
||||
/// Total surfaces invalidated by the deterministic six-surface workload.
|
||||
pub surface_updates: u64,
|
||||
pub terminal_lines_appended: u64,
|
||||
pub external_output_batches: u64,
|
||||
pub external_lines_appended: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
@@ -315,6 +329,16 @@ impl fmt::Display for InvalidTerminalLineLimit {
|
||||
}
|
||||
impl std::error::Error for InvalidTerminalLineLimit {}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct InvalidExternalOutputPane;
|
||||
|
||||
impl fmt::Display for InvalidExternalOutputPane {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter.write_str("external output requires a terminal pane")
|
||||
}
|
||||
}
|
||||
impl std::error::Error for InvalidExternalOutputPane {}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct ShellModel {
|
||||
panes: [PaneState; GRID_ROWS * GRID_COLUMNS],
|
||||
@@ -350,6 +374,7 @@ impl ShellModel {
|
||||
.then_some(PaneStatus::Working),
|
||||
lines,
|
||||
synthetic_line_sequence: 0,
|
||||
output_source: OutputSource::Deterministic,
|
||||
}
|
||||
});
|
||||
Ok(Self {
|
||||
@@ -363,6 +388,22 @@ impl ShellModel {
|
||||
})
|
||||
}
|
||||
|
||||
/// Creates the comparison model with exactly one actor-driven terminal.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`InvalidExternalOutputPane`] when `pane` is not a terminal.
|
||||
pub fn with_external_output(pane: PaneId) -> Result<Self, InvalidExternalOutputPane> {
|
||||
let mut model = Self::default();
|
||||
let pane_state = &mut model.panes[pane.index()];
|
||||
if pane_state.kind() != SurfaceKind::Terminal {
|
||||
return Err(InvalidExternalOutputPane);
|
||||
}
|
||||
pane_state.output_source = OutputSource::External;
|
||||
pane_state.lines.clear();
|
||||
Ok(model)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn panes(&self) -> &[PaneState; GRID_ROWS * GRID_COLUMNS] {
|
||||
&self.panes
|
||||
@@ -457,7 +498,11 @@ impl ShellModel {
|
||||
self.synthetic_tick += 1;
|
||||
self.counters.synthetic_ticks += 1;
|
||||
for pane in &mut self.panes {
|
||||
if pane.output_source == OutputSource::External {
|
||||
continue;
|
||||
}
|
||||
pane.synthetic_line_sequence += 1;
|
||||
self.counters.surface_updates += 1;
|
||||
if pane.kind() != SurfaceKind::Terminal {
|
||||
continue;
|
||||
}
|
||||
@@ -472,10 +517,25 @@ impl ShellModel {
|
||||
}
|
||||
appended += 1;
|
||||
}
|
||||
self.counters.surface_updates += self.panes.len() as u64;
|
||||
self.counters.terminal_lines_appended += appended as u64;
|
||||
changed = true;
|
||||
}
|
||||
ShellAction::AppendExternalOutput { pane, lines } => {
|
||||
let pane = &mut self.panes[pane.index()];
|
||||
if pane.output_source == OutputSource::External && !lines.is_empty() {
|
||||
appended = lines.len();
|
||||
pane.lines.extend(lines);
|
||||
if pane.lines.len() > self.terminal_line_limit {
|
||||
pane.lines
|
||||
.drain(..pane.lines.len() - self.terminal_line_limit);
|
||||
}
|
||||
self.counters.surface_updates += 1;
|
||||
self.counters.terminal_lines_appended += appended as u64;
|
||||
self.counters.external_output_batches += 1;
|
||||
self.counters.external_lines_appended += appended as u64;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if changed {
|
||||
self.revision += 1;
|
||||
@@ -511,8 +571,8 @@ pub const fn focus_neighbor(pane: PaneId, direction: FocusDirection) -> Option<P
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
FocusDirection, GRID_COLUMNS, GRID_ROWS, PANES, PaneId, PaneStatus, ShellAction,
|
||||
ShellModel, SurfaceKind, focus_neighbor,
|
||||
FocusDirection, GRID_COLUMNS, GRID_ROWS, OutputSource, PANES, PaneId, PaneStatus,
|
||||
ShellAction, ShellModel, SurfaceKind, focus_neighbor,
|
||||
};
|
||||
|
||||
#[test]
|
||||
@@ -699,6 +759,47 @@ mod tests {
|
||||
assert!(ShellModel::with_terminal_line_limit(0).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_external_terminal_leaves_five_deterministic_surfaces() {
|
||||
let mut model = ShellModel::with_external_output(PaneId::CodexRuntime).unwrap();
|
||||
assert_eq!(
|
||||
model.pane(PaneId::CodexRuntime).output_source(),
|
||||
OutputSource::External
|
||||
);
|
||||
assert!(model.pane(PaneId::CodexRuntime).lines().is_empty());
|
||||
|
||||
let tick = model.dispatch(ShellAction::SyntheticStreamTick);
|
||||
assert_eq!(tick.terminal_lines_appended, 2);
|
||||
assert!(model.pane(PaneId::CodexRuntime).lines().is_empty());
|
||||
assert_eq!(model.counters().surface_updates, 5);
|
||||
|
||||
let output = model.dispatch(ShellAction::AppendExternalOutput {
|
||||
pane: PaneId::CodexRuntime,
|
||||
lines: vec!["runtime ready".into(), "pty line".into()],
|
||||
});
|
||||
assert!(output.changed);
|
||||
assert_eq!(output.terminal_lines_appended, 2);
|
||||
assert_eq!(
|
||||
model.pane(PaneId::CodexRuntime).lines(),
|
||||
["runtime ready", "pty line"]
|
||||
);
|
||||
assert_eq!(model.counters().surface_updates, 6);
|
||||
assert_eq!(model.counters().external_output_batches, 1);
|
||||
assert_eq!(model.counters().external_lines_appended, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn external_output_rejects_nonterminals_and_deterministic_panes() {
|
||||
assert!(ShellModel::with_external_output(PaneId::Architecture).is_err());
|
||||
let mut model = ShellModel::default();
|
||||
let outcome = model.dispatch(ShellAction::AppendExternalOutput {
|
||||
pane: PaneId::CodexRuntime,
|
||||
lines: vec!["must not replace fixture output".into()],
|
||||
});
|
||||
assert!(!outcome.changed);
|
||||
assert_eq!(model.counters().external_output_batches, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn identical_action_replays_produce_identical_models_and_counters() {
|
||||
let actions = [
|
||||
|
||||
Reference in New Issue
Block a user