fix(startup): recover from core port 7788 conflict automatically (#2626)

This commit is contained in:
Mega Mind
2026-05-25 20:09:08 +05:30
committed by GitHub
parent d997394116
commit cdee8f73fb
27 changed files with 1209 additions and 45 deletions
+100
View File
@@ -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 77897798 ────────────────
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 77897798 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);
}