merge: panel dashboard readiness metric
This commit is contained in:
commit
2d4d11e476
|
|
@ -44,15 +44,19 @@ use crate::pod_list::{
|
||||||
use crate::role_session_registry::{
|
use crate::role_session_registry::{
|
||||||
PanelRegistryStore, RelatedTicketRef, RoleSessionOrigin, TicketClaimResult,
|
PanelRegistryStore, RelatedTicketRef, RoleSessionOrigin, TicketClaimResult,
|
||||||
};
|
};
|
||||||
|
#[cfg(not(feature = "e2e-test"))]
|
||||||
|
use crate::workspace_panel::build_workspace_panel;
|
||||||
|
#[cfg(feature = "e2e-test")]
|
||||||
|
use crate::workspace_panel::build_workspace_panel_with_e2e_timings;
|
||||||
use crate::workspace_panel::{
|
use crate::workspace_panel::{
|
||||||
ActionPriority, CompanionLifecyclePlan, CompanionPanelState, CompanionPanelStatus,
|
ActionPriority, CompanionLifecyclePlan, CompanionPanelState, CompanionPanelStatus,
|
||||||
CompanionPodPresence, ComposerTarget, NextUserAction, OrchestratorLifecyclePlan,
|
CompanionPodPresence, ComposerTarget, NextUserAction, OrchestratorLifecyclePlan,
|
||||||
OrchestratorPanelState, OrchestratorPanelStatus, OrchestratorPodPresence, PanelRow,
|
OrchestratorPanelState, OrchestratorPanelStatus, OrchestratorPodPresence, PanelRow,
|
||||||
PanelRowKey, PanelRowKind, TicketConfigAvailability, TicketLocalClaimStatus,
|
PanelRowKey, PanelRowKind, TicketConfigAvailability, TicketLocalClaimStatus,
|
||||||
WorkspacePanelViewModel, bounded_panel_diagnostic, build_current_ticket_row,
|
WorkspacePanelViewModel, bounded_panel_diagnostic, build_current_ticket_row,
|
||||||
build_workspace_panel, companion_pod_presence, decide_companion_lifecycle,
|
companion_pod_presence, decide_companion_lifecycle, decide_orchestrator_lifecycle,
|
||||||
decide_orchestrator_lifecycle, local_claim_status_for_pod, orchestrator_pod_presence,
|
local_claim_status_for_pod, orchestrator_pod_presence, ticket_config_availability,
|
||||||
ticket_config_availability, workspace_companion_pod_name, workspace_orchestrator_pod_name,
|
workspace_companion_pod_name, workspace_orchestrator_pod_name,
|
||||||
};
|
};
|
||||||
|
|
||||||
const MAX_ENTRIES: usize = 50;
|
const MAX_ENTRIES: usize = 50;
|
||||||
|
|
@ -925,14 +929,14 @@ impl PanelRowHitBox {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "e2e-test")]
|
#[cfg(feature = "e2e-test")]
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
struct PanelE2eRowKey {
|
struct PanelE2eRowKey {
|
||||||
kind: &'static str,
|
kind: &'static str,
|
||||||
id: String,
|
id: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "e2e-test")]
|
#[cfg(feature = "e2e-test")]
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
struct PanelE2eRect {
|
struct PanelE2eRect {
|
||||||
x: u16,
|
x: u16,
|
||||||
y: u16,
|
y: u16,
|
||||||
|
|
@ -941,22 +945,92 @@ struct PanelE2eRect {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "e2e-test")]
|
#[cfg(feature = "e2e-test")]
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
struct PanelE2eRenderedRow {
|
struct PanelE2eRenderedRow {
|
||||||
key: PanelE2eRowKey,
|
key: PanelE2eRowKey,
|
||||||
title: String,
|
title: String,
|
||||||
status: Option<String>,
|
status: Option<String>,
|
||||||
action: Option<&'static str>,
|
action: Option<&'static str>,
|
||||||
|
disabled_reason: Option<String>,
|
||||||
|
local_state: Option<String>,
|
||||||
|
overlay_state: Option<String>,
|
||||||
|
overlay_detail: Option<String>,
|
||||||
rect: PanelE2eRect,
|
rect: PanelE2eRect,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "e2e-test")]
|
#[cfg(feature = "e2e-test")]
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
struct PanelE2eRowsRendered {
|
struct PanelE2eRowsRendered {
|
||||||
selected: Option<PanelE2eRowKey>,
|
selected: Option<PanelE2eRowKey>,
|
||||||
|
header: PanelE2eDashboardHeader,
|
||||||
rows: Vec<PanelE2eRenderedRow>,
|
rows: Vec<PanelE2eRenderedRow>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "e2e-test")]
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
struct PanelE2eDashboardHeader {
|
||||||
|
ticket_configured: bool,
|
||||||
|
companion: Option<PanelE2eCompanionState>,
|
||||||
|
orchestrator: Option<PanelE2eOrchestratorState>,
|
||||||
|
diagnostics: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "e2e-test")]
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
struct PanelE2eCompanionState {
|
||||||
|
pod_name: String,
|
||||||
|
status: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "e2e-test")]
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
struct PanelE2eOrchestratorState {
|
||||||
|
pod_name: String,
|
||||||
|
status: &'static str,
|
||||||
|
detail: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "e2e-test")]
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct PanelE2eDashboardContentReady {
|
||||||
|
snapshot: PanelE2eDashboardSnapshot,
|
||||||
|
categories: PanelE2eDashboardCategories,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "e2e-test")]
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
struct PanelE2eDashboardSnapshot {
|
||||||
|
header: PanelE2eDashboardHeader,
|
||||||
|
rows: Vec<PanelE2eRenderedRow>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "e2e-test")]
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct PanelE2eDashboardCategories {
|
||||||
|
ticket_rows: usize,
|
||||||
|
ready_ticket_rows: usize,
|
||||||
|
planning_ticket_rows: usize,
|
||||||
|
pod_rows: usize,
|
||||||
|
actionable_rows: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "e2e-test")]
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct PanelE2eSourceTiming {
|
||||||
|
source: &'static str,
|
||||||
|
elapsed_ms: u128,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "e2e-test")]
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct PanelE2eDashboardSourceBreakdown {
|
||||||
|
total_elapsed_ms: u128,
|
||||||
|
sources: Vec<PanelE2eSourceTiming>,
|
||||||
|
ticket_rows: usize,
|
||||||
|
pod_rows: usize,
|
||||||
|
diagnostics: usize,
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "e2e-test")]
|
#[cfg(feature = "e2e-test")]
|
||||||
fn panel_e2e_row_key(key: &PanelRowKey) -> PanelE2eRowKey {
|
fn panel_e2e_row_key(key: &PanelRowKey) -> PanelE2eRowKey {
|
||||||
match key {
|
match key {
|
||||||
|
|
@ -992,6 +1066,76 @@ fn panel_e2e_rect(rect: Rect) -> PanelE2eRect {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "e2e-test")]
|
||||||
|
fn panel_e2e_dashboard_categories(rows: &[PanelE2eRenderedRow]) -> PanelE2eDashboardCategories {
|
||||||
|
PanelE2eDashboardCategories {
|
||||||
|
ticket_rows: rows.iter().filter(|row| row.key.kind == "ticket").count(),
|
||||||
|
ready_ticket_rows: rows
|
||||||
|
.iter()
|
||||||
|
.filter(|row| row.key.kind == "ticket" && row.local_state.as_deref() == Some("ready"))
|
||||||
|
.count(),
|
||||||
|
planning_ticket_rows: rows
|
||||||
|
.iter()
|
||||||
|
.filter(|row| {
|
||||||
|
row.key.kind == "ticket" && row.local_state.as_deref() == Some("planning")
|
||||||
|
})
|
||||||
|
.count(),
|
||||||
|
pod_rows: rows.iter().filter(|row| row.key.kind == "pod").count(),
|
||||||
|
actionable_rows: rows.iter().filter(|row| row.action.is_some()).count(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "e2e-test")]
|
||||||
|
fn panel_e2e_dashboard_header(panel: &WorkspacePanelViewModel) -> PanelE2eDashboardHeader {
|
||||||
|
PanelE2eDashboardHeader {
|
||||||
|
ticket_configured: panel.header.ticket_configured,
|
||||||
|
companion: panel
|
||||||
|
.header
|
||||||
|
.companion
|
||||||
|
.as_ref()
|
||||||
|
.map(|state| PanelE2eCompanionState {
|
||||||
|
pod_name: state.pod_name.clone(),
|
||||||
|
status: state.status.label(),
|
||||||
|
}),
|
||||||
|
orchestrator: panel
|
||||||
|
.header
|
||||||
|
.orchestrator
|
||||||
|
.as_ref()
|
||||||
|
.map(|state| PanelE2eOrchestratorState {
|
||||||
|
pod_name: state.pod_name.clone(),
|
||||||
|
status: state.status.label(),
|
||||||
|
detail: state.detail.clone(),
|
||||||
|
}),
|
||||||
|
diagnostics: panel.header.diagnostics.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "e2e-test")]
|
||||||
|
fn panel_e2e_dashboard_content_is_ready(
|
||||||
|
snapshot: &PanelE2eDashboardSnapshot,
|
||||||
|
categories: &PanelE2eDashboardCategories,
|
||||||
|
) -> bool {
|
||||||
|
snapshot.header.ticket_configured
|
||||||
|
&& snapshot.header.companion.is_some()
|
||||||
|
&& snapshot.header.orchestrator.is_some()
|
||||||
|
&& categories.ready_ticket_rows > 0
|
||||||
|
&& categories.planning_ticket_rows > 0
|
||||||
|
&& categories.pod_rows > 0
|
||||||
|
&& snapshot.rows.iter().any(|row| {
|
||||||
|
row.key.kind == "ticket"
|
||||||
|
&& row.local_state.as_deref() == Some("ready")
|
||||||
|
&& row.overlay_state.is_some()
|
||||||
|
&& row.action.is_some()
|
||||||
|
&& row.disabled_reason.is_some()
|
||||||
|
})
|
||||||
|
&& snapshot.rows.iter().any(|row| {
|
||||||
|
row.key.kind == "ticket"
|
||||||
|
&& row.local_state.as_deref() == Some("planning")
|
||||||
|
&& row.action.is_some()
|
||||||
|
&& row.disabled_reason.is_some()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) struct MultiPodApp {
|
pub(crate) struct MultiPodApp {
|
||||||
pub(crate) list: PodList,
|
pub(crate) list: PodList,
|
||||||
pub(crate) panel: WorkspacePanelViewModel,
|
pub(crate) panel: WorkspacePanelViewModel,
|
||||||
|
|
@ -1010,6 +1154,8 @@ pub(crate) struct MultiPodApp {
|
||||||
last_orchestrator_lifecycle_failure: Option<OrchestratorPanelState>,
|
last_orchestrator_lifecycle_failure: Option<OrchestratorPanelState>,
|
||||||
orchestrator_work_set: OrchestratorWorkSet,
|
orchestrator_work_set: OrchestratorWorkSet,
|
||||||
orchestrator_queue_attention: Option<OrchestratorQueueAttentionFreshness>,
|
orchestrator_queue_attention: Option<OrchestratorQueueAttentionFreshness>,
|
||||||
|
#[cfg(feature = "e2e-test")]
|
||||||
|
emitted_dashboard_content_ready: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MultiPodApp {
|
impl MultiPodApp {
|
||||||
|
|
@ -1046,6 +1192,8 @@ impl MultiPodApp {
|
||||||
last_orchestrator_lifecycle_failure: None,
|
last_orchestrator_lifecycle_failure: None,
|
||||||
orchestrator_work_set: OrchestratorWorkSet::default(),
|
orchestrator_work_set: OrchestratorWorkSet::default(),
|
||||||
orchestrator_queue_attention: None,
|
orchestrator_queue_attention: None,
|
||||||
|
#[cfg(feature = "e2e-test")]
|
||||||
|
emitted_dashboard_content_ready: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1355,25 +1503,52 @@ impl MultiPodApp {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "e2e-test")]
|
#[cfg(feature = "e2e-test")]
|
||||||
fn emit_rows_rendered(&self) {
|
fn emit_rows_rendered(&mut self) {
|
||||||
let rows = self
|
let rows: Vec<_> = self
|
||||||
.row_hit_boxes
|
.row_hit_boxes
|
||||||
.iter()
|
.iter()
|
||||||
.map(|hit| {
|
.map(|hit| {
|
||||||
let panel_row = self.panel.row(&hit.key);
|
let panel_row = self.panel.row(&hit.key);
|
||||||
let (title, status, action) = match panel_row {
|
let (
|
||||||
Some(row) => (
|
title,
|
||||||
|
status,
|
||||||
|
action,
|
||||||
|
disabled_reason,
|
||||||
|
local_state,
|
||||||
|
overlay_state,
|
||||||
|
overlay_detail,
|
||||||
|
) = match panel_row {
|
||||||
|
Some(row) => {
|
||||||
|
let ticket = row.ticket.as_ref();
|
||||||
|
(
|
||||||
row.title.clone(),
|
row.title.clone(),
|
||||||
Some(row.status.clone()),
|
Some(row.status.clone()),
|
||||||
row.next_action.map(NextUserAction::label),
|
row.next_action.map(NextUserAction::label),
|
||||||
),
|
row.disabled_reason.clone(),
|
||||||
|
ticket.map(|ticket| ticket.workflow_state.as_str().to_string()),
|
||||||
|
ticket
|
||||||
|
.and_then(|ticket| ticket.orchestration_overlay.as_ref())
|
||||||
|
.map(|overlay| overlay.workflow_state.as_str().to_string()),
|
||||||
|
ticket
|
||||||
|
.and_then(|ticket| ticket.orchestration_overlay.as_ref())
|
||||||
|
.map(|overlay| {
|
||||||
|
format!(
|
||||||
|
"{}:{}",
|
||||||
|
overlay.source,
|
||||||
|
overlay.workflow_state.as_str()
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
None => match &hit.key {
|
None => match &hit.key {
|
||||||
PanelRowKey::Pod(name) => (name.clone(), None, None),
|
PanelRowKey::Pod(name) => {
|
||||||
|
(name.clone(), None, None, None, None, None, None)
|
||||||
|
}
|
||||||
PanelRowKey::Ticket(id) | PanelRowKey::InvalidTicket(id) => {
|
PanelRowKey::Ticket(id) | PanelRowKey::InvalidTicket(id) => {
|
||||||
(id.clone(), None, None)
|
(id.clone(), None, None, None, None, None, None)
|
||||||
}
|
}
|
||||||
PanelRowKey::TicketIntakePod { pod_name, .. } => {
|
PanelRowKey::TicketIntakePod { pod_name, .. } => {
|
||||||
(pod_name.clone(), None, None)
|
(pod_name.clone(), None, None, None, None, None, None)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -1382,18 +1557,40 @@ impl MultiPodApp {
|
||||||
title,
|
title,
|
||||||
status,
|
status,
|
||||||
action,
|
action,
|
||||||
|
disabled_reason,
|
||||||
|
local_state,
|
||||||
|
overlay_state,
|
||||||
|
overlay_detail,
|
||||||
rect: panel_e2e_rect(hit.rect),
|
rect: panel_e2e_rect(hit.rect),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
let selected = self.selected_row.as_ref().map(panel_e2e_row_key);
|
||||||
|
let header = panel_e2e_dashboard_header(&self.panel);
|
||||||
crate::e2e_observer::emit(
|
crate::e2e_observer::emit(
|
||||||
"panel",
|
"panel",
|
||||||
"rows_rendered",
|
"rows_rendered",
|
||||||
PanelE2eRowsRendered {
|
PanelE2eRowsRendered {
|
||||||
selected: self.selected_row.as_ref().map(panel_e2e_row_key),
|
selected: selected.clone(),
|
||||||
rows,
|
header: header.clone(),
|
||||||
|
rows: rows.clone(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
if !self.emitted_dashboard_content_ready {
|
||||||
|
let categories = panel_e2e_dashboard_categories(&rows);
|
||||||
|
let snapshot = PanelE2eDashboardSnapshot { header, rows };
|
||||||
|
if panel_e2e_dashboard_content_is_ready(&snapshot, &categories) {
|
||||||
|
crate::e2e_observer::emit(
|
||||||
|
"panel",
|
||||||
|
"dashboard_content_ready",
|
||||||
|
PanelE2eDashboardContentReady {
|
||||||
|
snapshot,
|
||||||
|
categories,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
self.emitted_dashboard_content_ready = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ensure_selection_visible(&mut self) {
|
fn ensure_selection_visible(&mut self) {
|
||||||
|
|
@ -2286,12 +2483,35 @@ async fn load_multi_pod_snapshot(
|
||||||
lifecycle_mode: OrchestratorLifecycleMode,
|
lifecycle_mode: OrchestratorLifecycleMode,
|
||||||
) -> Result<MultiPodSnapshot, MultiPodError> {
|
) -> Result<MultiPodSnapshot, MultiPodError> {
|
||||||
let workspace_root = current_workspace_root();
|
let workspace_root = current_workspace_root();
|
||||||
|
#[cfg(feature = "e2e-test")]
|
||||||
|
let load_started = Instant::now();
|
||||||
|
#[cfg(feature = "e2e-test")]
|
||||||
|
let mut source_timings = Vec::new();
|
||||||
let companion_pod_name = workspace_companion_pod_name(&workspace_root);
|
let companion_pod_name = workspace_companion_pod_name(&workspace_root);
|
||||||
let list_selected_name = selected_name
|
let list_selected_name = selected_name
|
||||||
.clone()
|
.clone()
|
||||||
.or_else(|| Some(companion_pod_name.clone()));
|
.or_else(|| Some(companion_pod_name.clone()));
|
||||||
|
|
||||||
|
#[cfg(feature = "e2e-test")]
|
||||||
|
let source_started = Instant::now();
|
||||||
let mut list = load_pod_list(list_selected_name.clone(), MAX_ENTRIES).await?;
|
let mut list = load_pod_list(list_selected_name.clone(), MAX_ENTRIES).await?;
|
||||||
|
#[cfg(feature = "e2e-test")]
|
||||||
|
source_timings.push(PanelE2eSourceTiming {
|
||||||
|
source: "pod_metadata_status_probe.initial",
|
||||||
|
elapsed_ms: source_started.elapsed().as_millis(),
|
||||||
|
});
|
||||||
|
|
||||||
|
#[cfg(feature = "e2e-test")]
|
||||||
|
let source_started = Instant::now();
|
||||||
let companion_presence = load_exact_companion_pod_presence(&companion_pod_name).await?;
|
let companion_presence = load_exact_companion_pod_presence(&companion_pod_name).await?;
|
||||||
|
#[cfg(feature = "e2e-test")]
|
||||||
|
source_timings.push(PanelE2eSourceTiming {
|
||||||
|
source: "companion.presence",
|
||||||
|
elapsed_ms: source_started.elapsed().as_millis(),
|
||||||
|
});
|
||||||
|
|
||||||
|
#[cfg(feature = "e2e-test")]
|
||||||
|
let source_started = Instant::now();
|
||||||
let companion = match lifecycle_mode.clone() {
|
let companion = match lifecycle_mode.clone() {
|
||||||
OrchestratorLifecycleMode::Ensure { runtime_command } => {
|
OrchestratorLifecycleMode::Ensure { runtime_command } => {
|
||||||
ensure_workspace_companion(
|
ensure_workspace_companion(
|
||||||
|
|
@ -2306,17 +2526,48 @@ async fn load_multi_pod_snapshot(
|
||||||
observe_workspace_companion(companion_pod_name, companion_presence)
|
observe_workspace_companion(companion_pod_name, companion_presence)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
#[cfg(feature = "e2e-test")]
|
||||||
|
source_timings.push(PanelE2eSourceTiming {
|
||||||
|
source: "companion.lifecycle",
|
||||||
|
elapsed_ms: source_started.elapsed().as_millis(),
|
||||||
|
});
|
||||||
if companion.reload_pods {
|
if companion.reload_pods {
|
||||||
|
#[cfg(feature = "e2e-test")]
|
||||||
|
let source_started = Instant::now();
|
||||||
list = load_pod_list(list_selected_name.clone(), MAX_ENTRIES).await?;
|
list = load_pod_list(list_selected_name.clone(), MAX_ENTRIES).await?;
|
||||||
|
#[cfg(feature = "e2e-test")]
|
||||||
|
source_timings.push(PanelE2eSourceTiming {
|
||||||
|
source: "pod_metadata_status_probe.after_companion_reload",
|
||||||
|
elapsed_ms: source_started.elapsed().as_millis(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "e2e-test")]
|
||||||
|
let source_started = Instant::now();
|
||||||
let config = ticket_config_availability(&workspace_root);
|
let config = ticket_config_availability(&workspace_root);
|
||||||
|
#[cfg(feature = "e2e-test")]
|
||||||
|
source_timings.push(PanelE2eSourceTiming {
|
||||||
|
source: "ticket_config_probe",
|
||||||
|
elapsed_ms: source_started.elapsed().as_millis(),
|
||||||
|
});
|
||||||
|
|
||||||
let orchestrator_pod_name = workspace_orchestrator_pod_name(&workspace_root);
|
let orchestrator_pod_name = workspace_orchestrator_pod_name(&workspace_root);
|
||||||
|
#[cfg(feature = "e2e-test")]
|
||||||
|
let source_started = Instant::now();
|
||||||
let orchestrator_presence = match &config {
|
let orchestrator_presence = match &config {
|
||||||
TicketConfigAvailability::Absent | TicketConfigAvailability::Unusable(_) => None,
|
TicketConfigAvailability::Absent | TicketConfigAvailability::Unusable(_) => None,
|
||||||
TicketConfigAvailability::Usable => {
|
TicketConfigAvailability::Usable => {
|
||||||
Some(load_exact_pod_presence(&orchestrator_pod_name).await?)
|
Some(load_exact_pod_presence(&orchestrator_pod_name).await?)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
#[cfg(feature = "e2e-test")]
|
||||||
|
source_timings.push(PanelE2eSourceTiming {
|
||||||
|
source: "orchestrator.presence",
|
||||||
|
elapsed_ms: source_started.elapsed().as_millis(),
|
||||||
|
});
|
||||||
|
|
||||||
|
#[cfg(feature = "e2e-test")]
|
||||||
|
let source_started = Instant::now();
|
||||||
let orchestrator = match lifecycle_mode {
|
let orchestrator = match lifecycle_mode {
|
||||||
OrchestratorLifecycleMode::Ensure { runtime_command } => {
|
OrchestratorLifecycleMode::Ensure { runtime_command } => {
|
||||||
ensure_workspace_orchestrator(
|
ensure_workspace_orchestrator(
|
||||||
|
|
@ -2332,14 +2583,63 @@ async fn load_multi_pod_snapshot(
|
||||||
observe_workspace_orchestrator(config, orchestrator_pod_name, orchestrator_presence)
|
observe_workspace_orchestrator(config, orchestrator_pod_name, orchestrator_presence)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
#[cfg(feature = "e2e-test")]
|
||||||
|
source_timings.push(PanelE2eSourceTiming {
|
||||||
|
source: "orchestrator.lifecycle",
|
||||||
|
elapsed_ms: source_started.elapsed().as_millis(),
|
||||||
|
});
|
||||||
if orchestrator.reload_pods {
|
if orchestrator.reload_pods {
|
||||||
|
#[cfg(feature = "e2e-test")]
|
||||||
|
let source_started = Instant::now();
|
||||||
list = load_pod_list(list_selected_name, MAX_ENTRIES).await?;
|
list = load_pod_list(list_selected_name, MAX_ENTRIES).await?;
|
||||||
|
#[cfg(feature = "e2e-test")]
|
||||||
|
source_timings.push(PanelE2eSourceTiming {
|
||||||
|
source: "pod_metadata_status_probe.after_orchestrator_reload",
|
||||||
|
elapsed_ms: source_started.elapsed().as_millis(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "e2e-test")]
|
||||||
|
let source_started = Instant::now();
|
||||||
|
#[cfg(feature = "e2e-test")]
|
||||||
|
let (mut panel, panel_source_timings) =
|
||||||
|
build_workspace_panel_with_e2e_timings(&workspace_root, &list);
|
||||||
|
#[cfg(not(feature = "e2e-test"))]
|
||||||
let mut panel = build_workspace_panel(&workspace_root, &list);
|
let mut panel = build_workspace_panel(&workspace_root, &list);
|
||||||
panel.header.companion = companion.state;
|
panel.header.companion = companion.state;
|
||||||
panel.header.diagnostics.extend(companion.diagnostics);
|
panel.header.diagnostics.extend(companion.diagnostics);
|
||||||
panel.header.orchestrator = orchestrator.state;
|
panel.header.orchestrator = orchestrator.state;
|
||||||
panel.header.diagnostics.extend(orchestrator.diagnostics);
|
panel.header.diagnostics.extend(orchestrator.diagnostics);
|
||||||
|
#[cfg(feature = "e2e-test")]
|
||||||
|
{
|
||||||
|
source_timings.push(PanelE2eSourceTiming {
|
||||||
|
source: "workspace_panel.build.total",
|
||||||
|
elapsed_ms: source_started.elapsed().as_millis(),
|
||||||
|
});
|
||||||
|
source_timings.extend(panel_source_timings.into_iter().map(|timing| {
|
||||||
|
PanelE2eSourceTiming {
|
||||||
|
source: timing.source,
|
||||||
|
elapsed_ms: timing.elapsed_ms,
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "e2e-test")]
|
||||||
|
crate::e2e_observer::emit(
|
||||||
|
"panel",
|
||||||
|
"dashboard_source_breakdown",
|
||||||
|
PanelE2eDashboardSourceBreakdown {
|
||||||
|
total_elapsed_ms: load_started.elapsed().as_millis(),
|
||||||
|
sources: source_timings,
|
||||||
|
ticket_rows: panel
|
||||||
|
.rows
|
||||||
|
.iter()
|
||||||
|
.filter(|row| row.is_ticket_action())
|
||||||
|
.count(),
|
||||||
|
pod_rows: list.entries.len(),
|
||||||
|
diagnostics: panel.header.diagnostics.len(),
|
||||||
|
},
|
||||||
|
);
|
||||||
Ok(MultiPodSnapshot { list, panel })
|
Ok(MultiPodSnapshot { list, panel })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
#[cfg(feature = "e2e-test")]
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
use protocol::PodStatus;
|
use protocol::PodStatus;
|
||||||
use ticket::config::{
|
use ticket::config::{
|
||||||
|
|
@ -731,6 +733,7 @@ fn git_output(worktree_root: &Path, args: &[&str]) -> Result<String, String> {
|
||||||
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
|
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "e2e-test", allow(dead_code))]
|
||||||
pub(crate) fn build_workspace_panel(
|
pub(crate) fn build_workspace_panel(
|
||||||
workspace_root: &Path,
|
workspace_root: &Path,
|
||||||
pods: &PodList,
|
pods: &PodList,
|
||||||
|
|
@ -758,6 +761,148 @@ pub(crate) fn build_workspace_panel(
|
||||||
build_workspace_panel_with_registry(workspace_root, pods, ®istry)
|
build_workspace_panel_with_registry(workspace_root, pods, ®istry)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "e2e-test")]
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub(crate) struct WorkspacePanelE2eSourceTiming {
|
||||||
|
pub(crate) source: &'static str,
|
||||||
|
pub(crate) elapsed_ms: u128,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "e2e-test")]
|
||||||
|
pub(crate) fn build_workspace_panel_with_e2e_timings(
|
||||||
|
workspace_root: &Path,
|
||||||
|
pods: &PodList,
|
||||||
|
) -> (WorkspacePanelViewModel, Vec<WorkspacePanelE2eSourceTiming>) {
|
||||||
|
let mut timings = Vec::new();
|
||||||
|
let started = Instant::now();
|
||||||
|
let registry = match PanelRegistryStore::default_for_workspace(workspace_root)
|
||||||
|
.and_then(|store| store.snapshot())
|
||||||
|
{
|
||||||
|
Ok(snapshot) => snapshot,
|
||||||
|
Err(error) => {
|
||||||
|
timings.push(WorkspacePanelE2eSourceTiming {
|
||||||
|
source: "local_claim_scan",
|
||||||
|
elapsed_ms: started.elapsed().as_millis(),
|
||||||
|
});
|
||||||
|
let mut model = WorkspacePanelViewModel::empty(workspace_root);
|
||||||
|
model
|
||||||
|
.header
|
||||||
|
.diagnostics
|
||||||
|
.push(bounded_panel_diagnostic(format!(
|
||||||
|
"Panel local role registry unavailable: {error}"
|
||||||
|
)));
|
||||||
|
return (
|
||||||
|
build_workspace_panel_with_registry_model(
|
||||||
|
model,
|
||||||
|
workspace_root,
|
||||||
|
pods,
|
||||||
|
&PanelRegistrySnapshot::empty(),
|
||||||
|
),
|
||||||
|
timings,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
timings.push(WorkspacePanelE2eSourceTiming {
|
||||||
|
source: "local_claim_scan",
|
||||||
|
elapsed_ms: started.elapsed().as_millis(),
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut model = WorkspacePanelViewModel::empty(workspace_root);
|
||||||
|
let started = Instant::now();
|
||||||
|
let availability = ticket_config_availability(workspace_root);
|
||||||
|
timings.push(WorkspacePanelE2eSourceTiming {
|
||||||
|
source: "ticket_config_probe",
|
||||||
|
elapsed_ms: started.elapsed().as_millis(),
|
||||||
|
});
|
||||||
|
match availability {
|
||||||
|
TicketConfigAvailability::Absent => {}
|
||||||
|
TicketConfigAvailability::Usable => {
|
||||||
|
model.header.ticket_configured = true;
|
||||||
|
model.composer = WorkspacePanelComposer::ticket_enabled();
|
||||||
|
let started = Instant::now();
|
||||||
|
match TicketConfig::load_workspace(workspace_root) {
|
||||||
|
Ok(config) => {
|
||||||
|
timings.push(WorkspacePanelE2eSourceTiming {
|
||||||
|
source: "ticket_config_parse",
|
||||||
|
elapsed_ms: started.elapsed().as_millis(),
|
||||||
|
});
|
||||||
|
model.header.ticket_root = config.backend_root().to_path_buf();
|
||||||
|
let backend = LocalTicketBackend::new(config.backend_root().to_path_buf())
|
||||||
|
.with_record_language(config.ticket_record_language());
|
||||||
|
let started = Instant::now();
|
||||||
|
let orchestration_overlay =
|
||||||
|
load_orchestration_ticket_overlay(workspace_root, &config);
|
||||||
|
timings.push(WorkspacePanelE2eSourceTiming {
|
||||||
|
source: "orchestration_overlay_validation_read_git",
|
||||||
|
elapsed_ms: started.elapsed().as_millis(),
|
||||||
|
});
|
||||||
|
let started = Instant::now();
|
||||||
|
match build_ticket_rows(
|
||||||
|
&backend,
|
||||||
|
pods,
|
||||||
|
®istry,
|
||||||
|
&orchestration_overlay.states,
|
||||||
|
) {
|
||||||
|
Ok(ticket_rows) => {
|
||||||
|
timings.push(WorkspacePanelE2eSourceTiming {
|
||||||
|
source: "ticket_scan_parse",
|
||||||
|
elapsed_ms: started.elapsed().as_millis(),
|
||||||
|
});
|
||||||
|
model.rows.extend(ticket_rows.rows);
|
||||||
|
model.header.diagnostics.extend(ticket_rows.diagnostics);
|
||||||
|
model
|
||||||
|
.header
|
||||||
|
.diagnostics
|
||||||
|
.extend(orchestration_overlay.diagnostics);
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
timings.push(WorkspacePanelE2eSourceTiming {
|
||||||
|
source: "ticket_scan_parse",
|
||||||
|
elapsed_ms: started.elapsed().as_millis(),
|
||||||
|
});
|
||||||
|
model
|
||||||
|
.header
|
||||||
|
.diagnostics
|
||||||
|
.push(bounded_panel_diagnostic(format!(
|
||||||
|
"Ticket rows unavailable: {error}"
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
timings.push(WorkspacePanelE2eSourceTiming {
|
||||||
|
source: "ticket_config_parse",
|
||||||
|
elapsed_ms: started.elapsed().as_millis(),
|
||||||
|
});
|
||||||
|
model
|
||||||
|
.header
|
||||||
|
.diagnostics
|
||||||
|
.push(bounded_panel_diagnostic(format!(
|
||||||
|
"Ticket config is unusable: {error}"
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TicketConfigAvailability::Unusable(message) => {
|
||||||
|
model.header.ticket_configured = true;
|
||||||
|
model
|
||||||
|
.header
|
||||||
|
.diagnostics
|
||||||
|
.push(bounded_panel_diagnostic(format!(
|
||||||
|
"Ticket config is unusable: {message}"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let started = Instant::now();
|
||||||
|
model.rows.extend(pod_rows(pods));
|
||||||
|
timings.push(WorkspacePanelE2eSourceTiming {
|
||||||
|
source: "pod_row_materialization",
|
||||||
|
elapsed_ms: started.elapsed().as_millis(),
|
||||||
|
});
|
||||||
|
(model, timings)
|
||||||
|
}
|
||||||
|
|
||||||
fn build_workspace_panel_with_registry(
|
fn build_workspace_panel_with_registry(
|
||||||
workspace_root: &Path,
|
workspace_root: &Path,
|
||||||
pods: &PodList,
|
pods: &PodList,
|
||||||
|
|
|
||||||
|
|
@ -264,6 +264,14 @@ pub struct RenderedPanelRow {
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub status: Option<String>,
|
pub status: Option<String>,
|
||||||
pub action: Option<String>,
|
pub action: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub disabled_reason: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub local_state: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub overlay_state: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub overlay_detail: Option<String>,
|
||||||
pub rect: PanelRect,
|
pub rect: PanelRect,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -272,6 +280,10 @@ pub struct ExpectedPanelTicketRow {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub status: String,
|
pub status: String,
|
||||||
|
pub action: Option<String>,
|
||||||
|
pub disabled_reason: Option<String>,
|
||||||
|
pub local_state: Option<String>,
|
||||||
|
pub overlay_state: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ExpectedPanelTicketRow {
|
impl ExpectedPanelTicketRow {
|
||||||
|
|
@ -280,24 +292,221 @@ impl ExpectedPanelTicketRow {
|
||||||
id: id.into(),
|
id: id.into(),
|
||||||
title: title.into(),
|
title: title.into(),
|
||||||
status: status.into(),
|
status: status.into(),
|
||||||
|
action: None,
|
||||||
|
disabled_reason: None,
|
||||||
|
local_state: None,
|
||||||
|
overlay_state: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn with_action(mut self, action: impl Into<String>) -> Self {
|
||||||
|
self.action = Some(action.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_disabled_reason(mut self, disabled_reason: impl Into<String>) -> Self {
|
||||||
|
self.disabled_reason = Some(disabled_reason.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_local_state(mut self, local_state: impl Into<String>) -> Self {
|
||||||
|
self.local_state = Some(local_state.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_overlay_state(mut self, overlay_state: impl Into<String>) -> Self {
|
||||||
|
self.overlay_state = Some(overlay_state.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn matches(&self, row: &RenderedPanelRow) -> bool {
|
pub fn matches(&self, row: &RenderedPanelRow) -> bool {
|
||||||
row.key.kind == "ticket"
|
row.key.kind == "ticket"
|
||||||
&& row.key.id == self.id
|
&& row.key.id == self.id
|
||||||
&& row.title == self.title
|
&& row.title == self.title
|
||||||
&& row.status.as_deref() == Some(self.status.as_str())
|
&& row.status.as_deref() == Some(self.status.as_str())
|
||||||
|
&& self.action.as_ref().map_or(true, |action| {
|
||||||
|
row.action.as_deref() == Some(action.as_str())
|
||||||
|
})
|
||||||
|
&& self.disabled_reason.as_ref().map_or(true, |reason| {
|
||||||
|
row.disabled_reason
|
||||||
|
.as_deref()
|
||||||
|
.is_some_and(|actual| actual.contains(reason))
|
||||||
|
})
|
||||||
|
&& self.local_state.as_ref().map_or(true, |state| {
|
||||||
|
row.local_state.as_deref() == Some(state.as_str())
|
||||||
|
})
|
||||||
|
&& self.overlay_state.as_ref().map_or(true, |state| {
|
||||||
|
row.overlay_state.as_deref() == Some(state.as_str())
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn description(&self) -> String {
|
fn description(&self) -> String {
|
||||||
format!(
|
format!(
|
||||||
"ticket row id={} title={:?} status={}",
|
"ticket row id={} title={:?} status={} action={:?} disabled_reason={:?} local_state={:?} overlay_state={:?}",
|
||||||
self.id, self.title, self.status
|
self.id,
|
||||||
|
self.title,
|
||||||
|
self.status,
|
||||||
|
self.action,
|
||||||
|
self.disabled_reason,
|
||||||
|
self.local_state,
|
||||||
|
self.overlay_state,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct ExpectedDashboardContent {
|
||||||
|
pub tickets: Vec<ExpectedPanelTicketRow>,
|
||||||
|
pub pod_names: Vec<String>,
|
||||||
|
pub companion_status: String,
|
||||||
|
pub orchestrator_status: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ExpectedDashboardContent {
|
||||||
|
pub fn snapshot(&self) -> DashboardContentSnapshot {
|
||||||
|
DashboardContentSnapshot {
|
||||||
|
tickets: self.tickets.clone(),
|
||||||
|
pod_names: self.pod_names.clone(),
|
||||||
|
companion_status: self.companion_status.clone(),
|
||||||
|
orchestrator_status: self.orchestrator_status.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn description(&self) -> String {
|
||||||
|
let tickets = self
|
||||||
|
.tickets
|
||||||
|
.iter()
|
||||||
|
.map(ExpectedPanelTicketRow::description)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(", ");
|
||||||
|
let pods = self.pod_names.join(", ");
|
||||||
|
format!(
|
||||||
|
"tickets=[{tickets}] pods=[{pods}] companion={} orchestrator={}",
|
||||||
|
self.companion_status, self.orchestrator_status
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct DashboardContentSnapshot {
|
||||||
|
pub tickets: Vec<ExpectedPanelTicketRow>,
|
||||||
|
pub pod_names: Vec<String>,
|
||||||
|
pub companion_status: String,
|
||||||
|
pub orchestrator_status: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct DashboardContentReady {
|
||||||
|
pub snapshot: DashboardSnapshot,
|
||||||
|
pub categories: DashboardContentCategories,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct DashboardSnapshot {
|
||||||
|
pub header: DashboardHeader,
|
||||||
|
pub rows: Vec<RenderedPanelRow>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct DashboardHeader {
|
||||||
|
pub ticket_configured: bool,
|
||||||
|
pub companion: Option<DashboardCompanionState>,
|
||||||
|
pub orchestrator: Option<DashboardOrchestratorState>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub diagnostics: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct DashboardCompanionState {
|
||||||
|
pub pod_name: String,
|
||||||
|
pub status: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct DashboardOrchestratorState {
|
||||||
|
pub pod_name: String,
|
||||||
|
pub status: String,
|
||||||
|
pub detail: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct DashboardContentCategories {
|
||||||
|
pub ticket_rows: usize,
|
||||||
|
pub ready_ticket_rows: usize,
|
||||||
|
pub planning_ticket_rows: usize,
|
||||||
|
pub pod_rows: usize,
|
||||||
|
pub actionable_rows: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DashboardContentReady {
|
||||||
|
pub fn rows_rendered(&self) -> RowsRendered {
|
||||||
|
RowsRendered {
|
||||||
|
selected: None,
|
||||||
|
rows: self.snapshot.rows.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn snapshot_for_expected(
|
||||||
|
&self,
|
||||||
|
expected: &ExpectedDashboardContent,
|
||||||
|
) -> DashboardContentSnapshot {
|
||||||
|
DashboardContentSnapshot {
|
||||||
|
tickets: expected
|
||||||
|
.tickets
|
||||||
|
.iter()
|
||||||
|
.filter(|ticket| self.snapshot.rows.iter().any(|row| ticket.matches(row)))
|
||||||
|
.cloned()
|
||||||
|
.collect(),
|
||||||
|
pod_names: expected
|
||||||
|
.pod_names
|
||||||
|
.iter()
|
||||||
|
.filter(|pod_name| {
|
||||||
|
self.snapshot
|
||||||
|
.rows
|
||||||
|
.iter()
|
||||||
|
.any(|row| row.key.kind == "pod" && row.key.id == pod_name.as_str())
|
||||||
|
})
|
||||||
|
.cloned()
|
||||||
|
.collect(),
|
||||||
|
companion_status: self
|
||||||
|
.snapshot
|
||||||
|
.header
|
||||||
|
.companion
|
||||||
|
.as_ref()
|
||||||
|
.map(|companion| companion.status.clone())
|
||||||
|
.unwrap_or_default(),
|
||||||
|
orchestrator_status: self
|
||||||
|
.snapshot
|
||||||
|
.header
|
||||||
|
.orchestrator
|
||||||
|
.as_ref()
|
||||||
|
.map(|orchestrator| orchestrator.status.clone())
|
||||||
|
.unwrap_or_default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct DashboardSourceBreakdown {
|
||||||
|
pub total_elapsed_ms: u128,
|
||||||
|
pub sources: Vec<DashboardSourceTiming>,
|
||||||
|
pub ticket_rows: usize,
|
||||||
|
pub pod_rows: usize,
|
||||||
|
pub diagnostics: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct DashboardSourceTiming {
|
||||||
|
pub source: String,
|
||||||
|
pub elapsed_ms: u128,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DashboardSourceBreakdown {
|
||||||
|
pub fn has_source(&self, source: &str) -> bool {
|
||||||
|
self.sources.iter().any(|timing| timing.source == source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct RowsRendered {
|
pub struct RowsRendered {
|
||||||
pub selected: Option<PanelRowKey>,
|
pub selected: Option<PanelRowKey>,
|
||||||
|
|
@ -542,6 +751,49 @@ impl PanelHarness {
|
||||||
serde_json::from_value(event.data).map_err(HarnessError::from)
|
serde_json::from_value(event.data).map_err(HarnessError::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Waits for the dashboard-content-ready observer event. Unlike first-frame
|
||||||
|
/// or row-count readiness, this requires representative user-visible content:
|
||||||
|
/// ready + planning Ticket rows and a Pod row, then checks the fixture-specific
|
||||||
|
/// rows as a small snapshot of the expected dashboard content.
|
||||||
|
pub fn wait_for_dashboard_content_ready(
|
||||||
|
&mut self,
|
||||||
|
expected: &ExpectedDashboardContent,
|
||||||
|
timeout: Duration,
|
||||||
|
) -> Result<DashboardContentReady> {
|
||||||
|
let expected_snapshot = expected.snapshot();
|
||||||
|
let description = expected.description();
|
||||||
|
let event = self.wait_for(
|
||||||
|
format!("dashboard content ready ({description})"),
|
||||||
|
timeout,
|
||||||
|
|event| {
|
||||||
|
if event.event != "dashboard_content_ready" {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
serde_json::from_value::<DashboardContentReady>(event.data.clone())
|
||||||
|
.map(|ready| ready.snapshot_for_expected(expected) == expected_snapshot)
|
||||||
|
.unwrap_or(false)
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
serde_json::from_value(event.data).map_err(HarnessError::from)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn latest_dashboard_source_breakdown(
|
||||||
|
&mut self,
|
||||||
|
) -> Result<Option<DashboardSourceBreakdown>> {
|
||||||
|
Ok(self
|
||||||
|
.events()?
|
||||||
|
.into_iter()
|
||||||
|
.rev()
|
||||||
|
.filter(|event| event.event == "dashboard_source_breakdown")
|
||||||
|
.find_map(|event| serde_json::from_value(event.data).ok()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn expect_dashboard_source_breakdown(&mut self) -> Result<DashboardSourceBreakdown> {
|
||||||
|
self.latest_dashboard_source_breakdown()?.ok_or_else(|| {
|
||||||
|
HarnessError::Protocol("missing dashboard_source_breakdown observer event".to_string())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn assert_fixture_ticket_row_not_rendered(
|
pub fn assert_fixture_ticket_row_not_rendered(
|
||||||
&mut self,
|
&mut self,
|
||||||
expected: &ExpectedPanelTicketRow,
|
expected: &ExpectedPanelTicketRow,
|
||||||
|
|
@ -1029,6 +1281,7 @@ impl FixtureWorkspace {
|
||||||
)?;
|
)?;
|
||||||
fixture.ready_ticket_id = first;
|
fixture.ready_ticket_id = first;
|
||||||
fixture.planning_ticket_id = second;
|
fixture.planning_ticket_id = second;
|
||||||
|
fixture.setup_orchestration_overlay(binary)?;
|
||||||
fixture.write_fixture_metadata("ready", None)?;
|
fixture.write_fixture_metadata("ready", None)?;
|
||||||
Ok(fixture)
|
Ok(fixture)
|
||||||
}
|
}
|
||||||
|
|
@ -1039,6 +1292,95 @@ impl FixtureWorkspace {
|
||||||
READY_FIXTURE_TICKET_TITLE,
|
READY_FIXTURE_TICKET_TITLE,
|
||||||
"ready",
|
"ready",
|
||||||
)
|
)
|
||||||
|
.with_action("Queue")
|
||||||
|
.with_local_state("ready")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ready_overlay_ticket_row(&self) -> ExpectedPanelTicketRow {
|
||||||
|
ExpectedPanelTicketRow::new(
|
||||||
|
self.ready_ticket_id.clone(),
|
||||||
|
READY_FIXTURE_TICKET_TITLE,
|
||||||
|
"ready→prog",
|
||||||
|
)
|
||||||
|
.with_action("Wait")
|
||||||
|
.with_disabled_reason("orchestration worktree overlay shows Ticket state inprogress")
|
||||||
|
.with_local_state("ready")
|
||||||
|
.with_overlay_state("inprogress")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn planning_fixture_ticket_row(&self) -> ExpectedPanelTicketRow {
|
||||||
|
ExpectedPanelTicketRow::new(
|
||||||
|
self.planning_ticket_id.clone(),
|
||||||
|
PLANNING_FIXTURE_TICKET_TITLE,
|
||||||
|
"planning",
|
||||||
|
)
|
||||||
|
.with_action("Clarify")
|
||||||
|
.with_disabled_reason("Ticket is still in planning")
|
||||||
|
.with_local_state("planning")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn expected_dashboard_content(&self) -> ExpectedDashboardContent {
|
||||||
|
ExpectedDashboardContent {
|
||||||
|
tickets: vec![
|
||||||
|
self.ready_overlay_ticket_row(),
|
||||||
|
self.planning_fixture_ticket_row(),
|
||||||
|
],
|
||||||
|
pod_names: vec!["workspace".to_string()],
|
||||||
|
companion_status: "unavailable".to_string(),
|
||||||
|
orchestrator_status: "unavailable".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup_orchestration_overlay(&self, binary: &Path) -> Result<()> {
|
||||||
|
run_git(&self.workspace, &["init"])?;
|
||||||
|
run_git(&self.workspace, &["checkout", "-B", "develop"])?;
|
||||||
|
run_git(
|
||||||
|
&self.workspace,
|
||||||
|
&["config", "user.email", "fixture@example.invalid"],
|
||||||
|
)?;
|
||||||
|
run_git(&self.workspace, &["config", "user.name", "Yoi E2E Fixture"])?;
|
||||||
|
run_git(&self.workspace, &["add", ".yoi"])?;
|
||||||
|
run_git(&self.workspace, &["commit", "-m", "fixture tickets"])?;
|
||||||
|
let orchestration = self.workspace.join(".worktree/orchestration");
|
||||||
|
run_git(
|
||||||
|
&self.workspace,
|
||||||
|
&[
|
||||||
|
"worktree",
|
||||||
|
"add",
|
||||||
|
"-b",
|
||||||
|
"orchestration",
|
||||||
|
orchestration.to_string_lossy().as_ref(),
|
||||||
|
"HEAD",
|
||||||
|
],
|
||||||
|
)?;
|
||||||
|
run_yoi(
|
||||||
|
binary,
|
||||||
|
&orchestration,
|
||||||
|
&self.home,
|
||||||
|
&self.xdg_data_home,
|
||||||
|
&self.xdg_state_home,
|
||||||
|
&self.xdg_config_home,
|
||||||
|
&self.xdg_runtime_dir,
|
||||||
|
&self.artifacts_dir,
|
||||||
|
&["ticket", "state", &self.ready_ticket_id, "queued"],
|
||||||
|
)?;
|
||||||
|
run_yoi(
|
||||||
|
binary,
|
||||||
|
&orchestration,
|
||||||
|
&self.home,
|
||||||
|
&self.xdg_data_home,
|
||||||
|
&self.xdg_state_home,
|
||||||
|
&self.xdg_config_home,
|
||||||
|
&self.xdg_runtime_dir,
|
||||||
|
&self.artifacts_dir,
|
||||||
|
&["ticket", "state", &self.ready_ticket_id, "inprogress"],
|
||||||
|
)?;
|
||||||
|
run_git(&orchestration, &["add", ".yoi"])?;
|
||||||
|
run_git(
|
||||||
|
&orchestration,
|
||||||
|
&["commit", "-m", "fixture orchestration overlay"],
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn panel_config(&self, binary: PathBuf) -> PanelHarnessConfig {
|
pub fn panel_config(&self, binary: PathBuf) -> PanelHarnessConfig {
|
||||||
|
|
@ -1379,6 +1721,23 @@ fn create_ticket(
|
||||||
.ok_or_else(|| HarnessError::Protocol(format!("could not parse ticket id from {output:?}")))
|
.ok_or_else(|| HarnessError::Protocol(format!("could not parse ticket id from {output:?}")))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn run_git(workspace: &Path, args: &[&str]) -> Result<()> {
|
||||||
|
let output = Command::new("git")
|
||||||
|
.args(args)
|
||||||
|
.current_dir(workspace)
|
||||||
|
.output()?;
|
||||||
|
if output.status.success() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
Err(HarnessError::CommandFailed {
|
||||||
|
program: PathBuf::from("git"),
|
||||||
|
args: args.iter().map(|arg| (*arg).to_string()).collect(),
|
||||||
|
status: output.status,
|
||||||
|
stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
|
||||||
|
stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn run_yoi(
|
fn run_yoi(
|
||||||
binary: &Path,
|
binary: &Path,
|
||||||
workspace: &Path,
|
workspace: &Path,
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,109 @@
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
const FIRST_VISIBLE_RENDER_BUDGET: Duration = Duration::from_millis(1500);
|
const FIRST_VISIBLE_RENDER_BUDGET: Duration = Duration::from_millis(1500);
|
||||||
const ROWS_READY_BUDGET: Duration = Duration::from_secs(5);
|
const DASHBOARD_CONTENT_READY_BUDGET: Duration = Duration::from_secs(5);
|
||||||
|
|
||||||
use yoi_e2e::{
|
use yoi_e2e::{
|
||||||
|
DashboardCompanionState, DashboardContentCategories, DashboardContentReady, DashboardHeader,
|
||||||
|
DashboardOrchestratorState, DashboardSnapshot, ExpectedDashboardContent,
|
||||||
ExpectedPanelTicketRow, FixtureCleanupReport, FixtureWorkspace, KeyPress, PanelHarness,
|
ExpectedPanelTicketRow, FixtureCleanupReport, FixtureWorkspace, KeyPress, PanelHarness,
|
||||||
PanelRect, PanelRowKey, RenderedPanelRow, RowsRendered, yoi_binary,
|
PanelRect, PanelRowKey, RenderedPanelRow, RowsRendered, yoi_binary,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[test]
|
fn rendered_ticket_row(
|
||||||
fn panel_fixture_ticket_row_matcher_rejects_absent_fixture_data() {
|
id: &str,
|
||||||
let expected = ExpectedPanelTicketRow::new("0000000000000", "Ready E2E Ticket", "ready");
|
title: &str,
|
||||||
let wrong_title = RenderedPanelRow {
|
status: &str,
|
||||||
|
action: Option<&str>,
|
||||||
|
disabled_reason: Option<&str>,
|
||||||
|
local_state: Option<&str>,
|
||||||
|
overlay_state: Option<&str>,
|
||||||
|
) -> RenderedPanelRow {
|
||||||
|
RenderedPanelRow {
|
||||||
key: PanelRowKey {
|
key: PanelRowKey {
|
||||||
kind: "ticket".to_string(),
|
kind: "ticket".to_string(),
|
||||||
id: "0000000000000".to_string(),
|
id: id.to_string(),
|
||||||
},
|
},
|
||||||
title: "Different Ticket".to_string(),
|
title: title.to_string(),
|
||||||
status: Some("ready".to_string()),
|
status: Some(status.to_string()),
|
||||||
action: None,
|
action: action.map(ToOwned::to_owned),
|
||||||
|
disabled_reason: disabled_reason.map(ToOwned::to_owned),
|
||||||
|
local_state: local_state.map(ToOwned::to_owned),
|
||||||
|
overlay_state: overlay_state.map(ToOwned::to_owned),
|
||||||
|
overlay_detail: overlay_state.map(|state| format!("orchestration:{state}")),
|
||||||
rect: PanelRect {
|
rect: PanelRect {
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
width: 10,
|
width: 10,
|
||||||
height: 1,
|
height: 1,
|
||||||
},
|
},
|
||||||
};
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rendered_pod_row(name: &str) -> RenderedPanelRow {
|
||||||
|
RenderedPanelRow {
|
||||||
|
key: PanelRowKey {
|
||||||
|
kind: "pod".to_string(),
|
||||||
|
id: name.to_string(),
|
||||||
|
},
|
||||||
|
title: name.to_string(),
|
||||||
|
status: None,
|
||||||
|
action: None,
|
||||||
|
disabled_reason: None,
|
||||||
|
local_state: None,
|
||||||
|
overlay_state: None,
|
||||||
|
overlay_detail: None,
|
||||||
|
rect: PanelRect {
|
||||||
|
x: 0,
|
||||||
|
y: 1,
|
||||||
|
width: 10,
|
||||||
|
height: 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ready_snapshot(rows: Vec<RenderedPanelRow>) -> DashboardContentReady {
|
||||||
|
DashboardContentReady {
|
||||||
|
snapshot: DashboardSnapshot {
|
||||||
|
header: DashboardHeader {
|
||||||
|
ticket_configured: true,
|
||||||
|
companion: Some(DashboardCompanionState {
|
||||||
|
pod_name: "workspace".to_string(),
|
||||||
|
status: "unavailable".to_string(),
|
||||||
|
}),
|
||||||
|
orchestrator: Some(DashboardOrchestratorState {
|
||||||
|
pod_name: "workspace-orchestrator".to_string(),
|
||||||
|
status: "unavailable".to_string(),
|
||||||
|
detail: Some("fixture blocks host Pod launch".to_string()),
|
||||||
|
}),
|
||||||
|
diagnostics: vec![],
|
||||||
|
},
|
||||||
|
rows,
|
||||||
|
},
|
||||||
|
categories: DashboardContentCategories {
|
||||||
|
ticket_rows: 2,
|
||||||
|
ready_ticket_rows: 1,
|
||||||
|
planning_ticket_rows: 1,
|
||||||
|
pod_rows: 1,
|
||||||
|
actionable_rows: 2,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn panel_fixture_ticket_row_matcher_rejects_absent_fixture_data() {
|
||||||
|
let expected = ExpectedPanelTicketRow::new("0000000000000", "Ready E2E Ticket", "ready")
|
||||||
|
.with_action("Queue")
|
||||||
|
.with_local_state("ready");
|
||||||
|
let wrong_title = rendered_ticket_row(
|
||||||
|
"0000000000000",
|
||||||
|
"Different Ticket",
|
||||||
|
"ready",
|
||||||
|
Some("Queue"),
|
||||||
|
None,
|
||||||
|
Some("ready"),
|
||||||
|
None,
|
||||||
|
);
|
||||||
let wrong_kind = RenderedPanelRow {
|
let wrong_kind = RenderedPanelRow {
|
||||||
key: PanelRowKey {
|
key: PanelRowKey {
|
||||||
kind: "pod".to_string(),
|
kind: "pod".to_string(),
|
||||||
|
|
@ -33,7 +111,11 @@ fn panel_fixture_ticket_row_matcher_rejects_absent_fixture_data() {
|
||||||
},
|
},
|
||||||
title: "Ready E2E Ticket".to_string(),
|
title: "Ready E2E Ticket".to_string(),
|
||||||
status: Some("ready".to_string()),
|
status: Some("ready".to_string()),
|
||||||
action: None,
|
action: Some("Queue".to_string()),
|
||||||
|
disabled_reason: None,
|
||||||
|
local_state: Some("ready".to_string()),
|
||||||
|
overlay_state: None,
|
||||||
|
overlay_detail: None,
|
||||||
rect: PanelRect {
|
rect: PanelRect {
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
|
|
@ -51,6 +133,91 @@ fn panel_fixture_ticket_row_matcher_rejects_absent_fixture_data() {
|
||||||
assert!(!rows.has_fixture_ticket_row(&expected));
|
assert!(!rows.has_fixture_ticket_row(&expected));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dashboard_snapshot_rejects_missing_row_wrong_state_missing_overlay_and_missing_action() {
|
||||||
|
let expected_ready = ExpectedPanelTicketRow::new("ready-id", "Ready E2E Ticket", "ready→prog")
|
||||||
|
.with_action("Wait")
|
||||||
|
.with_disabled_reason("orchestration worktree overlay shows Ticket state inprogress")
|
||||||
|
.with_local_state("ready")
|
||||||
|
.with_overlay_state("inprogress");
|
||||||
|
let expected_planning =
|
||||||
|
ExpectedPanelTicketRow::new("planning-id", "Planning E2E Ticket", "planning")
|
||||||
|
.with_action("Clarify")
|
||||||
|
.with_disabled_reason("Ticket is still in planning")
|
||||||
|
.with_local_state("planning");
|
||||||
|
let expected = ExpectedDashboardContent {
|
||||||
|
tickets: vec![expected_ready.clone(), expected_planning.clone()],
|
||||||
|
pod_names: vec!["workspace".to_string()],
|
||||||
|
companion_status: "unavailable".to_string(),
|
||||||
|
orchestrator_status: "unavailable".to_string(),
|
||||||
|
};
|
||||||
|
let complete_rows = || {
|
||||||
|
vec![
|
||||||
|
rendered_ticket_row(
|
||||||
|
"ready-id",
|
||||||
|
"Ready E2E Ticket",
|
||||||
|
"ready→prog",
|
||||||
|
Some("Wait"),
|
||||||
|
Some("orchestration worktree overlay shows Ticket state inprogress"),
|
||||||
|
Some("ready"),
|
||||||
|
Some("inprogress"),
|
||||||
|
),
|
||||||
|
rendered_ticket_row(
|
||||||
|
"planning-id",
|
||||||
|
"Planning E2E Ticket",
|
||||||
|
"planning",
|
||||||
|
Some("Clarify"),
|
||||||
|
Some("Ticket is still in planning"),
|
||||||
|
Some("planning"),
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
rendered_pod_row("workspace"),
|
||||||
|
]
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
ready_snapshot(complete_rows()).snapshot_for_expected(&expected),
|
||||||
|
expected.snapshot()
|
||||||
|
);
|
||||||
|
|
||||||
|
let missing_row = ready_snapshot(vec![
|
||||||
|
rendered_ticket_row(
|
||||||
|
"ready-id",
|
||||||
|
"Ready E2E Ticket",
|
||||||
|
"ready→prog",
|
||||||
|
Some("Wait"),
|
||||||
|
Some("orchestration worktree overlay shows Ticket state inprogress"),
|
||||||
|
Some("ready"),
|
||||||
|
Some("inprogress"),
|
||||||
|
),
|
||||||
|
rendered_pod_row("workspace"),
|
||||||
|
]);
|
||||||
|
assert_ne!(
|
||||||
|
missing_row.snapshot_for_expected(&expected),
|
||||||
|
expected.snapshot()
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut wrong_state_rows = complete_rows();
|
||||||
|
wrong_state_rows[0].status = Some("ready".to_string());
|
||||||
|
assert_ne!(
|
||||||
|
ready_snapshot(wrong_state_rows).snapshot_for_expected(&expected),
|
||||||
|
expected.snapshot()
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut missing_overlay_rows = complete_rows();
|
||||||
|
missing_overlay_rows[0].overlay_state = None;
|
||||||
|
assert_ne!(
|
||||||
|
ready_snapshot(missing_overlay_rows).snapshot_for_expected(&expected),
|
||||||
|
expected.snapshot()
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut missing_action_rows = complete_rows();
|
||||||
|
missing_action_rows[0].action = None;
|
||||||
|
assert_ne!(
|
||||||
|
ready_snapshot(missing_action_rows).snapshot_for_expected(&expected),
|
||||||
|
expected.snapshot()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn panel_first_visible_render_arrives_before_background_reload() -> yoi_e2e::Result<()> {
|
fn panel_first_visible_render_arrives_before_background_reload() -> yoi_e2e::Result<()> {
|
||||||
let binary = yoi_binary()?;
|
let binary = yoi_binary()?;
|
||||||
|
|
@ -114,11 +281,11 @@ fn panel_first_visible_render_arrives_before_background_reload() -> yoi_e2e::Res
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn panel_fixture_ticket_row_ready_has_startup_budget() -> yoi_e2e::Result<()> {
|
fn panel_dashboard_content_ready_has_startup_budget() -> yoi_e2e::Result<()> {
|
||||||
let binary = yoi_binary()?;
|
let binary = yoi_binary()?;
|
||||||
let fixture = FixtureWorkspace::new(&binary)?;
|
let fixture = FixtureWorkspace::new(&binary)?;
|
||||||
assert_fixture_paths_are_isolated(&fixture);
|
assert_fixture_paths_are_isolated(&fixture);
|
||||||
let ready_ticket = fixture.ready_fixture_ticket_row();
|
let expected_content = fixture.expected_dashboard_content();
|
||||||
|
|
||||||
let started = Instant::now();
|
let started = Instant::now();
|
||||||
let mut panel = PanelHarness::spawn(fixture.panel_config(binary))?;
|
let mut panel = PanelHarness::spawn(fixture.panel_config(binary))?;
|
||||||
|
|
@ -137,23 +304,63 @@ fn panel_fixture_ticket_row_ready_has_startup_budget() -> yoi_e2e::Result<()> {
|
||||||
panel.artifacts().dir.display()
|
panel.artifacts().dir.display()
|
||||||
);
|
);
|
||||||
|
|
||||||
let rows_ready_remaining = ROWS_READY_BUDGET
|
let content_ready_remaining = DASHBOARD_CONTENT_READY_BUDGET
|
||||||
.checked_sub(started.elapsed())
|
.checked_sub(started.elapsed())
|
||||||
.unwrap_or_else(|| Duration::from_millis(0));
|
.unwrap_or_else(|| Duration::from_millis(0));
|
||||||
let rows = panel.wait_for_fixture_ticket_rows_ready(&ready_ticket, rows_ready_remaining)?;
|
let content_ready =
|
||||||
|
panel.wait_for_dashboard_content_ready(&expected_content, content_ready_remaining)?;
|
||||||
assert!(
|
assert!(
|
||||||
rows.has_fixture_ticket_row(&ready_ticket),
|
content_ready.snapshot.header.ticket_configured,
|
||||||
"rows-ready event must contain concrete ready fixture Ticket row; artifacts at {}",
|
"dashboard content ready must include usable Ticket configuration; artifacts at {}",
|
||||||
panel.artifacts().dir.display()
|
panel.artifacts().dir.display()
|
||||||
);
|
);
|
||||||
let rows_ready_elapsed = started.elapsed();
|
assert!(
|
||||||
|
content_ready.snapshot.header.companion.is_some()
|
||||||
|
&& content_ready.snapshot.header.orchestrator.is_some(),
|
||||||
|
"dashboard content ready must include Companion and Orchestrator header status; got {:?}; artifacts at {}",
|
||||||
|
content_ready.snapshot.header,
|
||||||
|
panel.artifacts().dir.display()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
content_ready.snapshot_for_expected(&expected_content),
|
||||||
|
expected_content.snapshot(),
|
||||||
|
"dashboard content ready must match expected Ticket/action/overlay/header snapshot; artifacts at {}",
|
||||||
|
panel.artifacts().dir.display()
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
content_ready.categories.ready_ticket_rows > 0
|
||||||
|
&& content_ready.categories.planning_ticket_rows > 0
|
||||||
|
&& content_ready.categories.pod_rows > 0,
|
||||||
|
"dashboard content ready must include ready Ticket, planning Ticket, and Pod categories; got {:?}; artifacts at {}",
|
||||||
|
content_ready.categories,
|
||||||
|
panel.artifacts().dir.display()
|
||||||
|
);
|
||||||
|
let content_ready_elapsed = started.elapsed();
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"panel fixture rows ready: {rows_ready_elapsed:?} (budget {ROWS_READY_BUDGET:?}); artifacts at {}",
|
"panel dashboard content ready: {content_ready_elapsed:?} (budget {DASHBOARD_CONTENT_READY_BUDGET:?}; first frame {first_visible_elapsed:?}); artifacts at {}",
|
||||||
panel.artifacts().dir.display()
|
panel.artifacts().dir.display()
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
rows_ready_elapsed <= ROWS_READY_BUDGET,
|
content_ready_elapsed <= DASHBOARD_CONTENT_READY_BUDGET,
|
||||||
"fixture rows ready took {rows_ready_elapsed:?}, budget {ROWS_READY_BUDGET:?}; artifacts at {}",
|
"dashboard content ready took {content_ready_elapsed:?}, budget {DASHBOARD_CONTENT_READY_BUDGET:?}; artifacts at {}",
|
||||||
|
panel.artifacts().dir.display()
|
||||||
|
);
|
||||||
|
|
||||||
|
let source_breakdown = panel.expect_dashboard_source_breakdown()?;
|
||||||
|
assert!(
|
||||||
|
source_breakdown.has_source("pod_metadata_status_probe.initial")
|
||||||
|
&& source_breakdown.has_source("ticket_config_probe")
|
||||||
|
&& source_breakdown.has_source("local_claim_scan")
|
||||||
|
&& source_breakdown.has_source("ticket_scan_parse")
|
||||||
|
&& source_breakdown.has_source("orchestration_overlay_validation_read_git")
|
||||||
|
&& source_breakdown.has_source("workspace_panel.build.total"),
|
||||||
|
"dashboard source breakdown should include pod metadata/status, ticket scan/parse, overlay validation/read/git, local claim scan, and panel-build sources; got {:?}; artifacts at {}",
|
||||||
|
source_breakdown,
|
||||||
|
panel.artifacts().dir.display()
|
||||||
|
);
|
||||||
|
eprintln!(
|
||||||
|
"panel dashboard source breakdown: {:?}; artifacts at {}",
|
||||||
|
source_breakdown,
|
||||||
panel.artifacts().dir.display()
|
panel.artifacts().dir.display()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user