Files
codex/codex-rs/tui/tests/suite/daemon_compatibility.rs
T
Eric Traut d6093d3228 Offer explicit recovery for incompatible background servers (#47318)
## Why

Automatic daemon launches could silently fall back to embedded mode when shared feature settings differed from the session's requirements. Changing those settings affects other clients, and restarting the server may interrupt active or queued work.

## What changed

- Offer interactive choices to run without the daemon, restart with the required settings, or cancel. Default to cancel and require explicit confirmation for restart.
- Allow restart only for managed daemons with a feature mismatch, then recheck compatibility once. Noninteractive required-daemon failures return an error with `--no-daemon` guidance; optional attachment retains its fallback behavior.
- Preserve saved feature overrides on startup and require confirmation before applying requested shared-feature disables, including on fresh starts.
- Merge confirmed settings under the lifecycle lock, persist changes after the old process stops, and avoid restarting when the saved overrides already match.

## Testing

Add recovery-menu snapshots and confirmation tests, compatibility tests for required and optional attachment, daemon ownership and settings-preservation tests, and CLI terminal tests for cancellation, embedded fallback, confirmed restart, and disabling shared features.

GitOrigin-RevId: d50a6cf7e476b9647237431e8fc0fc6041ed17db
2026-09-22 16:07:38 +00:00

105 lines
4.4 KiB
Rust

//! Exercises daemon compatibility fallback through real TUI startup.
use super::focus_palette::PtyCodex;
use super::focus_palette::write_test_config;
use anyhow::Context;
use anyhow::Result;
use anyhow::ensure;
use codex_app_server_protocol::JSONRPCMessage;
use futures::SinkExt;
use futures::StreamExt;
use pretty_assertions::assert_eq;
use serde_json::json;
use tokio::net::UnixListener;
use tokio_tungstenite::tungstenite::Message;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn incompatible_daemon_falls_back_for_default_and_explicit_features() -> Result<()> {
for scenario in ["default", "explicit", "host policy"] {
let cwd = codex_utils_cargo_bin::repo_root()?;
let home = tempfile::tempdir_in("/tmp")?;
write_test_config(home.path(), &cwd)?;
if scenario == "host policy" {
let path = home.path().join("config.toml");
let contents = std::fs::read_to_string(&path)?;
std::fs::write(
path,
format!(
"features.code_mode_host = {{enabled=false, disable_in_process_fallback=true}}\n{contents}"
),
)?;
}
let socket = codex_app_server_client::app_server_control_socket_path(home.path())?;
std::fs::create_dir_all(socket.parent().unwrap())?;
let listener = UnixListener::bind(socket.as_path())?;
let server = tokio::spawn(async move {
let mut socket = loop {
let (stream, _) = listener.accept().await?;
if let Ok(socket) = tokio_tungstenite::accept_async(stream).await {
break socket;
}
};
while let Some(Ok(Message::Text(text))) = socket.next().await {
let JSONRPCMessage::Request(request) = serde_json::from_str(&text)? else {
continue;
};
let response = if request.method == "initialize" {
json!({"id": request.id, "result": {"userAgent": "daemon-test/0.0.0"}})
} else {
assert_eq!(request.method, "experimentalFeature/list");
json!({"id": request.id, "result": {"data": [{
"name": "api_key_model_discovery", "stage": "underDevelopment",
"displayName": null, "description": null, "announcement": null,
"enabled": scenario == "default", "defaultEnabled": false,
}], "nextCursor": null}})
};
socket
.send(Message::Text(response.to_string().into()))
.await?;
}
Ok::<_, anyhow::Error>(())
});
let args = if scenario == "explicit" {
vec!["-c", "features.api_key_model_discovery=true"]
} else {
vec![]
};
let mut terminal = PtyCodex::start(&cwd, home, &args)?;
terminal.wait_for_startup()?;
terminal.wait_for_screen("warning")?;
terminal.write_input(b"\x14")?;
let (snapshot, warning_end) = match scenario {
"default" => (
"daemon_feature_mismatch",
"api_key_model_discovery to be disabled.",
),
"explicit" => (
"daemon_override_mismatch",
"api_key_model_discovery to be enabled.",
),
"host policy" => ("daemon_host_policy_mismatch", "requires embedded mode."),
_ => unreachable!(),
};
terminal.wait_for_screen(warning_end)?;
let screen = terminal.screen_contents();
let warning = screen
.lines()
.find(|line| line.contains("Running without the shared background server"))
.context("missing warning line")?;
insta::assert_snapshot!(snapshot, warning);
terminal.write_input(b"\x14")?;
terminal.write_input(b"/status")?;
terminal.wait_for_screen("show current session configuration")?;
terminal.read_output(std::time::Duration::from_millis(/*millis*/ 200))?;
terminal.write_input(b"\r")?;
terminal.wait_for_screen("Model:")?;
ensure!(!terminal.screen_contains("unix://"));
if scenario == "host policy" {
server.abort();
continue;
}
tokio::time::timeout(std::time::Duration::from_secs(/*secs*/ 5), server).await???;
}
Ok(())
}