1use std::collections::BTreeMap;
8use std::convert::Infallible;
9
10use crate::load::{DirEntry, FileKind, LoadedPlugin, Rejection};
11use crate::path::PackagePath;
12use crate::source::{self, Source};
13
14#[derive(Debug, Clone, Default)]
33pub struct MemSource {
34 files: BTreeMap<String, Vec<u8>>,
35}
36
37impl MemSource {
38 pub fn new() -> Self {
40 Self::default()
41 }
42
43 pub fn file(mut self, path: impl Into<String>, bytes: impl Into<Vec<u8>>) -> Self {
45 self.files.insert(path.into(), bytes.into());
46 self
47 }
48
49 pub fn load(mut self) -> Result<LoadedPlugin, Rejection> {
51 source::load(&mut self)
52 }
53}
54
55impl Source for MemSource {
56 type Error = Infallible;
57
58 fn kind(&mut self, path: &PackagePath) -> Result<Option<FileKind>, Infallible> {
59 let path = path.as_str();
60 if self.files.contains_key(path) {
61 return Ok(Some(FileKind::File));
62 }
63 let prefix = format!("{path}/");
64 Ok(self.files.keys().any(|k| k.starts_with(&prefix)).then_some(FileKind::Directory))
65 }
66
67 fn confined(&mut self, _path: &PackagePath) -> Result<bool, Infallible> {
68 Ok(true) }
70
71 fn read(&mut self, path: &PackagePath) -> Result<Vec<u8>, Infallible> {
72 Ok(self.files.get(path.as_str()).cloned().unwrap_or_default())
73 }
74
75 fn list(&mut self, path: &PackagePath) -> Result<Vec<DirEntry>, Infallible> {
76 let prefix = format!("{}/", path.as_str());
77 let mut names: Vec<DirEntry> = Vec::new();
78 for key in self.files.keys() {
79 let Some(rest) = key.strip_prefix(&prefix) else {
80 continue;
81 };
82 let (name, kind) = match rest.split_once('/') {
83 Some((dir, _)) => (dir, FileKind::Directory),
84 None => (rest, FileKind::File),
85 };
86 if names.last().map(|e: &DirEntry| e.name.as_str()) != Some(name) {
87 names.push(DirEntry::new(name, kind));
88 }
89 }
90 Ok(names) }
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97 use crate::load::McpOutcome;
98
99 const MANIFEST: &[u8] = br#"{
100 "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
101 "name": "mem-plugin"
102 }"#;
103
104 #[test]
105 fn loads_skills_and_ignores_deeper_nesting() {
106 let plugin = MemSource::new()
107 .file("plugin.json", MANIFEST)
108 .file(
109 "skills/greet/SKILL.md",
110 b"---\nname: greet\ndescription: Greets. Use when greeting.\n---\nSay hi.\n"
111 .to_vec(),
112 )
113 .file(
115 "skills/greet/nested/SKILL.md",
116 b"---\nname: nested\ndescription: d\n---\n".to_vec(),
117 )
118 .file("skills/loose-file.md", b"not a skill".to_vec())
119 .load()
120 .unwrap();
121 assert_eq!(plugin.skills.len(), 1);
122 assert_eq!(plugin.skills[0].meta.name, "greet");
123 assert_eq!(plugin.mcp, McpOutcome::Absent);
124 assert!(plugin.diagnostics.is_empty());
125 }
126
127 #[test]
128 fn missing_manifest_rejects() {
129 let outcome = MemSource::new().load();
130 assert!(matches!(outcome, Err(Rejection::ManifestMissing)));
131 }
132}