awaken-server 0.6.0

Multi-protocol HTTP server with SSE, mailbox, and protocol adapters for Awaken
Documentation
use std::sync::Arc;

use awaken_server_contract::{
    BuiltinSeedSet, BuiltinSpec, PreparedSkillSpecs, SkillSpec, SkillSpecSink,
};
use parking_lot::Mutex;

use super::tests::make_manager_with_store;

#[derive(Default)]
struct RecordingSkillSpecSink {
    specs: Arc<Mutex<Vec<SkillSpec>>>,
    replace_calls: Arc<Mutex<usize>>,
    fail_prepare: Arc<Mutex<Option<String>>>,
}

impl SkillSpecSink for RecordingSkillSpecSink {
    fn prepare_skill_specs(
        &self,
        specs: Vec<SkillSpec>,
    ) -> Result<Box<dyn PreparedSkillSpecs>, String> {
        if let Some(error) = self.fail_prepare.lock().clone() {
            return Err(error);
        }
        let mut ids = std::collections::HashSet::new();
        for spec in &specs {
            if !ids.insert(spec.id.clone()) {
                return Err(format!("duplicate skill id: {}", spec.id));
            }
        }
        Ok(Box::new(RecordingPreparedSkillSpecs {
            specs_target: Arc::clone(&self.specs),
            replace_calls: Arc::clone(&self.replace_calls),
            specs,
        }))
    }
}

struct RecordingPreparedSkillSpecs {
    specs_target: Arc<Mutex<Vec<SkillSpec>>>,
    replace_calls: Arc<Mutex<usize>>,
    specs: Vec<SkillSpec>,
}

impl PreparedSkillSpecs for RecordingPreparedSkillSpecs {
    fn commit(self: Box<Self>) {
        *self.specs_target.lock() = self.specs;
        *self.replace_calls.lock() += 1;
    }
}

fn skill_spec(id: &str) -> SkillSpec {
    SkillSpec {
        id: id.into(),
        name: "Database Management".into(),
        description: "Helps with database operations".into(),
        instructions_md: "Inspect schema before running SQL.".into(),
        ..Default::default()
    }
}

#[tokio::test]
async fn apply_publishes_config_managed_skills_to_sink() {
    let (manager, store) = make_manager_with_store().await;
    let sink = Arc::new(RecordingSkillSpecSink::default());
    let manager = manager.with_skill_spec_sink(sink.clone());

    let seed = BuiltinSeedSet {
        binary_version: "test".into(),
        specs: vec![BuiltinSpec::skill(skill_spec("db-management"))],
    };

    manager.apply_seed(&seed).await.expect("apply seed");
    manager.apply().await.expect("publish managed skills");

    let specs = sink.specs.lock().clone();
    assert_eq!(specs.len(), 1);
    assert_eq!(specs[0].id, "db-management");
    assert!(
        store
            .get("skills", "db-management")
            .await
            .expect("read skill")
            .is_some()
    );
}

#[tokio::test]
async fn apply_removes_deleted_config_managed_skills_from_sink() {
    let (manager, store) = make_manager_with_store().await;
    let sink = Arc::new(RecordingSkillSpecSink::default());
    let manager = manager.with_skill_spec_sink(sink.clone());

    manager
        .apply_seed(&BuiltinSeedSet {
            binary_version: "test".into(),
            specs: vec![BuiltinSpec::skill(skill_spec("db-management"))],
        })
        .await
        .expect("apply seed");
    manager.apply().await.expect("publish managed skills");
    assert_eq!(sink.specs.lock().len(), 1);

    store
        .delete("skills", "db-management")
        .await
        .expect("delete skill");
    manager.apply().await.expect("publish removal");

    assert!(sink.specs.lock().is_empty());
    assert_eq!(*sink.replace_calls.lock(), 2);
}

