feat: 增加技能管理模块试验性功能,等待优化
This commit is contained in:
@@ -0,0 +1,275 @@
|
||||
package com.easyagents.skill.codec;
|
||||
|
||||
import com.easyagents.skill.exception.SkillPackageException;
|
||||
import com.easyagents.skill.model.Skill;
|
||||
import com.easyagents.skill.store.SkillContentStore;
|
||||
import com.easyagents.skill.store.memory.InMemorySkillContentStore;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
/**
|
||||
* ZipSkillPackageCodec 单元测试。
|
||||
*/
|
||||
public class ZipSkillPackageCodecTest {
|
||||
|
||||
/**
|
||||
* 导入包含一个 Skill 文件夹的 zip。
|
||||
*/
|
||||
@Test
|
||||
public void importOneSkillFolder() {
|
||||
List<Skill> skills = importZip(files(
|
||||
"skill-a/SKILL.md", skillMd("Skill A", "Desc A"),
|
||||
"skill-a/references/rules/a.md", "# Rule",
|
||||
"skill-a/scripts/run.py", "print('ok')",
|
||||
"skill-a/assets/images/logo.png", "png-data"
|
||||
));
|
||||
|
||||
Assert.assertEquals(1, skills.size());
|
||||
Skill skill = skills.get(0);
|
||||
Assert.assertEquals("skill-a", skill.getId());
|
||||
Assert.assertEquals("Skill A", skill.getName());
|
||||
Assert.assertEquals("Desc A", skill.getDescription());
|
||||
Assert.assertEquals(1, skill.getReferences().size());
|
||||
Assert.assertEquals("references/rules/a.md", skill.getReferences().get(0).getPath());
|
||||
Assert.assertEquals(1, skill.getScripts().size());
|
||||
Assert.assertEquals(1, skill.getAssets().size());
|
||||
Assert.assertEquals("assets/images/logo.png", skill.getAssets().get(0).getPath());
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入包含多个 Skill 文件夹的 zip。
|
||||
*/
|
||||
@Test
|
||||
public void importMultipleSkillFolders() {
|
||||
List<Skill> skills = importZip(files(
|
||||
"skill-a/SKILL.md", skillMd("Skill A", "Desc A"),
|
||||
"skill-b/SKILL.md", skillMd("Skill B", "Desc B")
|
||||
));
|
||||
|
||||
Assert.assertEquals(2, skills.size());
|
||||
Assert.assertEquals("skill-a", skills.get(0).getId());
|
||||
Assert.assertEquals("skill-b", skills.get(1).getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* 拒绝 zip 根目录直接包含 SKILL.md。
|
||||
*/
|
||||
@Test(expected = SkillPackageException.class)
|
||||
public void rejectRootSkillMd() {
|
||||
importZip(files("SKILL.md", skillMd("Skill", "Desc")));
|
||||
}
|
||||
|
||||
/**
|
||||
* 拒绝未知顶级目录。
|
||||
*/
|
||||
@Test(expected = SkillPackageException.class)
|
||||
public void rejectUnknownTopLevelDirectory() {
|
||||
importZip(files(
|
||||
"skill-a/SKILL.md", skillMd("Skill A", "Desc A"),
|
||||
"skill-a/unknown/a.md", "# Unknown"
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* 拒绝 references 下的非 md 文件。
|
||||
*/
|
||||
@Test(expected = SkillPackageException.class)
|
||||
public void rejectNonMarkdownReference() {
|
||||
importZip(files(
|
||||
"skill-a/SKILL.md", skillMd("Skill A", "Desc A"),
|
||||
"skill-a/references/a.txt", "text"
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* 拒绝不支持的脚本扩展名。
|
||||
*/
|
||||
@Test(expected = SkillPackageException.class)
|
||||
public void rejectUnsupportedScript() {
|
||||
importZip(files(
|
||||
"skill-a/SKILL.md", skillMd("Skill A", "Desc A"),
|
||||
"skill-a/scripts/run.rb", "puts 'ok'"
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* assets 允许任意扩展名。
|
||||
*/
|
||||
@Test
|
||||
public void allowArbitraryAssetExtension() {
|
||||
List<Skill> skills = importZip(files(
|
||||
"skill-a/SKILL.md", skillMd("Skill A", "Desc A"),
|
||||
"skill-a/assets/model.custom", "asset"
|
||||
));
|
||||
|
||||
Assert.assertEquals(1, skills.get(0).getAssets().size());
|
||||
Assert.assertEquals("application/octet-stream", skills.get(0).getAssets().get(0).getMediaType());
|
||||
}
|
||||
|
||||
/**
|
||||
* 忽略 macOS 系统文件和空目录。
|
||||
*/
|
||||
@Test
|
||||
public void ignoreSystemFilesAndEmptyDirectories() {
|
||||
Map<String, String> files = files(
|
||||
"__MACOSX/._x", "ignored",
|
||||
"skill-a/.DS_Store", "ignored",
|
||||
"skill-a/SKILL.md", skillMd("Skill A", "Desc A")
|
||||
);
|
||||
List<Skill> skills = new ZipSkillPackageCodec().importZip(new ByteArrayInputStream(zip(files, "skill-a/references/")));
|
||||
|
||||
Assert.assertEquals(1, skills.size());
|
||||
Assert.assertEquals("skill-a", skills.get(0).getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* 嵌套 references 和 assets 路径正常导入。
|
||||
*/
|
||||
@Test
|
||||
public void allowNestedManagedDirectories() {
|
||||
List<Skill> skills = importZip(files(
|
||||
"skill-a/SKILL.md", skillMd("Skill A", "Desc A"),
|
||||
"skill-a/references/rules/a.md", "# Nested",
|
||||
"skill-a/assets/images/logo.png", "logo"
|
||||
));
|
||||
|
||||
Assert.assertEquals("references/rules/a.md", skills.get(0).getReferences().get(0).getPath());
|
||||
Assert.assertEquals("assets/images/logo.png", skills.get(0).getAssets().get(0).getPath());
|
||||
}
|
||||
|
||||
/**
|
||||
* 资产字节写入内容存储。
|
||||
*/
|
||||
@Test
|
||||
public void storeAssetContent() {
|
||||
InMemorySkillContentStore store = new InMemorySkillContentStore();
|
||||
List<Skill> skills = new ZipSkillPackageCodec(store).importZip(new ByteArrayInputStream(zip(files(
|
||||
"skill-a/SKILL.md", skillMd("Skill A", "Desc A"),
|
||||
"skill-a/assets/data.bin", "abc"
|
||||
))));
|
||||
|
||||
String contentRef = skills.get(0).getAssets().get(0).getContentRef();
|
||||
Assert.assertTrue(store.exists(contentRef));
|
||||
Assert.assertArrayEquals("abc".getBytes(StandardCharsets.UTF_8), store.readAllBytes(contentRef));
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入失败时不写入资产内容。
|
||||
*/
|
||||
@Test
|
||||
public void failedImportDoesNotWriteAssetContent() {
|
||||
CountingContentStore store = new CountingContentStore();
|
||||
|
||||
try {
|
||||
new ZipSkillPackageCodec(store).importZip(new ByteArrayInputStream(zip(files(
|
||||
"skill-a/SKILL.md", skillMd("Skill A", "Desc A"),
|
||||
"skill-a/assets/data.bin", "abc",
|
||||
"skill-a/unknown/a.md", "# Unknown"
|
||||
))));
|
||||
Assert.fail("Import should fail.");
|
||||
} catch (SkillPackageException expected) {
|
||||
Assert.assertEquals(0, store.putCount);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 多 Skill 包中后续 Skill 失败时,不提前写入前面 Skill 的资产。
|
||||
*/
|
||||
@Test
|
||||
public void failedMultiSkillImportDoesNotWriteEarlierAssetContent() {
|
||||
CountingContentStore store = new CountingContentStore();
|
||||
|
||||
try {
|
||||
new ZipSkillPackageCodec(store).importZip(new ByteArrayInputStream(zip(files(
|
||||
"skill-a/SKILL.md", skillMd("Skill A", "Desc A"),
|
||||
"skill-a/assets/data.bin", "abc",
|
||||
"skill-b/references/a.md", "# Missing SKILL.md"
|
||||
))));
|
||||
Assert.fail("Import should fail.");
|
||||
} catch (SkillPackageException expected) {
|
||||
Assert.assertEquals(0, store.putCount);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 拒绝嵌套 frontmatter。
|
||||
*/
|
||||
@Test(expected = SkillPackageException.class)
|
||||
public void rejectNestedFrontmatter() {
|
||||
importZip(files(
|
||||
"skill-a/SKILL.md", "---\nname: Skill A\ndescription: Desc A\nconfig:\n mode: strict\n---\n# Skill A\n"
|
||||
));
|
||||
}
|
||||
|
||||
private static List<Skill> importZip(Map<String, String> files) {
|
||||
return new ZipSkillPackageCodec().importZip(new ByteArrayInputStream(zip(files)));
|
||||
}
|
||||
|
||||
private static Map<String, String> files(String... keyValues) {
|
||||
Map<String, String> files = new LinkedHashMap<>();
|
||||
for (int i = 0; i < keyValues.length; i += 2) {
|
||||
files.put(keyValues[i], keyValues[i + 1]);
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
private static byte[] zip(Map<String, String> files, String... directories) {
|
||||
try {
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
try (ZipOutputStream zipOutputStream = new ZipOutputStream(bytes)) {
|
||||
for (String directory : directories) {
|
||||
zipOutputStream.putNextEntry(new ZipEntry(directory));
|
||||
zipOutputStream.closeEntry();
|
||||
}
|
||||
for (Map.Entry<String, String> file : files.entrySet()) {
|
||||
zipOutputStream.putNextEntry(new ZipEntry(file.getKey()));
|
||||
zipOutputStream.write(file.getValue().getBytes(StandardCharsets.UTF_8));
|
||||
zipOutputStream.closeEntry();
|
||||
}
|
||||
}
|
||||
return bytes.toByteArray();
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static String skillMd(String name, String description) {
|
||||
return "---\nname: " + name + "\ndescription: " + description + "\n---\n# " + name + "\n";
|
||||
}
|
||||
|
||||
private static final class CountingContentStore implements SkillContentStore {
|
||||
|
||||
private int putCount;
|
||||
|
||||
@Override
|
||||
public String put(byte[] bytes) {
|
||||
putCount++;
|
||||
return "sha256:" + com.easyagents.skill.util.SkillHashes.sha256Hex(bytes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream open(String contentRef) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] readAllBytes(String contentRef) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean exists(String contentRef) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user