codex: support explicit sqlite degraded mode

This commit is contained in:
Joe Gershenson
2026-07-09 13:13:47 -07:00
parent 22781d4001
commit c04945b4a1
16 changed files with 481 additions and 21 deletions
+45 -10
View File
@@ -526,17 +526,22 @@ pub async fn run_main_with_transport_options(
return Err(err);
}
let message = config_warning_from_error("Invalid configuration; using defaults.", &err);
let message = config_warning_from_error(
"Invalid configuration; using defaults with SQLite disabled.",
&err,
);
config_warnings.push(message);
(
config_manager.load_default_config().await.map_err(|e| {
std::io::Error::new(
ErrorKind::InvalidData,
format!("error loading default config after config error: {e}"),
)
})?,
false,
)
let mut config = config_manager.load_default_config().await.map_err(|e| {
std::io::Error::new(
ErrorKind::InvalidData,
format!("error loading default config after config error: {e}"),
)
})?;
config
.features
.disable(codex_features::Feature::Sqlite)
.map_err(std::io::Error::other)?;
(config, false)
}
};
@@ -1209,6 +1214,13 @@ struct StateDbInitResult {
async fn init_sqlite_state_db_with_fresh_start_on_corruption(
config: &Config,
) -> anyhow::Result<StateDbInitResult> {
if !config.features.enabled(codex_features::Feature::Sqlite) {
return Ok(StateDbInitResult {
state_db: None,
recovery_notice: None,
});
}
let mut attempted_backups = HashSet::new();
let mut recovered_databases = Vec::new();
loop {
@@ -1369,9 +1381,12 @@ mod tests {
use super::loader_overrides_with_test_user_config_file;
#[cfg(debug_assertions)]
use codex_config::LoaderOverrides;
use codex_core::config::ConfigBuilder;
use codex_features::Feature;
#[cfg(debug_assertions)]
use codex_utils_absolute_path::AbsolutePathBuf;
use pretty_assertions::assert_eq;
use tempfile::TempDir;
#[test]
fn log_format_from_env_value_matches_json_values_case_insensitively() {
@@ -1391,6 +1406,26 @@ mod tests {
assert_eq!(LogFormat::from_env_value(Some("jsonl")), LogFormat::Default);
}
#[tokio::test]
async fn sqlite_state_db_can_be_explicitly_disabled() -> anyhow::Result<()> {
let temp_dir = TempDir::new()?;
let mut config = ConfigBuilder::default()
.codex_home(temp_dir.path().to_path_buf())
.build()
.await?;
config.features.disable(Feature::Sqlite)?;
let occupied_sqlite_home = temp_dir.path().join("sqlite-home");
std::fs::write(&occupied_sqlite_home, "occupied")?;
config.sqlite_home = occupied_sqlite_home.clone();
let result = super::init_sqlite_state_db_with_fresh_start_on_corruption(&config).await?;
assert!(result.state_db.is_none());
assert!(result.recovery_notice.is_none());
assert_eq!(std::fs::read_to_string(occupied_sqlite_home)?, "occupied");
Ok(())
}
#[cfg(debug_assertions)]
#[test]
fn debug_test_user_config_file_overrides_loader_path() {
@@ -357,10 +357,9 @@ impl ExternalAgentConfigRequestProcessor {
pub(crate) async fn read_import_histories(
&self,
) -> Result<ExternalAgentConfigImportHistoriesReadResponse, JSONRPCErrorError> {
let state_db = self
.state_db
.as_ref()
.ok_or_else(|| internal_error("state database is unavailable"))?;
let Some(state_db) = self.state_db.as_ref() else {
return Ok(ExternalAgentConfigImportHistoriesReadResponse { data: Vec::new() });
};
let histories = state_db
.external_agent_config_import_history_records()
.await
@@ -1614,6 +1614,12 @@ impl ThreadRequestProcessor {
}
async fn memory_reset_response_inner(&self) -> Result<MemoryResetResponse, JSONRPCErrorError> {
if !self.config.features.enabled(Feature::Sqlite) {
return Err(invalid_request(
"memory reset requires SQLite, which is disabled",
));
}
let state_db = self
.state_db
.clone()
@@ -1643,6 +1649,12 @@ impl ThreadRequestProcessor {
&self,
params: ThreadMetadataUpdateParams,
) -> Result<ThreadMetadataUpdateResponse, JSONRPCErrorError> {
if !self.config.features.enabled(Feature::Sqlite) {
return Err(invalid_request(
"thread metadata updates require SQLite, which is disabled",
));
}
let ThreadMetadataUpdateParams {
thread_id,
git_info,
@@ -1988,6 +2000,11 @@ impl ThreadRequestProcessor {
)),
(None, None) => None,
};
if relation_filter.is_some() && !self.config.features.enabled(Feature::Sqlite) {
return Err(invalid_request(
"thread relationship filters require SQLite, which is disabled",
));
}
let requested_page_size = limit
.map(|value| value as usize)
@@ -31,3 +31,38 @@ foo = "bar"
Ok(())
}
#[test]
fn non_strict_config_fallback_disables_sqlite() -> Result<()> {
let codex_home = TempDir::new()?;
std::fs::write(
codex_home.path().join("config.toml"),
r#"
model = 123
[features]
sqlite = false
"#,
)?;
let output = Command::new(codex_utils_cargo_bin::cargo_bin("codex-app-server")?)
.env("CODEX_HOME", codex_home.path())
.env(codex_state::SQLITE_HOME_ENV, codex_home.path())
.env(
"CODEX_APP_SERVER_MANAGED_CONFIG_PATH",
codex_home.path().join("managed_config.toml"),
)
.args(["--listen", "off"])
.output()?;
assert!(!output.status.success());
let stderr = String::from_utf8(output.stderr)?;
assert!(
stderr.contains("no transport configured"),
"expected startup to reach transport validation, got: {stderr}"
);
for db in codex_state::runtime_db_paths(codex_home.path()) {
assert!(!db.path.exists(), "{} should not exist", db.label);
}
Ok(())
}
@@ -51,6 +51,37 @@ fn assert_import_response(response: ExternalAgentConfigImportResponse) -> String
response.import_id
}
#[tokio::test]
async fn external_agent_config_import_histories_are_empty_when_sqlite_is_disabled() -> Result<()> {
let codex_home = TempDir::new()?;
std::fs::write(
codex_home.path().join("config.toml"),
"[features]\nsqlite = false\n",
)?;
let mut mcp =
TestAppServer::new_with_env(codex_home.path(), &[("CODEX_SQLITE_HOME", None)]).await?;
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
let request_id = mcp
.send_raw_request(
"externalAgentConfig/import/readHistories",
/*params*/ None,
)
.await?;
let response: JSONRPCResponse = timeout(
DEFAULT_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
)
.await??;
let response: ExternalAgentConfigImportHistoriesReadResponse = to_response(response)?;
assert!(response.data.is_empty());
for db in codex_state::runtime_db_paths(codex_home.path()) {
assert!(!db.path.exists(), "{} should not exist", db.label);
}
Ok(())
}
#[tokio::test]
async fn external_agent_config_import_sends_completion_notification_for_sync_only_import()
-> Result<()> {
@@ -22,7 +22,7 @@ const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs
#[tokio::test]
async fn memory_reset_clears_memory_files_and_rows_preserves_threads() -> Result<()> {
let codex_home = TempDir::new()?;
create_config_toml(codex_home.path())?;
create_config_toml(codex_home.path(), /*sqlite_enabled*/ true)?;
let state_db = init_state_db(codex_home.path()).await?;
let memory_root = codex_home.path().join("memories");
@@ -72,6 +72,37 @@ async fn memory_reset_clears_memory_files_and_rows_preserves_threads() -> Result
Ok(())
}
#[tokio::test]
async fn memory_reset_reports_sqlite_disabled() -> Result<()> {
let codex_home = TempDir::new()?;
create_config_toml(codex_home.path(), /*sqlite_enabled*/ false)?;
let memory_root = codex_home.path().join("memories");
tokio::fs::create_dir_all(&memory_root).await?;
tokio::fs::write(memory_root.join("MEMORY.md"), "stale memory\n").await?;
let mut mcp = TestAppServer::new(codex_home.path()).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let request_id = mcp
.send_raw_request("memory/reset", /*params*/ None)
.await?;
let error = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_error_message(RequestId::Integer(request_id)),
)
.await??;
assert_eq!(error.error.code, -32600);
assert_eq!(
error.error.message,
"memory reset requires SQLite, which is disabled"
);
assert!(memory_root.join("MEMORY.md").is_file());
for db in codex_state::runtime_db_paths(codex_home.path()) {
assert!(!db.path.exists(), "{} should not exist", db.label);
}
Ok(())
}
async fn seed_stage1_output(state_db: &Arc<StateRuntime>, codex_home: &Path) -> Result<ThreadId> {
let now = Utc::now();
let thread_id = ThreadId::from_string(&Uuid::new_v4().to_string())?;
@@ -130,11 +161,12 @@ async fn init_state_db(codex_home: &Path) -> Result<Arc<StateRuntime>> {
Ok(state_db)
}
fn create_config_toml(codex_home: &Path) -> std::io::Result<()> {
fn create_config_toml(codex_home: &Path, sqlite_enabled: bool) -> std::io::Result<()> {
let config_toml = codex_home.join("config.toml");
std::fs::write(
config_toml,
r#"
format!(
r#"
model = "mock-model"
approval_policy = "never"
sandbox_mode = "read-only"
@@ -142,7 +174,7 @@ model_provider = "mock_provider"
suppress_unstable_features_warning = true
[features]
sqlite = true
sqlite = {sqlite_enabled}
[model_providers.mock_provider]
name = "Mock provider for test"
@@ -151,5 +183,6 @@ wire_api = "responses"
request_max_retries = 0
stream_max_retries = 0
"#,
),
)
}
@@ -1211,6 +1211,44 @@ async fn thread_list_relation_filters_reject_invalid_requests() -> Result<()> {
Ok(())
}
#[tokio::test]
async fn thread_list_relation_filters_report_sqlite_disabled() -> Result<()> {
let codex_home = TempDir::new()?;
std::fs::write(
codex_home.path().join("config.toml"),
"[features]\nsqlite = false\n",
)?;
let mut mcp = init_mcp(codex_home.path()).await?;
let request_id = mcp
.send_thread_list_request(codex_app_server_protocol::ThreadListParams {
cursor: None,
sort_key: None,
sort_direction: None,
model_providers: None,
source_kinds: None,
archived: None,
cwd: None,
use_state_db_only: false,
search_term: None,
parent_thread_id: Some(ThreadId::new().to_string()),
ancestor_thread_id: None,
limit: Some(10),
})
.await?;
let error = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_error_message(RequestId::Integer(request_id)),
)
.await??;
assert_eq!(error.error.code, -32600);
assert_eq!(
error.error.message,
"thread relationship filters require SQLite, which is disabled"
);
Ok(())
}
#[tokio::test]
async fn thread_list_empty_source_kinds_defaults_to_interactive_only() -> Result<()> {
let codex_home = TempDir::new()?;
@@ -35,6 +35,42 @@ use tokio::time::timeout;
const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
const INVALID_REQUEST_ERROR_CODE: i64 = -32600;
#[tokio::test]
async fn thread_metadata_update_reports_sqlite_disabled() -> Result<()> {
let codex_home = TempDir::new()?;
std::fs::write(
codex_home.path().join("config.toml"),
"[features]\nsqlite = false\n",
)?;
let mut mcp = TestAppServer::new(codex_home.path()).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let request_id = mcp
.send_thread_metadata_update_request(ThreadMetadataUpdateParams {
thread_id: ThreadId::new().to_string(),
git_info: Some(ThreadMetadataGitInfoUpdateParams {
sha: None,
branch: Some(Some("feature/test".to_string())),
origin_url: None,
}),
})
.await?;
let error = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_error_message(RequestId::Integer(request_id)),
)
.await??;
assert_eq!(error.error.code, INVALID_REQUEST_ERROR_CODE);
assert_eq!(
error.error.message,
"thread metadata updates require SQLite, which is disabled"
);
for db in codex_state::runtime_db_paths(codex_home.path()) {
assert!(!db.path.exists(), "{} should not exist", db.label);
}
Ok(())
}
#[tokio::test]
async fn thread_metadata_update_patches_git_branch_and_returns_updated_thread() -> Result<()> {
let server = create_mock_responses_server_repeating_assistant("Done").await;
+44 -2
View File
@@ -2161,13 +2161,19 @@ fn non_empty_trimmed(value: String) -> Option<String> {
async fn state_check(config: &Config) -> DoctorCheck {
let mut details = Vec::new();
let sqlite_enabled = config.features.enabled(codex_features::Feature::Sqlite);
path_readiness(&mut details, "CODEX_HOME", &config.codex_home);
path_readiness(&mut details, "log dir", &config.log_dir);
path_readiness(&mut details, "sqlite home", &config.sqlite_home);
let mut integrity_failures = Vec::new();
for db in codex_state::runtime_db_paths(&config.sqlite_home) {
path_readiness(&mut details, db.label, &db.path);
sqlite_integrity_detail(&mut details, &mut integrity_failures, db.label, &db.path).await;
if sqlite_enabled {
sqlite_integrity_detail(&mut details, &mut integrity_failures, db.label, &db.path)
.await;
} else {
details.push(format!("{} integrity: skipped (SQLite disabled)", db.label));
}
}
rollout_stats_details(&mut details, &config.codex_home);
standalone_release_cache_details(&mut details);
@@ -2177,7 +2183,9 @@ async fn state_check(config: &Config) -> DoctorCheck {
} else {
CheckStatus::Fail
};
let summary = if status == CheckStatus::Ok {
let summary = if !sqlite_enabled {
"state paths are inspectable; SQLite is disabled"
} else if status == CheckStatus::Ok {
"state paths and databases are inspectable"
} else {
"state database integrity check failed"
@@ -3098,6 +3106,40 @@ mod tests {
use super::*;
#[tokio::test]
async fn state_check_skips_inactive_corrupt_sqlite_database() {
let codex_home = tempfile::tempdir().expect("create temp dir");
let mut config = ConfigBuilder::default()
.codex_home(codex_home.path().to_path_buf())
.build()
.await
.expect("config");
config
.features
.disable(codex_features::Feature::Sqlite)
.expect("disable sqlite");
std::fs::write(
codex_state::state_db_path(config.sqlite_home.as_path()),
"not a sqlite database",
)
.expect("write corrupt database");
let check = state_check(&config).await;
assert_eq!(check.status, CheckStatus::Ok);
assert_eq!(
check.summary,
"state paths are inspectable; SQLite is disabled"
);
assert_eq!(check.remediation, None);
assert!(
check
.details
.iter()
.any(|detail| detail == "state DB integrity: skipped (SQLite disabled)")
);
}
#[derive(Default)]
struct RecordingProgress {
events: Mutex<Vec<String>>,
@@ -82,6 +82,16 @@ impl RolloutScan {
}
pub(super) async fn thread_inventory_check(config: &Config) -> DoctorCheck {
if !config.features.enabled(codex_features::Feature::Sqlite) {
return DoctorCheck::new(
CHECK_ID,
CHECK_CATEGORY,
CheckStatus::Ok,
"state database is disabled",
)
.details(vec!["rollout/state DB parity check skipped".to_string()]);
}
thread_inventory_check_for_roots(
config.codex_home.as_path(),
config.sqlite_home.as_path(),
@@ -677,6 +687,8 @@ where
#[cfg(test)]
mod tests {
use super::*;
use codex_core::config::ConfigBuilder;
use codex_features::Feature;
use codex_protocol::ThreadId;
use codex_protocol::protocol::RolloutItem;
use codex_protocol::protocol::RolloutLine;
@@ -685,6 +697,26 @@ mod tests {
use sqlx::sqlite::SqlitePoolOptions;
use tempfile::TempDir;
#[tokio::test]
async fn thread_inventory_check_skips_intentionally_disabled_database() {
let codex_home = TempDir::new().expect("codex home");
let mut config = ConfigBuilder::default()
.codex_home(codex_home.path().to_path_buf())
.build()
.await
.expect("config");
config
.features
.disable(Feature::Sqlite)
.expect("disable sqlite");
let check = thread_inventory_check(&config).await;
assert_eq!(check.status, CheckStatus::Ok);
assert_eq!(check.summary, "state database is disabled");
assert!(check.issues.is_empty());
}
#[tokio::test]
async fn thread_inventory_check_ok_when_rollouts_match_db() {
let fixture = Fixture::new().await;
+38
View File
@@ -2025,6 +2025,13 @@ async fn run_debug_clear_memories_command(
.cli_overrides(cli_kv_overrides)
.build()
.await?;
clear_memories_for_config(&config).await
}
async fn clear_memories_for_config(config: &codex_core::config::Config) -> anyhow::Result<()> {
if !config.features.enabled(codex_features::Feature::Sqlite) {
anyhow::bail!("`codex debug clear-memories` requires SQLite, which is disabled");
}
let memories_path = memories_db_path(config.sqlite_home.as_path());
let cleared_memories_db =
@@ -2500,6 +2507,37 @@ mod tests {
use codex_protocol::ThreadId;
use codex_tui::TokenUsage;
use pretty_assertions::assert_eq;
use tempfile::TempDir;
#[tokio::test]
async fn clear_memories_rejects_disabled_sqlite_without_mutating_state() -> anyhow::Result<()> {
let codex_home = TempDir::new()?;
let mut config = ConfigBuilder::default()
.codex_home(codex_home.path().to_path_buf())
.loader_overrides(LoaderOverrides::without_managed_config_for_tests())
.build()
.await?;
config.sqlite_home = codex_home.path().to_path_buf();
config.features.disable(codex_features::Feature::Sqlite)?;
let memory_root = codex_home.path().join("memories");
let memory_file = memory_root.join("memory_summary.md");
std::fs::create_dir_all(&memory_root)?;
std::fs::write(&memory_file, "keep me")?;
let error = clear_memories_for_config(&config)
.await
.expect_err("disabled SQLite should reject the command");
assert_eq!(
error.to_string(),
"`codex debug clear-memories` requires SQLite, which is disabled"
);
assert_eq!(std::fs::read_to_string(memory_file)?, "keep me");
for db in codex_state::runtime_db_paths(config.sqlite_home.as_path()) {
assert!(!db.path.exists(), "{} should not exist", db.label);
}
Ok(())
}
#[test]
fn exec_server_remote_auth_accepts_api_key_auth() {
+27
View File
@@ -4,5 +4,32 @@ pub use codex_rollout::state_db::StateDbHandle;
use crate::config::Config;
pub async fn init_state_db(config: &Config) -> Option<StateDbHandle> {
if !config.features.enabled(codex_features::Feature::Sqlite) {
return None;
}
rollout_state_db::init(config).await
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::ConfigBuilder;
use codex_features::Feature;
use tempfile::TempDir;
#[tokio::test]
async fn sqlite_disabled_skips_state_db_initialization() -> anyhow::Result<()> {
let codex_home = TempDir::new()?;
let mut config = ConfigBuilder::default()
.codex_home(codex_home.path().to_path_buf())
.build()
.await?;
config.features.disable(Feature::Sqlite)?;
assert!(init_state_db(&config).await.is_none());
for db in codex_state::runtime_db_paths(config.sqlite_home.as_path()) {
assert!(!db.path.exists(), "{} should not exist", db.label);
}
Ok(())
}
}
+6 -1
View File
@@ -549,6 +549,11 @@ impl Features {
}
pub fn normalize_dependencies(&mut self) {
if !self.enabled(Feature::Sqlite) {
// Keep every feature that cannot operate without SQLite in this block.
self.disable(Feature::Goals);
self.disable(Feature::SpawnCsv);
}
if self.enabled(Feature::SpawnCsv) && !self.enabled(Feature::Collab) {
self.enable(Feature::Collab);
}
@@ -919,7 +924,7 @@ pub const FEATURES: &[FeatureSpec] = &[
FeatureSpec {
id: Feature::Sqlite,
key: "sqlite",
stage: Stage::Removed,
stage: Stage::Stable,
default_enabled: true,
},
FeatureSpec {
+14
View File
@@ -47,6 +47,20 @@ fn use_legacy_landlock_is_deprecated_and_disabled_by_default() {
assert_eq!(Feature::UseLegacyLandlock.default_enabled(), false);
}
#[test]
fn sqlite_is_stable_enabled_by_default_and_configurable() {
assert_eq!(Feature::Sqlite.stage(), Stage::Stable);
assert_eq!(Feature::Sqlite.default_enabled(), true);
let mut features = Features::with_defaults();
features.enable(Feature::SpawnCsv);
features.apply_map(&BTreeMap::from([("sqlite".to_string(), false)]));
features.normalize_dependencies();
assert_eq!(features.enabled(Feature::Sqlite), false);
assert_eq!(features.enabled(Feature::Goals), false);
assert_eq!(features.enabled(Feature::SpawnCsv), false);
}
#[test]
fn use_linux_sandbox_bwrap_is_removed_and_disabled_by_default() {
assert_eq!(Feature::UseLinuxSandboxBwrap.stage(), Stage::Removed);
+74
View File
@@ -282,6 +282,10 @@ async fn init_state_db_for_app_server_target(
config: &Config,
app_server_target: &AppServerTarget,
) -> std::io::Result<Option<StateDbHandle>> {
if !config.features.enabled(codex_features::Feature::Sqlite) {
return Ok(None);
}
match app_server_target {
AppServerTarget::Embedded => state_db::try_init(config).await.map(Some).map_err(|err| {
let database_path = codex_state::runtime_db_path_for_corruption_error(&err)
@@ -814,6 +818,17 @@ fn app_server_target_for_launch(
}
}
fn app_server_target_for_effective_sqlite_setting(
target: AppServerTarget,
sqlite_enabled: bool,
) -> AppServerTarget {
// An implicitly reused daemon cannot adopt this invocation's effective feature set.
match target {
AppServerTarget::LocalDaemon { .. } if !sqlite_enabled => AppServerTarget::Embedded,
target => target,
}
}
fn loader_overrides_are_default(loader_overrides: &LoaderOverrides) -> bool {
let loader_overrides_are_default = loader_overrides.user_config_path.is_none()
&& loader_overrides.user_config_profile.is_none()
@@ -1067,6 +1082,10 @@ pub async fn run_main(
strict_config,
)
.await;
let app_server_target = app_server_target_for_effective_sqlite_setting(
app_server_target,
config.features.enabled(codex_features::Feature::Sqlite),
);
remove_legacy_tui_log_file(config.codex_home.as_path());
@@ -2319,6 +2338,44 @@ mod tests {
Ok(())
}
#[test]
fn sqlite_disabled_does_not_reuse_implicit_local_daemon() -> color_eyre::Result<()> {
let local_endpoint = RemoteAppServerEndpoint::UnixSocket {
socket_path: AbsolutePathBuf::relative_to_current_dir("local.sock")?,
};
let local_target = AppServerTarget::LocalDaemon {
endpoint: local_endpoint,
};
assert_eq!(
app_server_target_for_effective_sqlite_setting(
local_target.clone(),
/*sqlite_enabled*/ false,
),
AppServerTarget::Embedded
);
assert_eq!(
app_server_target_for_effective_sqlite_setting(
local_target.clone(),
/*sqlite_enabled*/ true,
),
local_target
);
let remote_target = AppServerTarget::Remote {
endpoint: RemoteAppServerEndpoint::UnixSocket {
socket_path: AbsolutePathBuf::relative_to_current_dir("remote.sock")?,
},
};
assert_eq!(
app_server_target_for_effective_sqlite_setting(
remote_target.clone(),
/*sqlite_enabled*/ false,
),
remote_target
);
Ok(())
}
#[test]
fn can_reuse_implicit_local_daemon_requires_default_launch_config() -> color_eyre::Result<()> {
let mut loader_overrides = LoaderOverrides::default();
@@ -2916,6 +2973,23 @@ mod tests {
Ok(())
}
#[tokio::test]
async fn embedded_state_db_can_be_explicitly_disabled() -> color_eyre::Result<()> {
let temp_dir = TempDir::new()?;
let mut config = build_config(&temp_dir).await?;
config.features.disable(codex_features::Feature::Sqlite)?;
let occupied_sqlite_home = temp_dir.path().join("sqlite-home");
std::fs::write(&occupied_sqlite_home, "occupied")?;
config.sqlite_home = occupied_sqlite_home.clone();
let state_db =
init_state_db_for_app_server_target(&config, &AppServerTarget::Embedded).await?;
assert!(state_db.is_none());
assert_eq!(std::fs::read_to_string(occupied_sqlite_home)?, "occupied");
Ok(())
}
#[tokio::test]
async fn embedded_state_db_corruption_preserves_failed_database_for_cli_recovery()
-> color_eyre::Result<()> {
@@ -384,6 +384,10 @@ async fn start_app_server_for_archive_command(
.build()
.await
.wrap_err("failed to load configuration")?;
let app_server_target = super::app_server_target_for_effective_sqlite_setting(
app_server_target,
config.features.enabled(codex_features::Feature::Sqlite),
);
let state_db = super::init_state_db_for_app_server_target(&config, &app_server_target)
.await
.wrap_err("failed to initialize state database")?;