Skip to content

Commit cb6db8d

Browse files
committed
fix(config): accept OpenBitFun 1.0 prerelease configurations
Apply the product-generation floor to the numeric version core so beta and nightly configurations written by OpenBitFun 1.0 can be loaded and imported. Keep product identity, schema, pre-1.0 rejection, and actual-version writes unchanged. Cover prerelease validation, load-save-restart cycles, explicit imports, and account-settings imports with focused regression tests.
1 parent 858fda9 commit cb6db8d

2 files changed

Lines changed: 88 additions & 8 deletions

File tree

‎src/crates/assembly/core/src/service/config/manager.rs‎

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,11 @@ fn invalid_config_error(context: &str, result: &ConfigValidationResult) -> OpenB
2929

3030
const MIN_OPENBITFUN_CONFIG_VERSION: (u64, u64, u64) = (1, 0, 0);
3131

32-
fn parse_semver_floor(version: &str) -> Option<((u64, u64, u64), bool)> {
32+
fn parse_semver_floor(version: &str) -> Option<(u64, u64, u64)> {
3333
let without_build = version.split_once('+').map_or(version, |(value, _)| value);
34-
let (core, has_prerelease) = without_build
34+
let core = without_build
3535
.split_once('-')
36-
.map_or((without_build, false), |(value, _)| (value, true));
36+
.map_or(without_build, |(value, _)| value);
3737
let mut parts = core.split('.');
3838
let parsed = (
3939
parts.next()?.parse().ok()?,
@@ -43,7 +43,7 @@ fn parse_semver_floor(version: &str) -> Option<((u64, u64, u64), bool)> {
4343
if parts.next().is_some() {
4444
return None;
4545
}
46-
Some((parsed, has_prerelease))
46+
Some(parsed)
4747
}
4848

4949
pub(crate) fn validate_openbitfun_product_version(
@@ -58,14 +58,14 @@ pub(crate) fn validate_openbitfun_product_version(
5858
)));
5959
}
6060

61-
let Some((parsed, has_prerelease)) = parse_semver_floor(version) else {
61+
let Some(parsed) = parse_semver_floor(version) else {
6262
return Err(OpenBitFunError::validation(format!(
6363
"{context} version '{version}' is not a valid OpenBitFun version"
6464
)));
6565
};
66-
if parsed < MIN_OPENBITFUN_CONFIG_VERSION
67-
|| (parsed == MIN_OPENBITFUN_CONFIG_VERSION && has_prerelease)
68-
{
66+
// This floor separates product generations, not release-channel precedence.
67+
// OpenBitFun 1.0 prereleases write the same product identity and schema.
68+
if parsed < MIN_OPENBITFUN_CONFIG_VERSION {
6969
return Err(OpenBitFunError::validation(format!(
7070
"{context} version '{version}' predates OpenBitFun 1.0.0"
7171
)));
@@ -934,6 +934,30 @@ mod tests {
934934
}
935935
}
936936

937+
#[test]
938+
fn current_config_contract_accepts_openbitfun_prerelease_versions() {
939+
for version in [
940+
"1.0.0-beta.1",
941+
"1.0.0-beta.2+build.7",
942+
"1.0.0-nightly.20260906",
943+
"1.0.0-rc.1",
944+
"1.0.0",
945+
"1.0.1-beta.1",
946+
] {
947+
let mut current = serde_json::to_value(GlobalConfig::default()).unwrap();
948+
current["version"] = serde_json::json!(version);
949+
validate_current_config_value(&current, "test config")
950+
.unwrap_or_else(|error| panic!("{version}: {error}"));
951+
}
952+
953+
for version in ["0.2.19", "0.9.9-beta.1", "0.9.9+build.7"] {
954+
let mut legacy = serde_json::to_value(GlobalConfig::default()).unwrap();
955+
legacy["version"] = serde_json::json!(version);
956+
let error = validate_current_config_value(&legacy, "test config").unwrap_err();
957+
assert!(error.to_string().contains("predates OpenBitFun 1.0.0"));
958+
}
959+
}
960+
937961
#[test]
938962
fn persistence_omits_default_memories_config() {
939963
let config = GlobalConfig::default();

‎src/crates/assembly/core/src/service/config/service.rs‎

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1043,6 +1043,62 @@ mod tests {
10431043
}
10441044
}
10451045

1046+
#[tokio::test]
1047+
async fn prerelease_config_survives_load_save_and_restart() {
1048+
for version in ["1.0.0-beta.1", "1.0.0-nightly.20260906"] {
1049+
let name = "prerelease-config-restart";
1050+
let (service, dir) = test_service(name).await;
1051+
let mut config: GlobalConfig = service.get_config(None).await.unwrap();
1052+
config.version = version.to_string();
1053+
config.app.language = "zh-CN".to_string();
1054+
drop(service);
1055+
1056+
let config_file = dir.path().join(name).join("config").join("app.json");
1057+
let original = serde_json::to_string_pretty(&config).unwrap();
1058+
tokio::fs::write(&config_file, &original).await.unwrap();
1059+
let service = restart_test_service(&dir, name).await;
1060+
let loaded: GlobalConfig = service.get_config(None).await.unwrap();
1061+
assert_eq!(loaded.version, version);
1062+
assert_eq!(loaded.app.language, "zh-CN");
1063+
assert_eq!(
1064+
tokio::fs::read_to_string(&config_file).await.unwrap(),
1065+
original
1066+
);
1067+
1068+
service.set_config("app.language", &"en-US").await.unwrap();
1069+
drop(service);
1070+
let restarted = restart_test_service(&dir, name).await;
1071+
let saved: GlobalConfig = restarted.get_config(None).await.unwrap();
1072+
assert_eq!(saved.version, env!("CARGO_PKG_VERSION"));
1073+
assert_eq!(saved.app.language, "en-US");
1074+
}
1075+
}
1076+
1077+
#[tokio::test]
1078+
async fn prerelease_exports_support_explicit_import_and_account_settings() {
1079+
for version in ["1.0.0-beta.1", "1.0.0-nightly.20260906"] {
1080+
for account_sync in [false, true] {
1081+
let (service, _dir) = test_service("prerelease-config-import").await;
1082+
let mut export = current_export(GlobalConfig::default());
1083+
export.version = version.to_string();
1084+
export.config.version = version.to_string();
1085+
export.config.app.language = "zh-CN".to_string();
1086+
let export: ConfigExport =
1087+
serde_json::from_str(&serde_json::to_string(&export).unwrap()).unwrap();
1088+
1089+
let imported = if account_sync {
1090+
service.import_account_settings(export).await.unwrap()
1091+
} else {
1092+
service.import_config(export).await.unwrap()
1093+
};
1094+
assert!(imported.success, "{:?}", imported.errors);
1095+
let config: GlobalConfig = service.get_config(None).await.unwrap();
1096+
assert_eq!(config.app.language, "zh-CN");
1097+
assert_eq!(config.version, env!("CARGO_PKG_VERSION"));
1098+
}
1099+
}
1100+
}
1101+
10461102
#[tokio::test]
10471103
async fn pre_1_0_exports_are_rejected_without_changing_current_config() {
10481104
for account_sync in [false, true] {

0 commit comments

Comments
 (0)