chore: merge develop into hare/develop
This commit is contained in:
@@ -2,7 +2,7 @@ use std::io;
|
||||
use std::time::Duration;
|
||||
|
||||
use client::{BackendWorkspaceProductClient, ObjectiveSummary};
|
||||
use crossterm::event::{self, Event, KeyCode, KeyEventKind};
|
||||
use crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers};
|
||||
use ratatui::{
|
||||
Frame, Terminal,
|
||||
backend::CrosstermBackend,
|
||||
@@ -79,8 +79,26 @@ async fn run_loop(
|
||||
app: &mut BackendDashboard,
|
||||
client: BackendWorkspaceProductClient,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut first_frame = true;
|
||||
let mut initial_snapshot_pending = false;
|
||||
loop {
|
||||
terminal.draw(|frame| draw(frame, app))?;
|
||||
if first_frame {
|
||||
first_frame = false;
|
||||
#[cfg(feature = "e2e-test")]
|
||||
{
|
||||
crate::e2e_observer::emit(
|
||||
"backend_dashboard",
|
||||
"panel_ready",
|
||||
serde_json::json!({ "workspace_id": app.workspace_id }),
|
||||
);
|
||||
initial_snapshot_pending = true;
|
||||
}
|
||||
} else if initial_snapshot_pending {
|
||||
#[cfg(feature = "e2e-test")]
|
||||
emit_e2e_snapshot(app, "backend_dashboard_content_ready");
|
||||
initial_snapshot_pending = false;
|
||||
}
|
||||
if !event::poll(Duration::from_millis(100))? {
|
||||
continue;
|
||||
}
|
||||
@@ -91,21 +109,50 @@ async fn run_loop(
|
||||
continue;
|
||||
}
|
||||
match key.code {
|
||||
KeyCode::Char('q') | KeyCode::Esc => return Ok(()),
|
||||
KeyCode::Char('q') | KeyCode::Esc => {
|
||||
#[cfg(feature = "e2e-test")]
|
||||
crate::e2e_observer::emit(
|
||||
"backend_dashboard",
|
||||
"quit_requested",
|
||||
serde_json::json!({}),
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => {
|
||||
#[cfg(feature = "e2e-test")]
|
||||
crate::e2e_observer::emit(
|
||||
"backend_dashboard",
|
||||
"quit_requested",
|
||||
serde_json::json!({}),
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
KeyCode::Tab | KeyCode::BackTab => {
|
||||
app.focus = match app.focus {
|
||||
Focus::Tickets => Focus::Objectives,
|
||||
Focus::Objectives => Focus::Tickets,
|
||||
};
|
||||
#[cfg(feature = "e2e-test")]
|
||||
emit_e2e_snapshot(app, "backend_dashboard_selection_changed");
|
||||
}
|
||||
KeyCode::Down | KeyCode::Char('j') => {
|
||||
app.select_next();
|
||||
#[cfg(feature = "e2e-test")]
|
||||
emit_e2e_snapshot(app, "backend_dashboard_selection_changed");
|
||||
}
|
||||
KeyCode::Up | KeyCode::Char('k') => {
|
||||
app.select_previous();
|
||||
#[cfg(feature = "e2e-test")]
|
||||
emit_e2e_snapshot(app, "backend_dashboard_selection_changed");
|
||||
}
|
||||
KeyCode::Down | KeyCode::Char('j') => app.select_next(),
|
||||
KeyCode::Up | KeyCode::Char('k') => app.select_previous(),
|
||||
KeyCode::Char('r') => match load(&client).await {
|
||||
Ok((tickets, objectives)) => {
|
||||
app.tickets = tickets;
|
||||
app.objectives = objectives;
|
||||
app.clamp_selection();
|
||||
app.status = "Reloaded from Backend authority".to_string();
|
||||
#[cfg(feature = "e2e-test")]
|
||||
emit_e2e_snapshot(app, "backend_dashboard_reloaded");
|
||||
}
|
||||
Err(error) => app.status = format!("Backend reload failed: {error}"),
|
||||
},
|
||||
@@ -144,6 +191,51 @@ async fn run_loop(
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-test")]
|
||||
fn emit_e2e_snapshot(app: &BackendDashboard, event: &'static str) {
|
||||
let focus = match app.focus {
|
||||
Focus::Tickets => "tickets",
|
||||
Focus::Objectives => "objectives",
|
||||
};
|
||||
let tickets = app
|
||||
.tickets
|
||||
.iter()
|
||||
.map(|ticket| {
|
||||
serde_json::json!({
|
||||
"id": ticket.id,
|
||||
"resource_key": ticket.resource_key,
|
||||
"title": ticket.title,
|
||||
"workflow_state": ticket.workflow_state.as_str(),
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let objectives = app
|
||||
.objectives
|
||||
.iter()
|
||||
.map(|objective| {
|
||||
serde_json::json!({
|
||||
"id": objective.id,
|
||||
"resource_key": objective.resource_key,
|
||||
"title": objective.title,
|
||||
"state": objective.state,
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
crate::e2e_observer::emit(
|
||||
"backend_dashboard",
|
||||
event,
|
||||
serde_json::json!({
|
||||
"workspace_id": app.workspace_id,
|
||||
"focus": focus,
|
||||
"selected_ticket_id": app.selected_ticket_id(),
|
||||
"selected_objective_id": app.objectives.get(app.selected_objective).map(|objective| &objective.id),
|
||||
"tickets": tickets,
|
||||
"objectives": objectives,
|
||||
"status": app.status,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
impl BackendDashboard {
|
||||
fn select_next(&mut self) {
|
||||
match self.focus {
|
||||
|
||||
@@ -623,6 +623,16 @@ fn read_terminal_events(stop: Arc<AtomicBool>, tx: mpsc::UnboundedSender<Termina
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-test")]
|
||||
pub(crate) async fn run_e2e_rewind(worker_name: String) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut terminal = enter_fullscreen()?;
|
||||
let result = run_e2e_rewind_fixture(&mut terminal, worker_name).await;
|
||||
let leave_result = leave_fullscreen(&mut terminal);
|
||||
result?;
|
||||
leave_result?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-test")]
|
||||
async fn run_e2e_rewind_fixture(
|
||||
terminal: &mut ConsoleTerminal,
|
||||
@@ -637,6 +647,8 @@ async fn run_e2e_rewind_fixture(
|
||||
entries: Vec::new(),
|
||||
},
|
||||
state: WorkerStatus::Idle.into(),
|
||||
in_flight: protocol::InFlightSnapshot::default(),
|
||||
internal_workers: Vec::new(),
|
||||
greeting: Greeting {
|
||||
worker_name: worker_name.clone(),
|
||||
cwd: workspace_root.display().to_string(),
|
||||
|
||||
@@ -2,12 +2,11 @@ use std::fs::{File, OpenOptions};
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
const EVENT_PATH_ENV: &str = "YOI_TUI_TEST_EVENTS";
|
||||
const HOLD_BACKGROUND_TASK_ENV: &str = "YOI_TUI_TEST_HOLD_BACKGROUND_TASK";
|
||||
|
||||
static EVENT_WRITER: OnceLock<Option<Mutex<File>>> = OnceLock::new();
|
||||
|
||||
@@ -44,25 +43,6 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn hold_background_task_if_requested(task: &'static str) {
|
||||
let requested = std::env::var(HOLD_BACKGROUND_TASK_ENV).unwrap_or_default();
|
||||
if !requested
|
||||
.split(',')
|
||||
.map(str::trim)
|
||||
.any(|requested| requested == task)
|
||||
{
|
||||
return;
|
||||
}
|
||||
emit(
|
||||
"panel",
|
||||
"background_task_hold_started",
|
||||
serde_json::json!({ "task": task }),
|
||||
);
|
||||
loop {
|
||||
tokio::time::sleep(Duration::from_millis(25)).await;
|
||||
}
|
||||
}
|
||||
|
||||
fn open_event_writer() -> Option<Mutex<File>> {
|
||||
let path = std::env::var_os(EVENT_PATH_ENV).map(PathBuf::from)?;
|
||||
if let Some(parent) = path.parent() {
|
||||
|
||||
+47
-26
@@ -137,7 +137,51 @@ pub async fn launch(options: LaunchOptions) -> ExitCode {
|
||||
}
|
||||
let mut terminal_mode = TerminalModeGuard::new();
|
||||
|
||||
let result = match mode {
|
||||
#[cfg(feature = "e2e-test")]
|
||||
let result = if std::env::var_os("YOI_TUI_TEST_REWIND_FIXTURE").is_some() {
|
||||
let worker_name = match &mode {
|
||||
LaunchMode::Spawn { worker_name, .. } => worker_name
|
||||
.clone()
|
||||
.unwrap_or_else(|| "e2e-rewind".to_string()),
|
||||
_ => "e2e-rewind".to_string(),
|
||||
};
|
||||
console::run_e2e_rewind(worker_name).await
|
||||
} else {
|
||||
launch_mode(target.as_ref(), mode, &workspace_root).await
|
||||
};
|
||||
#[cfg(not(feature = "e2e-test"))]
|
||||
let result = launch_mode(target.as_ref(), mode, &workspace_root).await;
|
||||
|
||||
// Always restore the terminal first so any pending eprintln below
|
||||
// shows up cleanly in scrollback rather than inside an active
|
||||
// alternate-screen buffer.
|
||||
#[cfg(feature = "e2e-test")]
|
||||
e2e_observer::emit("tui", "terminal_cleanup_started", serde_json::json!({}));
|
||||
let _ = terminal_mode.restore();
|
||||
#[cfg(feature = "e2e-test")]
|
||||
e2e_observer::emit("tui", "terminal_cleanup_finished", serde_json::json!({}));
|
||||
|
||||
match result {
|
||||
Ok(()) => {
|
||||
#[cfg(feature = "e2e-test")]
|
||||
e2e_observer::emit("tui", "exit", serde_json::json!({ "status": "success" }));
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("yoi: {e}");
|
||||
#[cfg(feature = "e2e-test")]
|
||||
e2e_observer::emit("tui", "exit", serde_json::json!({ "status": "failure" }));
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn launch_mode(
|
||||
target: &dyn Target,
|
||||
mode: LaunchMode,
|
||||
workspace_root: &std::path::Path,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
match mode {
|
||||
LaunchMode::Spawn {
|
||||
worker_name,
|
||||
profile,
|
||||
@@ -145,7 +189,7 @@ pub async fn launch(options: LaunchOptions) -> ExitCode {
|
||||
Ok(Some(selection)) => match target.spawn_worker() {
|
||||
Ok(spawn) => {
|
||||
console::run_standalone(
|
||||
workspace_root.clone(),
|
||||
workspace_root.to_path_buf(),
|
||||
spawn.state_dir,
|
||||
Some(selection.worker_name),
|
||||
Some(selection.profile),
|
||||
@@ -158,7 +202,7 @@ pub async fn launch(options: LaunchOptions) -> ExitCode {
|
||||
Err(error) => Err(Box::new(error) as Box<dyn std::error::Error>),
|
||||
},
|
||||
LaunchMode::StandaloneResume { include_all } => {
|
||||
match standalone_picker::pick(target.as_ref(), include_all) {
|
||||
match standalone_picker::pick(target, include_all) {
|
||||
Ok(Some(intent)) => console::run_standalone_restore(intent).await,
|
||||
Ok(None) => Ok(()),
|
||||
Err(error) => Err(Box::new(error) as Box<dyn std::error::Error>),
|
||||
@@ -195,28 +239,5 @@ pub async fn launch(options: LaunchOptions) -> ExitCode {
|
||||
}
|
||||
Err(e) => Err(Box::new(e) as Box<dyn std::error::Error>),
|
||||
},
|
||||
};
|
||||
|
||||
// Always restore the terminal first so any pending eprintln below
|
||||
// shows up cleanly in scrollback rather than inside an active
|
||||
// alternate-screen buffer.
|
||||
#[cfg(feature = "e2e-test")]
|
||||
e2e_observer::emit("tui", "terminal_cleanup_started", serde_json::json!({}));
|
||||
let _ = terminal_mode.restore();
|
||||
#[cfg(feature = "e2e-test")]
|
||||
e2e_observer::emit("tui", "terminal_cleanup_finished", serde_json::json!({}));
|
||||
|
||||
match result {
|
||||
Ok(()) => {
|
||||
#[cfg(feature = "e2e-test")]
|
||||
e2e_observer::emit("tui", "exit", serde_json::json!({ "status": "success" }));
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("yoi: {e}");
|
||||
#[cfg(feature = "e2e-test")]
|
||||
e2e_observer::emit("tui", "exit", serde_json::json!({ "status": "failure" }));
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user