KeiSeiKit-1.0/_primitives/_rust/kei-mcp/tests/skills_via_registry.rs
Parfii-bot 0be354a920 KeiSeiKit-public — clean state
Single-commit clean baseline after security scrub of niche-tells,
project codenames, internal jargon, and contributor-email leaks.

Contents:
- 100 Rust crates (_primitives/_rust/)
- 37 agent manifests (_manifests/) + generated specs (_generated/)
- 67 user-invocable skills (skills/)
- 33 hooks (hooks/)
- Composition blocks (_blocks/)
- Documentation (docs/, README.md)
- TS adapter packages (_ts_packages/)
- Assembler (_assembler/)
- Roles (_roles/)
- Templates (_templates/)
- Forgejo CI (.forgejo/)

Author: Denis Parfionovich <info@greendragon.info>

License: see LICENSE.
2026-05-01 12:09:03 +08:00

177 lines
6 KiB
Rust

//! Integration test — `resources/list` and `resources/read` flow through
//! the `kei-skills` `SkillRegistry` (Phase 3.1 SSoT).
//!
//! Walks ancestors of `CARGO_MANIFEST_DIR` to find the repo root's
//! `skills/` directory (KeiSeiKit corpus, ~45 SKILL.md files at time of
//! writing). Skips the test if the dir cannot be located — keeps the
//! suite green on isolated checkouts that don't carry the skills tree.
//!
//! Asserts:
//! 1. `resources/list` returns ≥ 30 entries (corpus headroom over 45).
//! 2. Three known-valid skill names — `research`, `refactor`, `onboard`
//! — are reachable both from the list and via `resources/read`.
//! 3. `resources/read` for a bogus skill returns an error envelope.
//! 4. `resources/list` URIs match `skill://<name>` shape and carry a
//! non-empty `description`.
use kei_mcp::{dispatch, JsonRpcRequest, ServerContext};
use serde_json::{json, Value};
use std::path::{Path, PathBuf};
fn make_request(method: &str, params: Value) -> JsonRpcRequest {
JsonRpcRequest {
jsonrpc: "2.0".into(),
id: Some(json!(1)),
method: method.into(),
params: Some(params),
}
}
/// Locate `skills/` by walking up from `CARGO_MANIFEST_DIR`. Returns
/// `None` if no ancestor directory contains a `skills/` subdir.
fn find_skills_root() -> Option<PathBuf> {
let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let mut cursor: &Path = &manifest;
loop {
let candidate = cursor.join("skills");
if candidate.is_dir() {
return Some(candidate);
}
match cursor.parent() {
Some(p) => cursor = p,
None => return None,
}
}
}
fn ctx_for_corpus() -> Option<ServerContext> {
let skills_root = find_skills_root()?;
let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let atoms_root = manifest
.parent()
.map(|p| p.to_path_buf())
.unwrap_or(manifest);
Some(ServerContext::new(atoms_root, skills_root))
}
fn names_in_list_response(result: &Value) -> Vec<String> {
result["resources"]
.as_array()
.cloned()
.unwrap_or_default()
.into_iter()
.filter_map(|r| r["name"].as_str().map(String::from))
.collect()
}
#[tokio::test]
async fn list_returns_at_least_thirty_skills_from_repo_corpus() {
let Some(ctx) = ctx_for_corpus() else {
eprintln!("skills/ not found in ancestors — skipping corpus test");
return;
};
let req = make_request("resources/list", json!({}));
let resp = dispatch(req, &ctx).await;
let result = resp.result.expect("resources/list should return result");
let names = names_in_list_response(&result);
assert!(
names.len() >= 30,
"expected ≥30 skills in corpus, found {}: {names:?}",
names.len()
);
let entries = result["resources"]
.as_array()
.expect("resources is an array");
for entry in entries {
let uri = entry["uri"].as_str().expect("uri string");
let name = entry["name"].as_str().expect("name string");
assert_eq!(
uri,
format!("skill://{name}"),
"uri must match skill://<name>"
);
assert_eq!(entry["mimeType"], "text/markdown");
let desc = entry["description"].as_str().unwrap_or("");
assert!(!desc.is_empty(), "description must be non-empty for {name}");
}
}
#[tokio::test]
async fn known_skills_are_findable_in_list() {
let Some(ctx) = ctx_for_corpus() else {
return;
};
let req = make_request("resources/list", json!({}));
let resp = dispatch(req, &ctx).await;
let result = resp.result.expect("result");
let names = names_in_list_response(&result);
for expected in ["research", "refactor", "onboard"] {
assert!(
names.iter().any(|n| n == expected),
"expected {expected} in list, got {names:?}"
);
}
}
#[tokio::test]
async fn read_known_skill_returns_serialized_text() {
let Some(ctx) = ctx_for_corpus() else {
return;
};
for name in ["research", "refactor", "onboard"] {
let req = make_request(
"resources/read",
json!({ "uri": format!("skill://{name}") }),
);
let resp = dispatch(req, &ctx).await;
let result = resp
.result
.unwrap_or_else(|| panic!("resources/read({name}) should return result"));
let contents = result["contents"]
.as_array()
.unwrap_or_else(|| panic!("contents array for {name}"));
assert_eq!(contents.len(), 1, "exactly one content entry for {name}");
let text = contents[0]["text"].as_str().expect("text string");
assert!(text.starts_with("---\n"), "{name}: must start with frontmatter fence");
assert!(
text.contains(&format!("name: {name}")),
"{name}: serialized text must contain its name field"
);
}
}
#[tokio::test]
async fn read_unknown_skill_returns_error_envelope() {
let Some(ctx) = ctx_for_corpus() else {
return;
};
let req = make_request(
"resources/read",
json!({ "uri": "skill://this-skill-does-not-exist-xyzzy" }),
);
let resp = dispatch(req, &ctx).await;
assert!(resp.result.is_none(), "unknown skill must not produce result");
let e = resp.error.expect("error envelope");
assert_eq!(e.code, -32602, "unknown skill maps to INVALID_PARAMS");
assert!(
e.message.contains("unknown skill"),
"error message should mention unknown skill, got: {}",
e.message
);
}
#[tokio::test]
async fn read_with_non_skill_uri_returns_error() {
let Some(ctx) = ctx_for_corpus() else {
return;
};
let req = make_request(
"resources/read",
json!({ "uri": "file:///etc/passwd" }),
);
let resp = dispatch(req, &ctx).await;
assert!(resp.result.is_none());
let e = resp.error.expect("error envelope");
assert_eq!(e.code, -32602);
assert!(e.message.contains("not a skill uri"));
}