Rebuild grid agent TUI with Ratatui dashboards
This commit is contained in:
@@ -22,6 +22,7 @@ metacrate-game-loop = { version = "0.0.1", path = "../metacrate-game-loop" }
|
||||
metacrate-rendering-wgpu = { version = "0.0.1", path = "../metacrate-rendering-wgpu" }
|
||||
mentra = { version = "0.18.3", default-features = false }
|
||||
jpeg-encoder = "0.6.1"
|
||||
ratatui = { version = "0.30.2", default-features = false, features = ["crossterm_0_29"] }
|
||||
rustls = { version = "0.23.43", default-features = false, features = ["aws_lc_rs", "std", "tls12"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -75,7 +75,7 @@ impl TuiTransport for FakeTransport {
|
||||
}
|
||||
ControlRequest::ListObservabilityEvents { .. } => {
|
||||
ControlPayload::ObservabilityEvents(crate::Page {
|
||||
items: Vec::new(),
|
||||
items: vec![structured_event()],
|
||||
next_cursor: None,
|
||||
})
|
||||
}
|
||||
@@ -91,6 +91,31 @@ impl TuiTransport for FakeTransport {
|
||||
}
|
||||
}
|
||||
|
||||
fn structured_event() -> crate::StructuredEvent {
|
||||
serde_json::from_value(serde_json::json!({
|
||||
"schema_version": 1,
|
||||
"event_id": 7,
|
||||
"unix_millis": 1234,
|
||||
"severity": "warning",
|
||||
"component": "control_queue",
|
||||
"family": "queue_pressure",
|
||||
"correlation": {
|
||||
"avatar_id": "avatar-7",
|
||||
"session_id": "session-7",
|
||||
"request_id": null,
|
||||
"action_id": "action-7"
|
||||
},
|
||||
"origin": "service",
|
||||
"duration_millis": 19,
|
||||
"retry_count": 1,
|
||||
"result_code": "delayed",
|
||||
"reason_code": "capacity",
|
||||
"redaction_flags": [],
|
||||
"fields": {}
|
||||
}))
|
||||
.expect("event fixture")
|
||||
}
|
||||
|
||||
fn empty_metrics() -> crate::MetricsSnapshot {
|
||||
serde_json::from_value(serde_json::json!({
|
||||
"ready":true,"reconnects":0,"active_sessions":0,"active_tasks":0,
|
||||
@@ -116,6 +141,96 @@ async fn snapshot_refresh_is_transport_neutral_and_bounded() {
|
||||
"running"
|
||||
);
|
||||
assert_eq!(transport.0.lock().expect("requests").len(), 8);
|
||||
app.refresh(&transport).await.expect("stable refresh");
|
||||
assert_eq!(
|
||||
app.snapshot.timeline.len(),
|
||||
1,
|
||||
"refresh must deduplicate events"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn performance_history_is_bounded_and_large_dashboards_are_deterministic() {
|
||||
let transport = FakeTransport::new();
|
||||
let mut app = OperatorTui::default();
|
||||
for _ in 0..140 {
|
||||
app.refresh(&transport).await.expect("refresh");
|
||||
}
|
||||
assert_eq!(app.performance_history_len(), 120);
|
||||
app.screen = OperatorScreen::Overview;
|
||||
let unicode = app.render(TuiRenderOptions {
|
||||
width: 140,
|
||||
height: 42,
|
||||
color: false,
|
||||
});
|
||||
assert!(unicode.contains("Café") && unicode.contains('世') && unicode.contains('界'));
|
||||
for (screen, marker) in [
|
||||
(OperatorScreen::Overview, "Runtime"),
|
||||
(OperatorScreen::Sessions, "Sessions"),
|
||||
(OperatorScreen::QueuesAndBudgets, "Work queue"),
|
||||
(OperatorScreen::Roaming, "Roaming schedules"),
|
||||
(OperatorScreen::Approvals, "Pending approvals"),
|
||||
(OperatorScreen::Timeline, "Timeline / audit"),
|
||||
(OperatorScreen::Health, "Health"),
|
||||
(OperatorScreen::Errors, "Recent errors"),
|
||||
(OperatorScreen::Diagnostics, "Diagnostics (redacted)"),
|
||||
(OperatorScreen::Preferences, "Preferences"),
|
||||
] {
|
||||
app.screen = screen;
|
||||
let first = app.render(TuiRenderOptions {
|
||||
width: 140,
|
||||
height: 42,
|
||||
color: true,
|
||||
});
|
||||
let second = app.render(TuiRenderOptions {
|
||||
width: 140,
|
||||
height: 42,
|
||||
color: true,
|
||||
});
|
||||
assert!(first.contains(marker), "missing {marker}");
|
||||
assert_eq!(first.lines().count(), second.lines().count());
|
||||
}
|
||||
app.screen = OperatorScreen::QueuesAndBudgets;
|
||||
let output = app.render(TuiRenderOptions {
|
||||
width: 140,
|
||||
height: 42,
|
||||
color: false,
|
||||
});
|
||||
for label in [
|
||||
"Work queue",
|
||||
"Rate limit",
|
||||
"Inference avg ms",
|
||||
"Tool avg ms",
|
||||
"Dropped total",
|
||||
] {
|
||||
assert!(output.contains(label), "missing {label}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn event_search_covers_correlation_duration_result_and_reason() {
|
||||
let mut app = OperatorTui::default();
|
||||
app.snapshot.timeline.push_back(structured_event());
|
||||
app.screen = OperatorScreen::Timeline;
|
||||
for query in [
|
||||
"warning",
|
||||
"avatar-7",
|
||||
"session-7",
|
||||
"action-7",
|
||||
"19",
|
||||
"delayed",
|
||||
"capacity",
|
||||
] {
|
||||
app.filter.query = query.into();
|
||||
assert!(
|
||||
app.render(TuiRenderOptions {
|
||||
width: 120,
|
||||
height: 30,
|
||||
color: false
|
||||
})
|
||||
.contains("queue_pressure")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -142,7 +257,7 @@ fn every_screen_renders_without_a_terminal_at_small_unicode_and_mono_sizes() {
|
||||
color: false,
|
||||
});
|
||||
assert!(!output.is_empty());
|
||||
assert!(output.lines().count() <= 4);
|
||||
assert!(output.lines().count() <= 5);
|
||||
assert!(
|
||||
output
|
||||
.lines()
|
||||
@@ -196,6 +311,21 @@ fn every_global_command_has_a_keyboard_path() {
|
||||
assert_eq!(app.command_shortcut('s'), Some(OperatorCommand::Shutdown));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn confirmation_dialog_is_visible_without_color() {
|
||||
let mut app = OperatorTui::default();
|
||||
assert_eq!(
|
||||
app.reduce(TuiInput::Command(OperatorCommand::Shutdown)),
|
||||
TuiAction::None
|
||||
);
|
||||
let output = app.render(TuiRenderOptions {
|
||||
width: 100,
|
||||
height: 24,
|
||||
color: false,
|
||||
});
|
||||
assert!(output.contains("confirm HighRisk (y/n)"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transport_errors_are_safe_display_text() {
|
||||
struct Broken;
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::collections::BTreeSet;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
const ALLOWED_DEPENDENCIES: [&str; 22] = [
|
||||
const ALLOWED_DEPENDENCIES: [&str; 23] = [
|
||||
"async-trait",
|
||||
"base64",
|
||||
"crossterm",
|
||||
@@ -16,6 +16,7 @@ const ALLOWED_DEPENDENCIES: [&str; 22] = [
|
||||
"mentra",
|
||||
"jpeg-encoder",
|
||||
"libremetaverse-types",
|
||||
"ratatui",
|
||||
"rustls",
|
||||
"serde",
|
||||
"serde_json",
|
||||
|
||||
@@ -108,3 +108,23 @@ fn dotenv_import_is_explicit_and_preferences_panel_never_renders_secrets() {
|
||||
let _ = tui.reduce(TuiInput::PreferenceSave);
|
||||
fs::remove_dir_all(directory).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preferences_form_keeps_validation_feedback_visible() {
|
||||
let directory = temporary("validation");
|
||||
let config = directory.join("config.yml");
|
||||
let mut tui = OperatorTui::with_preferences(config).unwrap();
|
||||
tui.screen = OperatorScreen::Preferences;
|
||||
let _ = tui.reduce(TuiInput::PreferenceEditOrCommit);
|
||||
for value in "not-a-mode".chars() {
|
||||
let _ = tui.reduce(TuiInput::PreferenceCharacter(value));
|
||||
}
|
||||
let _ = tui.reduce(TuiInput::PreferenceEditOrCommit);
|
||||
let rendered = tui.render(TuiRenderOptions {
|
||||
width: 100,
|
||||
height: 24,
|
||||
color: false,
|
||||
});
|
||||
assert!(rendered.contains("[VALIDATION] mode must be offline, integrated, or split"));
|
||||
fs::remove_dir_all(directory).unwrap();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user