chore: import upstream snapshot with attribution
FreeBSD Smoke / FreeBSD Smoke (x86_64) (push) Has been cancelled
CI / Quality Guardrails (push) Has been cancelled
CI / Build & Test (macos-latest) (push) Has been cancelled
CI / Build & Test (ubuntu-latest) (push) Has been cancelled
CI / Build & Test (windows-latest) (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / PowerShell Syntax (push) Has been cancelled
CI / Windows Cross-Target Check (Linux) (push) Has been cancelled
FreeBSD Smoke / FreeBSD Smoke (x86_64) (push) Has been cancelled
CI / Quality Guardrails (push) Has been cancelled
CI / Build & Test (macos-latest) (push) Has been cancelled
CI / Build & Test (ubuntu-latest) (push) Has been cancelled
CI / Build & Test (windows-latest) (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / PowerShell Syntax (push) Has been cancelled
CI / Windows Cross-Target Check (Linux) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,688 @@
|
||||
use crate::test_support::*;
|
||||
|
||||
// ============================================================================
|
||||
// Binary Integration Tests
|
||||
// These tests run the actual jcode binary and require real credentials.
|
||||
// Run with: cargo test --test e2e binary_integration -- --ignored
|
||||
// ============================================================================
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Reload/handoff robustness coverage map (for future contributors)
|
||||
//
|
||||
// Unit-level (no credentials, run by default):
|
||||
// - server::reload_state::tests + server::socket_tests: marker/handoff state
|
||||
// machine (Ready/Waiting/Failed/Idle verdicts, dead-pid crash detection,
|
||||
// stale/foreign/completed marker cleanup, Failed-marker preservation,
|
||||
// corrupt-marker tolerance, bounded handoff-event wait).
|
||||
// - server::reload::reload_tests: graceful shutdown signaling, timeout, and
|
||||
// partial-checkpoint behavior; recovery-intent persistence for peers.
|
||||
// - server::reload_recovery::tests: recovery-store path-traversal safety,
|
||||
// persist/peek roundtrip, non-consuming directive peek, delivery
|
||||
// idempotency + continuation mismatch.
|
||||
// - server::util::reload_target_tests: no-downgrade exec-target guard.
|
||||
//
|
||||
// E2E (real spawned process, run with --ignored; need a release binary):
|
||||
// - binary_integration_reload_handoff: server identity changes, marker clears.
|
||||
// - binary_integration_selfdev_reload_reconnects_quickly: repeated reloads.
|
||||
// - binary_integration_selfdev_client_reload_resumes_session.
|
||||
// - binary_integration_selfdev_full_reload_resumes_session_quickly.
|
||||
//
|
||||
// Known E2E gaps worth adding when a release binary is available:
|
||||
// - Concurrent/rapid `client.reload()` calls collapsing into one handoff
|
||||
// without stranding the client or leaving a stuck marker.
|
||||
// - A pre-existing *foreign* stale reload marker (different pid) in the
|
||||
// runtime dir at boot being cleared rather than blocking startup.
|
||||
// - Crash-during-boot of the replacement server (e.g. point the reload
|
||||
// candidate at a binary that exits non-zero) resolving the waiting client
|
||||
// to a Failed verdict instead of an indefinite hang.
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Test that the jcode binary can run independent with Claude provider
|
||||
#[tokio::test]
|
||||
#[ignore] // Requires Claude credentials
|
||||
async fn binary_integration_independent_claude() -> Result<()> {
|
||||
use std::process::Command;
|
||||
let _env = setup_test_env()?;
|
||||
|
||||
let output = Command::new("cargo")
|
||||
.args([
|
||||
"run",
|
||||
"--release",
|
||||
"--bin",
|
||||
"jcode",
|
||||
"--",
|
||||
"run",
|
||||
"Say 'test-ok' and nothing else",
|
||||
])
|
||||
.output()?;
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
|
||||
assert!(
|
||||
output.status.success() || stdout.contains("test") || stderr.contains("Claude"),
|
||||
"Binary should run successfully. stdout: {}, stderr: {}",
|
||||
stdout,
|
||||
stderr
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test that the jcode binary can run with OpenAI provider
|
||||
#[tokio::test]
|
||||
#[ignore] // Requires OpenAI/Codex credentials
|
||||
async fn binary_integration_openai_provider() -> Result<()> {
|
||||
use std::process::Command;
|
||||
let _env = setup_test_env()?;
|
||||
|
||||
let output = Command::new("cargo")
|
||||
.args([
|
||||
"run",
|
||||
"--release",
|
||||
"--bin",
|
||||
"jcode",
|
||||
"--",
|
||||
"--provider",
|
||||
"openai",
|
||||
"run",
|
||||
"Say 'openai-ok' and nothing else",
|
||||
])
|
||||
.output()?;
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
|
||||
// Check either success or identifiable OpenAI response
|
||||
let has_response = stdout.to_lowercase().contains("openai")
|
||||
|| stdout.to_lowercase().contains("ok")
|
||||
|| stderr.contains("OpenAI");
|
||||
|
||||
assert!(
|
||||
output.status.success() || has_response,
|
||||
"OpenAI provider should work. stdout: {}, stderr: {}",
|
||||
stdout,
|
||||
stderr
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test that jcode version command works
|
||||
#[tokio::test]
|
||||
async fn binary_version_command() -> Result<()> {
|
||||
use std::process::Command;
|
||||
let _env = setup_test_env()?;
|
||||
|
||||
let output = Command::new(env!("CARGO_BIN_EXE_jcode"))
|
||||
.arg("--version")
|
||||
.output()?;
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
|
||||
assert!(output.status.success(), "Version command should succeed");
|
||||
assert!(
|
||||
stdout.contains("jcode") || stdout.contains("20"),
|
||||
"Version should contain 'jcode' or date. Got: {}",
|
||||
stdout
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test full server reload handoff against a real spawned server process.
|
||||
///
|
||||
/// Requires a built release binary at target/release/jcode because the reload
|
||||
/// flow execs into the repo's reload candidate.
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn binary_integration_reload_handoff() -> Result<()> {
|
||||
let _env = setup_test_env()?;
|
||||
|
||||
let release_binary =
|
||||
jcode::build::release_binary_path(std::path::Path::new(env!("CARGO_MANIFEST_DIR")));
|
||||
if !release_binary.exists() {
|
||||
anyhow::bail!(
|
||||
"release binary missing at {} (run `cargo build --release` first)",
|
||||
release_binary.display()
|
||||
);
|
||||
}
|
||||
|
||||
let temp_root = tempfile::Builder::new()
|
||||
.prefix("jcode-reload-e2e-")
|
||||
.tempdir()?;
|
||||
let runtime_dir = temp_root.path().join("runtime");
|
||||
let home_dir = temp_root.path().join("home");
|
||||
let install_dir = temp_root.path().join("install");
|
||||
let stderr_path = temp_root.path().join("server-stderr.log");
|
||||
std::fs::create_dir_all(&runtime_dir)?;
|
||||
std::fs::create_dir_all(&home_dir)?;
|
||||
std::fs::create_dir_all(&install_dir)?;
|
||||
|
||||
let socket_path = runtime_dir.join("jcode.sock");
|
||||
let debug_socket_path = runtime_dir.join("jcode-debug.sock");
|
||||
|
||||
let stderr_file = std::fs::File::create(&stderr_path)?;
|
||||
let mut child = Command::new(env!("CARGO_BIN_EXE_jcode"))
|
||||
.arg("--no-update")
|
||||
.arg("--socket")
|
||||
.arg(&socket_path)
|
||||
.arg("serve")
|
||||
// This test must exercise the real exec-based reload handoff, not the
|
||||
// in-process test shortcut used by other e2e cases.
|
||||
.env_remove("JCODE_TEST_SESSION")
|
||||
.env("JCODE_HOME", &home_dir)
|
||||
.env("JCODE_RUNTIME_DIR", &runtime_dir)
|
||||
.env("JCODE_INSTALL_DIR", &install_dir)
|
||||
.env("JCODE_DEBUG_CONTROL", "1")
|
||||
.env("JCODE_TEMP_SERVER", "1")
|
||||
.env("JCODE_SERVER_OWNER_PID", std::process::id().to_string())
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::from(stderr_file))
|
||||
.spawn()?;
|
||||
|
||||
let test_result = async {
|
||||
wait_for_server_ready(&socket_path, &debug_socket_path).await?;
|
||||
let server_info_before =
|
||||
debug_run_command(debug_socket_path.clone(), "server:info", None).await?;
|
||||
let server_info_before_json: serde_json::Value = serde_json::from_str(&server_info_before)?;
|
||||
let server_id_before = server_info_before_json
|
||||
.get("id")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| anyhow::anyhow!("missing server id before reload"))?
|
||||
.to_string();
|
||||
|
||||
let mut client = wait_for_server_client(&socket_path).await?;
|
||||
client.reload().await?;
|
||||
|
||||
let disconnect_deadline = Instant::now() + Duration::from_secs(10);
|
||||
let mut saw_disconnect = false;
|
||||
while Instant::now() < disconnect_deadline {
|
||||
match tokio::time::timeout(Duration::from_secs(1), client.read_event()).await {
|
||||
Ok(Ok(_)) => continue,
|
||||
Ok(Err(_)) | Err(_) => {
|
||||
saw_disconnect = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
saw_disconnect,
|
||||
"old client connection never disconnected during reload"
|
||||
);
|
||||
|
||||
let marker_deadline = Instant::now() + Duration::from_secs(20);
|
||||
while jcode::server::reload_marker_active(Duration::from_secs(30)) {
|
||||
if Instant::now() >= marker_deadline {
|
||||
anyhow::bail!("reload marker remained active too long after restart");
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(25)).await;
|
||||
}
|
||||
|
||||
wait_for_server_ready(&socket_path, &debug_socket_path).await?;
|
||||
let _client = wait_for_server_client(&socket_path).await?;
|
||||
|
||||
let server_info_after =
|
||||
debug_run_command(debug_socket_path.clone(), "server:info", None).await?;
|
||||
let server_info_after_json: serde_json::Value = serde_json::from_str(&server_info_after)?;
|
||||
let server_id_after = server_info_after_json
|
||||
.get("id")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| anyhow::anyhow!("missing server id after reload"))?;
|
||||
|
||||
assert_ne!(
|
||||
server_id_after, server_id_before,
|
||||
"server identity should change after exec-based reload"
|
||||
);
|
||||
assert!(
|
||||
server_info_after_json
|
||||
.get("uptime_secs")
|
||||
.and_then(|v| v.as_u64())
|
||||
.is_some(),
|
||||
"replacement server should answer debug state queries after reload"
|
||||
);
|
||||
|
||||
Ok::<_, anyhow::Error>(())
|
||||
}
|
||||
.await;
|
||||
|
||||
kill_child(&mut child);
|
||||
if let Err(ref error) = test_result {
|
||||
if let Ok(stderr) = std::fs::read_to_string(&stderr_path) {
|
||||
eprintln!("spawned server stderr:\n{}", stderr);
|
||||
}
|
||||
eprintln!("reload e2e test error: {error:#}");
|
||||
}
|
||||
test_result
|
||||
}
|
||||
|
||||
/// Test repeated self-dev reload handoff against a real TUI client running in a PTY.
|
||||
///
|
||||
/// Requires a built release binary at target/release/jcode because the
|
||||
/// self-dev server reload path execs into the repo's reload candidate.
|
||||
#[cfg(unix)]
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn binary_integration_selfdev_reload_reconnects_quickly() -> Result<()> {
|
||||
let _env = setup_test_env()?;
|
||||
|
||||
let release_binary =
|
||||
jcode::build::release_binary_path(std::path::Path::new(env!("CARGO_MANIFEST_DIR")));
|
||||
if !release_binary.exists() {
|
||||
anyhow::bail!(
|
||||
"release binary missing at {} (run `cargo build --release` first)",
|
||||
release_binary.display()
|
||||
);
|
||||
}
|
||||
|
||||
let temp_root = tempfile::Builder::new()
|
||||
.prefix("jcode-selfdev-reload-e2e-")
|
||||
.tempdir()?;
|
||||
let runtime_dir = temp_root.path().join("runtime");
|
||||
let home_dir = temp_root.path().join("home");
|
||||
let install_dir = temp_root.path().join("install");
|
||||
std::fs::create_dir_all(&runtime_dir)?;
|
||||
std::fs::create_dir_all(&home_dir)?;
|
||||
std::fs::create_dir_all(&install_dir)?;
|
||||
|
||||
let _home_guard = EnvVarGuard::set("JCODE_HOME", &home_dir);
|
||||
let _runtime_guard = EnvVarGuard::set("JCODE_RUNTIME_DIR", &runtime_dir);
|
||||
let _install_guard = EnvVarGuard::set("JCODE_INSTALL_DIR", &install_dir);
|
||||
|
||||
let socket_path = runtime_dir.join("jcode.sock");
|
||||
let debug_socket_path = runtime_dir.join("jcode-debug.sock");
|
||||
let mut command = Command::new(&release_binary);
|
||||
command
|
||||
.arg("--no-update")
|
||||
.arg("--provider")
|
||||
.arg("antigravity")
|
||||
.arg("self-dev")
|
||||
.current_dir(env!("CARGO_MANIFEST_DIR"))
|
||||
.env_remove("JCODE_TEST_SESSION")
|
||||
.env("JCODE_HOME", &home_dir)
|
||||
.env("JCODE_RUNTIME_DIR", &runtime_dir)
|
||||
.env("JCODE_INSTALL_DIR", &install_dir);
|
||||
|
||||
let mut child = spawn_pty_child(command)?;
|
||||
|
||||
let test_result = async {
|
||||
wait_for_server_ready(&socket_path, &debug_socket_path).await?;
|
||||
let session_id = wait_for_default_connected_client_session(&debug_socket_path).await?;
|
||||
|
||||
let state_before =
|
||||
debug_run_command(debug_socket_path.clone(), "client:state", None).await?;
|
||||
let _: serde_json::Value = serde_json::from_str(&state_before)?;
|
||||
|
||||
let server_info_before =
|
||||
debug_run_command(debug_socket_path.clone(), "server:info", None).await?;
|
||||
let server_info_before_json: serde_json::Value = serde_json::from_str(&server_info_before)?;
|
||||
let mut server_id_before = server_info_before_json
|
||||
.get("id")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| anyhow::anyhow!("missing initial server id"))?
|
||||
.to_string();
|
||||
|
||||
for cycle in 1..=3 {
|
||||
child.send_command("/server-reload")?;
|
||||
|
||||
let server_id_after = wait_for_selfdev_reload_cycle(
|
||||
&debug_socket_path,
|
||||
&session_id,
|
||||
&server_id_before,
|
||||
Duration::from_secs(20),
|
||||
)
|
||||
.await?;
|
||||
assert_ne!(
|
||||
server_id_after, server_id_before,
|
||||
"self-dev reload cycle {} should replace the server process",
|
||||
cycle
|
||||
);
|
||||
server_id_before = server_id_after;
|
||||
}
|
||||
|
||||
Ok::<_, anyhow::Error>(())
|
||||
}
|
||||
.await;
|
||||
|
||||
let _ = tokio::time::timeout(
|
||||
Duration::from_secs(2),
|
||||
debug_run_command(debug_socket_path.clone(), "client:quit", None),
|
||||
)
|
||||
.await;
|
||||
kill_child(&mut child.child);
|
||||
|
||||
if let Err(ref error) = test_result {
|
||||
eprintln!("self-dev reload e2e test error: {error:#}");
|
||||
eprintln!("self-dev client PTY output:\n{}", child.output_text());
|
||||
if let Some(log_excerpt) = latest_log_excerpt(&home_dir) {
|
||||
eprintln!("self-dev reload logs (tail):\n{}", log_excerpt);
|
||||
}
|
||||
}
|
||||
|
||||
test_result
|
||||
}
|
||||
|
||||
/// Test self-dev client binary reload against a real TUI client running in a PTY.
|
||||
///
|
||||
/// Starts from the test binary, then forces `/client-reload` to re-exec into
|
||||
/// the built release candidate while keeping the shared server online.
|
||||
#[cfg(unix)]
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn binary_integration_selfdev_client_reload_resumes_session() -> Result<()> {
|
||||
let _env = setup_test_env()?;
|
||||
|
||||
let release_binary =
|
||||
jcode::build::release_binary_path(std::path::Path::new(env!("CARGO_MANIFEST_DIR")));
|
||||
if !release_binary.exists() {
|
||||
anyhow::bail!(
|
||||
"release binary missing at {} (run `cargo build --release` first)",
|
||||
release_binary.display()
|
||||
);
|
||||
}
|
||||
|
||||
let temp_root = tempfile::Builder::new()
|
||||
.prefix("jcode-selfdev-client-reload-e2e-")
|
||||
.tempdir()?;
|
||||
let runtime_dir = temp_root.path().join("runtime");
|
||||
let home_dir = temp_root.path().join("home");
|
||||
let install_dir = temp_root.path().join("install");
|
||||
std::fs::create_dir_all(&runtime_dir)?;
|
||||
std::fs::create_dir_all(&home_dir)?;
|
||||
std::fs::create_dir_all(&install_dir)?;
|
||||
|
||||
let _home_guard = EnvVarGuard::set("JCODE_HOME", &home_dir);
|
||||
let _runtime_guard = EnvVarGuard::set("JCODE_RUNTIME_DIR", &runtime_dir);
|
||||
let _install_guard = EnvVarGuard::set("JCODE_INSTALL_DIR", &install_dir);
|
||||
|
||||
let socket_path = runtime_dir.join("jcode.sock");
|
||||
let debug_socket_path = runtime_dir.join("jcode-debug.sock");
|
||||
let starter_binary = temp_root.path().join("jcode-selfdev-client-starter");
|
||||
std::fs::copy(env!("CARGO_BIN_EXE_jcode"), &starter_binary)?;
|
||||
let starter_mtime = std::fs::metadata(&release_binary)?
|
||||
.modified()?
|
||||
.checked_sub(Duration::from_secs(60))
|
||||
.unwrap_or(std::time::UNIX_EPOCH + Duration::from_secs(1));
|
||||
set_file_mtime(&starter_binary, starter_mtime)?;
|
||||
|
||||
let mut command = Command::new(&starter_binary);
|
||||
command
|
||||
.arg("--no-update")
|
||||
.arg("--provider")
|
||||
.arg("antigravity")
|
||||
.arg("self-dev")
|
||||
.current_dir(env!("CARGO_MANIFEST_DIR"))
|
||||
.env_remove("JCODE_TEST_SESSION")
|
||||
.env("JCODE_HOME", &home_dir)
|
||||
.env("JCODE_RUNTIME_DIR", &runtime_dir)
|
||||
.env("JCODE_INSTALL_DIR", &install_dir);
|
||||
|
||||
let mut child = spawn_pty_child(command)?;
|
||||
|
||||
let test_result = async {
|
||||
wait_for_server_ready(&socket_path, &debug_socket_path).await?;
|
||||
|
||||
let session_id = wait_for_default_connected_client_session(&debug_socket_path).await?;
|
||||
|
||||
let state_before =
|
||||
debug_run_command(debug_socket_path.clone(), "client:state", Some(&session_id)).await?;
|
||||
let state_before_json: serde_json::Value = serde_json::from_str(&state_before)?;
|
||||
let version_before = state_before_json
|
||||
.get("version")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| anyhow::anyhow!("missing client version before reload"))?
|
||||
.to_string();
|
||||
|
||||
let clients_before =
|
||||
debug_run_command(debug_socket_path.clone(), "clients:map", None).await?;
|
||||
let clients_before_json: serde_json::Value = serde_json::from_str(&clients_before)?;
|
||||
let client_id_before = clients_before_json
|
||||
.get("clients")
|
||||
.and_then(|v| v.as_array())
|
||||
.and_then(|clients| {
|
||||
clients.iter().find_map(|client| {
|
||||
let session = client.get("session_id").and_then(|v| v.as_str())?;
|
||||
if session != session_id {
|
||||
return None;
|
||||
}
|
||||
client
|
||||
.get("client_id")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(str::to_string)
|
||||
})
|
||||
})
|
||||
.ok_or_else(|| anyhow::anyhow!("missing client id before reload"))?;
|
||||
|
||||
let server_info_before =
|
||||
debug_run_command(debug_socket_path.clone(), "server:info", None).await?;
|
||||
let server_info_before_json: serde_json::Value = serde_json::from_str(&server_info_before)?;
|
||||
let server_id_before = server_info_before_json
|
||||
.get("id")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| anyhow::anyhow!("missing server id before client reload"))?
|
||||
.to_string();
|
||||
|
||||
child.send_command("/client-reload")?;
|
||||
|
||||
let client_id_after = wait_for_selfdev_client_reload_cycle(
|
||||
&debug_socket_path,
|
||||
&session_id,
|
||||
&client_id_before,
|
||||
&server_id_before,
|
||||
Duration::from_secs(20),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let state_after =
|
||||
debug_run_command(debug_socket_path.clone(), "client:state", Some(&session_id)).await?;
|
||||
let state_after_json: serde_json::Value = serde_json::from_str(&state_after)?;
|
||||
let version_after = state_after_json
|
||||
.get("version")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| anyhow::anyhow!("missing client version after reload"))?;
|
||||
|
||||
assert_ne!(
|
||||
client_id_after, client_id_before,
|
||||
"client reload should reconnect with a different client id"
|
||||
);
|
||||
assert_ne!(
|
||||
version_after, version_before,
|
||||
"client reload should switch binaries"
|
||||
);
|
||||
|
||||
let server_info_after =
|
||||
debug_run_command(debug_socket_path.clone(), "server:info", None).await?;
|
||||
let server_info_after_json: serde_json::Value = serde_json::from_str(&server_info_after)?;
|
||||
let server_id_after = server_info_after_json
|
||||
.get("id")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| anyhow::anyhow!("missing server id after client reload"))?;
|
||||
assert_eq!(
|
||||
server_id_after, server_id_before,
|
||||
"client reload should not replace the server process"
|
||||
);
|
||||
|
||||
Ok::<_, anyhow::Error>(())
|
||||
}
|
||||
.await;
|
||||
|
||||
let _ = tokio::time::timeout(
|
||||
Duration::from_secs(2),
|
||||
debug_run_command(debug_socket_path.clone(), "client:quit", None),
|
||||
)
|
||||
.await;
|
||||
kill_child(&mut child.child);
|
||||
|
||||
if let Err(ref error) = test_result {
|
||||
eprintln!("self-dev client reload e2e test error: {error:#}");
|
||||
eprintln!("self-dev client PTY output:\n{}", child.output_text());
|
||||
if let Some(log_excerpt) = latest_log_excerpt(&home_dir) {
|
||||
eprintln!("self-dev client reload logs (tail):\n{}", log_excerpt);
|
||||
}
|
||||
}
|
||||
|
||||
test_result
|
||||
}
|
||||
|
||||
/// Test full self-dev `/reload` against a real TUI client running in a PTY.
|
||||
///
|
||||
/// Starts from an older starter binary so the client reloads into the built
|
||||
/// release candidate while the shared server also restarts.
|
||||
#[cfg(unix)]
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn binary_integration_selfdev_full_reload_resumes_session_quickly() -> Result<()> {
|
||||
let _env = setup_test_env()?;
|
||||
|
||||
let release_binary =
|
||||
jcode::build::release_binary_path(std::path::Path::new(env!("CARGO_MANIFEST_DIR")));
|
||||
if !release_binary.exists() {
|
||||
anyhow::bail!(
|
||||
"release binary missing at {} (run `cargo build --release` first)",
|
||||
release_binary.display()
|
||||
);
|
||||
}
|
||||
|
||||
let temp_root = tempfile::Builder::new()
|
||||
.prefix("jcode-selfdev-full-reload-e2e-")
|
||||
.tempdir()?;
|
||||
let runtime_dir = temp_root.path().join("runtime");
|
||||
let home_dir = temp_root.path().join("home");
|
||||
let install_dir = temp_root.path().join("install");
|
||||
std::fs::create_dir_all(&runtime_dir)?;
|
||||
std::fs::create_dir_all(&home_dir)?;
|
||||
std::fs::create_dir_all(&install_dir)?;
|
||||
|
||||
let _home_guard = EnvVarGuard::set("JCODE_HOME", &home_dir);
|
||||
let _runtime_guard = EnvVarGuard::set("JCODE_RUNTIME_DIR", &runtime_dir);
|
||||
let _install_guard = EnvVarGuard::set("JCODE_INSTALL_DIR", &install_dir);
|
||||
|
||||
let socket_path = runtime_dir.join("jcode.sock");
|
||||
let debug_socket_path = runtime_dir.join("jcode-debug.sock");
|
||||
let starter_binary = temp_root.path().join("jcode-selfdev-full-reload-starter");
|
||||
std::fs::copy(env!("CARGO_BIN_EXE_jcode"), &starter_binary)?;
|
||||
let starter_mtime = std::fs::metadata(&release_binary)?
|
||||
.modified()?
|
||||
.checked_sub(Duration::from_secs(60))
|
||||
.unwrap_or(std::time::UNIX_EPOCH + Duration::from_secs(1));
|
||||
set_file_mtime(&starter_binary, starter_mtime)?;
|
||||
|
||||
let mut command = Command::new(&starter_binary);
|
||||
command
|
||||
.arg("--no-update")
|
||||
.arg("--provider")
|
||||
.arg("antigravity")
|
||||
.arg("self-dev")
|
||||
.current_dir(env!("CARGO_MANIFEST_DIR"))
|
||||
.env_remove("JCODE_TEST_SESSION")
|
||||
.env("JCODE_HOME", &home_dir)
|
||||
.env("JCODE_RUNTIME_DIR", &runtime_dir)
|
||||
.env("JCODE_INSTALL_DIR", &install_dir);
|
||||
|
||||
let mut child = spawn_pty_child(command)?;
|
||||
|
||||
let test_result = async {
|
||||
wait_for_server_ready(&socket_path, &debug_socket_path).await?;
|
||||
|
||||
let session_id = wait_for_default_connected_client_session(&debug_socket_path).await?;
|
||||
|
||||
let state_before =
|
||||
debug_run_command(debug_socket_path.clone(), "client:state", Some(&session_id)).await?;
|
||||
let state_before_json: serde_json::Value = serde_json::from_str(&state_before)?;
|
||||
let version_before = state_before_json
|
||||
.get("version")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| anyhow::anyhow!("missing client version before full reload"))?
|
||||
.to_string();
|
||||
|
||||
let clients_before =
|
||||
debug_run_command(debug_socket_path.clone(), "clients:map", None).await?;
|
||||
let clients_before_json: serde_json::Value = serde_json::from_str(&clients_before)?;
|
||||
let client_id_before = clients_before_json
|
||||
.get("clients")
|
||||
.and_then(|v| v.as_array())
|
||||
.and_then(|clients| {
|
||||
clients.iter().find_map(|client| {
|
||||
let session = client.get("session_id").and_then(|v| v.as_str())?;
|
||||
if session != session_id {
|
||||
return None;
|
||||
}
|
||||
client
|
||||
.get("client_id")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(str::to_string)
|
||||
})
|
||||
})
|
||||
.ok_or_else(|| anyhow::anyhow!("missing client id before full reload"))?;
|
||||
|
||||
let server_info_before =
|
||||
debug_run_command(debug_socket_path.clone(), "server:info", None).await?;
|
||||
let server_info_before_json: serde_json::Value = serde_json::from_str(&server_info_before)?;
|
||||
let server_id_before = server_info_before_json
|
||||
.get("id")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| anyhow::anyhow!("missing server id before full reload"))?
|
||||
.to_string();
|
||||
|
||||
child.send_command("/reload")?;
|
||||
|
||||
let server_id_after = wait_for_selfdev_reload_cycle(
|
||||
&debug_socket_path,
|
||||
&session_id,
|
||||
&server_id_before,
|
||||
Duration::from_secs(20),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let client_id_after = wait_for_selfdev_client_reload_cycle(
|
||||
&debug_socket_path,
|
||||
&session_id,
|
||||
&client_id_before,
|
||||
&server_id_after,
|
||||
Duration::from_secs(20),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let state_after =
|
||||
debug_run_command(debug_socket_path.clone(), "client:state", Some(&session_id)).await?;
|
||||
let state_after_json: serde_json::Value = serde_json::from_str(&state_after)?;
|
||||
let version_after = state_after_json
|
||||
.get("version")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| anyhow::anyhow!("missing client version after full reload"))?;
|
||||
|
||||
assert_ne!(
|
||||
server_id_after, server_id_before,
|
||||
"full reload should replace the server process"
|
||||
);
|
||||
assert_ne!(
|
||||
client_id_after, client_id_before,
|
||||
"full reload should reconnect with a different client id"
|
||||
);
|
||||
assert_ne!(
|
||||
version_after, version_before,
|
||||
"full reload should switch binaries"
|
||||
);
|
||||
|
||||
Ok::<_, anyhow::Error>(())
|
||||
}
|
||||
.await;
|
||||
|
||||
let _ = tokio::time::timeout(
|
||||
Duration::from_secs(2),
|
||||
debug_run_command(debug_socket_path.clone(), "client:quit", None),
|
||||
)
|
||||
.await;
|
||||
kill_child(&mut child.child);
|
||||
|
||||
if let Err(ref error) = test_result {
|
||||
eprintln!("self-dev full reload e2e test error: {error:#}");
|
||||
eprintln!("self-dev client PTY output:\n{}", child.output_text());
|
||||
if let Some(log_excerpt) = latest_log_excerpt(&home_dir) {
|
||||
eprintln!("self-dev full reload logs (tail):\n{}", log_excerpt);
|
||||
}
|
||||
}
|
||||
|
||||
test_result
|
||||
}
|
||||
Reference in New Issue
Block a user