mirror of
https://github.com/block/buzz.git
synced 2026-10-31 06:21:34 +00:00
fix: preserve migrated provider ownership
Co-authored-by: am <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz> Signed-off-by: am <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz>
This commit is contained in:
@@ -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;
|
||||
|
||||
711
desktop/src-tauri/src/migration/openai_credentials_tests.rs
Normal file
711
desktop/src-tauri/src/migration/openai_credentials_tests.rs
Normal 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());
|
||||
}
|
||||
Reference in New Issue
Block a user