blob: b73d194785dfe2d5e5047c527c9128fbaaa64a76 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
use maka_agent_facts::configuration::*;
use maka_config::{
ConfigurationStore,
model_fetch::{ModelFetchPreparation, PreparedModelFetch},
};
use maka_event_log::root::{RootNamespaces, RootOwner};
use serde_json::json;
use std::sync::Arc;
struct Fixture {
temp: tempfile::TempDir,
namespaces: RootNamespaces,
store: Arc<ConfigurationStore>,
id: String,
}
impl Fixture {
async fn new(selected: &[&str]) -> Self {
let temp = tempfile::tempdir().unwrap();
let namespaces = RootNamespaces {
ownership: temp.path().join("owners"),
control: temp.path().join("control"),
};
let owner = Arc::new(RootOwner::create(&temp.path().join("root"), &namespaces).unwrap());
let store = Arc::new(ConfigurationStore::for_root(owner).await.unwrap());
let input = serde_json::from_value(json!({
"expectedCatalogRevision":0,
"connection":{"slug":"relay","name":"Relay",
"provider":{"packageId":"example","entryId":"account","scope":"profile","name":"api"},
"configuration":{"baseUrl":"http://127.0.0.1:18080/v1"},"enabled":true,
"enabledModelIds":selected,"requestBodyOverlay":{"temperature":0.5}}
}))
.unwrap();
let CatalogMutationResult::Committed {
connection: Some(basis),
..
} = store.create_connection(input).await.unwrap()
else {
panic!("expected connection commit")
};
Self {
temp,
namespaces,
store,
id: basis.connection_id,
}
}
async fn row(&self) -> ConnectionCatalogEntry {
self.store.catalog().await.unwrap().connections.remove(0)
}
async fn edit(&self, row: ConnectionCatalogEntry) {
assert!(matches!(
self.store
.update_connection(UpdateCatalogConnectionInput {
expected: ConnectionVersionBasis {
connection_id: row.connection_id,
revision: row.revision
},
changes: ConnectionCatalogEntryUpdate {
name: Some(row.name),
configuration: Some(row.configuration),
enabled: Some(row.enabled),
enabled_model_ids: Some(row.enabled_model_ids),
model_overrides: Patch::Keep,
request_body_overlay: Patch::Keep,
},
})
.await
.unwrap(),
CatalogMutationResult::Committed { .. }
));
}
async fn key(&self, secret: &str) {
use maka_agent_facts::oauth::LoginStart;
use maka_agent_facts::oauth::Target;
use maka_agent_facts::provider::AuthenticationInput;
use maka_agent_facts::provider::Credential;
use maka_config::oauth::enrollment::{LoginCompletion, LoginPreparation};
let row = self.row().await;
let LoginPreparation::Ready(ticket) = self
.store
.prepare_oauth_login(LoginStart {
expected_credential: match self
.store
.credential_status(CredentialLocator::Connection {
connection_id: row.connection_id.clone(),
kind: ConnectionCredentialKind::Provider,
})
.await
.unwrap()
{
maka_agent_facts::configuration::CredentialVaultQueryResult::Status {
status,
} => status.basis(),
_ => panic!("connection status"),
},
attempt_id: uuid::Uuid::new_v4().to_string(),
target: Target::Existing {
expected: ConnectionCredentialTarget {
connection_id: row.connection_id,
revision: row.revision,
slug: row.slug,
provider: row.provider,
configuration: row.configuration.clone(),
},
configuration: row.configuration,
},
authentication: AuthenticationInput {
method: "key".into(),
input: json!({}),
},
})
.await
.unwrap()
else {
panic!("prepared credential receipt")
};
assert!(matches!(
ticket
.complete(
Credential {
secret: secret.into(),
refresh_at: None
},
42
)
.await
.unwrap(),
LoginCompletion::Committed(_)
));
}
async fn prepare(&self) -> Box<PreparedModelFetch> {
match self.store.prepare_model_fetch(&self.id).await.unwrap() {
ModelFetchPreparation::Ready(prepared) => prepared,
_ => panic!("expected ready discovery"),
}
}
}
#[tokio::test]
async fn rejected_preparation_and_invalid_completion_leave_catalog_untouched() {
let fixture = Fixture::new(&[]).await;
assert!(matches!(
fixture
.store
.prepare_model_fetch("00000000-0000-4000-8000-000000000000")
.await
.unwrap(),
ModelFetchPreparation::Rejected(ConnectionEffectRejectionReason::ConnectionNotFound)
));
assert!(
fixture
.prepare()
.await
.provider_credential()
.unwrap()
.is_none(),
"configuration preparation must allow anonymous providers"
);
let mut row = fixture.row().await;
row.enabled = false;
fixture.edit(row).await;
assert!(matches!(
fixture
.store
.prepare_model_fetch(&fixture.id)
.await
.unwrap(),
ModelFetchPreparation::Rejected(ConnectionEffectRejectionReason::ConnectionDisabled)
));
let mut row = fixture.row().await;
row.enabled = true;
fixture.edit(row).await;
fixture.key("fixture-key").await;
let before = fixture.store.catalog().await.unwrap();
for models in [vec![], vec![ModelInfo::new("valid"), ModelInfo::new("")]] {
assert!(fixture.prepare().await.complete(models, 100).await.is_err());
assert_eq!(fixture.store.catalog().await.unwrap(), before);
}
}
#[tokio::test]
async fn completion_preserves_concurrent_metadata_and_choices_across_reopen() {
let fixture = Fixture::new(&["manual-model"]).await;
fixture.key("fixture-key").await;
let target = ConnectionTarget {
connection_id: fixture.id.clone(),
model_id: "manual-model".into(),
};
assert!(matches!(
fixture
.store
.set_default_target(SetDefaultConnectionTargetInput {
expected_catalog_revision: fixture.store.catalog().await.unwrap().revision,
target: Some(target.clone()),
})
.await
.unwrap(),
CatalogMutationResult::Committed { .. }
));
let prepared = fixture.prepare().await;
let mut row = fixture.row().await;
row.name = "Renamed while fetching".into();
fixture.edit(row).await;
let before = fixture.store.catalog().await.unwrap();
let models = vec![ModelInfo {
display_name: Some("Discovered".into()),
..ModelInfo::new("discovered-model")
}];
assert!(matches!(
prepared.complete(models.clone(), 100).await.unwrap(),
ConnectionModelFetchResult::Committed {
model_count: 1,
fetched_at: 100,
..
}
));
let saved = fixture.store.catalog().await.unwrap();
let mut expected = before.clone();
expected.revision += 1;
expected.connections[0].revision += 1;
expected.connections[0].models = models;
expected.connections[0].model_source = Some(ModelDiscoverySource::Fetched);
expected.connections[0].models_fetched_at = Some(100);
assert_eq!(saved, expected);
let store = Arc::try_unwrap(fixture.store)
.ok()
.expect("preparation released store");
store.close().await.unwrap();
let owner =
Arc::new(RootOwner::open(&fixture.temp.path().join("root"), &fixture.namespaces).unwrap());
let reopened = ConfigurationStore::for_root(owner).await.unwrap();
assert_eq!(reopened.catalog().await.unwrap(), saved);
}
#[tokio::test]
async fn selection_seed_is_one_time_and_changed_fetch_basis_cannot_overwrite_inventory() {
let fixture = Fixture::new(&[]).await;
fixture.key("original-key").await;
assert!(matches!(
fixture
.prepare()
.await
.complete(vec![ModelInfo::new("first")], 100)
.await
.unwrap(),
ConnectionModelFetchResult::Committed { .. }
));
assert_eq!(fixture.row().await.enabled_model_ids, ["first"]);
let mut row = fixture.row().await;
row.enabled_model_ids.clear();
fixture.edit(row).await;
assert!(matches!(
fixture
.prepare()
.await
.complete(vec![ModelInfo::new("second")], 101)
.await
.unwrap(),
ConnectionModelFetchResult::Committed { .. }
));
assert!(fixture.row().await.enabled_model_ids.is_empty());
let prepared = fixture.prepare().await;
let mut row = fixture.row().await;
row.configuration = json!({"baseUrl":"http://127.0.0.1:18081/v1"});
fixture.edit(row).await;
let before = fixture.store.catalog().await.unwrap();
assert_eq!(
prepared
.complete(vec![ModelInfo::new("stale")], 102)
.await
.unwrap(),
ConnectionModelFetchResult::Superseded {
changed: vec![ConnectionEffectChangedDomain::Connection]
}
);
assert_eq!(fixture.store.catalog().await.unwrap(), before);
let prepared = fixture.prepare().await;
fixture.key("replacement-key").await;
fixture.key("original-key").await;
let before = fixture.store.catalog().await.unwrap();
assert_eq!(
prepared
.complete(vec![ModelInfo::new("stale")], 103)
.await
.unwrap(),
ConnectionModelFetchResult::Superseded {
changed: vec![ConnectionEffectChangedDomain::Credential]
}
);
assert_eq!(
fixture.store.catalog().await.unwrap(),
before,
"credential ABA must not revive an old preparation"
);
}