am
2026-08-18 16:52:44 -07:00
parent d4032190ef
commit 71e2d6a4ab
2 changed files with 835 additions and 682 deletions

View File

@@ -40,7 +40,8 @@ fn migrate_openai_credential_record(
record: &mut Map<String, Value>,
effective_provider: Option<&str>,
inherited_env_vars: Option<&Map<String, Value>>,
) -> bool {
owns_effective_provider: bool,
) -> Result<bool, String> {
let local_provider = record
.get("provider")
.and_then(Value::as_str)
@@ -51,7 +52,7 @@ fn migrate_openai_credential_record(
.map(str::to_ascii_lowercase)
.filter(|provider| matches!(provider.as_str(), "openai" | "openai-compat"))
else {
return false;
return Ok(false);
};
let official_endpoint = uses_official_openai_endpoint(
record.get("env_vars").and_then(Value::as_object),
@@ -63,7 +64,18 @@ fn migrate_openai_credential_record(
"openai-compat"
};
let mut changed = false;
if local_provider.is_some() && provider != target_provider {
if provider != target_provider {
if !owns_effective_provider {
return Err(format!(
"cannot migrate a linked instance whose local endpoint requires provider {target_provider:?} while its definition resolves to {provider:?}"
));
}
// Classification is based on the fully layered endpoint. Persist the
// resulting identity at the tier that owns provider resolution even
// when that tier previously inherited its provider. Without this, the
// rewrite is locally plausible but resolves back to the old provider
// after restart — precisely the kind of lie a one-shot marker makes
// permanent.
record.insert(
"provider".to_string(),
Value::String(target_provider.to_string()),
@@ -71,21 +83,21 @@ fn migrate_openai_credential_record(
changed = true;
}
if target_provider != "openai" {
return changed;
return Ok(changed);
}
let Some(env_vars) = record.get_mut("env_vars").and_then(Value::as_object_mut) else {
return changed;
return Ok(changed);
};
changed |= env_vars.remove(OPENAI_COMPAT_BASE_URL).is_some();
if env_vars.contains_key(OPENAI_API_KEY) {
return changed;
return Ok(changed);
}
let Some(legacy_key) = env_vars.remove(OPENAI_COMPAT_API_KEY) else {
return changed;
return Ok(changed);
};
env_vars.insert(OPENAI_API_KEY.to_string(), legacy_key);
true
Ok(true)
}
#[derive(Clone, Debug)]
@@ -151,105 +163,130 @@ fn migrate_openai_credentials_in_file(
.map_err(|error| format!("failed to parse {}: {error}", path.display()))?;
let changed = match &mut root {
Value::Array(records) => {
// Linked instances resolve provider and env from their definition,
// not from the instance's legacy snapshot. Build that inheritance
// view before mutating any record so classification follows the same
// precedence as spawn/readiness.
let definitions: std::collections::HashMap<String, DefinitionContext> = records
.iter()
.filter_map(Value::as_object)
.filter(|record| record.get("pubkey").and_then(Value::as_str) == Some(""))
.filter_map(|record| {
let slug = record.get("slug")?.as_str()?.to_string();
let provider = record
.get("provider")
.and_then(Value::as_str)
.map(str::to_owned);
let runtime = record
.get("runtime")
.and_then(Value::as_str)
.map(str::to_owned);
let env = record
.get("env_vars")
.and_then(Value::as_object)
.cloned()
.unwrap_or_default();
Some((
slug,
DefinitionContext {
provider,
runtime,
env,
},
))
})
.collect();
let definition_contexts = |records: &[Value]| {
records
.iter()
.filter_map(Value::as_object)
.filter(|record| record.get("pubkey").and_then(Value::as_str) == Some(""))
.filter_map(|record| {
let slug = record.get("slug")?.as_str()?.to_string();
let provider = record
.get("provider")
.and_then(Value::as_str)
.map(str::to_owned);
let runtime = record
.get("runtime")
.and_then(Value::as_str)
.map(str::to_owned);
let env = record
.get("env_vars")
.and_then(Value::as_object)
.cloned()
.unwrap_or_default();
Some((
slug,
DefinitionContext {
provider,
runtime,
env,
},
))
})
.collect::<std::collections::HashMap<String, DefinitionContext>>()
};
let mut changed = false;
for record in records.iter_mut().filter_map(Value::as_object_mut) {
let definition = record
.get("persona_id")
.and_then(Value::as_str)
.and_then(|id| definitions.get(id));
// Provider ownership and env ownership are not the same graph.
// Definitions and standalone records own their provider tier, so
// migrate them first. Linked instances are definition-authoritative
// and must be evaluated only after those definitions have reached
// their persisted post-migration identity.
for record in records
.iter_mut()
.filter_map(Value::as_object_mut)
.filter(|record| record.get("persona_id").and_then(Value::as_str).is_none())
{
let local_provider = record
.get("provider")
.and_then(Value::as_str)
.map(str::trim)
.filter(|provider| !provider.is_empty())
.map(str::to_owned);
// Linked instances ignore their legacy provider snapshot. Their
// definition is authoritative and falls back directly to the
// global provider; only definition-less records consult their
// own provider before the global fallback. Own the selected
// value so mutating the record below cannot overlap a borrow of
// its provider field.
let effective_provider = match definition {
Some(definition) => definition
.provider
.as_deref()
.map(str::trim)
.filter(|provider| !provider.is_empty())
.map(str::to_owned)
.or_else(|| effective_provider.map(str::to_owned)),
None => local_provider.or_else(|| effective_provider.map(str::to_owned)),
};
let local_runtime = record
let resolved_provider = local_provider
.as_deref()
.or(effective_provider)
.map(str::to_owned);
let selected_runtime = record
.get("runtime")
.and_then(Value::as_str)
.map(str::trim)
.filter(|runtime| !runtime.is_empty());
let selected_runtime = local_runtime
.or_else(|| definition.and_then(|definition| definition.runtime.as_deref()))
.map(str::trim)
.filter(|runtime| !runtime.is_empty());
let command_override_present = record
.get("agent_command_override")
.and_then(Value::as_str)
.map(str::trim)
.is_some_and(|command| !command.is_empty());
let merged_inherited_env = {
let mut merged = harness_env_for_runtime(
selected_runtime,
command_override_present,
harnesses,
)?;
if let Some(inherited) = inherited_env_vars {
merged.extend(inherited.clone());
}
if let Some(definition) = definition {
merged.extend(definition.env.clone());
}
(!merged.is_empty()).then_some(merged)
};
let mut inherited_env =
harness_env_for_runtime(selected_runtime, command_override_present, harnesses)?;
if let Some(global_env) = inherited_env_vars {
inherited_env.extend(global_env.clone());
}
changed |= migrate_openai_credential_record(
record,
effective_provider.as_deref(),
merged_inherited_env.as_ref().or(inherited_env_vars),
);
resolved_provider.as_deref(),
(!inherited_env.is_empty()).then_some(&inherited_env),
true,
)?;
}
let definitions = definition_contexts(records);
for record in records.iter_mut().filter_map(Value::as_object_mut) {
let Some(persona_id) = record.get("persona_id").and_then(Value::as_str) else {
continue;
};
let definition = definitions
.get(persona_id)
.ok_or_else(|| format!("cannot resolve linked definition {persona_id:?}"))?;
let resolved_provider = definition
.provider
.as_deref()
.map(str::trim)
.filter(|provider| !provider.is_empty())
.or(effective_provider);
let selected_runtime = record
.get("runtime")
.and_then(Value::as_str)
.map(str::trim)
.filter(|runtime| !runtime.is_empty())
.or_else(|| {
definition
.runtime
.as_deref()
.map(str::trim)
.filter(|runtime| !runtime.is_empty())
});
let command_override_present = record
.get("agent_command_override")
.and_then(Value::as_str)
.map(str::trim)
.is_some_and(|command| !command.is_empty());
let mut inherited_env =
harness_env_for_runtime(selected_runtime, command_override_present, harnesses)?;
if let Some(global_env) = inherited_env_vars {
inherited_env.extend(global_env.clone());
}
inherited_env.extend(definition.env.clone());
changed |= migrate_openai_credential_record(
record,
resolved_provider,
(!inherited_env.is_empty()).then_some(&inherited_env),
false,
)?;
}
changed
}
Value::Object(record) => {
migrate_openai_credential_record(record, effective_provider, inherited_env_vars)
migrate_openai_credential_record(record, effective_provider, inherited_env_vars, true)?
}
_ => {
return Err(format!(
@@ -330,600 +367,5 @@ pub(super) fn migrate_openai_credentials(app: &tauri::AppHandle) {
}
#[cfg(test)]
mod tests {
use super::*;
fn custom_gateway_harnesses() -> std::collections::HashMap<String, Map<String, Value>> {
std::collections::HashMap::from([(
"custom-gateway".to_string(),
serde_json::json!({
"OPENAI_COMPAT_BASE_URL": "https://gateway.example/v1"
})
.as_object()
.unwrap()
.clone(),
)])
}
fn migrate_file(
path: &Path,
effective_provider: Option<&str>,
inherited_env_vars: Option<&Map<String, Value>>,
) -> Result<(), String> {
migrate_openai_credentials_in_file(
path,
effective_provider,
inherited_env_vars,
&std::collections::HashMap::new(),
)
}
fn migrate(value: Value) -> (Value, bool) {
let mut record = value.as_object().unwrap().clone();
let changed = migrate_openai_credential_record(&mut record, None, None);
(Value::Object(record), changed)
}
#[test]
fn official_openai_renames_the_legacy_credential_and_removes_routing_state() {
let (record, changed) = migrate(serde_json::json!({
"provider": "openai",
"env_vars": {
"OPENAI_COMPAT_API_KEY": "official-secret",
"OPENAI_COMPAT_BASE_URL": "https://api.openai.com/v1",
"KEEP": "value"
}
}));
assert!(changed);
assert_eq!(record["env_vars"][OPENAI_API_KEY], "official-secret");
assert!(record["env_vars"].get(OPENAI_COMPAT_API_KEY).is_none());
assert!(record["env_vars"].get(OPENAI_COMPAT_BASE_URL).is_none());
assert_eq!(record["env_vars"]["KEEP"], "value");
}
#[test]
fn custom_endpoint_stays_openai_compat_with_its_credential() {
let original = serde_json::json!({
"provider": "openai-compat",
"env_vars": {
"OPENAI_COMPAT_API_KEY": "compat-secret",
"OPENAI_COMPAT_BASE_URL": "https://gateway.example/v1"
}
});
let (record, changed) = migrate(original.clone());
assert!(!changed);
assert_eq!(record, original);
}
#[test]
fn legacy_openai_custom_endpoint_becomes_openai_compat() {
let (record, changed) = migrate(serde_json::json!({
"provider": "openai",
"env_vars": {
"OPENAI_COMPAT_API_KEY": "compat-secret",
"OPENAI_COMPAT_BASE_URL": "http://localhost:11434/v1"
}
}));
assert!(changed);
assert_eq!(record["provider"], "openai-compat");
assert_eq!(record["env_vars"][OPENAI_COMPAT_API_KEY], "compat-secret");
assert!(record["env_vars"].get(OPENAI_API_KEY).is_none());
}
#[test]
fn non_api_openai_subdomain_is_still_a_compat_endpoint() {
let (record, changed) = migrate(serde_json::json!({
"provider": "openai",
"env_vars": {
"OPENAI_COMPAT_API_KEY": "compat-secret",
"OPENAI_COMPAT_BASE_URL": "https://gateway.openai.com/v1"
}
}));
assert!(changed);
assert_eq!(record["provider"], "openai-compat");
assert_eq!(record["env_vars"][OPENAI_COMPAT_API_KEY], "compat-secret");
}
#[test]
fn non_api_path_on_openai_host_is_still_a_compat_endpoint() {
let (record, changed) = migrate(serde_json::json!({
"provider": "openai",
"env_vars": {
"OPENAI_COMPAT_API_KEY": "compat-secret",
"OPENAI_COMPAT_BASE_URL": "https://api.openai.com/proxy/v1"
}
}));
assert!(changed);
assert_eq!(record["provider"], "openai-compat");
assert_eq!(record["env_vars"][OPENAI_COMPAT_API_KEY], "compat-secret");
assert!(record["env_vars"].get(OPENAI_API_KEY).is_none());
}
#[test]
fn legacy_openai_compat_without_custom_endpoint_becomes_official() {
let (record, changed) = migrate(serde_json::json!({
"provider": "openai-compat",
"env_vars": { "OPENAI_COMPAT_API_KEY": "official-secret" }
}));
assert!(changed);
assert_eq!(record["provider"], "openai");
assert_eq!(record["env_vars"][OPENAI_API_KEY], "official-secret");
assert!(record["env_vars"].get(OPENAI_COMPAT_API_KEY).is_none());
}
#[test]
fn existing_distinct_credentials_are_both_preserved() {
let original = serde_json::json!({
"provider": "openai",
"env_vars": {
"OPENAI_API_KEY": "official-secret",
"OPENAI_COMPAT_API_KEY": "compat-secret"
}
});
let (record, changed) = migrate(original.clone());
assert!(!changed);
assert_eq!(record, original);
}
#[test]
fn providerless_record_is_not_guessed() {
let original = serde_json::json!({
"env_vars": { "OPENAI_COMPAT_API_KEY": "ambiguous-secret" }
});
let (record, changed) = migrate(original.clone());
assert!(!changed);
assert_eq!(record, original);
}
#[test]
fn record_inherits_custom_global_endpoint_for_classification() {
let inherited_env = serde_json::json!({
"OPENAI_COMPAT_BASE_URL": "https://gateway.example/v1"
});
let mut record = serde_json::json!({
"provider": "openai",
"env_vars": { "OPENAI_COMPAT_API_KEY": "compat-secret" }
})
.as_object()
.unwrap()
.clone();
let changed =
migrate_openai_credential_record(&mut record, None, inherited_env.as_object());
assert!(changed);
assert_eq!(record["provider"], "openai-compat");
assert_eq!(record["env_vars"][OPENAI_COMPAT_API_KEY], "compat-secret");
assert!(record["env_vars"].get(OPENAI_API_KEY).is_none());
}
#[test]
fn local_endpoint_override_wins_over_inherited_endpoint() {
let inherited_env = serde_json::json!({
"OPENAI_COMPAT_BASE_URL": "https://gateway.example/v1"
});
let mut record = serde_json::json!({
"provider": "openai",
"env_vars": {
"OPENAI_COMPAT_API_KEY": "official-secret",
"OPENAI_COMPAT_BASE_URL": "https://api.openai.com/v1"
}
})
.as_object()
.unwrap()
.clone();
let changed =
migrate_openai_credential_record(&mut record, None, inherited_env.as_object());
assert!(changed);
assert_eq!(record["provider"], "openai");
assert_eq!(record["env_vars"][OPENAI_API_KEY], "official-secret");
}
#[test]
fn credentialed_openai_host_is_not_classified_as_official() {
let (record, changed) = migrate(serde_json::json!({
"provider": "openai",
"env_vars": {
"OPENAI_COMPAT_API_KEY": "compat-secret",
"OPENAI_COMPAT_BASE_URL": "https://attacker@example.com@api.openai.com/v1"
}
}));
assert!(changed);
assert_eq!(record["provider"], "openai-compat");
assert_eq!(record["env_vars"][OPENAI_COMPAT_API_KEY], "compat-secret");
}
#[test]
fn custom_harness_origin_prevents_official_credential_relabeling() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("managed-agents.json");
std::fs::write(
&path,
serde_json::to_vec(&serde_json::json!([{
"pubkey": "",
"slug": "custom-definition",
"runtime": "custom-gateway",
"provider": "openai",
"env_vars": { "OPENAI_COMPAT_API_KEY": "compat-secret" }
}]))
.unwrap(),
)
.unwrap();
let harnesses = custom_gateway_harnesses();
migrate_openai_credentials_in_file(&path, None, None, &harnesses).unwrap();
let records: Value =
serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
assert_eq!(records[0]["provider"], "openai-compat");
assert_eq!(
records[0]["env_vars"][OPENAI_COMPAT_API_KEY],
"compat-secret"
);
assert!(records[0]["env_vars"].get(OPENAI_API_KEY).is_none());
}
#[test]
fn unresolved_custom_harness_leaves_file_unmarked() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("managed-agents.json");
let original = serde_json::to_vec(&serde_json::json!([{
"pubkey": "",
"slug": "custom-definition",
"runtime": "missing-custom-harness",
"provider": "openai",
"env_vars": { "OPENAI_COMPAT_API_KEY": "compat-secret" }
}]))
.unwrap();
std::fs::write(&path, &original).unwrap();
let error = migrate_openai_credentials_in_file(
&path,
None,
None,
&std::collections::HashMap::new(),
)
.unwrap_err();
assert!(error.contains("unresolved custom harness"), "{error}");
assert_eq!(std::fs::read(&path).unwrap(), original);
assert!(!sibling_path(&path, MIGRATION_SUFFIX).exists());
}
#[test]
fn raw_command_override_does_not_block_runtime_less_record_migration() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("managed-agents.json");
std::fs::write(
&path,
serde_json::to_vec(&serde_json::json!([{
"pubkey": "agent-pubkey",
"agent_command_override": "/opt/custom/my-agent",
"provider": "openai",
"env_vars": { "OPENAI_COMPAT_API_KEY": "official-secret" }
}]))
.unwrap(),
)
.unwrap();
migrate_openai_credentials_in_file(&path, None, None, &std::collections::HashMap::new())
.unwrap();
let records: Value =
serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
assert_eq!(records[0]["env_vars"][OPENAI_API_KEY], "official-secret");
assert!(sibling_path(&path, MIGRATION_SUFFIX).exists());
}
#[test]
fn raw_command_override_allows_migration_past_dangling_runtime() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("managed-agents.json");
std::fs::write(
&path,
serde_json::to_vec(&serde_json::json!([
{
"pubkey": "overridden-agent",
"runtime": "missing-custom-harness",
"agent_command_override": "/opt/custom/my-agent",
"provider": "openai",
"env_vars": { "OPENAI_COMPAT_API_KEY": "overridden-secret" }
},
{
"pubkey": "unrelated-agent",
"provider": "openai",
"env_vars": { "OPENAI_COMPAT_API_KEY": "unrelated-secret" }
}
]))
.unwrap(),
)
.unwrap();
migrate_openai_credentials_in_file(&path, None, None, &std::collections::HashMap::new())
.unwrap();
let records: Value =
serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
assert_eq!(records[0]["env_vars"][OPENAI_API_KEY], "overridden-secret");
assert_eq!(records[1]["env_vars"][OPENAI_API_KEY], "unrelated-secret");
assert!(sibling_path(&path, MIGRATION_SUFFIX).exists());
}
#[test]
fn global_credentials_ignore_preferred_runtime_harness_env() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("global-agent-config.json");
std::fs::write(
&path,
serde_json::to_vec(&serde_json::json!({
"preferred_runtime": "custom-gateway",
"provider": "openai",
"env_vars": { "OPENAI_COMPAT_API_KEY": "official-secret" }
}))
.unwrap(),
)
.unwrap();
let harnesses = custom_gateway_harnesses();
migrate_openai_credentials_in_file(&path, None, None, &harnesses).unwrap();
let global: Value = serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
assert_eq!(global["provider"], "openai");
assert_eq!(global["env_vars"][OPENAI_API_KEY], "official-secret");
assert!(global["env_vars"].get(OPENAI_COMPAT_API_KEY).is_none());
}
#[test]
fn runtime_less_record_ignores_custom_preferred_runtime() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("managed-agents.json");
std::fs::write(
&path,
serde_json::to_vec(&serde_json::json!([{
"pubkey": "agent-pubkey",
"provider": "openai",
"env_vars": { "OPENAI_COMPAT_API_KEY": "official-secret" }
}]))
.unwrap(),
)
.unwrap();
let harnesses = custom_gateway_harnesses();
migrate_openai_credentials_in_file(&path, None, None, &harnesses).unwrap();
let records: Value =
serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
assert_eq!(records[0]["provider"], "openai");
assert_eq!(records[0]["env_vars"][OPENAI_API_KEY], "official-secret");
}
#[test]
fn command_override_does_not_suppress_record_runtime_env() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("managed-agents.json");
std::fs::write(
&path,
serde_json::to_vec(&serde_json::json!([{
"pubkey": "agent-pubkey",
"runtime": "custom-gateway",
"agent_command_override": "buzz-agent",
"provider": "openai",
"env_vars": { "OPENAI_COMPAT_API_KEY": "official-secret" }
}]))
.unwrap(),
)
.unwrap();
let harnesses = custom_gateway_harnesses();
migrate_openai_credentials_in_file(&path, None, None, &harnesses).unwrap();
let records: Value =
serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
assert_eq!(records[0]["provider"], "openai-compat");
assert_eq!(
records[0]["env_vars"][OPENAI_COMPAT_API_KEY],
"official-secret"
);
assert!(records[0]["env_vars"].get(OPENAI_API_KEY).is_none());
}
#[test]
fn linked_instance_uses_definition_provider_and_layered_endpoint() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("managed-agents.json");
std::fs::write(
&path,
serde_json::to_vec(&serde_json::json!([
{
"pubkey": "",
"slug": "shared-definition",
"provider": "openai",
"env_vars": {
"OPENAI_COMPAT_API_KEY": "definition-compat-key",
"OPENAI_COMPAT_BASE_URL": "https://gateway.example/v1"
}
},
{
"pubkey": "agent-pubkey",
"persona_id": "shared-definition",
"provider": "openai",
"env_vars": { "OPENAI_COMPAT_API_KEY": "instance-compat-key" }
}
]))
.unwrap(),
)
.unwrap();
migrate_file(&path, Some("openai"), None).unwrap();
let records: Value =
serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
assert_eq!(records[0]["provider"], "openai-compat");
assert_eq!(records[1]["provider"], "openai-compat");
assert_eq!(
records[1]["env_vars"][OPENAI_COMPAT_API_KEY],
"instance-compat-key"
);
assert!(records[1]["env_vars"].get(OPENAI_API_KEY).is_none());
}
#[test]
fn linked_instance_ignores_legacy_provider_snapshot_when_definition_uses_global() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("managed-agents.json");
std::fs::write(
&path,
serde_json::to_vec(&serde_json::json!([
{
"pubkey": "",
"slug": "shared-definition",
"provider": null,
"env_vars": {}
},
{
"pubkey": "agent-pubkey",
"persona_id": "shared-definition",
"provider": "anthropic",
"env_vars": { "OPENAI_COMPAT_API_KEY": "legacy-official-key" }
}
]))
.unwrap(),
)
.unwrap();
migrate_file(&path, Some("openai"), None).unwrap();
let records: Value =
serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
assert_eq!(records[1]["provider"], "anthropic");
assert_eq!(
records[1]["env_vars"][OPENAI_API_KEY],
"legacy-official-key"
);
assert!(records[1]["env_vars"].get(OPENAI_COMPAT_API_KEY).is_none());
}
#[test]
fn array_migration_updates_every_record() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("managed-agents.json");
std::fs::write(
&path,
serde_json::to_vec(&serde_json::json!([
{
"provider": "openai",
"env_vars": { "OPENAI_COMPAT_API_KEY": "first" }
},
{
"provider": "openai",
"env_vars": { "OPENAI_COMPAT_API_KEY": "second" }
}
]))
.unwrap(),
)
.unwrap();
migrate_file(&path, None, None).unwrap();
let records: Value =
serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
assert_eq!(records[0]["env_vars"][OPENAI_API_KEY], "first");
assert_eq!(records[1]["env_vars"][OPENAI_API_KEY], "second");
}
#[test]
fn migration_marker_preserves_post_upgrade_compat_credentials() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("managed-agents.json");
std::fs::write(
&path,
serde_json::to_vec(&serde_json::json!([{
"provider": "openai",
"env_vars": { "OPENAI_COMPAT_API_KEY": "legacy-secret" }
}]))
.unwrap(),
)
.unwrap();
migrate_file(&path, None, None).unwrap();
let mut records: Value =
serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
records[0]["env_vars"][OPENAI_COMPAT_API_KEY] = Value::String("new-compat".to_string());
std::fs::write(&path, serde_json::to_vec(&records).unwrap()).unwrap();
migrate_file(&path, None, None).unwrap();
let records: Value =
serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
assert_eq!(records[0]["env_vars"][OPENAI_API_KEY], "legacy-secret");
assert_eq!(records[0]["env_vars"][OPENAI_COMPAT_API_KEY], "new-compat");
}
#[test]
fn changed_file_gets_pristine_backup_before_marker() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("global-agent-config.json");
let original = serde_json::json!({
"provider": "openai",
"env_vars": { "OPENAI_COMPAT_API_KEY": "official-secret" }
});
let original_bytes = serde_json::to_vec(&original).unwrap();
std::fs::write(&path, &original_bytes).unwrap();
migrate_file(&path, None, None).unwrap();
assert_eq!(
std::fs::read(sibling_path(&path, BACKUP_SUFFIX)).unwrap(),
original_bytes
);
assert_eq!(
std::fs::read_to_string(sibling_path(&path, MIGRATION_SUFFIX)).unwrap(),
"1\n"
);
let migrated: Value =
serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
assert_eq!(migrated["env_vars"][OPENAI_API_KEY], "official-secret");
}
#[test]
fn malformed_file_is_preserved_and_not_marked() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("managed-agents.json");
let original = b"{ not valid json";
std::fs::write(&path, original).unwrap();
let error = migrate_file(&path, None, None).unwrap_err();
assert!(error.contains("failed to parse"), "{error}");
assert_eq!(std::fs::read(&path).unwrap(), original);
assert!(!sibling_path(&path, BACKUP_SUFFIX).exists());
assert!(!sibling_path(&path, MIGRATION_SUFFIX).exists());
}
#[test]
fn absent_file_is_not_marked_so_later_legacy_content_can_be_migrated() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("global-agent-config.json");
migrate_file(&path, None, None).unwrap();
assert!(!sibling_path(&path, MIGRATION_SUFFIX).exists());
let legacy = serde_json::json!({
"provider": "openai",
"env_vars": { "OPENAI_COMPAT_API_KEY": "official-secret" }
});
std::fs::write(&path, serde_json::to_vec(&legacy).unwrap()).unwrap();
migrate_file(&path, None, None).unwrap();
let after: Value = serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
assert_eq!(after["env_vars"][OPENAI_API_KEY], "official-secret");
assert!(after["env_vars"].get(OPENAI_COMPAT_API_KEY).is_none());
}
}
#[path = "openai_credentials_tests.rs"]
mod tests;

