fix(core/version): enforce minimum wire-compatible version

Clamp wire-facing client version to 0.98.0 for User-Agent and /models client_version when local builds report older versions like 0.0.0.

Adds regression tests for UA and remote model query behavior.

Fixes #539
This commit is contained in:
James Peter
2026-02-08 07:58:13 +10:00
parent 02cd20f2f0
commit a058583fb9
4 changed files with 90 additions and 11 deletions
+61
View File
@@ -8,7 +8,68 @@ pub const CODE_VERSION: &str = {
}
};
pub const MIN_WIRE_COMPAT_VERSION: &str = "0.98.0";
fn parse_semver_triplet(version: &str) -> Option<(u64, u64, u64)> {
let core = version.split_once(['-', '+']).map_or(version, |(v, _)| v);
let mut parts = core.split('.');
let major = parts.next()?.parse().ok()?;
let minor = parts.next()?.parse().ok()?;
let patch = parts.next()?.parse().ok()?;
Some((major, minor, patch))
}
fn wire_compatible_version_for(version: &str) -> &str {
let Some(version_triplet) = parse_semver_triplet(version) else {
return version;
};
let Some(min_triplet) = parse_semver_triplet(MIN_WIRE_COMPAT_VERSION) else {
return version;
};
if version_triplet < min_triplet {
MIN_WIRE_COMPAT_VERSION
} else {
version
}
}
#[inline]
pub fn version() -> &'static str {
CODE_VERSION
}
#[inline]
pub fn wire_compatible_version() -> &'static str {
wire_compatible_version_for(CODE_VERSION)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn wire_compat_clamps_old_versions() {
assert_eq!(wire_compatible_version_for("0.0.0"), MIN_WIRE_COMPAT_VERSION);
assert_eq!(wire_compatible_version_for("0.6.59"), MIN_WIRE_COMPAT_VERSION);
assert_eq!(
wire_compatible_version_for("0.6.59-dev+abc123"),
MIN_WIRE_COMPAT_VERSION
);
}
#[test]
fn wire_compat_keeps_new_versions() {
assert_eq!(wire_compatible_version_for("0.98.0"), "0.98.0");
assert_eq!(wire_compatible_version_for("0.99.1"), "0.99.1");
assert_eq!(wire_compatible_version_for("1.0.0"), "1.0.0");
}
#[test]
fn wire_compat_keeps_invalid_versions() {
assert_eq!(wire_compatible_version_for("dev"), "dev");
assert_eq!(wire_compatible_version_for("0.1"), "0.1");
}
}
+13 -1
View File
@@ -87,7 +87,7 @@ pub fn originator() -> Originator {
}
pub fn get_code_user_agent(originator_override: Option<&str>) -> String {
let build_version = code_version::version();
let build_version = code_version::wire_compatible_version();
let os_info = os_info::get();
let originator_value = originator_override
.map(str::to_string)
@@ -228,6 +228,18 @@ mod tests {
});
}
#[test]
fn test_get_code_user_agent_uses_wire_compatible_version() {
with_originator_env_cleared(|| {
let user_agent = get_code_user_agent(None);
let expected = format!("{DEFAULT_ORIGINATOR}/{}", code_version::wire_compatible_version());
assert!(
user_agent.starts_with(&expected),
"expected user-agent to start with {expected}, got {user_agent}"
);
});
}
#[tokio::test]
async fn test_create_client_sets_default_headers() {
use wiremock::Mock;
+2 -7
View File
@@ -427,12 +427,7 @@ fn map_truncation_policy(
}
}
/// Convert the build's version triple into a whole semver string.
/// Build a client version string that remains wire-compatible with hosted models.
fn format_client_version_to_whole() -> String {
format!(
"{}.{}.{}",
env!("CARGO_PKG_VERSION_MAJOR"),
env!("CARGO_PKG_VERSION_MINOR"),
env!("CARGO_PKG_VERSION_PATCH")
)
code_version::wire_compatible_version().to_string()
}
+14 -3
View File
@@ -66,6 +66,14 @@ fn auth_manager_chatgpt() -> std::sync::Arc<AuthManager> {
AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing())
}
fn query_param(url: &str, key: &str) -> Option<String> {
let (_, query) = url.split_once('?')?;
query.split('&').find_map(|part| {
let (k, v) = part.split_once('=')?;
(k == key).then(|| v.to_string())
})
}
#[tokio::test]
async fn refresh_remote_models_uses_cache_when_fresh() {
if skip_if_no_network() {
@@ -103,9 +111,12 @@ async fn refresh_remote_models_uses_cache_when_fresh() {
let requests = server.received_requests().await.expect("requests");
assert_eq!(requests.len(), 1);
assert!(
requests[0].url.as_str().contains("client_version="),
"expected client_version query param"
let request_url = requests[0].url.as_str();
let client_version = query_param(request_url, "client_version").expect("client_version query param");
assert_eq!(
client_version,
code_version::wire_compatible_version(),
"expected client_version query param in {request_url}"
);
// Second refresh should hit the fresh in-memory snapshot and avoid the network.