use std::collections::HashMap;
use std::sync::Arc;
use async_trait::async_trait;
use indexmap::IndexMap;
use serde::Serialize;
use serde_json::json;
use crate::error::{Error, ErrorCode, Result};
use crate::server::cancellation::RequestHandlerExtra;
use crate::server::{PromptHandler, ResourceHandler};
use crate::types::content::Role;
use crate::types::{
Content, GetPromptResult, ListResourcesResult, PromptMessage, ReadResourceResult, ResourceInfo,
};
pub mod projection;
pub use projection::{ProjectionOutput, ProjectionWarning, ProjectionWarningKind, SkillProjection};
pub(crate) const SKILLS_EXTENSION_KEY: &str = "io.modelcontextprotocol/skills";
const SKILL_MD_MIME: &str = "text/markdown";
pub(crate) fn set_skills_capabilities(caps: &mut crate::types::ServerCapabilities) {
if caps.resources.is_none() {
caps.resources = Some(crate::types::ResourceCapabilities {
subscribe: Some(false),
list_changed: Some(false),
});
}
caps.extensions
.get_or_insert_with(HashMap::new)
.entry(SKILLS_EXTENSION_KEY.to_string())
.or_insert_with(|| json!({}));
}
#[derive(Clone, Debug)]
pub struct SkillReference {
relative_path: String,
mime_type: String,
body: String,
}
impl SkillReference {
pub fn new(
relative_path: impl Into<String>,
mime_type: impl Into<String>,
body: impl Into<String>,
) -> Self {
Self {
relative_path: relative_path.into(),
mime_type: mime_type.into(),
body: body.into(),
}
}
pub fn relative_path(&self) -> &str {
&self.relative_path
}
pub fn mime_type(&self) -> &str {
&self.mime_type
}
pub fn body(&self) -> &str {
&self.body
}
}
#[derive(Clone, Debug)]
pub struct Skill {
name: String,
body: String,
path: Option<String>,
description: String,
references: Vec<SkillReference>,
}
impl Skill {
pub fn new(name: impl Into<String>, body: impl Into<String>) -> Self {
let body = body.into();
let description = resolved_frontmatter_description(&body);
Self {
name: name.into(),
body,
path: None,
description,
references: Vec::new(),
}
}
#[must_use]
pub fn with_path(mut self, path: impl Into<String>) -> Self {
self.path = Some(path.into());
self
}
#[must_use]
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = description.into();
self
}
#[must_use]
pub fn with_reference(self, reference: SkillReference) -> Self {
match self.try_with_reference(reference) {
Ok(s) => s,
Err(e) => panic!("Skill::with_reference: {e}"),
}
}
pub fn try_with_reference(mut self, reference: SkillReference) -> Result<Self> {
validate_reference_path(&reference.relative_path, &self.references)?;
self.references.push(reference);
Ok(self)
}
pub fn name(&self) -> &str {
&self.name
}
pub fn body(&self) -> &str {
&self.body
}
pub fn references(&self) -> impl Iterator<Item = &SkillReference> {
self.references.iter()
}
pub fn resolved_description(&self) -> &str {
&self.description
}
pub(crate) fn resolved_path(&self) -> &str {
self.path.as_deref().unwrap_or(&self.name)
}
pub(crate) fn skill_md_uri(&self) -> String {
format!("skill://{}/SKILL.md", self.resolved_path())
}
pub(crate) fn reference_uri(&self, relative_path: &str) -> String {
format!("skill://{}/{}", self.resolved_path(), relative_path)
}
pub fn as_prompt_text(&self) -> String {
let mut out = String::new();
out.push_str(&self.body);
if !self.body.ends_with('\n') {
out.push('\n');
}
for r in &self.references {
out.push_str("\n--- ");
out.push_str(&r.relative_path);
out.push_str(" ---\n");
out.push_str(&r.body);
if !r.body.ends_with('\n') {
out.push('\n');
}
}
out
}
}
fn validate_reference_path(path: &str, existing: &[SkillReference]) -> Result<()> {
if path.is_empty() {
return Err(Error::validation(
"SkillReference relative_path must not be empty",
));
}
if path.contains('\0') {
return Err(Error::validation(
"SkillReference relative_path must not contain null bytes",
));
}
if path == "SKILL.md" {
return Err(Error::validation(
"SkillReference relative_path 'SKILL.md' collides with the canonical SKILL.md URI",
));
}
if path.split('/').any(|seg| seg == "..") {
return Err(Error::validation(format!(
"SkillReference relative_path '{path}' must not contain '..' segments"
)));
}
if path.starts_with('/') {
return Err(Error::validation(format!(
"SkillReference relative_path '{path}' must be relative (no leading '/')"
)));
}
if path.contains("://") {
return Err(Error::validation(format!(
"SkillReference relative_path '{path}' must not contain a URI scheme"
)));
}
if existing.iter().any(|r| r.relative_path == path) {
return Err(Error::validation(format!(
"SkillReference relative_path '{path}' is already registered on this Skill"
)));
}
Ok(())
}
#[derive(Clone, Debug, Serialize)]
#[non_exhaustive]
pub struct SkillResourceRef {
uri: String,
digest: String,
size: usize,
}
impl SkillResourceRef {
pub fn uri(&self) -> &str {
&self.uri
}
pub fn digest(&self) -> &str {
&self.digest
}
pub fn size(&self) -> usize {
self.size
}
}
#[derive(Clone, Debug, Serialize)]
#[non_exhaustive]
pub struct SkillEntry {
uri: String,
frontmatter: serde_json::Value,
resources: Vec<SkillResourceRef>,
}
impl SkillEntry {
pub fn uri(&self) -> &str {
&self.uri
}
pub fn frontmatter(&self) -> &serde_json::Value {
&self.frontmatter
}
pub fn resources(&self) -> &[SkillResourceRef] {
&self.resources
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum SkillDiagnostic {
FrontmatterAbsent {
uri: String,
},
FrontmatterInvalid {
uri: String,
reason: String,
},
FrontmatterNotAMapping {
uri: String,
reason: String,
},
NameMismatch {
uri: String,
uri_segment: String,
skill_name: String,
},
FrontmatterNameNotAString {
uri: String,
found: String,
uri_segment: String,
},
LimitExceeded {
uri: String,
breach: SkillLimitBreach,
},
}
impl SkillDiagnostic {
pub(crate) fn uri(&self) -> &str {
match self {
Self::FrontmatterAbsent { uri }
| Self::FrontmatterInvalid { uri, .. }
| Self::FrontmatterNotAMapping { uri, .. }
| Self::NameMismatch { uri, .. }
| Self::FrontmatterNameNotAString { uri, .. }
| Self::LimitExceeded { uri, .. } => uri,
}
}
pub(crate) fn message(&self) -> String {
match self {
Self::FrontmatterAbsent { uri } => format!(
"skill {uri} is excluded from skills/list: its SKILL.md carries NO frontmatter \
block. Add a `---`-delimited YAML block at the top of the file; the skill stays \
readable via resources/read either way."
),
Self::FrontmatterInvalid { uri, reason } => format!(
"skill {uri} is excluded from skills/list: a frontmatter block IS present but is \
unusable ({reason}). This is a broken block, not a missing one."
),
Self::FrontmatterNotAMapping { uri, reason } => format!(
"skill {uri} is excluded from skills/list: its frontmatter block is valid YAML but \
is not a mapping ({reason}). SEP-2640 entries need top-level `key: value` pairs."
),
Self::NameMismatch {
uri,
uri_segment,
skill_name,
} => format!(
"skill {uri} is still listed, but its URI's final segment '{uri_segment}' differs \
from its constructed name '{skill_name}'. SEP-2640 hosts key a skill by its URI; \
this is a warning because a deliberate `with_path` override is legitimate."
),
Self::FrontmatterNameNotAString {
uri,
found,
uri_segment,
} => format!(
"skill {uri} is still listed, but its frontmatter `name` is {found}, not a \
string. The SEP-2640 name-identity rule compares that value against the URI \
segment '{uri_segment}' and a non-string can never equal it, so a conforming \
host will REFUSE this skill. Quote the name or write it as a plain scalar."
),
Self::LimitExceeded { uri, breach } => format!(
"skill {uri} is still listed, but it exceeds a SEP-2640 Limits bound: {}",
breach.describe()
),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum SkillLimitBreach {
TooManyResources(usize),
TooManyBytes(u64),
}
impl SkillLimitBreach {
fn describe(&self) -> String {
match self {
Self::TooManyResources(count) => format!(
"{count} resource entries against a limit of {MAX_SKILL_RESOURCES} (SKILL.md \
included in the count)"
),
Self::TooManyBytes(total) => format!(
"{total} total bytes against a limit of {MAX_SKILL_TOTAL_BYTES} (16 MiB), summed \
over the manifest's `size` values"
),
}
}
}
const MAX_SKILL_RESOURCES: usize = 512;
const MAX_SKILL_TOTAL_BYTES: u64 = 16_777_216;
fn exceeds_skill_limits(count: usize, total_bytes: u64) -> Option<SkillLimitBreach> {
if count > MAX_SKILL_RESOURCES {
return Some(SkillLimitBreach::TooManyResources(count));
}
if total_bytes > MAX_SKILL_TOTAL_BYTES {
return Some(SkillLimitBreach::TooManyBytes(total_bytes));
}
None
}
#[derive(Default, Clone, Debug)]
pub struct Skills {
skills: Vec<Skill>,
}
impl Skills {
pub fn new() -> Self {
Self { skills: Vec::new() }
}
#[must_use]
#[allow(clippy::should_implement_trait)] pub fn add(mut self, skill: Skill) -> Self {
self.skills.push(skill);
self
}
#[must_use]
pub fn merge(mut self, other: Self) -> Self {
self.skills.extend(other.skills);
self
}
pub fn skill_md_uris(&self) -> Vec<String> {
self.skills.iter().map(Skill::skill_md_uri).collect()
}
pub fn entries(&self) -> Result<Vec<SkillEntry>> {
let (entries, diagnostics) = self.entries_with_diagnostics()?;
log_skill_diagnostics(&diagnostics);
Ok(entries)
}
pub(crate) fn entries_with_diagnostics(
&self,
) -> Result<(Vec<SkillEntry>, Vec<SkillDiagnostic>)> {
let artifacts = self.build_artifacts();
validate_names(&artifacts)?;
validate_unique_uris(artifact_uri_rows(&artifacts))?;
Ok(entries_from_artifacts(artifacts))
}
fn build_artifacts(&self) -> Vec<SkillBuildArtifact> {
self.skills.iter().map(build_artifact).collect()
}
fn build_name_artifacts(&self) -> Vec<SkillBuildArtifact> {
self.skills.iter().map(build_name_artifact).collect()
}
pub(crate) fn finalize(self) -> Result<FinalizedSkills> {
let artifacts = self.build_artifacts();
validate_names(&artifacts)?;
validate_unique_uris(artifact_uri_rows(&artifacts))?;
let (entries, diagnostics) = entries_from_artifacts(artifacts);
Ok((self.build_handler(), entries, diagnostics))
}
pub fn into_handler(self) -> Result<Arc<dyn ResourceHandler>> {
let artifacts = self.build_name_artifacts();
validate_names(&artifacts)?;
validate_unique_uris(artifact_uri_rows(&artifacts))?;
let diagnostics: Vec<SkillDiagnostic> = artifacts
.into_iter()
.flat_map(|artifact| artifact.diagnostics)
.collect();
log_skill_diagnostics(&diagnostics);
Ok(self.build_handler())
}
fn build_handler(self) -> Arc<dyn ResourceHandler> {
let mut skill_md: IndexMap<String, Skill> = IndexMap::with_capacity(self.skills.len());
let mut references: IndexMap<String, (String, String)> = IndexMap::new();
for skill in self.skills {
for r in &skill.references {
references.insert(
skill.reference_uri(&r.relative_path),
(r.mime_type.clone(), r.body.clone()),
);
}
skill_md.insert(skill.skill_md_uri(), skill);
}
Arc::new(SkillsHandler::new(skill_md, references))
}
fn uri_rows(&self) -> Vec<(String, Vec<String>)> {
self.skills
.iter()
.map(|skill| (skill.skill_md_uri(), skill_reference_uris(skill)))
.collect()
}
pub(crate) fn validate_unique_uris(&self) -> Result<()> {
let rows = self.uri_rows();
validate_unique_uris(rows.iter().map(|(md, refs)| (md.as_str(), refs.as_slice())))
}
pub(crate) fn validate_name_identity(&self) -> Result<()> {
validate_names(&self.build_name_artifacts())
}
}
struct SkillBuildArtifact {
uri: String,
reference_uris: Vec<String>,
uri_segment: String,
frontmatter: Option<serde_json::Value>,
resources: Vec<SkillResourceRef>,
diagnostics: Vec<SkillDiagnostic>,
}
pub(crate) fn log_skill_diagnostics(diagnostics: &[SkillDiagnostic]) {
use crate::shared::log_sanitize::sanitize_for_log;
for diagnostic in diagnostics {
tracing::warn!(
target: "mcp.skills",
uri = %sanitize_for_log(diagnostic.uri()),
"{}",
sanitize_for_log(&diagnostic.message())
);
}
}
fn final_path_segment(path: &str) -> &str {
path.rsplit('/').next().unwrap_or(path)
}
fn build_artifact_inner(skill: &Skill, with_manifest: bool) -> SkillBuildArtifact {
let uri = skill.skill_md_uri();
let reference_uris = skill_reference_uris(skill);
let uri_segment = final_path_segment(skill.resolved_path()).to_string();
let mut diagnostics = Vec::new();
let frontmatter = match parse_frontmatter_value(skill.body()) {
FrontmatterParse::Parsed(value) => Some(value),
FrontmatterParse::Absent => {
diagnostics.push(SkillDiagnostic::FrontmatterAbsent { uri: uri.clone() });
None
},
FrontmatterParse::Invalid(reason) => {
diagnostics.push(SkillDiagnostic::FrontmatterInvalid {
uri: uri.clone(),
reason,
});
None
},
FrontmatterParse::NotAMapping(reason) => {
diagnostics.push(SkillDiagnostic::FrontmatterNotAMapping {
uri: uri.clone(),
reason,
});
None
},
};
if let Some(found) = frontmatter
.as_ref()
.and_then(|fm| fm.get("name"))
.filter(|name| !name.is_string())
{
diagnostics.push(SkillDiagnostic::FrontmatterNameNotAString {
uri: uri.clone(),
found: found.to_string(),
uri_segment: uri_segment.clone(),
});
}
if uri_segment != skill.name() {
diagnostics.push(SkillDiagnostic::NameMismatch {
uri: uri.clone(),
uri_segment: uri_segment.clone(),
skill_name: skill.name().to_string(),
});
}
let resources = if with_manifest {
let rows = skill_resource_manifest(skill);
let total_bytes = rows.iter().fold(0u64, |acc, row| {
acc.saturating_add(u64::try_from(row.size).unwrap_or(u64::MAX))
});
if let Some(breach) = exceeds_skill_limits(rows.len(), total_bytes) {
diagnostics.push(SkillDiagnostic::LimitExceeded {
uri: uri.clone(),
breach,
});
}
rows
} else {
Vec::new()
};
SkillBuildArtifact {
uri,
reference_uris,
uri_segment,
frontmatter,
resources,
diagnostics,
}
}
fn skill_reference_uris(skill: &Skill) -> Vec<String> {
skill
.references
.iter()
.map(|r| skill.reference_uri(r.relative_path()))
.collect()
}
fn build_artifact(skill: &Skill) -> SkillBuildArtifact {
build_artifact_inner(skill, true)
}
fn build_name_artifact(skill: &Skill) -> SkillBuildArtifact {
build_artifact_inner(skill, false)
}
pub(crate) type FinalizedSkills = (
Arc<dyn ResourceHandler>,
Vec<SkillEntry>,
Vec<SkillDiagnostic>,
);
fn entries_from_artifacts(
artifacts: Vec<SkillBuildArtifact>,
) -> (Vec<SkillEntry>, Vec<SkillDiagnostic>) {
let mut entries = Vec::with_capacity(artifacts.len());
let mut diagnostics = Vec::new();
for artifact in artifacts {
diagnostics.extend(artifact.diagnostics);
if let Some(frontmatter) = artifact.frontmatter {
entries.push(SkillEntry {
uri: artifact.uri,
frontmatter,
resources: artifact.resources,
});
}
}
(entries, diagnostics)
}
fn validate_unique_uris<'a>(rows: impl IntoIterator<Item = (&'a str, &'a [String])>) -> Result<()> {
let mut skill_md: std::collections::HashSet<&str> = std::collections::HashSet::new();
let mut references: std::collections::HashSet<&str> = std::collections::HashSet::new();
let mut dup_skill: Vec<&str> = Vec::new();
let mut dup_ref: Vec<&str> = Vec::new();
for (skill_md_uri, reference_uris) in rows {
for uri in reference_uris {
if !references.insert(uri.as_str()) {
dup_ref.push(uri.as_str());
}
}
if !skill_md.insert(skill_md_uri) {
dup_skill.push(skill_md_uri);
}
}
let mut dup_cross: Vec<&str> = references
.iter()
.filter(|uri| skill_md.contains(*uri))
.copied()
.collect();
dup_cross.sort_unstable();
if dup_skill.is_empty() && dup_ref.is_empty() && dup_cross.is_empty() {
return Ok(());
}
let mut msg = String::from("Skills: duplicate URI(s):");
if !dup_skill.is_empty() {
msg.push_str(&format!(" SKILL.md=[{}]", dup_skill.join(", ")));
}
if !dup_ref.is_empty() {
msg.push_str(&format!(" references=[{}]", dup_ref.join(", ")));
}
if !dup_cross.is_empty() {
msg.push_str(&format!(
" a reference collides with another skill's SKILL.md=[{}]",
dup_cross.join(", ")
));
}
Err(Error::validation(msg))
}
fn artifact_uri_rows(artifacts: &[SkillBuildArtifact]) -> impl Iterator<Item = (&str, &[String])> {
artifacts
.iter()
.map(|a| (a.uri.as_str(), a.reference_uris.as_slice()))
}
fn validate_names(artifacts: &[SkillBuildArtifact]) -> Result<()> {
let mut offenders: Vec<String> = Vec::new();
for artifact in artifacts {
let Some(name) = artifact
.frontmatter
.as_ref()
.and_then(|fm| fm.get("name"))
.and_then(serde_json::Value::as_str)
else {
continue;
};
if name != artifact.uri_segment {
offenders.push(format!(
"{} (frontmatter name '{}', URI segment '{}')",
artifact.uri, name, artifact.uri_segment
));
}
}
if offenders.is_empty() {
return Ok(());
}
Err(Error::validation(format!(
"Skills: frontmatter `name` must equal the final segment of the skill's URI path: [{}]",
offenders.join(", ")
)))
}
fn skill_resource_manifest(skill: &Skill) -> Vec<SkillResourceRef> {
let mut rows = Vec::with_capacity(1 + skill.references.len());
let body = skill.body();
rows.push(SkillResourceRef {
uri: skill.skill_md_uri(),
digest: sha256_digest_hex(body.as_bytes()),
size: body.len(),
});
for r in &skill.references {
let ref_body = r.body();
rows.push(SkillResourceRef {
uri: skill.reference_uri(r.relative_path()),
digest: sha256_digest_hex(ref_body.as_bytes()),
size: ref_body.len(),
});
}
rows
}
pub(crate) struct SkillsHandler {
list_resources: Vec<ResourceInfo>,
skill_md: IndexMap<String, Skill>,
references: IndexMap<String, (String, String)>, }
impl SkillsHandler {
fn new(
skill_md: IndexMap<String, Skill>,
references: IndexMap<String, (String, String)>,
) -> Self {
let list_resources: Vec<ResourceInfo> = skill_md
.values()
.map(|s| {
ResourceInfo::new(s.skill_md_uri(), s.name().to_string())
.with_description(s.resolved_description())
.with_mime_type(SKILL_MD_MIME)
})
.collect();
Self {
list_resources,
skill_md,
references,
}
}
}
#[async_trait]
impl ResourceHandler for SkillsHandler {
async fn list(
&self,
_cursor: Option<String>,
_extra: RequestHandlerExtra,
) -> Result<ListResourcesResult> {
Ok(ListResourcesResult::new(self.list_resources.clone()))
}
async fn read(&self, uri: &str, _extra: RequestHandlerExtra) -> Result<ReadResourceResult> {
if let Some(skill) = self.skill_md.get(uri) {
return Ok(ReadResourceResult::new(vec![Content::resource_with_text(
uri,
skill.body().to_string(),
SKILL_MD_MIME,
)]));
}
if let Some((mime, body)) = self.references.get(uri) {
return Ok(ReadResourceResult::new(vec![Content::resource_with_text(
uri,
body.clone(),
mime.clone(),
)]));
}
Err(Error::protocol(
ErrorCode::METHOD_NOT_FOUND,
format!("Skill resource not found: {uri}"),
))
}
}
pub(crate) struct SkillPromptHandler {
prompt_text: String,
description: String,
}
impl SkillPromptHandler {
pub(crate) fn new(skill: &Skill) -> Self {
let prompt_text = skill.as_prompt_text();
let description = skill.resolved_description().to_string();
Self {
prompt_text,
description,
}
}
}
#[async_trait]
impl PromptHandler for SkillPromptHandler {
async fn handle(
&self,
_args: HashMap<String, String>,
_extra: RequestHandlerExtra,
) -> Result<GetPromptResult> {
let message = PromptMessage::new(Role::User, Content::text(self.prompt_text.clone()));
Ok(GetPromptResult::new(
vec![message],
Some(self.description.clone()),
))
}
}
pub(crate) struct ComposedResources {
pub(crate) skills: Arc<dyn ResourceHandler>,
pub(crate) other: Arc<dyn ResourceHandler>,
}
#[async_trait]
impl ResourceHandler for ComposedResources {
async fn list(
&self,
cursor: Option<String>,
extra: RequestHandlerExtra,
) -> Result<ListResourcesResult> {
let mut combined = self.other.list(cursor.clone(), extra).await?;
if cursor.is_none() {
let skills = self
.skills
.list(None, RequestHandlerExtra::default())
.await?;
let mut resources = skills.resources;
resources.append(&mut combined.resources);
combined.resources = resources;
}
Ok(combined)
}
async fn read(&self, uri: &str, extra: RequestHandlerExtra) -> Result<ReadResourceResult> {
if uri.starts_with("skill://") {
self.skills.read(uri, extra).await
} else {
self.other.read(uri, extra).await
}
}
}
fn resolved_frontmatter_description(body: &str) -> String {
if let FrontmatterParse::Parsed(value) = parse_frontmatter_value(body) {
if let Some(description) = value.get("description").and_then(serde_json::Value::as_str) {
return description.to_string();
}
}
parse_frontmatter_description(body).unwrap_or_default()
}
fn is_frontmatter_delimiter(line: &str) -> bool {
line.trim_end() == "---"
}
fn parse_frontmatter_description(body: &str) -> Option<String> {
let body = body.strip_prefix('\u{FEFF}').unwrap_or(body);
let mut in_frontmatter = false;
for line in body.lines().take(40) {
if is_frontmatter_delimiter(line) {
if in_frontmatter {
break;
}
in_frontmatter = true;
continue;
}
if in_frontmatter {
if let Some(rest) = line.strip_prefix("description: ") {
return Some(rest.trim().to_string());
}
}
}
None
}
#[derive(Debug)]
enum FrontmatterParse {
Absent,
Parsed(serde_json::Value),
Invalid(String),
NotAMapping(String),
}
fn parse_frontmatter_value(body: &str) -> FrontmatterParse {
let body = body.strip_prefix('\u{FEFF}').unwrap_or(body);
let mut lines = body.lines();
match lines.next() {
Some(first) if is_frontmatter_delimiter(first) => {},
_ => return FrontmatterParse::Absent,
}
let mut block = String::new();
let mut terminated = false;
for line in lines {
let line = line.trim_end_matches('\r');
if is_frontmatter_delimiter(line) {
terminated = true;
break;
}
block.push_str(line);
block.push('\n');
}
if !terminated {
return FrontmatterParse::Invalid(
"frontmatter block opens with `---` but is never closed by a `---` line".to_string(),
);
}
match serde_yaml::from_str::<serde_json::Value>(&block) {
Ok(serde_json::Value::Object(map)) => {
FrontmatterParse::Parsed(serde_json::Value::Object(map))
},
Ok(other) => FrontmatterParse::NotAMapping(format!(
"frontmatter must be a YAML mapping, got {}",
match other {
serde_json::Value::Array(_) => "a sequence",
serde_json::Value::Null => "an empty document",
serde_json::Value::Bool(_) | serde_json::Value::Number(_) => "a scalar",
serde_json::Value::String(_) => "a string scalar",
serde_json::Value::Object(_) => unreachable!("matched by the arm above"),
}
)),
Err(e) => FrontmatterParse::Invalid(format!("frontmatter is not valid YAML: {e}")),
}
}
fn sha256_digest_hex(bytes: &[u8]) -> String {
use sha2::{Digest, Sha256};
const HEX: &[u8; 16] = b"0123456789abcdef";
let mut hasher = Sha256::new();
hasher.update(bytes);
let digest = hasher.finalize();
let mut out = String::with_capacity("sha256:".len() + 64);
out.push_str("sha256:");
for byte in &digest {
out.push(HEX[(byte >> 4) as usize] as char);
out.push(HEX[(byte & 0x0f) as usize] as char);
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
fn extra() -> RequestHandlerExtra {
RequestHandlerExtra::default()
}
#[test]
fn test_1_1_skill_new_and_builders() {
let s = Skill::new("foo", "body");
assert_eq!(s.name(), "foo");
assert_eq!(s.body(), "body");
assert_eq!(s.references().count(), 0);
assert_eq!(s.resolved_description(), "");
let s = s
.with_path("p")
.with_description("d")
.with_reference(SkillReference::new(
"references/x.md",
"text/markdown",
"ref body",
));
assert_eq!(s.resolved_path(), "p");
assert_eq!(s.resolved_description(), "d");
assert_eq!(s.references().count(), 1);
}
#[test]
fn test_1_2_skill_md_uri_default_and_override() {
let s = Skill::new("foo", "");
assert_eq!(s.skill_md_uri(), "skill://foo/SKILL.md");
let s = s.with_path("acme/refunds");
assert_eq!(s.skill_md_uri(), "skill://acme/refunds/SKILL.md");
}
#[test]
fn test_1_3_skill_reference_uri_resolution() {
let s = Skill::new("x", "").with_reference(SkillReference::new(
"references/a.md",
"text/markdown",
"...",
));
assert_eq!(
s.reference_uri("references/a.md"),
"skill://x/references/a.md"
);
let s = s.with_path("y/z");
assert_eq!(
s.reference_uri("references/a.md"),
"skill://y/z/references/a.md"
);
}
#[test]
fn test_1_4_as_prompt_text_no_references() {
let s = Skill::new("x", "---\nname: x\n---\nbody");
assert_eq!(s.as_prompt_text(), "---\nname: x\n---\nbody\n");
}
#[test]
fn test_1_5_as_prompt_text_with_references() {
let s = Skill::new("x", "A").with_reference(SkillReference::new(
"ref1.md",
"text/markdown",
"refbody",
));
assert_eq!(s.as_prompt_text(), "A\n\n--- ref1.md ---\nrefbody\n");
let s = Skill::new("x", "A")
.with_reference(SkillReference::new("r1.md", "text/markdown", "b1"))
.with_reference(SkillReference::new("r2.md", "text/markdown", "b2"));
assert_eq!(
s.as_prompt_text(),
"A\n\n--- r1.md ---\nb1\n\n--- r2.md ---\nb2\n"
);
}
#[test]
fn test_1_6_resolved_description_frontmatter_parsing() {
let s = Skill::new("x", "---\nname: x\ndescription: hello\n---\nbody");
assert_eq!(s.resolved_description(), "hello");
let s = Skill::new("x", "---\nname: x\ndescription: hello\n---\nbody")
.with_description("override");
assert_eq!(s.resolved_description(), "override");
let s = Skill::new("x", "no frontmatter");
assert_eq!(s.resolved_description(), "");
}
#[test]
fn test_1_6a_parse_frontmatter_crlf() {
let s = Skill::new("x", "---\r\nname: x\r\ndescription: hello\r\n---\r\nbody");
assert_eq!(s.resolved_description(), "hello");
}
#[test]
fn test_1_6b_parse_frontmatter_utf8_bom() {
let s = Skill::new("x", "\u{FEFF}---\nname: x\ndescription: hello\n---\nbody");
assert_eq!(s.resolved_description(), "hello");
}
#[tokio::test]
async fn test_1_7_skills_into_handler_happy_path() {
let handler = Skills::new()
.add(Skill::new("a", ""))
.add(Skill::new("b", ""))
.into_handler()
.unwrap();
let list = handler.list(None, extra()).await.unwrap();
assert_eq!(list.resources.len(), 2);
assert_eq!(list.resources[0].uri, "skill://a/SKILL.md");
assert_eq!(list.resources[1].uri, "skill://b/SKILL.md");
for r in &list.resources {
assert!(!r.uri.contains("/references/"));
}
}
#[tokio::test]
async fn test_1_7a_skills_into_handler_preserves_registration_order() {
for _ in 0..10 {
let handler = Skills::new()
.add(Skill::new("zeta", ""))
.add(Skill::new("alpha", ""))
.add(Skill::new("mu", ""))
.into_handler()
.unwrap();
let list = handler.list(None, extra()).await.unwrap();
assert_eq!(list.resources.len(), 3);
assert_eq!(list.resources[0].uri, "skill://zeta/SKILL.md");
assert_eq!(list.resources[1].uri, "skill://alpha/SKILL.md");
assert_eq!(list.resources[2].uri, "skill://mu/SKILL.md");
}
}
#[test]
fn test_1_8_skills_into_handler_duplicate_skill_md_uri_rejected() {
match Skills::new()
.add(Skill::new("refunds", "a"))
.add(Skill::new("refunds", "b"))
.into_handler()
{
Err(Error::Validation(msg)) => {
assert!(msg.contains("skill://refunds/SKILL.md"), "msg = {msg}");
},
Err(other) => panic!("expected Validation, got {other:?}"),
Ok(_) => panic!("expected Err for duplicate names"),
}
match Skills::new()
.add(Skill::new("a", "").with_path("p"))
.add(Skill::new("b", "").with_path("p"))
.into_handler()
{
Err(Error::Validation(msg)) => assert!(msg.contains("skill://p/SKILL.md")),
Err(other) => panic!("expected Validation, got {other:?}"),
Ok(_) => panic!("expected Err for colliding paths"),
}
}
#[test]
fn test_1_8a_skills_into_handler_duplicate_reference_uri_rejected() {
let s1 = Skill::new("a", "").with_reference(SkillReference::new(
"references/shared.md",
"text/markdown",
"x",
));
let s2 = Skill::new("b", "")
.with_path("a")
.with_reference(SkillReference::new(
"references/shared.md",
"text/markdown",
"y",
));
match Skills::new().add(s1).add(s2).into_handler() {
Err(Error::Validation(msg)) => {
assert!(
msg.contains("skill://a/references/shared.md"),
"msg = {msg}"
);
assert!(msg.contains("references="), "msg = {msg}");
},
Err(other) => panic!("expected Validation, got {other:?}"),
Ok(_) => panic!("expected Err for colliding reference URIs"),
}
}
#[test]
fn every_entry_point_refuses_a_duplicate_uri_with_the_same_message() {
type Case = (&'static str, fn() -> Skills);
let cases: Vec<Case> =
vec![
("two SKILL.md", || {
Skills::new()
.add(Skill::new("a", "").with_path("p"))
.add(Skill::new("b", "").with_path("p"))
}),
("two references", || {
Skills::new()
.add(Skill::new("a", "").with_reference(SkillReference::new(
"references/shared.md",
"text/markdown",
"x",
)))
.add(Skill::new("b", "").with_path("a").with_reference(
SkillReference::new("references/shared.md", "text/markdown", "y"),
))
}),
("a reference shadowing a SKILL.md", || {
Skills::new()
.add(Skill::new("a", "").with_reference(SkillReference::new(
"sub/SKILL.md",
"text/markdown",
"x",
)))
.add(Skill::new("b", "").with_path("a/sub"))
}),
];
for (label, build) in cases {
let via_entries = build()
.entries()
.expect_err(&format!("{label}: entries() must refuse"));
let via_handler = build()
.into_handler()
.err()
.unwrap_or_else(|| panic!("{label}: into_handler() must refuse"));
let via_finalize = build()
.finalize()
.err()
.unwrap_or_else(|| panic!("{label}: finalize() must refuse"));
let via_probe = build()
.validate_unique_uris()
.expect_err(&format!("{label}: the try_skills probe must refuse"));
let Error::Validation(expected) = &via_entries else {
panic!("{label}: expected Validation, got {via_entries:?}");
};
for (door, got) in [
("into_handler", &via_handler),
("finalize", &via_finalize),
("try_skills probe", &via_probe),
] {
let Error::Validation(msg) = got else {
panic!("{label}/{door}: expected Validation, got {got:?}");
};
assert_eq!(
msg, expected,
"{label}: entries() and {door} must produce the same message"
);
}
assert!(
expected.starts_with("Skills: duplicate URI(s):"),
"{label}: msg = {expected}"
);
}
}
#[tokio::test]
async fn test_1_9_skills_handler_list_excludes_references() {
let s = Skill::new("a", "")
.with_reference(SkillReference::new(
"references/r1.md",
"text/markdown",
"1",
))
.with_reference(SkillReference::new(
"references/r2.md",
"text/markdown",
"2",
));
let handler = Skills::new().add(s).into_handler().unwrap();
let list = handler.list(None, extra()).await.unwrap();
let skill_md_count = list
.resources
.iter()
.filter(|r| r.uri == "skill://a/SKILL.md")
.count();
assert_eq!(skill_md_count, 1);
for r in &list.resources {
assert!(!r.uri.contains("/references/"), "leaked: {}", r.uri);
}
let retired_count = list
.resources
.iter()
.filter(|r| r.uri == "skill://index.json")
.count();
assert_eq!(retired_count, 0, "retired discovery entry reappeared");
assert_eq!(list.resources.len(), 1, "SKILL.md only");
}
#[tokio::test]
async fn test_1_10_skills_handler_read_skill_md_returns_resource_with_text() {
let handler = Skills::new()
.add(Skill::new("a", "the body"))
.into_handler()
.unwrap();
let res = handler.read("skill://a/SKILL.md", extra()).await.unwrap();
assert_eq!(res.contents.len(), 1);
match &res.contents[0] {
Content::Resource {
uri,
text,
mime_type,
..
} => {
assert_eq!(uri, "skill://a/SKILL.md");
assert_eq!(text.as_deref(), Some("the body"));
assert_eq!(mime_type.as_deref(), Some("text/markdown"));
},
other => panic!("expected Content::Resource, got {other:?}"),
}
}
#[tokio::test]
async fn test_1_11_skills_handler_read_reference_carries_per_resource_mime() {
let s = Skill::new("a", "").with_reference(SkillReference::new(
"references/schema.graphql",
"application/graphql",
"schema { query: Q }",
));
let handler = Skills::new().add(s).into_handler().unwrap();
let res = handler
.read("skill://a/references/schema.graphql", extra())
.await
.unwrap();
match &res.contents[0] {
Content::Resource {
uri,
text,
mime_type,
..
} => {
assert_eq!(uri, "skill://a/references/schema.graphql");
assert_eq!(text.as_deref(), Some("schema { query: Q }"));
assert_eq!(mime_type.as_deref(), Some("application/graphql"));
},
other => panic!("expected Content::Resource, got {other:?}"),
}
}
#[tokio::test]
async fn test_1_12_skills_handler_read_retired_index_uri_is_unknown() {
let s = Skill::new("a", "").with_reference(SkillReference::new(
"references/r.md",
"text/markdown",
"x",
));
let handler = Skills::new().add(s).into_handler().unwrap();
let err = handler
.read("skill://index.json", extra())
.await
.expect_err("the retired discovery URI must no longer be served");
let control = handler
.read("skill://totally-unregistered/SKILL.md", extra())
.await
.expect_err("control: an unregistered URI must error");
match (&err, &control) {
(
Error::Protocol {
code, message: m1, ..
},
Error::Protocol {
code: cc,
message: m2,
..
},
) => {
assert_eq!(*code, ErrorCode::METHOD_NOT_FOUND);
assert_eq!(code, cc, "retired URI must take the ordinary path");
assert!(m1.contains("skill://index.json"), "m1 = {m1}");
assert!(m2.contains("skill://totally-unregistered"), "m2 = {m2}");
assert_eq!(
m1.replace("skill://index.json", "URI"),
m2.replace("skill://totally-unregistered/SKILL.md", "URI")
);
},
other => panic!("expected two Error::Protocol values, got {other:?}"),
}
assert!(handler.read("skill://a/SKILL.md", extra()).await.is_ok());
}
#[tokio::test]
async fn test_1_13_skills_handler_read_unknown_uri_method_not_found() {
let handler = Skills::new()
.add(Skill::new("a", "body"))
.into_handler()
.unwrap();
let err = handler
.read("skill://nonexistent/SKILL.md", extra())
.await
.expect_err("unknown URI must error");
match err {
Error::Protocol { code, .. } => assert_eq!(code, ErrorCode::METHOD_NOT_FOUND),
other => panic!("expected Protocol, got {other:?}"),
}
let err = handler
.read("skill://a/references/missing.md", extra())
.await
.expect_err("unknown reference must error");
match err {
Error::Protocol { code, .. } => assert_eq!(code, ErrorCode::METHOD_NOT_FOUND),
other => panic!("expected Protocol, got {other:?}"),
}
}
#[tokio::test]
async fn test_1_14_skill_prompt_handler_returns_byte_equal_text() {
let skill = Skill::new("x", "A").with_reference(SkillReference::new(
"ref1.md",
"text/markdown",
"refbody",
));
let handler = SkillPromptHandler::new(&skill);
let result = handler.handle(HashMap::new(), extra()).await.unwrap();
assert_eq!(result.messages.len(), 1);
assert_eq!(result.messages[0].role, Role::User);
match &result.messages[0].content {
Content::Text { text } => assert_eq!(text, &skill.as_prompt_text()),
other => panic!("expected Content::Text, got {other:?}"),
}
}
#[test]
fn the_module_doctest_assertions_actually_hold() {
let greeting = Skill::new(
"hello-world",
"---\nname: hello-world\ndescription: A minimal skill\n---\n\n# Hello\nThis is a minimal skill.\n",
);
let prompt_text = greeting.as_prompt_text();
assert!(prompt_text.starts_with("---\nname: hello-world\n"));
assert!(prompt_text.contains("# Hello"));
assert!(!prompt_text.starts_with("# Hello"));
let entries = Skills::new()
.add(greeting)
.entries()
.expect("frontmatter name equals the URI's final segment");
assert_eq!(
entries.len(),
1,
"the canonical snippet must be discoverable"
);
assert_eq!(entries[0].uri(), "skill://hello-world/SKILL.md");
assert_eq!(
entries[0]
.frontmatter()
.get("name")
.and_then(|v| v.as_str()),
Some("hello-world")
);
}
struct DocsHandler;
#[async_trait]
impl ResourceHandler for DocsHandler {
async fn read(&self, uri: &str, _extra: RequestHandlerExtra) -> Result<ReadResourceResult> {
Ok(ReadResourceResult::new(vec![Content::text(format!(
"DOCS:{uri}"
))]))
}
async fn list(
&self,
_cursor: Option<String>,
_extra: RequestHandlerExtra,
) -> Result<ListResourcesResult> {
Ok(ListResourcesResult::new(vec![ResourceInfo::new(
"docs://handbook",
"handbook",
)]))
}
}
#[tokio::test]
async fn test_1_15_composed_resources_uri_prefix_routing() {
let skills: Arc<dyn ResourceHandler> = Skills::new()
.add(Skill::new("a", "skill-a"))
.into_handler()
.unwrap();
let other: Arc<dyn ResourceHandler> = Arc::new(DocsHandler);
let composed = ComposedResources { skills, other };
let res = composed.read("skill://a/SKILL.md", extra()).await.unwrap();
match &res.contents[0] {
Content::Resource { uri, .. } => assert_eq!(uri, "skill://a/SKILL.md"),
other => panic!("expected Content::Resource, got {other:?}"),
}
let res = composed.read("docs://handbook", extra()).await.unwrap();
match &res.contents[0] {
Content::Text { text } => assert_eq!(text, "DOCS:docs://handbook"),
other => panic!("expected Content::Text, got {other:?}"),
}
let res = composed.read("ftp://foo", extra()).await.unwrap();
match &res.contents[0] {
Content::Text { text } => assert_eq!(text, "DOCS:ftp://foo"),
other => panic!("expected Content::Text, got {other:?}"),
}
}
#[tokio::test]
async fn test_1_16_composed_resources_list_concatenates_skills_first() {
let skills: Arc<dyn ResourceHandler> = Skills::new()
.add(Skill::new("a", ""))
.into_handler()
.unwrap();
let other: Arc<dyn ResourceHandler> = Arc::new(DocsHandler);
let composed = ComposedResources { skills, other };
let list = composed.list(None, extra()).await.unwrap();
assert_eq!(list.resources.len(), 2);
assert_eq!(list.resources[0].uri, "skill://a/SKILL.md");
assert_eq!(list.resources[1].uri, "docs://handbook");
}
struct PaginatedDocsHandler {
seen: Arc<std::sync::Mutex<Vec<Option<String>>>>,
}
#[async_trait]
impl ResourceHandler for PaginatedDocsHandler {
async fn read(&self, uri: &str, _extra: RequestHandlerExtra) -> Result<ReadResourceResult> {
Ok(ReadResourceResult::new(vec![Content::text(format!(
"DOCS:{uri}"
))]))
}
async fn list(
&self,
cursor: Option<String>,
_extra: RequestHandlerExtra,
) -> Result<ListResourcesResult> {
self.seen.lock().unwrap().push(cursor.clone());
match cursor.as_deref() {
None => Ok(ListResourcesResult::new(vec![ResourceInfo::new(
"docs://page1",
"page1",
)])
.with_next_cursor("p2")),
Some("p2") => Ok(ListResourcesResult::new(vec![ResourceInfo::new(
"docs://page2",
"page2",
)])),
Some(other) => panic!("unexpected cursor {other:?}"),
}
}
}
#[tokio::test]
async fn test_1_16b_composed_resources_emits_skills_on_the_first_page_only() {
let seen = Arc::new(std::sync::Mutex::new(Vec::new()));
let skills: Arc<dyn ResourceHandler> = Skills::new()
.add(Skill::new("a", ""))
.into_handler()
.unwrap();
let other: Arc<dyn ResourceHandler> = Arc::new(PaginatedDocsHandler {
seen: Arc::clone(&seen),
});
let composed = ComposedResources { skills, other };
let page1 = composed.list(None, extra()).await.unwrap();
assert_eq!(
page1
.resources
.iter()
.map(|r| r.uri.as_str())
.collect::<Vec<_>>(),
vec!["skill://a/SKILL.md", "docs://page1"]
);
assert_eq!(page1.next_cursor.as_deref(), Some("p2"));
let page2 = composed
.list(Some("p2".to_string()), extra())
.await
.unwrap();
assert_eq!(
page2
.resources
.iter()
.map(|r| r.uri.as_str())
.collect::<Vec<_>>(),
vec!["docs://page2"]
);
assert_eq!(page2.next_cursor, None);
assert_eq!(
*seen.lock().unwrap(),
vec![None, Some("p2".to_string())],
"ComposedResources must forward the caller's cursor to the user handler"
);
}
#[tokio::test]
async fn test_1_16c_composed_resources_never_repeats_a_skill_across_pages() {
let skills: Arc<dyn ResourceHandler> = Skills::new()
.add(Skill::new("a", ""))
.add(Skill::new("b", ""))
.into_handler()
.unwrap();
let other: Arc<dyn ResourceHandler> = Arc::new(PaginatedDocsHandler {
seen: Arc::new(std::sync::Mutex::new(Vec::new())),
});
let composed = ComposedResources { skills, other };
let mut skill_uris = Vec::new();
let mut cursor = None;
loop {
let page = composed.list(cursor, extra()).await.unwrap();
skill_uris.extend(
page.resources
.iter()
.map(|r| r.uri.clone())
.filter(|u| u.starts_with("skill://")),
);
match page.next_cursor {
Some(c) => cursor = Some(c),
None => break,
}
}
assert_eq!(
skill_uris,
vec!["skill://a/SKILL.md", "skill://b/SKILL.md"],
"each skill must appear exactly once across the full paginated walk"
);
}
#[tokio::test]
async fn test_1_16d_composed_resources_drops_skills_for_a_cursor_ignoring_handler() {
let skills: Arc<dyn ResourceHandler> = Skills::new()
.add(Skill::new("a", ""))
.into_handler()
.unwrap();
let other: Arc<dyn ResourceHandler> = Arc::new(DocsHandler);
let composed = ComposedResources { skills, other };
let with_cursor = composed
.list(Some("stale".to_string()), extra())
.await
.unwrap();
assert_eq!(
with_cursor
.resources
.iter()
.map(|r| r.uri.as_str())
.collect::<Vec<_>>(),
vec!["docs://handbook"],
"a cursor-bearing call yields the user handler's resources and no skills"
);
let without_cursor = composed.list(None, extra()).await.unwrap();
assert_eq!(
without_cursor
.resources
.iter()
.map(|r| r.uri.as_str())
.collect::<Vec<_>>(),
vec!["skill://a/SKILL.md", "docs://handbook"]
);
}
fn skill_strategy() -> impl Strategy<Value = Skill> {
let name = "[a-z]{1,8}";
let ref_strategy = (
"ref_[a-z]{1,6}\\.md",
Just("text/markdown".to_string()),
"[a-zA-Z]{1,12}",
)
.prop_map(|(p, m, b)| SkillReference::new(p, m, b));
(
name,
"[a-zA-Z]{0,20}",
proptest::collection::vec(ref_strategy, 0..=5),
)
.prop_map(|(name, body, refs)| {
let mut s = Skill::new(name, body);
let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
for r in refs {
if seen.insert(r.relative_path().to_string()) {
s = s.with_reference(r);
}
}
s
})
}
fn skills_strategy_with_refs() -> impl Strategy<Value = Vec<Skill>> {
proptest::collection::vec(skill_strategy(), 1..=10).prop_map(|skills| {
skills
.into_iter()
.enumerate()
.map(|(i, s)| {
let new_path = format!("p{i}");
let mut rebuilt = Skill::new(s.name().to_string(), s.body().to_string())
.with_path(new_path)
.with_description(s.resolved_description());
for r in s.references() {
rebuilt = rebuilt.with_reference(SkillReference::new(
r.relative_path(),
r.mime_type(),
r.body(),
));
}
rebuilt
})
.collect()
})
}
proptest! {
#[test]
fn prop_1_17_no_reference_ever_listed(skills in skills_strategy_with_refs()) {
let mut registry = Skills::new();
for s in skills {
registry = registry.add(s);
}
let Ok(handler) = registry.into_handler() else { return Ok(()); };
let rt = tokio::runtime::Runtime::new().unwrap();
let list = rt.block_on(handler.list(None, RequestHandlerExtra::default())).unwrap();
for r in &list.resources {
prop_assert!(!r.uri.contains("/references/"), "leaked: {}", r.uri);
}
}
}
proptest! {
#[test]
fn prop_1_18_duplicate_uri_always_rejected(
name in "[a-z]{1,6}",
body_a in "[a-zA-Z]{0,12}",
body_b in "[a-zA-Z]{0,12}",
) {
let result = Skills::new()
.add(Skill::new(name.clone(), body_a))
.add(Skill::new(name, body_b))
.into_handler();
prop_assert!(result.is_err());
}
#[test]
fn prop_1_18b_distinct_names_always_ok(
name_a in "[a-z]{1,6}",
name_b in "[a-z]{7,12}",
) {
prop_assume!(name_a != name_b);
let result = Skills::new()
.add(Skill::new(name_a, ""))
.add(Skill::new(name_b, ""))
.into_handler();
prop_assert!(result.is_ok());
}
}
proptest! {
#[test]
fn prop_1_19_as_prompt_text_byte_equal_concat(skill in skill_strategy()) {
let mut expected = String::new();
expected.push_str(skill.body());
if !skill.body().ends_with('\n') {
expected.push('\n');
}
for r in skill.references() {
expected.push_str("\n--- ");
expected.push_str(r.relative_path());
expected.push_str(" ---\n");
expected.push_str(r.body());
if !r.body().ends_with('\n') {
expected.push('\n');
}
}
prop_assert_eq!(skill.as_prompt_text(), expected);
}
}
fn collect_all_uris(skills: &[Skill]) -> Vec<String> {
let mut uris: Vec<String> = Vec::new();
for s in skills {
uris.push(s.skill_md_uri());
for r in s.references() {
uris.push(s.reference_uri(r.relative_path()));
}
}
uris
}
fn assert_read_response_has_uri_and_mime(
contents: &[Content],
expected_uri: &str,
) -> std::result::Result<(), proptest::test_runner::TestCaseError> {
prop_assert_eq!(contents.len(), 1);
match &contents[0] {
Content::Resource {
uri,
text,
mime_type,
..
} => {
prop_assert_eq!(uri, expected_uri);
prop_assert!(text.is_some(), "text missing for {}", expected_uri);
prop_assert!(mime_type.is_some(), "mime missing for {}", expected_uri);
Ok(())
},
other => {
prop_assert!(false, "expected Content::Resource, got {:?}", other);
Ok(())
},
}
}
proptest! {
#[test]
fn prop_1_19a_read_responses_always_have_uri_and_mime(skills in skills_strategy_with_refs()) {
let mut registry = Skills::new();
for s in skills.clone() {
registry = registry.add(s);
}
let Ok(handler) = registry.into_handler() else { return Ok(()); };
let rt = tokio::runtime::Runtime::new().unwrap();
let uris = collect_all_uris(&skills);
for uri in uris {
let Ok(res) = rt.block_on(handler.read(&uri, RequestHandlerExtra::default())) else { continue; };
assert_read_response_has_uri_and_mime(&res.contents, &uri)?;
}
}
}
#[test]
#[should_panic(expected = "must not be empty")]
fn test_1_20_with_reference_panic_empty() {
let _ =
Skill::new("x", "b").with_reference(SkillReference::new("", "text/markdown", "body"));
}
#[test]
#[should_panic(expected = "SKILL.md")]
fn test_1_20_with_reference_panic_skill_md_collision() {
let _ = Skill::new("x", "b").with_reference(SkillReference::new(
"SKILL.md",
"text/markdown",
"body",
));
}
#[test]
#[should_panic(expected = "..")]
fn test_1_20_with_reference_panic_dotdot() {
let _ = Skill::new("x", "b").with_reference(SkillReference::new(
"../escape.md",
"text/markdown",
"body",
));
}
#[test]
#[should_panic(expected = "leading")]
fn test_1_20_with_reference_panic_absolute() {
let _ = Skill::new("x", "b").with_reference(SkillReference::new(
"/abs/path.md",
"text/markdown",
"body",
));
}
#[test]
#[should_panic(expected = "URI scheme")]
fn test_1_20_with_reference_panic_scheme() {
let _ = Skill::new("x", "b").with_reference(SkillReference::new(
"http://example.com/x",
"text/markdown",
"body",
));
}
#[test]
#[should_panic(expected = "already registered")]
fn test_1_20_with_reference_panic_duplicate_within_skill() {
let _ = Skill::new("x", "b")
.with_reference(SkillReference::new("a.md", "text/markdown", "body1"))
.with_reference(SkillReference::new("a.md", "text/markdown", "body2"));
}
#[test]
fn test_1_20a_try_with_reference_returns_err() {
let invalid = [
"",
"SKILL.md",
"../escape.md",
"/abs/path.md",
"http://example.com/x",
];
for p in invalid {
let res = Skill::new("x", "b").try_with_reference(SkillReference::new(
p,
"text/markdown",
"body",
));
assert!(res.is_err(), "expected Err for path = {p:?}");
assert!(matches!(res.unwrap_err(), Error::Validation(_)));
}
let res = Skill::new("x", "b")
.try_with_reference(SkillReference::new("a.md", "text/markdown", "1"))
.and_then(|s| s.try_with_reference(SkillReference::new("a.md", "text/markdown", "2")));
assert!(res.is_err());
let res = Skill::new("x", "b").try_with_reference(SkillReference::new(
"references/ok.md",
"text/markdown",
"body",
));
assert!(res.is_ok());
}
#[tokio::test]
async fn test_1_21_skills_merge_concatenates() {
let combined = Skills::new()
.add(Skill::new("a", ""))
.merge(Skills::new().add(Skill::new("b", "")));
let handler = combined.into_handler().unwrap();
let list = handler.list(None, extra()).await.unwrap();
assert_eq!(list.resources.len(), 2);
assert_eq!(list.resources[0].uri, "skill://a/SKILL.md");
assert_eq!(list.resources[1].uri, "skill://b/SKILL.md");
}
#[test]
fn parse_frontmatter_value_absent_and_parsed() {
assert!(matches!(
parse_frontmatter_value("just a body\nwith no frontmatter\n"),
FrontmatterParse::Absent
));
assert!(matches!(
parse_frontmatter_value(""),
FrontmatterParse::Absent
));
assert!(matches!(
parse_frontmatter_value("# Title\n\n---\n\nmore prose\n\n---\n"),
FrontmatterParse::Absent
));
let lf =
"---\nname: refunds\ndescription: Issue refunds\nmetadata:\n tier: gold\n---\n# R\n";
let FrontmatterParse::Parsed(value) = parse_frontmatter_value(lf) else {
panic!("LF frontmatter must parse");
};
assert_eq!(value["name"], "refunds");
assert_eq!(value["description"], "Issue refunds");
assert_eq!(value["metadata"]["tier"], "gold");
let crlf = lf.replace('\n', "\r\n");
let FrontmatterParse::Parsed(crlf_value) = parse_frontmatter_value(&crlf) else {
panic!("CRLF frontmatter must parse");
};
assert_eq!(
crlf_value, value,
"CRLF and LF must produce the same object"
);
let bommed = format!("\u{FEFF}{lf}");
assert!(matches!(
parse_frontmatter_value(&bommed),
FrontmatterParse::Parsed(_)
));
}
#[test]
fn parse_frontmatter_value_invalid_is_not_absent() {
let FrontmatterParse::Invalid(reason) =
parse_frontmatter_value("---\nname: x\ndescription: d\n")
else {
panic!("an unterminated block must be Invalid, never Absent");
};
assert!(
reason.contains("never closed"),
"diagnostic must name the cause, got: {reason}"
);
let FrontmatterParse::Invalid(reason) =
parse_frontmatter_value("---\nname: [unclosed\n---\nbody\n")
else {
panic!("a malformed YAML block must be Invalid");
};
assert!(
reason.contains("not valid YAML"),
"diagnostic must name the cause, got: {reason}"
);
let FrontmatterParse::NotAMapping(reason) =
parse_frontmatter_value("---\n- one\n- two\n---\nbody\n")
else {
panic!("a YAML sequence must be NotAMapping");
};
assert!(
reason.contains("must be a YAML mapping"),
"diagnostic must name the cause, got: {reason}"
);
}
#[test]
fn sha256_digest_hex_is_prefixed_lowercase_64_hex() {
assert_eq!(
sha256_digest_hex(b""),
"sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
);
let d = sha256_digest_hex(b"abc");
assert_eq!(d.len(), "sha256:".len() + 64);
let hex = d
.strip_prefix("sha256:")
.expect("carries the sha256: prefix");
assert!(
hex.chars()
.all(|c| c.is_ascii_digit() || ('a'..='f').contains(&c)),
"digest must be 64 LOWERCASE hex characters, got {hex}"
);
}
#[test]
fn entries_synthesizes_one_conforming_entry() {
let body =
"---\nname: refunds\ndescription: Issue refunds\nlicense: Apache-2.0\n---\n# Refunds\n";
let entries = Skills::new()
.add(Skill::new("refunds", body))
.entries()
.expect("entries build");
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].uri(), "skill://refunds/SKILL.md");
assert_eq!(entries[0].frontmatter()["name"], "refunds");
assert_eq!(entries[0].frontmatter()["description"], "Issue refunds");
assert_eq!(entries[0].frontmatter()["license"], "Apache-2.0");
let manifest = entries[0].resources();
assert_eq!(manifest.len(), 1, "SKILL.md is the only manifest row here");
assert_eq!(manifest[0].uri(), "skill://refunds/SKILL.md");
assert_eq!(manifest[0].size(), body.len());
assert_eq!(manifest[0].digest(), sha256_digest_hex(body.as_bytes()));
}
#[test]
fn entries_excludes_frontmatter_less_and_malformed_skills() {
let good_a = "---\nname: a\ndescription: da\n---\nbody-a\n";
let good_b = "---\nname: b\ndescription: db\n---\nbody-b\n";
let entries = Skills::new()
.add(Skill::new("a", good_a))
.add(Skill::new("bare", "no frontmatter here"))
.add(Skill::new("broken", "---\nname: [unclosed\n---\nbody\n"))
.add(Skill::new("b", good_b))
.entries()
.expect("entries build");
let uris: Vec<&str> = entries.iter().map(SkillEntry::uri).collect();
assert_eq!(
uris,
vec!["skill://a/SKILL.md", "skill://b/SKILL.md"],
"entry order equals REGISTRATION order, with non-conformers dropped"
);
assert!(Skills::new().entries().expect("entries build").is_empty());
}
#[test]
fn entry_serialization_emits_exactly_the_wire_keys() {
let entries = Skills::new()
.add(Skill::new("x", "---\nname: x\ndescription: d\n---\nbody\n"))
.entries()
.expect("entries build");
let value = serde_json::to_value(&entries[0]).expect("entry serializes");
let obj = value.as_object().expect("an entry is a JSON object");
let mut keys: Vec<&str> = obj.keys().map(String::as_str).collect();
keys.sort_unstable();
assert_eq!(keys, vec!["frontmatter", "resources", "uri"]);
let row = value["resources"][0].as_object().expect("a manifest row");
let mut row_keys: Vec<&str> = row.keys().map(String::as_str).collect();
row_keys.sort_unstable();
assert_eq!(row_keys, vec!["digest", "size", "uri"]);
assert!(
value["resources"][0]["size"].is_u64(),
"size is a byte count"
);
}
#[test]
fn entries_manifest_lists_skill_md_first_then_every_reference() {
let body = "---\nname: refunds\ndescription: Issue refunds\n---\n# Refunds\n";
let policy = "# Policy\n\nRefund within 30 days.\n";
let email = "Dear customer,\n";
let entries = Skills::new()
.add(
Skill::new("refunds", body)
.with_reference(SkillReference::new(
"references/policy.md",
"text/markdown",
policy,
))
.with_reference(SkillReference::new(
"examples/email.md",
"text/markdown",
email,
)),
)
.entries()
.expect("entries build");
let manifest = entries[0].resources();
assert_eq!(manifest.len(), 3, "1 SKILL.md + 2 references");
assert_eq!(manifest[0].uri(), "skill://refunds/SKILL.md");
assert_eq!(manifest[1].uri(), "skill://refunds/references/policy.md");
assert_eq!(manifest[2].uri(), "skill://refunds/examples/email.md");
assert_eq!(manifest[0].size(), body.len());
assert_eq!(manifest[1].size(), policy.len());
assert_eq!(manifest[2].size(), email.len());
assert_eq!(manifest[0].digest(), sha256_digest_hex(body.as_bytes()));
assert_eq!(manifest[1].digest(), sha256_digest_hex(policy.as_bytes()));
assert_eq!(manifest[2].digest(), sha256_digest_hex(email.as_bytes()));
}
#[test]
fn entries_preserve_registration_order_across_skills() {
let fm = |n: &str| format!("---\nname: {n}\ndescription: d\n---\nbody\n");
let entries = Skills::new()
.add(Skill::new("zeta", fm("zeta")))
.add(Skill::new("alpha", fm("alpha")))
.add(Skill::new("mu", fm("mu")))
.entries()
.expect("entries build");
let uris: Vec<&str> = entries.iter().map(SkillEntry::uri).collect();
assert_eq!(
uris,
vec![
"skill://zeta/SKILL.md",
"skill://alpha/SKILL.md",
"skill://mu/SKILL.md"
]
);
}
#[test]
fn entries_frontmatter_is_verbatim_including_nested_and_list_fields() {
let body = "---\n\
name: refunds\n\
description: Issue refunds\n\
license: Apache-2.0\n\
keywords:\n - billing\n - support\n\
metadata:\n tier: gold\n owner:\n team: payments\n\
---\n# Refunds\n";
let entries = Skills::new()
.add(Skill::new("refunds", body))
.entries()
.expect("entries build");
let fm = entries[0].frontmatter();
let obj = fm.as_object().expect("frontmatter is a JSON object");
let mut keys: Vec<&str> = obj.keys().map(String::as_str).collect();
keys.sort_unstable();
assert_eq!(
keys,
vec!["description", "keywords", "license", "metadata", "name"],
"all five authored keys survive"
);
assert_eq!(fm["license"], "Apache-2.0");
assert_eq!(
fm["keywords"],
serde_json::json!(["billing", "support"]),
"a list-valued field is a JSON array"
);
assert_eq!(
fm["metadata"],
serde_json::json!({"tier": "gold", "owner": {"team": "payments"}}),
"a nested mapping is a JSON object, arbitrarily deep"
);
}
#[test]
fn entries_frontmatter_ignores_the_with_description_override() {
let body = "---\nname: x\ndescription: AUTHORED\n---\nbody\n";
let skill = Skill::new("x", body).with_description("OVERRIDE");
assert_eq!(
skill.resolved_description(),
"OVERRIDE",
"the override is live on the accessor"
);
let entries = Skills::new().add(skill).entries().expect("entries build");
assert_eq!(
entries[0].frontmatter()["description"],
"AUTHORED",
"the emitted object comes from the FILE, not from the override"
);
}
#[test]
fn entries_frontmatter_is_identical_for_lf_and_crlf() {
let lf =
"---\nname: widget\ndescription: Build widgets\nmetadata:\n tier: gold\n---\n# W\n";
let crlf = lf.replace('\n', "\r\n");
let lf_entries = Skills::new()
.add(Skill::new("widget", lf))
.entries()
.expect("entries build");
let crlf_entries = Skills::new()
.add(Skill::new("widget", crlf))
.entries()
.expect("entries build");
assert_eq!(
lf_entries[0].frontmatter(),
crlf_entries[0].frontmatter(),
"line endings must not reach the emitted frontmatter"
);
}
fn annotate_with_frontmatter(skills: &[Skill]) -> Vec<Skill> {
skills
.iter()
.map(|s| {
let path = s.resolved_path().to_string();
let body = format!(
"---\nname: {path}\ndescription: generated\n---\n{}\n",
s.body()
);
let mut rebuilt = Skill::new(s.name().to_string(), body).with_path(path);
for r in s.references() {
rebuilt = rebuilt.with_reference(SkillReference::new(
r.relative_path(),
r.mime_type(),
r.body(),
));
}
rebuilt
})
.collect()
}
proptest! {
#[test]
fn prop_manifest_rows_are_the_bytes_the_handler_serves(
skills in skills_strategy_with_refs(),
) {
let annotated = annotate_with_frontmatter(&skills);
let mut registry = Skills::new();
for s in &annotated {
registry = registry.add(s.clone());
}
let entries = registry.entries().expect("entries build");
prop_assert_eq!(
entries.len(),
annotated.len(),
"anti-vacuity: every annotated skill must yield an entry"
);
let handler = registry.into_handler().expect("unique p{i} paths cannot collide");
let rt = tokio::runtime::Runtime::new().unwrap();
for (entry, skill) in entries.iter().zip(annotated.iter()) {
prop_assert_eq!(
entry.resources().len(),
1 + skill.references().count(),
"manifest is SKILL.md plus every reference"
);
for row in entry.resources() {
let hex = row.digest().strip_prefix("sha256:");
prop_assert!(hex.is_some(), "digest must carry the sha256: prefix");
let hex = hex.unwrap();
prop_assert_eq!(hex.len(), 64);
prop_assert!(
hex.chars().all(|c| c.is_ascii_digit() || ('a'..='f').contains(&c)),
"digest hex must be 64 LOWERCASE hex characters"
);
let read = rt
.block_on(handler.read(row.uri(), RequestHandlerExtra::default()))
.expect("every manifest URI must be readable");
let served = match &read.contents[0] {
Content::Resource { text, .. } => {
text.clone().expect("skills handler always emits text")
},
other => panic!("expected Content::Resource, got {other:?}"),
};
prop_assert_eq!(row.size(), served.len());
prop_assert_eq!(row.digest(), sha256_digest_hex(served.as_bytes()));
}
}
}
}
fn assert_entry_synthesis_is_total(body: &str) -> bool {
let registry = Skills::new().add(Skill::new("fuzzed", body));
let handler_ok = Skills::new()
.add(Skill::new("fuzzed", body))
.into_handler()
.is_ok();
let Ok((entries, diagnostics)) = registry.entries_with_diagnostics() else {
assert!(
!handler_ok,
"entries() rejected but into_handler() accepted; both run the same \
validate_names over artifacts of the same shape"
);
return false;
};
assert!(
handler_ok,
"entries() accepted but into_handler() rejected the same one-skill registry"
);
assert_eq!(
entries.len()
+ diagnostics
.iter()
.filter(|d| matches!(
d,
SkillDiagnostic::FrontmatterAbsent { .. }
| SkillDiagnostic::FrontmatterInvalid { .. }
| SkillDiagnostic::FrontmatterNotAMapping { .. }
))
.count(),
1,
"a one-skill registry yields either ONE entry or ONE exclusion \
diagnostic, never both and never neither (body: {body:?})"
);
for entry in &entries {
assert!(
entry.frontmatter().is_object(),
"an emitted frontmatter is always a JSON object; a scalar or a \
sequence takes the exclusion path (body: {body:?})"
);
let manifest = entry.resources();
assert_eq!(manifest.len(), 1, "no references were registered");
assert_eq!(manifest[0].uri(), entry.uri());
assert_eq!(
manifest[0].size(),
body.len(),
"the SKILL.md row's size must be the body's byte length (body: {body:?})"
);
let hex = manifest[0]
.digest()
.strip_prefix("sha256:")
.unwrap_or_else(|| panic!("digest must carry the sha256: prefix (body: {body:?})"));
assert_eq!(
hex.len(),
64,
"digest hex is 64 characters (body: {body:?})"
);
assert!(
hex.bytes()
.all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b)),
"digest hex must be LOWERCASE — an uppercase rendering is a silent \
host-side comparison failure (body: {body:?})"
);
}
!entries.is_empty()
}
#[test]
fn entry_synthesis_survives_the_named_malformed_frontmatter_shapes() {
let cases: &[(&str, &str)] = &[
(
"leading BOM then a real block",
"\u{FEFF}---\nname: fuzzed\ndescription: d\n---\n\n# Body\n",
),
("a lone --- line", "---\n"),
("unterminated block", "---\nname: fuzzed\ndescription: d\n"),
(
"frontmatter parses to a sequence",
"---\n- a\n- b\n---\n\nbody\n",
),
(
"frontmatter parses to a scalar",
"---\njust a scalar\n---\n\nbody\n",
),
("empty body", ""),
(
"a markdown horizontal rule, not frontmatter",
"# Title\n\n---\n\nbody\n",
),
(
"CRLF block",
"---\r\nname: fuzzed\r\ndescription: d\r\n---\r\n\r\nbody\r\n",
),
(
"a YAML alias reference",
"---\nname: fuzzed\na: &x [1]\nb: *x\n---\n\nbody\n",
),
(
"a duplicate YAML key",
"---\nname: fuzzed\nname: fuzzed\n---\n\nbody\n",
),
("a tab-indented block", "---\n\tname: fuzzed\n---\n\nbody\n"),
(
"a NUL byte inside the block",
"---\nname: fuzz\u{0}ed\n---\n\nbody\n",
),
];
assert!(
cases.len() >= 4,
"the named-shape set must not shrink below the four the plan requires"
);
let mut produced_entries = 0usize;
for (label, body) in cases {
if assert_entry_synthesis_is_total(body) {
produced_entries += 1;
}
let handler = Skills::new()
.add(Skill::new("fuzzed", *body))
.into_handler();
assert!(
handler.is_ok()
|| Skills::new()
.add(Skill::new("fuzzed", *body))
.entries()
.is_err(),
"case `{label}`: into_handler failed for a reason entries() did not share"
);
}
assert!(
produced_entries >= 3,
"only {produced_entries} of the named shapes produced an entry; the \
digest and size assertions are barely exercised"
);
}
proptest! {
#[test]
fn prop_entry_synthesis_never_panics_on_arbitrary_bodies(raw in "\\PC*") {
assert_entry_synthesis_is_total(&raw);
assert_entry_synthesis_is_total(&format!("---\n{raw}\n---\n\n# Body\n"));
let encoded = serde_json::to_string(&raw).expect("a String always encodes as JSON");
let valued = format!("---\nname: fuzzed\nfree: {encoded}\n---\n\n# Body\n");
prop_assert!(
assert_entry_synthesis_is_total(&valued),
"the `valued` shape must always yield an entry — it is what keeps \
the digest and size assertions non-vacuous"
);
}
}
#[test]
fn entries_with_diagnostics_excludes_one_and_names_it() {
let (entries, diagnostics) = Skills::new()
.add(Skill::new(
"good",
"---\nname: good\ndescription: d\n---\nbody\n",
))
.add(Skill::new("bare", "# Bare\n\nNo frontmatter.\n"))
.entries_with_diagnostics()
.expect("entries build");
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].uri(), "skill://good/SKILL.md");
assert_eq!(diagnostics.len(), 1);
assert_eq!(diagnostics[0].uri(), "skill://bare/SKILL.md");
assert!(
diagnostics[0].message().contains("skill://bare/SKILL.md"),
"the warning must NAME the excluded skill, got: {}",
diagnostics[0].message()
);
}
#[test]
fn entries_diagnose_an_absent_frontmatter_block() {
let (entries, diagnostics) = Skills::new()
.add(Skill::new("bare", "# Bare\n\nNo frontmatter.\n"))
.entries_with_diagnostics()
.expect("entries build");
assert!(entries.is_empty());
assert!(
matches!(diagnostics[0], SkillDiagnostic::FrontmatterAbsent { .. }),
"got {:?}",
diagnostics[0]
);
assert!(diagnostics[0].message().contains("NO frontmatter"));
}
#[test]
fn entries_diagnose_an_invalid_frontmatter_block() {
for (body, expected_reason) in [
("---\nname: x\ndescription: d\n", "never closed"),
("---\nname: [unclosed\n---\nbody\n", "not valid YAML"),
] {
let (entries, diagnostics) = Skills::new()
.add(Skill::new("x", body))
.entries_with_diagnostics()
.expect("entries build");
assert!(entries.is_empty());
let SkillDiagnostic::FrontmatterInvalid { uri, reason } = &diagnostics[0] else {
panic!("expected FrontmatterInvalid, got {:?}", diagnostics[0]);
};
assert_eq!(uri, "skill://x/SKILL.md");
assert!(
reason.contains(expected_reason),
"reason must carry the parser's message, got: {reason}"
);
assert!(
diagnostics[0].message().contains("not a missing one"),
"the warning must distinguish a BROKEN block from an absent one"
);
}
}
#[test]
fn entries_diagnose_a_non_mapping_frontmatter_block() {
for body in [
"---\n- one\n- two\n---\nbody\n",
"---\njust a scalar\n---\nbody\n",
"---\n\n---\nbody\n",
] {
let (entries, diagnostics) = Skills::new()
.add(Skill::new("x", body))
.entries_with_diagnostics()
.expect("entries build");
assert!(entries.is_empty(), "body = {body:?}");
let SkillDiagnostic::FrontmatterNotAMapping { uri, reason } = &diagnostics[0] else {
panic!(
"expected FrontmatterNotAMapping for {body:?}, got {:?}",
diagnostics[0]
);
};
assert_eq!(uri, "skill://x/SKILL.md");
assert!(reason.contains("must be a YAML mapping"), "got: {reason}");
}
}
#[test]
fn the_three_frontmatter_diagnostics_are_distinct_variants() {
let of = |body: &str| {
Skills::new()
.add(Skill::new("x", body))
.entries_with_diagnostics()
.expect("entries build")
.1
.remove(0)
};
let absent = of("# Bare\n");
let invalid = of("---\nname: [unclosed\n---\nbody\n");
let not_mapping = of("---\n- one\n---\nbody\n");
assert!(matches!(absent, SkillDiagnostic::FrontmatterAbsent { .. }));
assert!(matches!(
invalid,
SkillDiagnostic::FrontmatterInvalid { .. }
));
assert!(matches!(
not_mapping,
SkillDiagnostic::FrontmatterNotAMapping { .. }
));
assert_ne!(absent, invalid);
assert_ne!(invalid, not_mapping);
assert_ne!(absent, not_mapping);
assert_ne!(absent.message(), invalid.message());
assert_ne!(invalid.message(), not_mapping.message());
assert_ne!(absent.message(), not_mapping.message());
}
#[test]
fn entries_survive_yaml_anchors_and_deep_nesting() {
let aliased = "---\nname: x\ndescription: d\ndefaults: &shared\n tier: gold\n\
metadata: *shared\n---\nbody\n";
let (entries, diagnostics) = Skills::new()
.add(Skill::new("x", aliased))
.entries_with_diagnostics()
.expect("entries build");
assert!(diagnostics.is_empty(), "got {diagnostics:?}");
assert_eq!(
entries[0].frontmatter()["metadata"]["tier"],
"gold",
"the alias must RESOLVE in the emitted JSON"
);
let mut deep = String::from("---\nname: x\ndescription: d\ndeep:\n");
for level in 1..=20u32 {
let indent = " ".repeat(level as usize);
deep.push_str(&format!("{indent}l{level}:\n"));
}
deep.push_str(&format!("{}leaf: bottom\n", " ".repeat(21)));
deep.push_str("---\nbody\n");
let (entries, diagnostics) = Skills::new()
.add(Skill::new("x", deep))
.entries_with_diagnostics()
.expect("entries build");
assert_eq!(
entries.len() + diagnostics.len(),
1,
"a deep block must parse or diagnose, exactly once"
);
}
#[test]
fn entries_on_an_all_bare_registry_is_ok_and_empty() {
let result = Skills::new()
.add(Skill::new("a", "body-a"))
.add(Skill::new("b", "body-b"))
.add(Skill::new("c", ""))
.entries();
let entries = result.expect("an all-bare registry is Ok, not Err");
assert!(entries.is_empty());
}
mod warn_capture {
use std::sync::{Arc, Mutex};
pub(super) struct WarnCollector {
pub(super) events: Arc<Mutex<Vec<String>>>,
}
#[derive(Default)]
struct Fields {
text: String,
}
impl Fields {
fn push(&mut self, name: &str, value: &str) {
use std::fmt::Write as _;
let _ = write!(self.text, "{name}={value} ");
}
}
impl tracing::field::Visit for Fields {
fn record_str(&mut self, field: &tracing::field::Field, value: &str) {
self.push(field.name(), value);
}
fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
self.push(field.name(), &format!("{value:?}"));
}
}
impl tracing::Subscriber for WarnCollector {
fn enabled(&self, _metadata: &tracing::Metadata<'_>) -> bool {
true
}
fn new_span(&self, _span: &tracing::span::Attributes<'_>) -> tracing::span::Id {
tracing::span::Id::from_u64(1)
}
fn record(&self, _span: &tracing::span::Id, _values: &tracing::span::Record<'_>) {}
fn record_follows_from(&self, _span: &tracing::span::Id, _follows: &tracing::span::Id) {
}
fn event(&self, event: &tracing::Event<'_>) {
if *event.metadata().level() != tracing::Level::WARN {
return;
}
let mut fields = Fields::default();
event.record(&mut fields);
if let Ok(mut events) = self.events.lock() {
events.push(fields.text);
}
}
fn enter(&self, _span: &tracing::span::Id) {}
fn exit(&self, _span: &tracing::span::Id) {}
}
}
#[test]
fn entries_emits_exactly_one_warn_event_per_diagnostic() {
let registry = Skills::new()
.add(Skill::new(
"good",
"---\nname: good\ndescription: d\n---\nb\n",
))
.add(Skill::new("bare", "no frontmatter"))
.add(Skill::new("broken", "---\nname: [unclosed\n---\nb\n"))
.add(Skill::new("seq", "---\n- one\n---\nb\n"));
let (entries, diagnostics) = registry.entries_with_diagnostics().expect("entries build");
assert_eq!(entries.len(), 1);
assert_eq!(diagnostics.len(), 3);
let events = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
let collector = warn_capture::WarnCollector {
events: std::sync::Arc::clone(&events),
};
let logged = tracing::subscriber::with_default(collector, || {
registry.entries().expect("entries build")
});
assert_eq!(logged.len(), 1, "the wrapper returns only the entries");
let captured = events.lock().unwrap().clone();
assert_eq!(
captured.len(),
diagnostics.len(),
"exactly one WARN per diagnostic, captured: {captured:?}"
);
for diagnostic in &diagnostics {
assert!(
captured.iter().any(|e| e.contains(diagnostic.uri())),
"no captured WARN names {}; captured: {captured:?}",
diagnostic.uri()
);
}
}
#[test]
fn frontmatter_name_identity_is_rejected_by_entries_and_into_handler() {
let body = "---\nname: refunds\ndescription: d\n---\nbody\n";
let build = || Skills::new().add(Skill::new("refunds", body).with_path("acme/billing"));
match build().entries() {
Err(Error::Validation(msg)) => {
assert!(msg.contains("skill://acme/billing/SKILL.md"), "msg = {msg}");
assert!(msg.contains("refunds"), "the frontmatter name: {msg}");
assert!(msg.contains("billing"), "the URI segment: {msg}");
},
other => panic!("expected Err(Validation) from entries(), got {other:?}"),
}
match build().into_handler() {
Err(Error::Validation(msg)) => {
assert!(msg.contains("skill://acme/billing/SKILL.md"), "msg = {msg}");
},
Err(other) => panic!("expected Validation, got {other:?}"),
Ok(_) => panic!("expected Err from into_handler() — the two must agree"),
}
}
#[test]
fn the_name_rule_never_touches_a_skill_without_a_frontmatter_name() {
let cases = [
("", "no frontmatter at all"),
("---\ndescription: d\n---\nbody\n", "no `name` key"),
(
"---\nname: 42\ndescription: d\n---\nbody\n",
"non-string name",
),
];
for (body, why) in cases {
let registry =
|| Skills::new().add(Skill::new("a", body).with_path("totally/other/place"));
assert!(registry().entries().is_ok(), "entries() rejected {why}");
assert!(
registry().into_handler().is_ok(),
"into_handler() rejected {why}"
);
}
}
#[test]
fn two_name_mismatches_produce_one_error_naming_both() {
let err = Skills::new()
.add(Skill::new(
"a",
"---\nname: alpha\ndescription: d\n---\nb\n",
))
.add(Skill::new("b", "---\nname: beta\ndescription: d\n---\nb\n"))
.entries()
.expect_err("both skills violate name identity");
let Error::Validation(msg) = err else {
panic!("expected Validation");
};
assert!(msg.contains("skill://a/SKILL.md"), "msg = {msg}");
assert!(msg.contains("skill://b/SKILL.md"), "msg = {msg}");
assert!(msg.contains("alpha"), "msg = {msg}");
assert!(msg.contains("beta"), "msg = {msg}");
}
#[test]
fn constructor_name_mismatch_warns_rather_than_rejects() {
let body = "---\nname: billing\ndescription: d\n---\nbody\n";
let (entries, diagnostics) = Skills::new()
.add(Skill::new("refunds", body).with_path("billing"))
.entries_with_diagnostics()
.expect("gap 4a warns, it does not reject");
assert_eq!(entries.len(), 1, "the skill is still listed");
let SkillDiagnostic::NameMismatch {
uri,
uri_segment,
skill_name,
} = &diagnostics[0]
else {
panic!("expected NameMismatch, got {:?}", diagnostics[0]);
};
assert_eq!(uri, "skill://billing/SKILL.md");
assert_eq!(uri_segment, "billing");
assert_eq!(skill_name, "refunds");
assert!(
Skills::new()
.add(Skill::new("refunds", body).with_path("billing"))
.into_handler()
.is_ok(),
"and into_handler agrees — a warning is not a refusal"
);
}
#[test]
fn exceeds_skill_limits_bounds_are_inclusive() {
assert_eq!(exceeds_skill_limits(0, 0), None);
assert_eq!(
exceeds_skill_limits(MAX_SKILL_RESOURCES, MAX_SKILL_TOTAL_BYTES),
None,
"512 entries and 16,777,216 bytes exactly are WITHIN limits"
);
assert_eq!(
exceeds_skill_limits(MAX_SKILL_RESOURCES + 1, 0),
Some(SkillLimitBreach::TooManyResources(513))
);
assert_eq!(
exceeds_skill_limits(0, MAX_SKILL_TOTAL_BYTES + 1),
Some(SkillLimitBreach::TooManyBytes(16_777_217))
);
assert_eq!(MAX_SKILL_RESOURCES, 512);
assert_eq!(MAX_SKILL_TOTAL_BYTES, 16_777_216);
}
#[test]
fn an_over_count_registry_produces_a_wired_limit_diagnostic() {
let make = |refs: usize| {
let mut skill = Skill::new("big", "---\nname: big\ndescription: d\n---\nb\n");
for i in 0..refs {
skill = skill.with_reference(SkillReference::new(
format!("references/r{i}.md"),
"text/markdown",
"x",
));
}
Skills::new().add(skill)
};
let (entries, diagnostics) = make(MAX_SKILL_RESOURCES)
.entries_with_diagnostics()
.expect("an over-limit skill is a WARNING, never a rejection");
assert_eq!(entries.len(), 1, "still listed");
assert_eq!(entries[0].resources().len(), MAX_SKILL_RESOURCES + 1);
let found = diagnostics
.iter()
.find_map(|d| match d {
SkillDiagnostic::LimitExceeded { uri, breach } => Some((uri.as_str(), *breach)),
_ => None,
})
.expect("the over-count skill must produce a limit diagnostic");
assert_eq!(found.0, "skill://big/SKILL.md");
assert_eq!(found.1, SkillLimitBreach::TooManyResources(513));
let (entries, diagnostics) = make(MAX_SKILL_RESOURCES - 1)
.entries_with_diagnostics()
.expect("entries build");
assert_eq!(entries[0].resources().len(), MAX_SKILL_RESOURCES);
assert!(
!diagnostics
.iter()
.any(|d| matches!(d, SkillDiagnostic::LimitExceeded { .. })),
"512 rows exactly is WITHIN limits, got {diagnostics:?}"
);
}
#[test]
fn entries_honor_with_path() {
let entries = Skills::new()
.add(
Skill::new("refunds", "---\nname: refunds\ndescription: d\n---\nb\n")
.with_path("acme/billing/refunds"),
)
.entries()
.expect("entries build");
assert_eq!(entries[0].uri(), "skill://acme/billing/refunds/SKILL.md");
assert_eq!(
entries[0].resources()[0].uri(),
"skill://acme/billing/refunds/SKILL.md"
);
}
}