View File

@@ -0,0 +1,711 @@
use super::*;
fn custom_gateway_harnesses() -> std::collections::HashMap<String, Map<String, Value>> {
std::collections::HashMap::from([(
"custom-gateway".to_string(),
serde_json::json!({
"OPENAI_COMPAT_BASE_URL": "https://gateway.example/v1"
})
.as_object()
.unwrap()
.clone(),
)])
}
fn migrate_file(
path: &Path,
effective_provider: Option<&str>,
inherited_env_vars: Option<&Map<String, Value>>,
) -> Result<(), String> {
migrate_openai_credentials_in_file(
path,
effective_provider,
inherited_env_vars,
&std::collections::HashMap::new(),
)
}
fn migrate(value: Value) -> (Value, bool) {
let mut record = value.as_object().unwrap().clone();
let changed = migrate_openai_credential_record(&mut record, None, None, true).unwrap();
(Value::Object(record), changed)
}
#[test]
fn official_openai_renames_the_legacy_credential_and_removes_routing_state() {
let (record, changed) = migrate(serde_json::json!({
"provider": "openai",
"env_vars": {
"OPENAI_COMPAT_API_KEY": "official-secret",
"OPENAI_COMPAT_BASE_URL": "https://api.openai.com/v1",
"KEEP": "value"
}
}));
assert!(changed);
assert_eq!(record["env_vars"][OPENAI_API_KEY], "official-secret");
assert!(record["env_vars"].get(OPENAI_COMPAT_API_KEY).is_none());
assert!(record["env_vars"].get(OPENAI_COMPAT_BASE_URL).is_none());
assert_eq!(record["env_vars"]["KEEP"], "value");
}
#[test]
fn custom_endpoint_stays_openai_compat_with_its_credential() {
let original = serde_json::json!({
"provider": "openai-compat",
"env_vars": {
"OPENAI_COMPAT_API_KEY": "compat-secret",
"OPENAI_COMPAT_BASE_URL": "https://gateway.example/v1"
}
});
let (record, changed) = migrate(original.clone());
assert!(!changed);
assert_eq!(record, original);
}
#[test]
fn legacy_openai_custom_endpoint_becomes_openai_compat() {
let (record, changed) = migrate(serde_json::json!({
"provider": "openai",
"env_vars": {
"OPENAI_COMPAT_API_KEY": "compat-secret",
"OPENAI_COMPAT_BASE_URL": "http://localhost:11434/v1"
}
}));
assert!(changed);
assert_eq!(record["provider"], "openai-compat");
assert_eq!(record["env_vars"][OPENAI_COMPAT_API_KEY], "compat-secret");
assert!(record["env_vars"].get(OPENAI_API_KEY).is_none());
}
#[test]
fn non_api_openai_subdomain_is_still_a_compat_endpoint() {
let (record, changed) = migrate(serde_json::json!({
"provider": "openai",
"env_vars": {
"OPENAI_COMPAT_API_KEY": "compat-secret",
"OPENAI_COMPAT_BASE_URL": "https://gateway.openai.com/v1"
}
}));
assert!(changed);
assert_eq!(record["provider"], "openai-compat");
assert_eq!(record["env_vars"][OPENAI_COMPAT_API_KEY], "compat-secret");
}
#[test]
fn non_api_path_on_openai_host_is_still_a_compat_endpoint() {
let (record, changed) = migrate(serde_json::json!({
"provider": "openai",
"env_vars": {
"OPENAI_COMPAT_API_KEY": "compat-secret",
"OPENAI_COMPAT_BASE_URL": "https://api.openai.com/proxy/v1"
}
}));
assert!(changed);
assert_eq!(record["provider"], "openai-compat");
assert_eq!(record["env_vars"][OPENAI_COMPAT_API_KEY], "compat-secret");
assert!(record["env_vars"].get(OPENAI_API_KEY).is_none());
}
#[test]
fn legacy_openai_compat_without_custom_endpoint_becomes_official() {
let (record, changed) = migrate(serde_json::json!({
"provider": "openai-compat",
"env_vars": { "OPENAI_COMPAT_API_KEY": "official-secret" }
}));
assert!(changed);
assert_eq!(record["provider"], "openai");
assert_eq!(record["env_vars"][OPENAI_API_KEY], "official-secret");
assert!(record["env_vars"].get(OPENAI_COMPAT_API_KEY).is_none());
}
#[test]
fn existing_distinct_credentials_are_both_preserved() {
let original = serde_json::json!({
"provider": "openai",
"env_vars": {
"OPENAI_API_KEY": "official-secret",
"OPENAI_COMPAT_API_KEY": "compat-secret"
}
});
let (record, changed) = migrate(original.clone());
assert!(!changed);
assert_eq!(record, original);
}
#[test]
fn providerless_record_is_not_guessed() {
let original = serde_json::json!({
"env_vars": { "OPENAI_COMPAT_API_KEY": "ambiguous-secret" }
});
let (record, changed) = migrate(original.clone());
assert!(!changed);
assert_eq!(record, original);
}
#[test]
fn record_inherits_custom_global_endpoint_for_classification() {
let inherited_env = serde_json::json!({
"OPENAI_COMPAT_BASE_URL": "https://gateway.example/v1"
});
let mut record = serde_json::json!({
"provider": "openai",
"env_vars": { "OPENAI_COMPAT_API_KEY": "compat-secret" }
})
.as_object()
.unwrap()
.clone();
let changed =
migrate_openai_credential_record(&mut record, None, inherited_env.as_object(), true)
.unwrap();
assert!(changed);
assert_eq!(record["provider"], "openai-compat");
assert_eq!(record["env_vars"][OPENAI_COMPAT_API_KEY], "compat-secret");
assert!(record["env_vars"].get(OPENAI_API_KEY).is_none());
}
#[test]
fn local_endpoint_override_wins_over_inherited_endpoint() {
let inherited_env = serde_json::json!({
"OPENAI_COMPAT_BASE_URL": "https://gateway.example/v1"
});
let mut record = serde_json::json!({
"provider": "openai",
"env_vars": {
"OPENAI_COMPAT_API_KEY": "official-secret",
"OPENAI_COMPAT_BASE_URL": "https://api.openai.com/v1"
}
})
.as_object()
.unwrap()
.clone();
let changed =
migrate_openai_credential_record(&mut record, None, inherited_env.as_object(), true)
.unwrap();
assert!(changed);
assert_eq!(record["provider"], "openai");
assert_eq!(record["env_vars"][OPENAI_API_KEY], "official-secret");
}
#[test]
fn credentialed_openai_host_is_not_classified_as_official() {
let (record, changed) = migrate(serde_json::json!({
"provider": "openai",
"env_vars": {
"OPENAI_COMPAT_API_KEY": "compat-secret",
"OPENAI_COMPAT_BASE_URL": "https://attacker@example.com@api.openai.com/v1"
}
}));
assert!(changed);
assert_eq!(record["provider"], "openai-compat");
assert_eq!(record["env_vars"][OPENAI_COMPAT_API_KEY], "compat-secret");
}
#[test]
fn custom_harness_origin_prevents_official_credential_relabeling() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("managed-agents.json");
std::fs::write(
&path,
serde_json::to_vec(&serde_json::json!([{
"pubkey": "",
"slug": "custom-definition",
"runtime": "custom-gateway",
"provider": "openai",
"env_vars": { "OPENAI_COMPAT_API_KEY": "compat-secret" }
}]))
.unwrap(),
)
.unwrap();
let harnesses = custom_gateway_harnesses();
migrate_openai_credentials_in_file(&path, None, None, &harnesses).unwrap();
let records: Value = serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
assert_eq!(records[0]["provider"], "openai-compat");
assert_eq!(
records[0]["env_vars"][OPENAI_COMPAT_API_KEY],
"compat-secret"
);
assert!(records[0]["env_vars"].get(OPENAI_API_KEY).is_none());
}
#[test]
fn unresolved_custom_harness_leaves_file_unmarked() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("managed-agents.json");
let original = serde_json::to_vec(&serde_json::json!([{
"pubkey": "",
"slug": "custom-definition",
"runtime": "missing-custom-harness",
"provider": "openai",
"env_vars": { "OPENAI_COMPAT_API_KEY": "compat-secret" }
}]))
.unwrap();
std::fs::write(&path, &original).unwrap();
let error =
migrate_openai_credentials_in_file(&path, None, None, &std::collections::HashMap::new())
.unwrap_err();
assert!(error.contains("unresolved custom harness"), "{error}");
assert_eq!(std::fs::read(&path).unwrap(), original);
assert!(!sibling_path(&path, MIGRATION_SUFFIX).exists());
}
#[test]
fn raw_command_override_does_not_block_runtime_less_record_migration() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("managed-agents.json");
std::fs::write(
&path,
serde_json::to_vec(&serde_json::json!([{
"pubkey": "agent-pubkey",
"agent_command_override": "/opt/custom/my-agent",
"provider": "openai",
"env_vars": { "OPENAI_COMPAT_API_KEY": "official-secret" }
}]))
.unwrap(),
)
.unwrap();
migrate_openai_credentials_in_file(&path, None, None, &std::collections::HashMap::new())
.unwrap();
let records: Value = serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
assert_eq!(records[0]["env_vars"][OPENAI_API_KEY], "official-secret");
assert!(sibling_path(&path, MIGRATION_SUFFIX).exists());
}
#[test]
fn raw_command_override_allows_migration_past_dangling_runtime() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("managed-agents.json");
std::fs::write(
&path,
serde_json::to_vec(&serde_json::json!([
{
"pubkey": "overridden-agent",
"runtime": "missing-custom-harness",
"agent_command_override": "/opt/custom/my-agent",
"provider": "openai",
"env_vars": { "OPENAI_COMPAT_API_KEY": "overridden-secret" }
},
{
"pubkey": "unrelated-agent",
"provider": "openai",
"env_vars": { "OPENAI_COMPAT_API_KEY": "unrelated-secret" }
}
]))
.unwrap(),
)
.unwrap();
migrate_openai_credentials_in_file(&path, None, None, &std::collections::HashMap::new())
.unwrap();
let records: Value = serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
assert_eq!(records[0]["env_vars"][OPENAI_API_KEY], "overridden-secret");
assert_eq!(records[1]["env_vars"][OPENAI_API_KEY], "unrelated-secret");
assert!(sibling_path(&path, MIGRATION_SUFFIX).exists());
}
#[test]
fn global_credentials_ignore_preferred_runtime_harness_env() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("global-agent-config.json");
std::fs::write(
&path,
serde_json::to_vec(&serde_json::json!({
"preferred_runtime": "custom-gateway",
"provider": "openai",
"env_vars": { "OPENAI_COMPAT_API_KEY": "official-secret" }
}))
.unwrap(),
)
.unwrap();
let harnesses = custom_gateway_harnesses();
migrate_openai_credentials_in_file(&path, None, None, &harnesses).unwrap();
let global: Value = serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
assert_eq!(global["provider"], "openai");
assert_eq!(global["env_vars"][OPENAI_API_KEY], "official-secret");
assert!(global["env_vars"].get(OPENAI_COMPAT_API_KEY).is_none());
}
#[test]
fn runtime_less_record_ignores_custom_preferred_runtime() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("managed-agents.json");
std::fs::write(
&path,
serde_json::to_vec(&serde_json::json!([{
"pubkey": "agent-pubkey",
"provider": "openai",
"env_vars": { "OPENAI_COMPAT_API_KEY": "official-secret" }
}]))
.unwrap(),
)
.unwrap();
let harnesses = custom_gateway_harnesses();
migrate_openai_credentials_in_file(&path, None, None, &harnesses).unwrap();
let records: Value = serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
assert_eq!(records[0]["provider"], "openai");
assert_eq!(records[0]["env_vars"][OPENAI_API_KEY], "official-secret");
}
#[test]
fn command_override_does_not_suppress_record_runtime_env() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("managed-agents.json");
std::fs::write(
&path,
serde_json::to_vec(&serde_json::json!([{
"pubkey": "agent-pubkey",
"runtime": "custom-gateway",
"agent_command_override": "buzz-agent",
"provider": "openai",
"env_vars": { "OPENAI_COMPAT_API_KEY": "official-secret" }
}]))
.unwrap(),
)
.unwrap();
let harnesses = custom_gateway_harnesses();
migrate_openai_credentials_in_file(&path, None, None, &harnesses).unwrap();
let records: Value = serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
assert_eq!(records[0]["provider"], "openai-compat");
assert_eq!(
records[0]["env_vars"][OPENAI_COMPAT_API_KEY],
"official-secret"
);
assert!(records[0]["env_vars"].get(OPENAI_API_KEY).is_none());
}
#[test]
fn inherited_openai_is_materialized_when_local_endpoint_is_compat() {
let dir = tempfile::tempdir().unwrap();
let global_path = dir.path().join("global-agent-config.json");
let managed_path = dir.path().join("managed-agents.json");
std::fs::write(
&global_path,
serde_json::to_vec(&serde_json::json!({
"provider": "openai",
"env_vars": {}
}))
.unwrap(),
)
.unwrap();
std::fs::write(
&managed_path,
serde_json::to_vec(&serde_json::json!([{
"pubkey": "agent-pubkey",
"agent_command_override": "buzz-agent",
"env_vars": {
"OPENAI_COMPAT_BASE_URL": "https://gateway.example/v1",
"OPENAI_COMPAT_API_KEY": "compat-secret"
}
}]))
.unwrap(),
)
.unwrap();
let harnesses = std::collections::HashMap::new();
migrate_openai_credentials_in_file(&global_path, None, None, &harnesses).unwrap();
let global: Value =
serde_json::from_str(&std::fs::read_to_string(&global_path).unwrap()).unwrap();
migrate_openai_credentials_in_file(
&managed_path,
global.get("provider").and_then(Value::as_str),
global.get("env_vars").and_then(Value::as_object),
&harnesses,
)
.unwrap();
let records: Value =
serde_json::from_str(&std::fs::read_to_string(&managed_path).unwrap()).unwrap();
assert_eq!(records[0]["provider"], "openai-compat");
assert_eq!(
records[0]["env_vars"][OPENAI_COMPAT_API_KEY],
"compat-secret"
);
assert_eq!(
records[0]["env_vars"][OPENAI_COMPAT_BASE_URL],
"https://gateway.example/v1"
);
assert!(records[0]["env_vars"].get(OPENAI_API_KEY).is_none());
assert!(sibling_path(&managed_path, MIGRATION_SUFFIX).exists());
}
#[test]
fn inherited_openai_is_materialized_on_providerless_definition() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("managed-agents.json");
std::fs::write(
&path,
serde_json::to_vec(&serde_json::json!([
{
"pubkey": "",
"slug": "shared-definition",
"env_vars": {
"OPENAI_COMPAT_BASE_URL": "https://gateway.example/v1",
"OPENAI_COMPAT_API_KEY": "definition-secret"
}
},
{
"pubkey": "agent-pubkey",
"persona_id": "shared-definition",
"env_vars": {}
}
]))
.unwrap(),
)
.unwrap();
migrate_file(&path, Some("openai"), None).unwrap();
let records: Value = serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
assert_eq!(records[0]["provider"], "openai-compat");
assert_eq!(
records[0]["env_vars"][OPENAI_COMPAT_API_KEY],
"definition-secret"
);
assert!(records[1].get("provider").is_none());
assert!(sibling_path(&path, MIGRATION_SUFFIX).exists());
}
#[test]
fn linked_local_endpoint_conflict_is_left_unwritten_and_unmarked() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("managed-agents.json");
let original = serde_json::to_vec(&serde_json::json!([
{
"pubkey": "",
"slug": "shared-definition",
"provider": "openai",
"env_vars": {}
},
{
"pubkey": "agent-pubkey",
"persona_id": "shared-definition",
"env_vars": {
"OPENAI_COMPAT_BASE_URL": "https://gateway.example/v1",
"OPENAI_COMPAT_API_KEY": "compat-secret"
}
}
]))
.unwrap();
std::fs::write(&path, &original).unwrap();
let error = migrate_file(&path, Some("openai"), None).unwrap_err();
assert!(
error.contains("cannot migrate a linked instance"),
"{error}"
);
assert_eq!(std::fs::read(&path).unwrap(), original);
assert!(!sibling_path(&path, BACKUP_SUFFIX).exists());
assert!(!sibling_path(&path, MIGRATION_SUFFIX).exists());
}
#[test]
fn linked_instance_uses_definition_provider_and_layered_endpoint() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("managed-agents.json");
std::fs::write(
&path,
serde_json::to_vec(&serde_json::json!([
{
"pubkey": "",
"slug": "shared-definition",
"provider": "openai",
"env_vars": {
"OPENAI_COMPAT_API_KEY": "definition-compat-key",
"OPENAI_COMPAT_BASE_URL": "https://gateway.example/v1"
}
},
{
"pubkey": "agent-pubkey",
"persona_id": "shared-definition",
"provider": "openai",
"env_vars": { "OPENAI_COMPAT_API_KEY": "instance-compat-key" }
}
]))
.unwrap(),
)
.unwrap();
migrate_file(&path, Some("openai"), None).unwrap();
let records: Value = serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
assert_eq!(records[0]["provider"], "openai-compat");
assert_eq!(
records[1]["provider"], "openai",
"linked instance provider is a stale snapshot and is not an ownership tier"
);
assert_eq!(
records[1]["env_vars"][OPENAI_COMPAT_API_KEY],
"instance-compat-key"
);
assert!(records[1]["env_vars"].get(OPENAI_API_KEY).is_none());
}
#[test]
fn linked_instance_ignores_legacy_provider_snapshot_when_definition_uses_global() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("managed-agents.json");
std::fs::write(
&path,
serde_json::to_vec(&serde_json::json!([
{
"pubkey": "",
"slug": "shared-definition",
"provider": null,
"env_vars": {}
},
{
"pubkey": "agent-pubkey",
"persona_id": "shared-definition",
"provider": "anthropic",
"env_vars": { "OPENAI_COMPAT_API_KEY": "legacy-official-key" }
}
]))
.unwrap(),
)
.unwrap();
migrate_file(&path, Some("openai"), None).unwrap();
let records: Value = serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
assert_eq!(records[1]["provider"], "anthropic");
assert_eq!(
records[1]["env_vars"][OPENAI_API_KEY],
"legacy-official-key"
);
assert!(records[1]["env_vars"].get(OPENAI_COMPAT_API_KEY).is_none());
}
#[test]
fn array_migration_updates_every_record() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("managed-agents.json");
std::fs::write(
&path,
serde_json::to_vec(&serde_json::json!([
{
"provider": "openai",
"env_vars": { "OPENAI_COMPAT_API_KEY": "first" }
},
{
"provider": "openai",
"env_vars": { "OPENAI_COMPAT_API_KEY": "second" }
}
]))
.unwrap(),
)
.unwrap();
migrate_file(&path, None, None).unwrap();
let records: Value = serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
assert_eq!(records[0]["env_vars"][OPENAI_API_KEY], "first");
assert_eq!(records[1]["env_vars"][OPENAI_API_KEY], "second");
}
#[test]
fn migration_marker_preserves_post_upgrade_compat_credentials() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("managed-agents.json");
std::fs::write(
&path,
serde_json::to_vec(&serde_json::json!([{
"provider": "openai",
"env_vars": { "OPENAI_COMPAT_API_KEY": "legacy-secret" }
}]))
.unwrap(),
)
.unwrap();
migrate_file(&path, None, None).unwrap();
let mut records: Value =
serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
records[0]["env_vars"][OPENAI_COMPAT_API_KEY] = Value::String("new-compat".to_string());
std::fs::write(&path, serde_json::to_vec(&records).unwrap()).unwrap();
migrate_file(&path, None, None).unwrap();
let records: Value = serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
assert_eq!(records[0]["env_vars"][OPENAI_API_KEY], "legacy-secret");
assert_eq!(records[0]["env_vars"][OPENAI_COMPAT_API_KEY], "new-compat");
}
#[test]
fn changed_file_gets_pristine_backup_before_marker() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("global-agent-config.json");
let original = serde_json::json!({
"provider": "openai",
"env_vars": { "OPENAI_COMPAT_API_KEY": "official-secret" }
});
let original_bytes = serde_json::to_vec(&original).unwrap();
std::fs::write(&path, &original_bytes).unwrap();
migrate_file(&path, None, None).unwrap();
assert_eq!(
std::fs::read(sibling_path(&path, BACKUP_SUFFIX)).unwrap(),
original_bytes
);
assert_eq!(
std::fs::read_to_string(sibling_path(&path, MIGRATION_SUFFIX)).unwrap(),
"1\n"
);
let migrated: Value = serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
assert_eq!(migrated["env_vars"][OPENAI_API_KEY], "official-secret");
}
#[test]
fn malformed_file_is_preserved_and_not_marked() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("managed-agents.json");
let original = b"{ not valid json";
std::fs::write(&path, original).unwrap();
let error = migrate_file(&path, None, None).unwrap_err();
assert!(error.contains("failed to parse"), "{error}");
assert_eq!(std::fs::read(&path).unwrap(), original);
assert!(!sibling_path(&path, BACKUP_SUFFIX).exists());
assert!(!sibling_path(&path, MIGRATION_SUFFIX).exists());
}
#[test]
fn absent_file_is_not_marked_so_later_legacy_content_can_be_migrated() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("global-agent-config.json");
migrate_file(&path, None, None).unwrap();
assert!(!sibling_path(&path, MIGRATION_SUFFIX).exists());
let legacy = serde_json::json!({
"provider": "openai",
"env_vars": { "OPENAI_COMPAT_API_KEY": "official-secret" }
});
std::fs::write(&path, serde_json::to_vec(&legacy).unwrap()).unwrap();
migrate_file(&path, None, None).unwrap();
let after: Value = serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
assert_eq!(after["env_vars"][OPENAI_API_KEY], "official-secret");
assert!(after["env_vars"].get(OPENAI_COMPAT_API_KEY).is_none());
}