- 1
#![allow( - 2
clippy::expect_used, - 3
clippy::panic, - 4
clippy::unwrap_used, - 5
clippy::field_reassign_with_default, - 6
clippy::needless_borrow, - 7
clippy::eq_op - 8
)] - 9
- 10
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent, MouseEventKind}; - 11
use ratatui::buffer::Buffer; - 12
use ratatui::layout::Rect; - 13
use ratatui::widgets::Widget; - 14
use std::sync::Arc; - 15
- 16
use vak_terminal::api::{ApiClient, ApiError, HealthReport, ProviderList}; - 17
use vak_terminal::app::{IncidentSeverity, TerminalApp}; - 18
use vak_terminal::hil::{HilApprovalState, HilOutcome, RememberMode}; - 19
use vak_terminal::repl::ReplComposer; - 20
use vak_terminal::telemetry::TelemetryState; - 21
use vak_terminal::theme::{Symbols, ThemeKind}; - 22
use vak_terminal::ui::header::ActiveTab; - 23
- 24
/// Build a TerminalApp wired to a real (but non-functional in tests) - 25
/// ApiClient. Tests that exercise command dispatch or SSE event handling - 26
/// operate on the app's internal state machine without needing a live server. - 27
fn make_test_app() -> TerminalApp { - 28
let health = HealthReport::default(); - 29
let api = Arc::new(ApiClient::new("http://127.0.0.1:8901", "test-token")); - 30
TerminalApp::new( - 31
"test-session", - 32
health.model.clone(), - 33
"http://127.0.0.1:8901", - 34
health, - 35
api, - 36
) - 37
} - 38
- 39
#[test] - 40
fn test_theme_cycling_and_palettes() { - 41
let mut kind = ThemeKind::default(); - 42
assert_eq!(kind, ThemeKind::VakWarm); - 43
- 44
let expected_order = [ - 45
ThemeKind::VakWarm, - 46
ThemeKind::VakSlate, - 47
ThemeKind::VakPaper, - 48
ThemeKind::VakContrast, - 49
ThemeKind::TokyoNight, - 50
ThemeKind::VakWarm, - 51
]; - 52
- 53
for expected in expected_order { - 54
assert_eq!(kind, expected); - 55
let theme = kind.theme(); - 56
assert_eq!(theme.kind, kind); - 57
let _ = theme.style_tab_active(); - 58
let _ = theme.style_tab_inactive(); - 59
let _ = theme.style_border(); - 60
let _ = theme.style_border_focus(); - 61
let _ = theme.style_accent(); - 62
let _ = theme.style_ok(); - 63
let _ = theme.style_danger(); - 64
let _ = theme.style_warn(); - 65
let _ = theme.style_info(); - 66
kind = kind.next(); - 67
} - 68
} - 69
- 70
#[test] - 71
fn test_repl_composer_navigation_and_editing() { - 72
let mut composer = ReplComposer::new(); - 73
assert_eq!(composer.buffer, ""); - 74
assert_eq!(composer.cursor_pos, 0); - 75
- 76
composer.insert_char('h'); - 77
composer.insert_char('e'); - 78
composer.insert_char('l'); - 79
composer.insert_char('l'); - 80
composer.insert_char('o'); - 81
assert_eq!(composer.buffer, "hello"); - 82
assert_eq!(composer.cursor_pos, 5); - 83
- 84
composer.move_cursor_left(); - 85
composer.move_cursor_left(); - 86
assert_eq!(composer.cursor_pos, 3); - 87
- 88
composer.insert_char('p'); - 89
assert_eq!(composer.buffer, "helplo"); - 90
- 91
composer.delete_backspace(); - 92
assert_eq!(composer.buffer, "hello"); - 93
- 94
composer.move_to_start(); - 95
assert_eq!(composer.cursor_pos, 0); - 96
- 97
composer.move_to_end(); - 98
assert_eq!(composer.cursor_pos, 5); - 99
- 100
let submitted = composer.submit(); - 101
assert_eq!(submitted.as_deref(), Some("hello")); - 102
assert_eq!(composer.buffer, ""); - 103
assert_eq!(composer.history.len(), 1); - 104
- 105
composer.history_up(); - 106
assert_eq!(composer.buffer, "hello"); - 107
- 108
composer.history_down(); - 109
assert_eq!(composer.buffer, ""); - 110
- 111
composer.insert_char('/'); - 112
assert!(composer.slash_palette_open); - 113
assert!(!composer.matching_slash_commands().is_empty()); - 114
- 115
composer.insert_char('t'); - 116
composer.insert_char('h'); - 117
let matching = composer.matching_slash_commands(); - 118
assert_eq!(matching.len(), 1); - 119
assert_eq!(matching[0].name, "/theme"); - 120
- 121
composer.complete_selected_slash(); - 122
assert_eq!(composer.buffer, "/theme "); - 123
assert!(!composer.slash_palette_open); - 124
} - 125
- 126
#[tokio::test] - 127
async fn test_slash_command_tab_navigation() { - 128
let mut app = make_test_app(); - 129
- 130
// /ops switches to Observability tab - 131
app.composer.buffer = "/ops".into(); - 132
app.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::empty())); - 133
assert_eq!(app.active_tab, ActiveTab::Observability); - 134
- 135
// /admin switches to Admin tab - 136
app.composer.buffer = "/admin".into(); - 137
app.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::empty())); - 138
assert_eq!(app.active_tab, ActiveTab::Admin); - 139
- 140
// /inbox switches to Inbox tab - 141
app.composer.buffer = "/inbox".into(); - 142
app.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::empty())); - 143
assert_eq!(app.active_tab, ActiveTab::Inbox); - 144
- 145
// /diff switches to Studio and focuses Right deck - 146
app.composer.buffer = "/diff".into(); - 147
app.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::empty())); - 148
assert_eq!(app.active_tab, ActiveTab::Studio); - 149
assert_eq!(app.deck_focus, vak_terminal::app::DeckFocus::Right); - 150
- 151
// /theme cycles theme - 152
let initial_theme = app.theme.kind; - 153
app.composer.buffer = "/theme".into(); - 154
app.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::empty())); - 155
assert_ne!(app.theme.kind, initial_theme); - 156
- 157
// /detach sets should_detach - 158
assert!(!app.should_detach); - 159
app.composer.buffer = "/detach".into(); - 160
app.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::empty())); - 161
assert!(app.should_detach); - 162
- 163
// /quit sets should_quit - 164
app.composer.buffer = "/quit".into(); - 165
app.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::empty())); - 166
assert!(app.should_quit); - 167
} - 168
- 169
#[test] - 170
fn test_hil_approval_flow_and_editing() { - 171
// Build a real HilApprovalState from approval event data. - 172
let mut hil = HilApprovalState::from_approval_event( - 173
"test-session", - 174
"req-1", - 175
"bash", - 176
r#"{"command":"npm run build && vite preview --port 5173"}"#, - 177
"High network & port binding impact", - 178
"/workspace/web-app", - 179
Some(0.042), - 180
); - 181
- 182
assert_eq!(hil.tool_name, "bash"); - 183
assert_eq!( - 184
hil.original_command, - 185
"npm run build && vite preview --port 5173" - 186
); - 187
assert!(!hil.is_editing); - 188
assert_eq!(hil.remember, RememberMode::No); - 189
- 190
// Unmodified approval - 191
let outcome = hil.finish_approval(); - 192
assert_eq!( - 193
outcome, - 194
HilOutcome::ApproveOnce { - 195
modified_command: None - 196
} - 197
); - 198
- 199
// Start in-place edit - 200
hil.start_editing(); - 201
assert!(hil.is_editing); - 202
- 203
// Backspace 11 times to remove "--port 5173" - 204
for _ in 0..11 { - 205
hil.delete_backspace(); - 206
} - 207
// Type replacement - 208
for c in "--port 3000".chars() { - 209
hil.insert_char(c); - 210
} - 211
- 212
assert_eq!( - 213
hil.edited_command, - 214
"npm run build && vite preview --port 3000" - 215
); - 216
- 217
let outcome_edited = hil.finish_approval(); - 218
assert_eq!( - 219
outcome_edited, - 220
HilOutcome::ApproveOnce { - 221
modified_command: Some("npm run build && vite preview --port 3000".into()) - 222
} - 223
); - 224
- 225
// Cancel edit restores original - 226
hil.cancel_editing(); - 227
assert_eq!(hil.edited_command, hil.original_command); - 228
assert!(!hil.is_editing); - 229
- 230
// Toggle remember mode - 231
hil.toggle_remember(); - 232
assert_eq!(hil.remember, RememberMode::Yes); - 233
hil.toggle_remember(); - 234
assert_eq!(hil.remember, RememberMode::No); - 235
} - 236
- 237
#[test] - 238
fn test_graphics_halfblock_render() { - 239
// Verify we can construct and render a minimal image. - 240
use vak_terminal::graphics::{GraphicsProtocol, HalfBlockImage}; - 241
let proto = GraphicsProtocol::detect(); - 242
assert!(matches!( - 243
proto, - 244
GraphicsProtocol::Kitty - 245
| GraphicsProtocol::ITerm2 - 246
| GraphicsProtocol::Sixel - 247
| GraphicsProtocol::HalfBlockAnsi - 248
)); - 249
- 250
let img = HalfBlockImage::placeholder(10, 4); - 251
assert_eq!(img.width, 10); - 252
assert_eq!(img.height, 4); - 253
assert_eq!(img.cells.len(), 40); - 254
- 255
let mut buf = Buffer::empty(Rect::new(0, 0, 10, 4)); - 256
img.render(Rect::new(0, 0, 10, 4), &mut buf); - 257
let sym = buf.cell((0, 0)).map(|c| c.symbol()); - 258
assert!(sym.is_some()); - 259
} - 260
- 261
#[test] - 262
fn test_telemetry_tick_and_rates() { - 263
let mut telem = TelemetryState::new(); - 264
let initial_angle = telem.radar_angle_deg; - 265
- 266
telem.tick(); - 267
assert_eq!(telem.radar_angle_deg, (initial_angle + 6.0) % 360.0); - 268
- 269
telem.push_token_rate(175); - 270
assert_eq!(*telem.token_rate_history.last().unwrap(), 175); - 271
- 272
// Push 70 rates to test max 60 buffer bound - 273
for i in 0..70 { - 274
telem.push_token_rate(i); - 275
} - 276
assert_eq!(telem.token_rate_history.len(), 60); - 277
- 278
// Default telemetry should not have hardcoded values - 279
let default = TelemetryState::default(); - 280
assert!(!default.connected); - 281
assert!(default.last_error.is_none()); - 282
assert_eq!(default.cpu_percent, 0.0); - 283
assert_eq!(default.rss_mb, 0.0); - 284
assert_eq!(default.spend_today_usd, 0.0); - 285
assert_eq!(default.active_workers, 0); - 286
assert_eq!(default.bus_dlq_count, 0); - 287
assert_eq!(default.anthropic_latency_ms, 0); - 288
assert!(default.token_rate_history.is_empty()); - 289
assert_eq!(default.max_rate, 0); - 290
} - 291
- 292
#[test] - 293
fn test_telemetry_update_from_health() { - 294
let mut telem = TelemetryState::new(); - 295
let health = HealthReport::default(); - 296
telem.update_from_health(&health); - 297
// Default health: healthy=false, circuit_breaker_healthy=false - 298
assert!(!telem.connected); - 299
assert!(!telem.circuit_breaker_healthy); - 300
- 301
// Simulate a healthy response - 302
let mut healthy = HealthReport::default(); - 303
healthy.healthy = true; - 304
healthy.circuit_breaker_healthy = true; - 305
telem.update_from_health(&healthy); - 306
assert!(telem.connected); - 307
assert!(telem.circuit_breaker_healthy); - 308
- 309
// Error handling - 310
telem.set_connection_error("server down".into()); - 311
assert!(!telem.connected); - 312
assert_eq!(telem.last_error.as_deref(), Some("server down")); - 313
- 314
// Simulate a healthy response to clear the error - 315
let mut healthy = HealthReport::default(); - 316
healthy.healthy = true; - 317
healthy.circuit_breaker_healthy = true; - 318
telem.update_from_health(&healthy); - 319
telem.clear_error(); - 320
assert!(telem.connected); - 321
assert!(telem.last_error.is_none()); - 322
} - 323
- 324
#[tokio::test] - 325
async fn test_app_key_and_mouse_dispatch() { - 326
use vak_terminal::app::DeckFocus; - 327
- 328
let mut app = make_test_app(); - 329
assert_eq!(app.active_tab, ActiveTab::Studio); - 330
assert_eq!(app.deck_focus, DeckFocus::Left); - 331
assert!(!app.connected); - 332
- 333
// Tab key cycles deck focus (Left <-> Right) - 334
app.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::empty())); - 335
assert_eq!(app.deck_focus, DeckFocus::Right); - 336
- 337
app.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::empty())); - 338
assert_eq!(app.deck_focus, DeckFocus::Left); - 339
- 340
// Space key toggles card expansion in Studio - 341
assert!(!app.card_expanded); - 342
app.handle_key(KeyEvent::new(KeyCode::Char(' '), KeyModifiers::empty())); - 343
assert!(app.card_expanded); - 344
app.handle_key(KeyEvent::new(KeyCode::Char(' '), KeyModifiers::empty())); - 345
assert!(!app.card_expanded); - 346
- 347
// Number keys switch active tabs directly - 348
app.handle_key(KeyEvent::new(KeyCode::Char('2'), KeyModifiers::empty())); - 349
assert_eq!(app.active_tab, ActiveTab::Observability); - 350
- 351
app.handle_key(KeyEvent::new(KeyCode::Char('3'), KeyModifiers::empty())); - 352
assert_eq!(app.active_tab, ActiveTab::Admin); - 353
- 354
app.handle_key(KeyEvent::new(KeyCode::Char('4'), KeyModifiers::empty())); - 355
assert_eq!(app.active_tab, ActiveTab::Inbox); - 356
- 357
// Test Inbox hotkeys: 'a' (ack), 'p' (promote), 'r' (restart) - 358
assert!(!app.alert_acknowledged); - 359
assert!(!app.skill_promoted); - 360
assert!(!app.watchdog_restarted); - 361
app.handle_key(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty())); - 362
assert!(app.alert_acknowledged); - 363
app.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::empty())); - 364
assert!(app.skill_promoted); - 365
app.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::empty())); - 366
assert!(app.watchdog_restarted); - 367
- 368
// Switch back to Studio - 369
app.handle_key(KeyEvent::new(KeyCode::Char('1'), KeyModifiers::empty())); - 370
assert_eq!(app.active_tab, ActiveTab::Studio); - 371
- 372
// Mouse click on header row (row 0) - 373
// Click on Admin tab (col ~60) - 374
app.handle_mouse(MouseEvent { - 375
kind: MouseEventKind::Down(MouseButton::Left), - 376
column: 60, - 377
row: 0, - 378
modifiers: KeyModifiers::empty(), - 379
}); - 380
assert_eq!(app.active_tab, ActiveTab::Admin); - 381
- 382
// Mouse click on Permission Mode in Admin (col 10, row 12 -> ReadOnly) - 383
app.handle_mouse(MouseEvent { - 384
kind: MouseEventKind::Down(MouseButton::Left), - 385
column: 10, - 386
row: 12, - 387
modifiers: KeyModifiers::empty(), - 388
}); - 389
assert_eq!(app.selected_permission_mode, "ReadOnly"); - 390
- 391
// Mouse click on Approval Mode (col 30, row 18 -> AutoApprove) - 392
app.handle_mouse(MouseEvent { - 393
kind: MouseEventKind::Down(MouseButton::Left), - 394
column: 30, - 395
row: 18, - 396
modifiers: KeyModifiers::empty(), - 397
}); - 398
assert_eq!(app.approval_mode, "AutoApprove"); - 399
- 400
// Mouse click on Pending Authorization Queue Approve (col 65, row 18) - 401
assert_eq!(app.pending_chat_status, None); - 402
app.handle_mouse(MouseEvent { - 403
kind: MouseEventKind::Down(MouseButton::Left), - 404
column: 65, - 405
row: 18, - 406
modifiers: KeyModifiers::empty(), - 407
}); - 408
assert_eq!(app.pending_chat_status, Some(true)); - 409
- 410
// F2 key cycles theme - 411
let initial_theme = app.theme.kind; - 412
app.handle_key(KeyEvent::new(KeyCode::F(2), KeyModifiers::empty())); - 413
assert_ne!(app.theme.kind, initial_theme); - 414
- 415
// Test hotkeys 'm' and 'p' in Admin tab - 416
app.active_tab = ActiveTab::Admin; - 417
app.handle_key(KeyEvent::new(KeyCode::Char('m'), KeyModifiers::empty())); - 418
assert_eq!(app.selected_permission_mode, "FullAccess"); - 419
app.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::empty())); - 420
assert_eq!(app.approval_mode, "Ask"); - 421
- 422
// Hotkey 'd' sets pending_chat_status to Some(false) - 423
app.handle_key(KeyEvent::new(KeyCode::Char('d'), KeyModifiers::empty())); - 424
assert_eq!(app.pending_chat_status, Some(false)); - 425
- 426
// /hil command without any pending chats should open an empty modal - 427
// from the pending_chats list. With no pending chats, hil_state stays None. - 428
app.composer.buffer = "/hil".into(); - 429
app.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::empty())); - 430
// No pending chats in test, so hil_state should be None - 431
assert!(app.hil_state.is_none()); - 432
- 433
// Simulate a real approval request arriving via SSE - 434
let approval_event = serde_json::json!({ - 435
"ApprovalRequested": { - 436
"id": "req-real-123", - 437
"tool": "bash", - 438
"args_json": r#"{"command":"echo hello"}"#, - 439
"reason": "Testing real approval" - 440
} - 441
}); - 442
app.handle_terminal_event(vak_terminal::api::TerminalEvent::Agent { - 443
event: approval_event, - 444
seq: 42, - 445
}); - 446
assert!(app.hil_state.is_some()); - 447
let hil = app.hil_state.as_ref().unwrap(); - 448
assert_eq!(hil.request_id, "req-real-123"); - 449
assert_eq!(hil.tool_name, "bash"); - 450
- 451
// Approve the request - 452
app.dispatch_approval(HilOutcome::ApproveOnce { - 453
modified_command: None, - 454
}); - 455
// The HIL state should be cleared after dispatch - 456
assert!(app.hil_state.is_none()); - 457
- 458
// Test that pending chats were populated from the approval event - 459
assert!(!app.pending_chats.is_empty()); - 460
assert_eq!(app.pending_chats[0].request_id, "req-real-123"); - 461
- 462
// Test health event handling - 463
let mut healthy = HealthReport::default(); - 464
healthy.healthy = true; - 465
healthy.circuit_breaker_healthy = true; - 466
healthy.provider = "anthropic".into(); - 467
healthy.model = "claude-3-7-sonnet".into(); - 468
healthy.permission_mode = "WorkspaceWrite".into(); - 469
app.handle_terminal_event(vak_terminal::api::TerminalEvent::Health(healthy.clone())); - 470
assert!(app.connected); - 471
assert_eq!(app.model_name, "claude-3-7-sonnet"); - 472
assert_eq!(app.selected_permission_mode, "WorkspaceWrite"); - 473
- 474
// Test health error - 475
app.handle_terminal_event(vak_terminal::api::TerminalEvent::Error( - 476
"connection lost".into(), - 477
)); - 478
assert!(!app.connected); - 479
assert_eq!(app.telemetry.last_error.as_deref(), Some("connection lost")); - 480
- 481
// Test connected event - 482
app.handle_terminal_event(vak_terminal::api::TerminalEvent::Connected); - 483
assert!(app.connected); - 484
assert!(app.telemetry.last_error.is_none()); - 485
- 486
// Test incident recording from RouteFallback - 487
let fallback_event = serde_json::json!({ - 488
"RouteFallback": { - 489
"to_provider": "openai", - 490
"to_model": "gpt-4o" - 491
} - 492
}); - 493
let mut before = app.incidents.len(); - 494
app.handle_terminal_event(vak_terminal::api::TerminalEvent::Agent { - 495
event: fallback_event, - 496
seq: 99, - 497
}); - 498
assert_eq!(app.incidents.len(), before + 1); - 499
assert_eq!( - 500
app.incidents.last().unwrap().severity, - 501
IncidentSeverity::Warn - 502
); - 503
- 504
// Test ProviderError -> Critical incident - 505
let error_event = serde_json::json!({ - 506
"ProviderError": { - 507
"provider": "anthropic", - 508
"error": "rate limited" - 509
} - 510
}); - 511
before = app.incidents.len(); - 512
app.handle_terminal_event(vak_terminal::api::TerminalEvent::Agent { - 513
event: error_event, - 514
seq: 100, - 515
}); - 516
assert_eq!(app.incidents.len(), before + 1); - 517
assert_eq!( - 518
app.incidents.last().unwrap().severity, - 519
IncidentSeverity::Critical - 520
); - 521
- 522
// Widget rendering with active HIL modal across all tabs without panic - 523
app.hil_state = Some(HilApprovalState::from_approval_event( - 524
"test-session", - 525
"req-ui-456", - 526
"bash", - 527
r#"{"command":"ls -la"}"#, - 528
"Filesystem listing", - 529
"/workspace", - 530
None, - 531
)); - 532
- 533
let mut buf = Buffer::empty(Rect::new(0, 0, 100, 30)); - 534
(&app).render(Rect::new(0, 0, 100, 30), &mut buf); - 535
- 536
app.active_tab = ActiveTab::Observability; - 537
(&app).render(Rect::new(0, 0, 100, 30), &mut buf); - 538
- 539
app.active_tab = ActiveTab::Admin; - 540
(&app).render(Rect::new(0, 0, 100, 30), &mut buf); - 541
- 542
app.active_tab = ActiveTab::Inbox; - 543
(&app).render(Rect::new(0, 0, 100, 30), &mut buf); - 544
- 545
// Dismiss the HIL modal with Escape - 546
app.handle_key(KeyEvent::new(KeyCode::Esc, KeyModifiers::empty())); - 547
assert!(app.hil_state.is_none()); - 548
- 549
// Symbols test - 550
assert_eq!(Symbols::PROMPT_CHEVRON, "❯"); - 551
assert_eq!(Symbols::STATUS_ACTIVE, "●"); - 552
} - 553
- 554
// ---- Real server integration tests ------------------------------------------ - 555
// These tests connect to a live vak server (if VAK_TEST_URL + VAK_TEST_TOKEN - 556
// are set in the environment). They are #[ignore]d by default — run with: - 557
// cargo test -p vak-terminal --test terminal_tests -- --include-ignored - 558
// VAK_TEST_URL=http://127.0.0.1:8901 VAK_TEST_TOKEN=$VAK_GATEWAY_TOKEN - 559
- 560
mod real_server { - 561
use super::*; - 562
- 563
fn maybe_server() -> Option<(String, String)> { - 564
let url = std::env::var("VAK_TEST_URL") - 565
.or_else(|_| std::env::var("VAK_SERVER_URL")) - 566
.ok()?; - 567
let token = std::env::var("VAK_TEST_TOKEN") - 568
.or_else(|_| std::env::var("VAK_GATEWAY_TOKEN")) - 569
.ok()?; - 570
if url.is_empty() || token.is_empty() { - 571
return None; - 572
} - 573
Some((url, token)) - 574
} - 575
- 576
fn require_server() -> (String, String) { - 577
match maybe_server() { - 578
Some((url, token)) => (url, token), - 579
None => { - 580
eprintln!("SKIP: set VAK_TEST_URL + VAK_TEST_TOKEN to run real-server tests"); - 581
panic!("no vak server configured for live test"); - 582
} - 583
} - 584
} - 585
- 586
#[tokio::test] - 587
#[ignore] - 588
async fn real_health() { - 589
let (url, token) = require_server(); - 590
let client = ApiClient::new(&url, &token); - 591
let health = client.health().await.expect("health() must succeed"); - 592
assert!(!health.provider.is_empty(), "provider should be set"); - 593
assert!(!health.model.is_empty(), "model should be set"); - 594
assert!(health.context_window > 0, "context_window should be > 0"); - 595
assert!( - 596
!health.provider.is_empty(), - 597
"provider must resolve from health" - 598
); - 599
} - 600
- 601
#[tokio::test] - 602
#[ignore] - 603
async fn real_sessions() { - 604
let (url, token) = require_server(); - 605
let client = ApiClient::new(&url, &token); - 606
let sessions = client.list_sessions().await.expect("list_sessions"); - 607
eprintln!("sessions returned: {}", sessions.len()); - 608
for s in &sessions { - 609
assert!(!s.session_id.is_empty(), "session id must be non-empty"); - 610
} - 611
} - 612
- 613
#[tokio::test] - 614
#[ignore] - 615
async fn real_providers() { - 616
let (url, token) = require_server(); - 617
let client = ApiClient::new(&url, &token); - 618
let providers = client.list_providers().await.expect("list_providers"); - 619
assert!( - 620
!providers.current.is_empty(), - 621
"current provider must be set" - 622
); - 623
assert!(!providers.providers.is_empty(), "must have provider list"); - 624
} - 625
- 626
#[tokio::test] - 627
#[ignore] - 628
async fn real_mcp() { - 629
let (url, token) = require_server(); - 630
let client = ApiClient::new(&url, &token); - 631
let mcp = client.get_mcp_servers().await.expect("get_mcp_servers"); - 632
eprintln!("mcp servers configured: {}", mcp.len()); - 633
} - 634
- 635
#[tokio::test] - 636
#[ignore] - 637
async fn real_gateway_approvals() { - 638
let (url, token) = require_server(); - 639
let client = ApiClient::new(&url, &token); - 640
let gw = client.get_gateway_approvals().await.expect("get_gateway"); - 641
assert!(!gw.mode.is_empty(), "gateway mode must be set"); - 642
} - 643
- 644
#[tokio::test] - 645
#[ignore] - 646
async fn real_config() { - 647
let (url, token) = require_server(); - 648
let client = ApiClient::new(&url, &token); - 649
let config = client.get_config().await.expect("get_config"); - 650
assert!( - 651
config.permission_mode.is_some() - 652
|| config.approval_mode.is_some() - 653
|| config.theme.is_some(), - 654
"config should have at least one populated field" - 655
); - 656
} - 657
- 658
#[tokio::test] - 659
#[ignore] - 660
async fn real_create_and_attach() { - 661
let (url, token) = require_server(); - 662
let client = ApiClient::new(&url, &token); - 663
let sid = client.create_session().await.expect("create_session"); - 664
assert!(!sid.is_empty(), "session id must be non-empty"); - 665
let attach = client.attach_session(&sid).await.expect("attach_session"); - 666
assert!( - 667
!attach.session_id.is_empty(), - 668
"attach must return session id" - 669
); - 670
} - 671
- 672
#[tokio::test] - 673
#[ignore] - 674
async fn real_control_state() { - 675
let (url, token) = require_server(); - 676
let client = ApiClient::new(url, token); - 677
let sessions = client.list_sessions().await.expect("list_sessions"); - 678
let sid = if let Some(s) = sessions.first() { - 679
s.session_id.clone() - 680
} else { - 681
client.create_session().await.expect("create_session") - 682
}; - 683
// The control-state endpoint may not exist on older server builds. - 684
// A 404 is acceptable — we just verify the client handles it as an error. - 685
match client.get_control_state(&sid).await { - 686
Ok(_cs) => eprintln!("control_state fetched ok"), - 687
Err(ApiError::Server { status: 404, .. }) => { - 688
eprintln!("control-state endpoint not available on this server build (404)") - 689
} - 690
Err(e) => panic!("get_control_state failed unexpectedly: {e:?}"), - 691
} - 692
} - 693
- 694
#[tokio::test] - 695
#[ignore] - 696
async fn real_full_app_flow() { - 697
// This test builds a TerminalApp from real server data and verifies - 698
// the whole pipeline works end-to-end. - 699
let (url, token) = require_server(); - 700
let api = Arc::new(ApiClient::new(&url, &token)); - 701
let health = api.health().await.expect("health"); - 702
assert!(!health.provider.is_empty(), "provider must resolve"); - 703
- 704
let sessions = api.list_sessions().await.unwrap_or_default(); - 705
let session_id = sessions - 706
.first() - 707
.map(|s| s.session_id.clone()) - 708
.unwrap_or_else(|| "test-session-real".to_string()); - 709
- 710
let providers = api.list_providers().await.unwrap_or_default(); - 711
let models = if !providers.current.is_empty() { - 712
api.discover_models(&providers.current) - 713
.await - 714
.unwrap_or_default() - 715
} else { - 716
Vec::new() - 717
}; - 718
- 719
// Build the app with real data — verifies all builder methods work - 720
let _app = TerminalApp::new_with_api( - 721
&session_id, - 722
health.model.clone(), - 723
health.clone(), - 724
api.clone(), - 725
) - 726
.with_workspace("real-test") - 727
.with_providers(providers) - 728
.with_models(models) - 729
.with_mcp_servers(api.get_mcp_servers().await.unwrap_or_default()) - 730
.with_gateway(api.get_gateway_approvals().await.unwrap_or_default()) - 731
.with_config(api.get_config().await.unwrap_or_default()) - 732
.with_launch_servers( - 733
if !session_id.is_empty() && !session_id.starts_with("test-session-real") { - 734
api.get_launch_servers(&session_id) - 735
.await - 736
.unwrap_or_default() - 737
} else { - 738
Vec::new() - 739
}, - 740
) - 741
.with_sessions(sessions); - 742
- 743
// Verify the app picked up real health data - 744
// (The app stores health.provider via update_from_health in on_tick, - 745
// but TerminalApp::new_with_api sets health directly.) - 746
assert_eq!(_app.health.provider, health.provider); - 747
assert!(!_app.health.model.is_empty()); - 748
} - 749
} - 750
- 751
#[test] - 752
fn test_api_client_url_construction() { - 753
let client = ApiClient::new("http://127.0.0.1:8901", "secret"); - 754
assert_eq!(client.base_url(), "http://127.0.0.1:8901"); - 755
// internal_url is private, but we can verify base_url - 756
} - 757
- 758
#[test] - 759
fn test_api_client_auth_is_bearer() { - 760
// The token must never be echoed or logged — it is only used as - 761
// "Bearer <token>" in the Authorization header. - 762
let client = ApiClient::new("http://localhost:8901", "my-secret-key"); - 763
// Verify the client was constructed with the correct base URL. - 764
assert_eq!(client.base_url(), "http://localhost:8901"); - 765
// The token is not exposed via any public API — only used internally - 766
// for the Authorization header. - 767
} - 768
- 769
#[test] - 770
fn test_provider_list_default() { - 771
let list = ProviderList::default(); - 772
assert!(list.providers.is_empty()); - 773
assert_eq!(list.current, ""); - 774
assert_eq!(list.current_model, ""); - 775
assert!(!list.current_configured); - 776
} - 777
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.