mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
fix(startup): recover from core port 7788 conflict automatically (#2626)
This commit is contained in:
@@ -18,6 +18,7 @@ use tempfile::tempdir;
|
||||
|
||||
use openhuman_core::core::auth::{init_rpc_token, CORE_TOKEN_ENV_VAR};
|
||||
use openhuman_core::core::jsonrpc::build_core_http_router;
|
||||
use openhuman_core::openhuman::connectivity::rpc::pick_listen_port;
|
||||
use openhuman_core::openhuman::memory_tree::all_memory_tree_registered_controllers;
|
||||
|
||||
const TEST_RPC_TOKEN: &str = "json-rpc-e2e-local-token";
|
||||
@@ -7705,3 +7706,102 @@ async fn json_rpc_config_autonomy_settings_roundtrip() {
|
||||
mock_join.abort();
|
||||
rpc_join.abort();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Port-conflict recovery E2E
|
||||
// ---------------------------------------------------------------------------
|
||||
//
|
||||
// Verifies that when the preferred core port (7788) is already occupied, the
|
||||
// RPC stack starts successfully on a fallback port and remains fully
|
||||
// reachable. A second pass confirms that once the blocker is dropped, port
|
||||
// 7788 becomes available again — matching the "repro gone" acceptance
|
||||
// criterion from issue #2617.
|
||||
|
||||
#[tokio::test]
|
||||
async fn port_conflict_recovery_core_starts_on_fallback_port_e2e() {
|
||||
let _env_lock = json_rpc_e2e_env_lock();
|
||||
|
||||
// ── 1. occupy port 7788 with a dummy listener ─────────────────────────
|
||||
// Use std::net so the binding is synchronous and stable before we call
|
||||
// pick_listen_port.
|
||||
let blocker =
|
||||
std::net::TcpListener::bind("127.0.0.1:7788").expect("bind blocker on 7788 for e2e test");
|
||||
blocker
|
||||
.set_nonblocking(true)
|
||||
.expect("set blocker non-blocking");
|
||||
|
||||
// ── 2. pick_listen_port should fall back to 7789–7798 ────────────────
|
||||
let pick_result = pick_listen_port(7788)
|
||||
.await
|
||||
.expect("pick_listen_port must succeed when port is occupied by non-OpenHuman process");
|
||||
assert!(
|
||||
pick_result.fallback_from.is_some(),
|
||||
"expected fallback_from to be Some(7788) when preferred port is occupied, got None"
|
||||
);
|
||||
assert_eq!(
|
||||
pick_result.fallback_from,
|
||||
Some(7788),
|
||||
"fallback_from should record the originally preferred port"
|
||||
);
|
||||
let fallback_port = pick_result.port;
|
||||
assert!(
|
||||
(7789..=7798).contains(&fallback_port),
|
||||
"fallback port {fallback_port} should be in the 7789–7798 range"
|
||||
);
|
||||
|
||||
// ── 3. serve the core router on the fallback listener ────────────────
|
||||
ensure_test_rpc_auth();
|
||||
let router = build_core_http_router(false);
|
||||
let listener =
|
||||
tokio::net::TcpListener::from_std(pick_result.listener.into_std().expect("into_std"))
|
||||
.expect("from_std");
|
||||
let addr: SocketAddr = listener.local_addr().expect("local_addr");
|
||||
let rpc_join = tokio::spawn(async move { axum::serve(listener, router).await });
|
||||
let rpc_base = format!("http://{addr}");
|
||||
assert_eq!(
|
||||
addr.port(),
|
||||
fallback_port,
|
||||
"server should be on the fallback port"
|
||||
);
|
||||
|
||||
// ── 4. RPC health-check on the fallback port ─────────────────────────
|
||||
let diag = post_json_rpc(&rpc_base, 26170, "openhuman.connectivity_diag", json!({})).await;
|
||||
// JSON-RPC result envelope → inner cli-compatible wrapper → diag payload.
|
||||
// post_json_rpc returns the raw JSON-RPC response; assert_no_jsonrpc_error
|
||||
// unwraps the outer "result". The inner value is {"logs":[...],"result":{"diag":{...}}},
|
||||
// so we need one more "result" hop before accessing "diag".
|
||||
let outer = assert_no_jsonrpc_error(&diag, "connectivity_diag on fallback port");
|
||||
let inner = outer.get("result").unwrap_or_else(|| {
|
||||
panic!("connectivity_diag outer result missing 'result' key; got: {outer}")
|
||||
});
|
||||
let diag_payload = inner.get("diag").unwrap_or_else(|| {
|
||||
panic!("connectivity_diag inner result missing 'diag' key; got: {inner}")
|
||||
});
|
||||
assert!(
|
||||
diag_payload.get("sidecar_pid").is_some(),
|
||||
"connectivity_diag should return sidecar_pid field; got: {diag_payload}"
|
||||
);
|
||||
assert!(
|
||||
diag_payload.get("listen_port").is_some(),
|
||||
"connectivity_diag should return listen_port field; got: {diag_payload}"
|
||||
);
|
||||
|
||||
rpc_join.abort();
|
||||
|
||||
// ── 5. drop blocker — verify 7788 is now free ────────────────────────
|
||||
drop(blocker);
|
||||
|
||||
let after_drop = pick_listen_port(7788)
|
||||
.await
|
||||
.expect("pick_listen_port should succeed with 7788 free");
|
||||
assert_eq!(
|
||||
after_drop.port, 7788,
|
||||
"after releasing the blocker, pick_listen_port should bind directly on 7788"
|
||||
);
|
||||
assert!(
|
||||
after_drop.fallback_from.is_none(),
|
||||
"fallback_from should be None when 7788 is free"
|
||||
);
|
||||
// Release the listener so the port is not held across tests.
|
||||
drop(after_drop.listener);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user