use std::collections::{HashMap, HashSet};
use awaken_contract::contract::storage::StorageError;
use awaken_contract::{
BuiltinSeedSet, BuiltinSpec, ConfigRecord, ConfigStore, RecordMeta, RecordSource,
};
const SEED_LIST_PAGE_SIZE: usize = 256;
const BUILTIN_SEED_NAMESPACES: [&str; 5] =
["agents", "providers", "models", "mcp-servers", "tools"];
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct SeedReport {
pub created: Vec<RecordRef>,
pub updated: Vec<RecordRef>,
pub unchanged: Vec<RecordRef>,
pub deleted: Vec<RecordRef>,
pub preserved_user: Vec<RecordRef>,
pub preserved_overridden: Vec<RecordRef>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RecordRef {
pub namespace: String,
pub id: String,
}
impl RecordRef {
fn new(namespace: &str, id: &str) -> Self {
Self {
namespace: namespace.to_owned(),
id: id.to_owned(),
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum SeedError {
#[error("storage error: {0}")]
Storage(#[from] StorageError),
#[error("serialization error: {0}")]
Serde(#[from] serde_json::Error),
}
pub async fn apply_builtin_seed(
store: &dyn ConfigStore,
seed: &BuiltinSeedSet,
) -> Result<SeedReport, SeedError> {
let mut report = SeedReport::default();
let mut seeded: HashMap<&str, HashSet<String>> = HashMap::new();
for ns in BUILTIN_SEED_NAMESPACES {
seeded.insert(ns, HashSet::new());
}
for spec in &seed.specs {
let namespace = spec.namespace();
let id = spec.id();
let new_spec_value = builtin_spec_to_value(spec)?;
seeded.entry(namespace).or_default().insert(id.to_owned());
let existing_raw = store.get(namespace, id).await?;
match existing_raw {
None => {
let mut record = ConfigRecord {
spec: new_spec_value,
meta: RecordMeta::new_builtin(&seed.binary_version),
};
record.meta.revision = 1;
store
.put_if_absent(namespace, id, &record.to_value()?)
.await?;
report.created.push(RecordRef::new(namespace, id));
}
Some(raw) => {
let existing: ConfigRecord<serde_json::Value> = ConfigRecord::from_value(raw)?;
match &existing.meta.source {
RecordSource::User => {
report.preserved_user.push(RecordRef::new(namespace, id));
}
RecordSource::Builtin {
binary_version: stored_version,
} => {
let same_version = stored_version == &seed.binary_version;
let same_spec = existing.spec == new_spec_value;
if same_version && same_spec {
report.unchanged.push(RecordRef::new(namespace, id));
} else {
let now = awaken_contract::time::now_ms();
let expected_revision = existing.meta.revision;
let record = ConfigRecord {
spec: new_spec_value,
meta: RecordMeta {
source: RecordSource::Builtin {
binary_version: seed.binary_version.clone(),
},
hidden: false,
user_overrides: existing.meta.user_overrides,
created_at: existing.meta.created_at,
updated_at: now,
revision: expected_revision + 1,
},
};
store
.put_if_revision(
namespace,
id,
&record.to_value()?,
expected_revision,
)
.await?;
report.updated.push(RecordRef::new(namespace, id));
}
}
}
}
}
}
for namespace in BUILTIN_SEED_NAMESPACES {
let empty = HashSet::new();
let seeded_ids: &HashSet<String> = seeded.get(namespace).unwrap_or(&empty);
let mut candidates: Vec<String> = Vec::new();
let mut offset = 0usize;
loop {
let page = store.list(namespace, offset, SEED_LIST_PAGE_SIZE).await?;
let page_len = page.len();
for (id, raw) in page {
if seeded_ids.contains(&id) {
continue;
}
let record: ConfigRecord<serde_json::Value> = ConfigRecord::from_value(raw)?;
if matches!(record.meta.source, RecordSource::Builtin { .. }) {
candidates.push(id);
}
}
if page_len < SEED_LIST_PAGE_SIZE {
break;
}
offset += page_len;
}
for id in candidates {
let Some(raw) = store.get(namespace, &id).await? else {
continue;
};
let mut record: ConfigRecord<serde_json::Value> = ConfigRecord::from_value(raw)?;
let expected_revision = record.meta.revision;
if record.meta.user_overrides.is_some() {
record.meta.hidden = true;
record.meta.updated_at = awaken_contract::time::now_ms();
record.meta.revision = expected_revision + 1;
store
.put_if_revision(namespace, &id, &record.to_value()?, expected_revision)
.await?;
report
.preserved_overridden
.push(RecordRef::new(namespace, &id));
} else {
store
.delete_if_revision(namespace, &id, expected_revision)
.await?;
report.deleted.push(RecordRef::new(namespace, &id));
}
}
}
Ok(report)
}
fn builtin_spec_to_value(spec: &BuiltinSpec) -> Result<serde_json::Value, serde_json::Error> {
match spec {
BuiltinSpec::Agent(s) => serde_json::to_value(s.as_ref()),
BuiltinSpec::Provider(s) => serde_json::to_value(s),
BuiltinSpec::Model(s) => serde_json::to_value(s),
BuiltinSpec::McpServer(s) => serde_json::to_value(s),
BuiltinSpec::Tool(s) => serde_json::to_value(s),
}
}
#[cfg(test)]
mod tests {
use super::*;
use awaken_contract::config_record::ConfigRecord;
use awaken_contract::{AgentSpec, McpServerSpec, ModelBindingSpec, ProviderSpec};
use awaken_stores::memory::InMemoryStore;
fn agent_spec(id: &str, prompt: &str) -> AgentSpec {
AgentSpec {
id: id.to_owned(),
model_id: "gpt-4o".to_owned(),
system_prompt: prompt.to_owned(),
..Default::default()
}
}
fn provider_spec(id: &str) -> ProviderSpec {
ProviderSpec {
id: id.to_owned(),
adapter: "openai".to_owned(),
..Default::default()
}
}
fn model_spec(id: &str) -> ModelBindingSpec {
ModelBindingSpec {
id: id.to_owned(),
provider_id: "openai".to_owned(),
upstream_model: "gpt-4o".to_owned(),
}
}
fn mcp_spec(id: &str) -> McpServerSpec {
McpServerSpec {
id: id.to_owned(),
..Default::default()
}
}
fn seed_v1(specs: Vec<BuiltinSpec>) -> BuiltinSeedSet {
BuiltinSeedSet {
binary_version: "v1".to_owned(),
specs,
}
}
fn seed_v2(specs: Vec<BuiltinSpec>) -> BuiltinSeedSet {
BuiltinSeedSet {
binary_version: "v2".to_owned(),
specs,
}
}
fn store() -> InMemoryStore {
InMemoryStore::new()
}
#[tokio::test]
async fn cold_seed_creates_all_records() {
let s = store();
let seed = seed_v1(vec![
BuiltinSpec::Agent(Box::new(agent_spec("a1", "hello"))),
BuiltinSpec::Provider(provider_spec("p1")),
BuiltinSpec::Model(model_spec("m1")),
]);
let report = apply_builtin_seed(&s, &seed).await.unwrap();
assert_eq!(report.created.len(), 3, "expected 3 created");
assert!(report.updated.is_empty());
assert!(report.unchanged.is_empty());
assert!(report.deleted.is_empty());
assert!(report.preserved_user.is_empty());
for (ns, id) in [("agents", "a1"), ("providers", "p1"), ("models", "m1")] {
let raw = s.get(ns, id).await.unwrap().expect("record missing");
let rec: ConfigRecord<serde_json::Value> = ConfigRecord::from_value(raw).unwrap();
assert_eq!(
rec.meta.source,
RecordSource::Builtin {
binary_version: "v1".to_owned()
}
);
}
}
#[tokio::test]
async fn idempotent_re_apply_is_noop() {
let s = store();
let seed = seed_v1(vec![
BuiltinSpec::Agent(Box::new(agent_spec("a1", "hello"))),
BuiltinSpec::Provider(provider_spec("p1")),
BuiltinSpec::Model(model_spec("m1")),
]);
apply_builtin_seed(&s, &seed).await.unwrap();
let raw_before = s.get("agents", "a1").await.unwrap().unwrap();
let rec_before: ConfigRecord<serde_json::Value> =
ConfigRecord::from_value(raw_before).unwrap();
let updated_at_before = rec_before.meta.updated_at;
let report = apply_builtin_seed(&s, &seed).await.unwrap();
assert_eq!(report.unchanged.len(), 3, "expected 3 unchanged");
assert!(report.created.is_empty());
assert!(report.updated.is_empty());
assert!(report.deleted.is_empty());
assert!(report.preserved_user.is_empty());
let raw_after = s.get("agents", "a1").await.unwrap().unwrap();
let rec_after: ConfigRecord<serde_json::Value> =
ConfigRecord::from_value(raw_after).unwrap();
assert_eq!(rec_after.meta.updated_at, updated_at_before);
}
#[tokio::test]
async fn same_version_edit_updates_record() {
let s = store();
apply_builtin_seed(
&s,
&seed_v1(vec![BuiltinSpec::Agent(Box::new(agent_spec(
"a1",
"old prompt",
)))]),
)
.await
.unwrap();
let report = apply_builtin_seed(
&s,
&seed_v1(vec![BuiltinSpec::Agent(Box::new(agent_spec(
"a1",
"new prompt",
)))]),
)
.await
.unwrap();
assert_eq!(report.updated.len(), 1);
assert!(report.created.is_empty());
assert!(report.unchanged.is_empty());
let raw = s.get("agents", "a1").await.unwrap().unwrap();
let rec: ConfigRecord<serde_json::Value> = ConfigRecord::from_value(raw).unwrap();
assert_eq!(rec.spec["system_prompt"], "new prompt");
}
#[tokio::test]
async fn version_upgrade_refreshes_record() {
let s = store();
apply_builtin_seed(
&s,
&seed_v1(vec![BuiltinSpec::Agent(Box::new(agent_spec("a1", "v1")))]),
)
.await
.unwrap();
let report = apply_builtin_seed(
&s,
&seed_v2(vec![BuiltinSpec::Agent(Box::new(agent_spec("a1", "v2")))]),
)
.await
.unwrap();
assert_eq!(report.updated.len(), 1);
let raw = s.get("agents", "a1").await.unwrap().unwrap();
let rec: ConfigRecord<serde_json::Value> = ConfigRecord::from_value(raw).unwrap();
assert_eq!(
rec.meta.source,
RecordSource::Builtin {
binary_version: "v2".to_owned()
}
);
assert_eq!(rec.spec["system_prompt"], "v2");
}
#[tokio::test]
async fn user_record_preserved_through_seed() {
let s = store();
let user_record = ConfigRecord {
spec: serde_json::to_value(agent_spec("coder", "user version")).unwrap(),
meta: RecordMeta::new_user(),
};
s.put("agents", "coder", &user_record.to_value().unwrap())
.await
.unwrap();
let report = apply_builtin_seed(
&s,
&seed_v1(vec![BuiltinSpec::Agent(Box::new(agent_spec(
"coder",
"builtin version",
)))]),
)
.await
.unwrap();
assert_eq!(report.preserved_user.len(), 1);
assert!(report.created.is_empty());
assert!(report.updated.is_empty());
let raw = s.get("agents", "coder").await.unwrap().unwrap();
let rec: ConfigRecord<serde_json::Value> = ConfigRecord::from_value(raw).unwrap();
assert_eq!(rec.meta.source, RecordSource::User);
assert_eq!(rec.spec["system_prompt"], "user version");
}
#[tokio::test]
async fn orphan_builtin_cleaned() {
let s = store();
apply_builtin_seed(
&s,
&seed_v1(vec![
BuiltinSpec::Agent(Box::new(agent_spec("a1", "a"))),
BuiltinSpec::Agent(Box::new(agent_spec("b1", "b"))),
]),
)
.await
.unwrap();
let report = apply_builtin_seed(
&s,
&seed_v2(vec![BuiltinSpec::Agent(Box::new(agent_spec("a1", "a")))]),
)
.await
.unwrap();
assert_eq!(report.deleted.len(), 1);
assert_eq!(report.deleted[0].id, "b1");
assert!(s.get("agents", "b1").await.unwrap().is_none());
assert!(s.get("agents", "a1").await.unwrap().is_some());
}
#[tokio::test]
async fn orphan_cleanup_only_targets_builtin() {
let s = store();
let user_record = ConfigRecord {
spec: serde_json::to_value(agent_spec("user-only", "user")).unwrap(),
meta: RecordMeta::new_user(),
};
s.put("agents", "user-only", &user_record.to_value().unwrap())
.await
.unwrap();
let report = apply_builtin_seed(&s, &seed_v1(vec![])).await.unwrap();
assert!(!report.deleted.iter().any(|r| r.id == "user-only"));
assert!(s.get("agents", "user-only").await.unwrap().is_some());
}
#[tokio::test]
async fn reintroduced_spec_clears_hidden_flag() {
let s = store();
apply_builtin_seed(
&s,
&seed_v1(vec![BuiltinSpec::Agent(Box::new(agent_spec("a1", "v1")))]),
)
.await
.unwrap();
let raw = s.get("agents", "a1").await.unwrap().unwrap();
let mut rec: ConfigRecord<serde_json::Value> = ConfigRecord::from_value(raw).unwrap();
rec.meta.hidden = true;
s.put("agents", "a1", &rec.to_value().unwrap())
.await
.unwrap();
apply_builtin_seed(
&s,
&seed_v2(vec![BuiltinSpec::Agent(Box::new(agent_spec("a1", "v2")))]),
)
.await
.unwrap();
let raw = s.get("agents", "a1").await.unwrap().unwrap();
let rec: ConfigRecord<serde_json::Value> = ConfigRecord::from_value(raw).unwrap();
assert!(!rec.meta.hidden, "reintroduced spec must clear hidden");
assert_eq!(
rec.meta.source,
RecordSource::Builtin {
binary_version: "v2".to_owned()
}
);
assert_eq!(rec.spec["system_prompt"], "v2");
}
#[tokio::test]
async fn mixed_namespace_seed_routes_correctly() {
let s = store();
let seed = seed_v1(vec![
BuiltinSpec::Agent(Box::new(agent_spec("agent-1", "hi"))),
BuiltinSpec::Provider(provider_spec("prov-1")),
BuiltinSpec::Model(model_spec("model-1")),
BuiltinSpec::McpServer(mcp_spec("mcp-1")),
]);
let report = apply_builtin_seed(&s, &seed).await.unwrap();
assert_eq!(report.created.len(), 4);
assert!(s.get("agents", "agent-1").await.unwrap().is_some());
assert!(s.get("providers", "prov-1").await.unwrap().is_some());
assert!(s.get("models", "model-1").await.unwrap().is_some());
assert!(s.get("mcp-servers", "mcp-1").await.unwrap().is_some());
assert!(s.get("providers", "agent-1").await.unwrap().is_none());
}
#[tokio::test]
async fn legacy_bare_spec_treated_as_user_during_seed() {
let s = store();
let bare = serde_json::to_value(agent_spec("legacy", "bare")).unwrap();
s.put("agents", "legacy", &bare).await.unwrap();
let report = apply_builtin_seed(
&s,
&seed_v1(vec![BuiltinSpec::Agent(Box::new(agent_spec(
"other", "other",
)))]),
)
.await
.unwrap();
assert!(!report.deleted.iter().any(|r| r.id == "legacy"));
assert!(s.get("agents", "legacy").await.unwrap().is_some());
}
#[tokio::test]
async fn orphan_cleanup_handles_more_than_one_page() {
const RECORD_COUNT: usize = 300;
const _: () = assert!(
RECORD_COUNT > SEED_LIST_PAGE_SIZE,
"test must exceed page size to exercise the multi-page path"
);
let s = store();
for i in 0..RECORD_COUNT {
let id = format!("prov-{i:04}");
let record = ConfigRecord {
spec: serde_json::to_value(provider_spec(&id)).unwrap(),
meta: RecordMeta::new_builtin("v1"),
};
s.put("providers", &id, &record.to_value().unwrap())
.await
.unwrap();
}
let report = apply_builtin_seed(&s, &seed_v2(vec![])).await.unwrap();
assert_eq!(
report.deleted.len(),
RECORD_COUNT,
"all {RECORD_COUNT} orphans must be deleted, not just the first page"
);
assert!(report.created.is_empty());
assert!(report.updated.is_empty());
assert!(report.unchanged.is_empty());
assert!(report.preserved_user.is_empty());
assert!(
s.get("providers", "prov-0256").await.unwrap().is_none(),
"record past first page boundary must also be deleted"
);
}
#[tokio::test]
async fn seed_upgrade_preserves_user_overrides() {
let s = store();
apply_builtin_seed(
&s,
&seed_v1(vec![BuiltinSpec::Agent(Box::new(agent_spec("a1", "v1")))]),
)
.await
.unwrap();
let raw = s.get("agents", "a1").await.unwrap().unwrap();
let mut rec: ConfigRecord<serde_json::Value> = ConfigRecord::from_value(raw).unwrap();
rec.meta.user_overrides = Some(serde_json::json!({"system_prompt": "user-custom"}));
s.put("agents", "a1", &rec.to_value().unwrap())
.await
.unwrap();
apply_builtin_seed(
&s,
&seed_v2(vec![BuiltinSpec::Agent(Box::new(agent_spec("a1", "v2")))]),
)
.await
.unwrap();
let raw = s.get("agents", "a1").await.unwrap().unwrap();
let rec: ConfigRecord<serde_json::Value> = ConfigRecord::from_value(raw).unwrap();
assert_eq!(
rec.meta.source,
RecordSource::Builtin {
binary_version: "v2".to_owned()
},
"binary_version must be updated to v2"
);
assert_eq!(
rec.meta.user_overrides,
Some(serde_json::json!({"system_prompt": "user-custom"})),
"user_overrides must be preserved across version upgrade"
);
assert_eq!(rec.spec["system_prompt"], "v2");
}
#[tokio::test]
async fn orphan_cleanup_uses_config_namespace_iter() {
let s = store();
let namespaces_and_ids = [
("agents", "orphan-agent"),
("providers", "orphan-provider"),
("models", "orphan-model"),
("mcp-servers", "orphan-mcp"),
("tools", "orphan-tool"),
];
for (ns, id) in namespaces_and_ids {
let spec_value = serde_json::json!({ "id": id, "ns": ns });
let record = ConfigRecord {
spec: spec_value,
meta: RecordMeta::new_builtin("v1"),
};
s.put(ns, id, &record.to_value().unwrap()).await.unwrap();
}
let report = apply_builtin_seed(&s, &seed_v1(vec![])).await.unwrap();
assert_eq!(
report.deleted.len(),
namespaces_and_ids.len(),
"expected one deleted orphan per namespace"
);
for (ns, id) in namespaces_and_ids {
assert!(
report
.deleted
.iter()
.any(|r| r.namespace == ns && r.id == id),
"deleted must contain {ns}/{id}"
);
assert!(
s.get(ns, id).await.unwrap().is_none(),
"{ns}/{id} must be removed from the store"
);
}
}
#[tokio::test]
async fn orphan_with_override_is_hidden_not_deleted() {
let store = InMemoryStore::new();
let v1 = seed_v1(vec![BuiltinSpec::Agent(Box::new(agent_spec(
"a1",
"v1-prompt",
)))]);
apply_builtin_seed(&store, &v1).await.unwrap();
let raw = store.get("agents", "a1").await.unwrap().unwrap();
let mut record: ConfigRecord<serde_json::Value> = ConfigRecord::from_value(raw).unwrap();
record.meta.user_overrides = Some(serde_json::json!({"system_prompt": "patched"}));
store
.put("agents", "a1", &record.to_value().unwrap())
.await
.unwrap();
let v2 = BuiltinSeedSet {
binary_version: "v2".into(),
specs: vec![],
};
let report = apply_builtin_seed(&store, &v2).await.unwrap();
assert!(
report
.preserved_overridden
.iter()
.any(|r| r.namespace == "agents" && r.id == "a1")
);
assert!(!report.deleted.iter().any(|r| r.id == "a1"));
let raw = store.get("agents", "a1").await.unwrap().unwrap();
let record: ConfigRecord<serde_json::Value> = ConfigRecord::from_value(raw).unwrap();
assert!(record.meta.hidden);
assert_eq!(
record.meta.user_overrides,
Some(serde_json::json!({"system_prompt": "patched"}))
);
}
#[tokio::test]
async fn orphan_without_override_is_hard_deleted() {
let store = InMemoryStore::new();
let v1 = seed_v1(vec![BuiltinSpec::Agent(Box::new(agent_spec(
"a1",
"v1-prompt",
)))]);
apply_builtin_seed(&store, &v1).await.unwrap();
let v2 = BuiltinSeedSet {
binary_version: "v2".into(),
specs: vec![],
};
let report = apply_builtin_seed(&store, &v2).await.unwrap();
assert!(
report
.deleted
.iter()
.any(|r| r.namespace == "agents" && r.id == "a1")
);
assert!(report.preserved_overridden.is_empty());
assert!(store.get("agents", "a1").await.unwrap().is_none());
}
#[tokio::test]
async fn reintroduced_spec_clears_hidden_and_keeps_override() {
let store = InMemoryStore::new();
let v1 = seed_v1(vec![BuiltinSpec::Agent(Box::new(agent_spec(
"a1",
"v1-prompt",
)))]);
apply_builtin_seed(&store, &v1).await.unwrap();
let raw = store.get("agents", "a1").await.unwrap().unwrap();
let mut record: ConfigRecord<serde_json::Value> = ConfigRecord::from_value(raw).unwrap();
record.meta.user_overrides = Some(serde_json::json!({"system_prompt": "patched"}));
store
.put("agents", "a1", &record.to_value().unwrap())
.await
.unwrap();
let v2 = BuiltinSeedSet {
binary_version: "v2".into(),
specs: vec![],
};
apply_builtin_seed(&store, &v2).await.unwrap();
let raw = store.get("agents", "a1").await.unwrap().unwrap();
let record: ConfigRecord<serde_json::Value> = ConfigRecord::from_value(raw).unwrap();
assert!(record.meta.hidden, "should be hidden after v2 orphans it");
let v3 = BuiltinSeedSet {
binary_version: "v3".into(),
specs: vec![BuiltinSpec::Agent(Box::new(agent_spec("a1", "v3-prompt")))],
};
apply_builtin_seed(&store, &v3).await.unwrap();
let raw = store.get("agents", "a1").await.unwrap().unwrap();
let record: ConfigRecord<serde_json::Value> = ConfigRecord::from_value(raw).unwrap();
assert!(!record.meta.hidden, "reintroduced spec must be live again");
assert_eq!(
record.meta.user_overrides,
Some(serde_json::json!({"system_prompt": "patched"})),
"override must survive the orphan→reintroduce cycle"
);
}
}