#[tokio::test]
async fn apply_rejects_duplicate_skill_ids_before_replacing_sink() {
    let (manager, store) = make_manager_with_store().await;
    let sink = Arc::new(RecordingSkillSpecSink::default());
    let manager = manager.with_skill_spec_sink(sink.clone());

    store
        .put(
            "skills",
            "primary",
            &serde_json::to_value(skill_spec("db-management")).expect("serialize skill"),
        )
        .await
        .expect("write primary skill");
    manager.apply().await.expect("publish primary");
    assert_eq!(sink.specs.lock().len(), 1);
    assert_eq!(*sink.replace_calls.lock(), 1);

    store
        .put(
            "skills",
            "duplicate-key",
            &serde_json::to_value(skill_spec("db-management")).expect("serialize skill"),
        )
        .await
        .expect("write duplicate skill");

    let error = manager
        .apply()
        .await
        .expect_err("duplicate skill id must fail publish");
    assert!(
        error
            .to_string()
            .contains("duplicate skill id: db-management"),
        "unexpected error: {error}"
    );
    assert_eq!(
        sink.specs.lock()[0].id,
        "db-management",
        "failed publish must leave the previous live specs intact"
    );
    assert_eq!(
        *sink.replace_calls.lock(),
        1,
        "validation failure must happen before replacing live specs"
    );
}

#[tokio::test]
async fn apply_rejects_config_managed_skills_without_sink() {
    let (manager, _store) = make_manager_with_store().await;
    let before_version = manager.runtime.registry_version();

    manager
        .apply_seed(&BuiltinSeedSet {
            binary_version: "test".into(),
            specs: vec![BuiltinSpec::skill(skill_spec("db-management"))],
        })
        .await
        .expect("apply seed");

    let error = manager
        .apply()
        .await
        .expect_err("skills require a live skill sink");
    assert!(
        error.to_string().contains("skill_spec_sink"),
        "unexpected error: {error}"
    );
    assert_eq!(
        manager.runtime.registry_version(),
        before_version,
        "failed skill publish must not replace the core runtime registry"
    );
}

#[tokio::test]
async fn skill_prepare_failure_happens_before_runtime_registry_replace() {
    let (manager, store) = make_manager_with_store().await;
    let sink = Arc::new(RecordingSkillSpecSink::default());
    *sink.fail_prepare.lock() = Some("prepared skill map failed".into());
    let manager = manager.with_skill_spec_sink(sink.clone());
    let before_version = manager.runtime.registry_version();

    store
        .put(
            "skills",
            "db-management",
            &serde_json::to_value(skill_spec("db-management")).expect("serialize skill"),
        )
        .await
        .expect("write skill");

    let error = manager
        .apply()
        .await
        .expect_err("prepare failure must fail publish");
    assert!(
        error.to_string().contains("prepared skill map failed"),
        "unexpected error: {error}"
    );
    assert_eq!(
        manager.runtime.registry_version(),
        before_version,
        "prepare failure must be atomic with respect to runtime registry replacement"
    );
    assert_eq!(*sink.replace_calls.lock(), 0);
}

#[tokio::test]
async fn apply_rejects_invalid_skill_allowed_tool_pattern_before_replace() {
    let (manager, store) = make_manager_with_store().await;
    let sink = Arc::new(RecordingSkillSpecSink::default());
    let manager = manager.with_skill_spec_sink(sink.clone());
    let before_version = manager.runtime.registry_version();

    let mut invalid_regex = skill_spec("invalid-regex-skill");
    invalid_regex.allowed_tools = vec!["/[invalid/".into()];
    store
        .put(
            "skills",
            "invalid-regex-skill",
            &serde_json::to_value(invalid_regex).expect("serialize skill"),
        )
        .await
        .expect("write invalid skill");

    let error = manager
        .apply()
        .await
        .expect_err("invalid skill matcher must fail publish");
    assert!(
        error.to_string().contains("invalid allowed-tools pattern"),
        "unexpected error: {error}"
    );
    assert_eq!(
        manager.runtime.registry_version(),
        before_version,
        "invalid skill matcher must not replace the core runtime registry"
    );
    assert_eq!(*sink.replace_calls.lock(), 0);

    store
        .delete("skills", "invalid-regex-skill")
        .await
        .expect("delete invalid regex skill");

    let mut invalid_glob = skill_spec("invalid-glob-skill");
    invalid_glob.allowed_tools = vec![r"mcp__db__*\".into()];
    store
        .put(
            "skills",
            "invalid-glob-skill",
            &serde_json::to_value(invalid_glob).expect("serialize skill"),
        )
        .await
        .expect("write invalid skill");

    let error = manager
        .apply()
        .await
        .expect_err("invalid skill glob must fail publish");
    assert!(
        error.to_string().contains("dangling escape"),
        "unexpected error: {error}"
    );
    assert_eq!(manager.runtime.registry_version(), before_version);
    assert_eq!(*sink.replace_calls.lock(), 0);
}