test(tui): cover keyless recovery with a real Engine error

Submit a keyless route to Engine::new, project the typed authentication
error through the real UI handler, then project a real config
acknowledgement and render at 80x24 and 140x40. A loopback server guards
against provider I/O and must receive zero requests. This replaces the
old WIP premise that Engine construction returns a missing-key error.

Validation: cargo fmt --all -- --check and git diff --check pass.
Rust tests were not run locally; hosted CI must qualify this head.

Refs #6455

Signed-off-by: CodeWhale Bot <[email protected]>
This commit is contained in:
CodeWhale Bot committed 2026-09-23 14:24:45 -07:00
1 parent 25e32b05ce
commit 795906a3d8
1 file changed
+141
+141
View File
@@ -25229,6 +25229,147 @@ fn non_recoverable_engine_error_enters_offline_mode() {
assert!(app.pending_provider_switch.is_none());
}
/// U1: use the real Engine's typed auth failure, not the old WIP assumption
/// that Engine construction fails synchronously. A routine acknowledgement
/// may replace the footer; the recovery must still be readable in the frame.
#[tokio::test]
async fn keyless_engine_error_stays_visible_after_a_config_ack() {
use crate::core::engine::{Engine, EngineConfig};
use crate::core::ops::{Op, TurnSpec, UserInputProvenance};
use crate::error_taxonomy::ErrorCategory;
let _home = SettingsHomeGuard::new();
let _key = crate::test_support::EnvVarGuard::remove("DEEPSEEK_API_KEY");
let workspace = TempDir::new().expect("workspace");
// Even an accidental credential fallback can only reach this local
// server. Requiring zero requests proves rejection before provider I/O.
let server = wiremock::MockServer::start().await;
let config = Config {
provider: Some("deepseek".to_string()),
api_key: Some(String::new()),
providers: Some(ProvidersConfig {
deepseek: ProviderConfig {
base_url: Some(format!("{}/v1", server.uri())),
auth_mode: Some("api_key".to_string()),
..ProviderConfig::default()
},
..ProvidersConfig::default()
}),
..Config::default()
};
let mut app = create_test_app();
app.workspace = workspace.path().to_path_buf();
app.onboarding = OnboardingState::None;
app.launch.visible = false;
app.api_key_env_only = false;
app.is_loading = true;
app.runtime_turn_status = Some("in_progress".to_string());
app.add_message(HistoryCell::User {
content: "hello without a key".to_string(),
});
let (engine, handle) = Engine::new(
EngineConfig {
workspace: workspace.path().to_path_buf(),
snapshots_enabled: false,
subagents_enabled: false,
terminal_chrome_enabled: false,
..EngineConfig::default()
},
&config,
);
let run = tokio::spawn(engine.run());
handle
.send(Op::SendMessage(TurnSpec {
content: "hello without a key".to_string(),
images: Vec::new(),
mode: AppMode::Agent,
route: Box::new(
resolve_runtime_route(&config, ApiProvider::Deepseek, Some(&app.model))
.expect("structural route resolution"),
),
compaction: Box::default(),
initial_routed_usage: Box::default(),
max_output_tokens: None,
goal_objective: None,
goal_token_budget: None,
goal_status: crate::tools::goal::GoalStatus::Active,
reasoning_effort: None,
reasoning_effort_auto: false,
auto_model: false,
allow_shell: false,
trust_mode: false,
auto_approve: false,
approval_mode: ApprovalMode::Suggest,
translation_enabled: false,
allowed_tools: None,
dynamic_tools: Vec::new(),
hook_executor: None,
verbosity: None,
provenance: UserInputProvenance::ExternalUser,
}))
.await
.expect("submit to the real Engine");
let mut events = handle.rx_event.write().await;
loop {
let event = tokio::time::timeout(Duration::from_secs(10), events.recv())
.await
.expect("keyless Engine response")
.expect("Engine event");
if let EngineEvent::Error { envelope, .. } = event {
assert_eq!(envelope.category, ErrorCategory::Authentication);
assert!(!envelope.recoverable);
assert!(envelope.message.contains("API key not found"));
apply_engine_error_to_app(&mut app, envelope);
break;
}
}
let mut compaction = crate::compaction::CompactionConfig::default();
compaction.enabled = !compaction.enabled;
handle
.send(Op::SetCompaction { config: compaction })
.await
.expect("change config after the failed turn");
loop {
let event = tokio::time::timeout(Duration::from_secs(10), events.recv())
.await
.expect("config acknowledgement")
.expect("Engine event");
if let EngineEvent::Status { message } = event {
// Same projection as the event loop's Status arm.
app.status_message = Some(message);
break;
}
}
drop(events);
assert!(app.offline_mode);
assert!(!app.is_loading);
for (width, height) in [(80, 24), (140, 40)] {
let frame = render_test_app(&mut app, &config, width, height);
let text = frame.split_whitespace().collect::<Vec<_>>().join(" ");
assert!(
text.contains("API key not found"),
"{width}x{height}: {text}"
);
assert!(
text.contains("codewhale auth set"),
"{width}x{height}: {text}"
);
}
assert!(
server
.received_requests()
.await
.expect("requests")
.is_empty()
);
handle.send(Op::Shutdown).await.expect("shutdown");
tokio::time::timeout(Duration::from_secs(10), run)
.await
.expect("Engine shutdown")
.expect("Engine task");
}
#[test]
fn env_only_auth_failure_reopens_provider_onboarding() {
use crate::error_taxonomy::ErrorEnvelope;