mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
d61dfeb23a
## Why
Remote-control runtime enablement and persisted enrollment preference
were represented by separate flags. That made startup rehydration, RPC
persistence, and new-enrollment seeding race with one another, and it
did not cleanly distinguish runtime-only CLI or daemon starts from
durable app-server RPC changes.
## What Changed
- Replace the parallel enablement, seed, and rehydration flags with one
transport-owned `RemoteControlDesiredState`.
- Add nullable enrollment-scoped persistence and preserve existing
preferences during enrollment upserts.
- Rehydrate plain startup only after auth and client scope resolve,
without overwriting a concurrent RPC transition.
- Make ordinary `remoteControl/enable` and `remoteControl/disable`
durable while retaining `ephemeral: true` for runtime-only callers.
- Have the daemon explicitly request ephemeral enablement and regenerate
the app-server schemas.
## Verification
- Covered migration and `NULL`/`0`/`1` persistence round trips.
- Covered plain-start rehydration and runtime-only versus durable
enrollment seeding.
- Covered durable enable, durable disable, and ephemeral enable through
app-server RPC.
- Covered the daemon's exact `{ "ephemeral": true }` request payload.
Related issue: N/A (internal remote-control persistence architecture
change).
217 lines
6.0 KiB
Rust
217 lines
6.0 KiB
Rust
use std::time::Duration;
|
|
|
|
use pretty_assertions::assert_eq;
|
|
use tempfile::TempDir;
|
|
|
|
use codex_app_server_transport::REMOTE_CONTROL_DISABLED_ENV_VAR;
|
|
|
|
use super::PidBackend;
|
|
use super::PidCommandKind;
|
|
use super::PidFileState;
|
|
use super::PidLogTail;
|
|
use super::PidRecord;
|
|
use super::read_stderr_log_tail;
|
|
use super::stderr_log_file_for_pid_file;
|
|
use super::try_lock_file;
|
|
|
|
#[tokio::test]
|
|
async fn locked_empty_pid_file_is_treated_as_active_reservation() {
|
|
let temp_dir = TempDir::new().expect("temp dir");
|
|
let pid_file = temp_dir.path().join("app-server.pid");
|
|
tokio::fs::write(&pid_file, "")
|
|
.await
|
|
.expect("write pid file");
|
|
let backend = PidBackend::new(
|
|
temp_dir.path().join("codex"),
|
|
pid_file.clone(),
|
|
/*remote_control_enabled*/ false,
|
|
);
|
|
let reservation = tokio::fs::OpenOptions::new()
|
|
.create(true)
|
|
.truncate(false)
|
|
.write(true)
|
|
.open(&backend.lock_file)
|
|
.await
|
|
.expect("open pid lock file");
|
|
assert!(try_lock_file(&reservation).expect("lock reservation"));
|
|
|
|
assert_eq!(
|
|
backend.read_pid_file_state().await.expect("read pid"),
|
|
PidFileState::Starting
|
|
);
|
|
assert!(pid_file.exists());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn unlocked_empty_pid_file_is_treated_as_stale_reservation() {
|
|
let temp_dir = TempDir::new().expect("temp dir");
|
|
let pid_file = temp_dir.path().join("app-server.pid");
|
|
tokio::fs::write(&pid_file, "")
|
|
.await
|
|
.expect("write pid file");
|
|
let backend = PidBackend::new(
|
|
temp_dir.path().join("codex"),
|
|
pid_file.clone(),
|
|
/*remote_control_enabled*/ false,
|
|
);
|
|
|
|
assert_eq!(
|
|
backend.read_pid_file_state().await.expect("read pid"),
|
|
PidFileState::Missing
|
|
);
|
|
assert!(!pid_file.exists());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn stop_waits_for_live_reservation_to_resolve() {
|
|
let temp_dir = TempDir::new().expect("temp dir");
|
|
let pid_file = temp_dir.path().join("app-server.pid");
|
|
tokio::fs::write(&pid_file, "")
|
|
.await
|
|
.expect("write pid file");
|
|
let backend = PidBackend::new(
|
|
temp_dir.path().join("codex"),
|
|
pid_file.clone(),
|
|
/*remote_control_enabled*/ false,
|
|
);
|
|
let reservation = tokio::fs::OpenOptions::new()
|
|
.create(true)
|
|
.truncate(false)
|
|
.write(true)
|
|
.open(&backend.lock_file)
|
|
.await
|
|
.expect("open pid lock file");
|
|
assert!(try_lock_file(&reservation).expect("lock reservation"));
|
|
let cleanup = tokio::spawn(async move {
|
|
tokio::time::sleep(Duration::from_millis(50)).await;
|
|
drop(reservation);
|
|
tokio::fs::remove_file(pid_file)
|
|
.await
|
|
.expect("remove pid file");
|
|
});
|
|
|
|
backend.stop().await.expect("stop");
|
|
cleanup.await.expect("cleanup task");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn start_retries_stale_empty_pid_file_under_its_own_lock() {
|
|
let temp_dir = TempDir::new().expect("temp dir");
|
|
let pid_file = temp_dir.path().join("app-server.pid");
|
|
tokio::fs::write(&pid_file, "")
|
|
.await
|
|
.expect("write pid file");
|
|
let backend = PidBackend::new(
|
|
temp_dir.path().join("missing-codex"),
|
|
pid_file,
|
|
/*remote_control_enabled*/ false,
|
|
);
|
|
|
|
let err = backend.start().await.expect_err("start");
|
|
assert!(
|
|
err.to_string()
|
|
.starts_with("failed to spawn detached app-server process using ")
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn stale_record_cleanup_preserves_replacement_record() {
|
|
let temp_dir = TempDir::new().expect("temp dir");
|
|
let pid_file = temp_dir.path().join("app-server.pid");
|
|
let backend = PidBackend::new(
|
|
temp_dir.path().join("codex"),
|
|
pid_file.clone(),
|
|
/*remote_control_enabled*/ false,
|
|
);
|
|
let stale = PidRecord {
|
|
pid: 1,
|
|
process_start_time: "old".to_string(),
|
|
};
|
|
let replacement = PidRecord {
|
|
pid: 2,
|
|
process_start_time: "new".to_string(),
|
|
};
|
|
tokio::fs::write(
|
|
&pid_file,
|
|
serde_json::to_vec(&replacement).expect("serialize replacement"),
|
|
)
|
|
.await
|
|
.expect("write replacement pid file");
|
|
|
|
assert_eq!(
|
|
backend
|
|
.refresh_after_stale_record(&stale)
|
|
.await
|
|
.expect("cleanup"),
|
|
PidFileState::Running(replacement)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn update_loop_uses_hidden_app_server_subcommand() {
|
|
let backend = PidBackend {
|
|
codex_bin: "codex".into(),
|
|
pid_file: "updater.pid".into(),
|
|
lock_file: "updater.pid.lock".into(),
|
|
command_kind: PidCommandKind::UpdateLoop,
|
|
};
|
|
|
|
assert_eq!(
|
|
backend.command_args(),
|
|
vec!["app-server", "daemon", "pid-update-loop"]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn app_server_remote_control_uses_runtime_flag() {
|
|
let backend = PidBackend::new(
|
|
"codex".into(),
|
|
"app-server.pid".into(),
|
|
/*remote_control_enabled*/ true,
|
|
);
|
|
|
|
assert_eq!(
|
|
backend.command_args(),
|
|
vec!["app-server", "--remote-control", "--listen", "unix://"]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn app_server_disabled_remote_control_uses_compatible_args_and_runtime_env() {
|
|
let backend = PidBackend::new(
|
|
"codex".into(),
|
|
"app-server.pid".into(),
|
|
/*remote_control_enabled*/ false,
|
|
);
|
|
|
|
assert_eq!(
|
|
backend.command_args(),
|
|
vec!["app-server", "--listen", "unix://"]
|
|
);
|
|
assert_eq!(
|
|
backend.command_env(),
|
|
Some((REMOTE_CONTROL_DISABLED_ENV_VAR, "1"))
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn read_stderr_log_tail_returns_recent_complete_lines() {
|
|
let temp_dir = TempDir::new().expect("temp dir");
|
|
let pid_file = temp_dir.path().join("app-server.pid");
|
|
let log_file = stderr_log_file_for_pid_file(&pid_file);
|
|
let contents = format!("{}\nrecent error\nusage", "x".repeat(4100));
|
|
tokio::fs::write(&log_file, contents)
|
|
.await
|
|
.expect("write stderr log");
|
|
|
|
assert_eq!(
|
|
read_stderr_log_tail(&pid_file)
|
|
.await
|
|
.expect("read stderr log"),
|
|
Some(PidLogTail {
|
|
path: log_file,
|
|
contents: "recent error\nusage".to_string(),
|
|
})
|
|
);
|
|
}
|