| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210 |
- package com.agent.management.engine;
- import com.agent.management.config.SkillProperties;
- import org.junit.jupiter.api.BeforeEach;
- import org.junit.jupiter.api.Test;
- import org.junit.jupiter.api.io.TempDir;
- import java.io.IOException;
- import java.nio.file.Files;
- import java.nio.file.Path;
- import static org.assertj.core.api.Assertions.assertThat;
- /**
- * {@link SkillFileSyncer} 单元测试。
- *
- * <p>覆盖镜像同步的跳过规则:
- * <ul>
- * <li>技能根目录 SKILL.md(合法主入口)正常复制;</li>
- * <li>{@code _versions/} 子目录整树跳过;</li>
- * <li>非根子目录直接含 SKILL.md 时整树跳过(任意深度,常见于 AI 生成残留);</li>
- * <li>目标侧残留清除;</li>
- * <li>普通子目录与文件正常复制。</li>
- * </ul>
- */
- class SkillFileSyncerTest {
- @TempDir
- Path tempDir;
- private Path sourceRoot;
- private Path targetRoot;
- private SkillFileSyncer syncer;
- @BeforeEach
- void setUp() throws IOException {
- sourceRoot = tempDir.resolve("uploads-skills");
- targetRoot = tempDir.resolve("hermes-home-skills");
- Files.createDirectories(sourceRoot);
- SkillProperties props = new SkillProperties();
- props.setBasePath(sourceRoot.toString());
- syncer = new SkillFileSyncer(props);
- }
- /** 创建技能根目录 + SKILL.md */
- private void createSkill(String folderName, String skillMdContent) throws IOException {
- Path skillDir = sourceRoot.resolve(folderName);
- Files.createDirectories(skillDir);
- Files.writeString(skillDir.resolve("SKILL.md"), skillMdContent);
- }
- /** 在技能内创建子目录(可选拓展子文件) */
- private Path mkSubDir(String skillFolder, String subRel) throws IOException {
- Path p = sourceRoot.resolve(skillFolder).resolve(subRel);
- Files.createDirectories(p);
- return p;
- }
- private void write(Path dir, String fileName, String content) throws IOException {
- Files.writeString(dir.resolve(fileName), content);
- }
- // ==================== 测试用例 ====================
- @Test
- void copiesRootSkillMdAndRegularFiles() throws IOException {
- createSkill("foo", "name: foo\n");
- write(sourceRoot.resolve("foo"), "notes.md", "hello");
- int copied = syncer.syncTo(targetRoot);
- assertThat(copied).isEqualTo(1);
- assertThat(targetRoot.resolve("foo/SKILL.md")).exists();
- assertThat(targetRoot.resolve("foo/notes.md")).exists();
- }
- @Test
- void skipsVersionsSubtree() throws IOException {
- createSkill("foo", "name: foo\n");
- Path versions = mkSubDir("foo", "_versions/v1");
- write(versions, "SKILL.md", "name: foo-old\n");
- syncer.syncTo(targetRoot);
- assertThat(targetRoot.resolve("foo/_versions")).doesNotExist();
- }
- @Test
- void skipsChildDirContainingSkillMd_generatedSkills() throws IOException {
- createSkill("foo", "name: foo\n");
- // 模拟 AI 生成的残留:foo/generated_skills/SKILL.md
- Path genDir = mkSubDir("foo", "generated_skills");
- write(genDir, "SKILL.md", "name: generated\n");
- write(genDir, "extra.txt", "keep me?");
- syncer.syncTo(targetRoot);
- // generated_skills/ 整树不复制
- assertThat(targetRoot.resolve("foo/generated_skills")).doesNotExist();
- // 但技能自身的 SKILL.md 仍正常
- assertThat(targetRoot.resolve("foo/SKILL.md")).exists();
- }
- @Test
- void skipsChildDirContainingSkillMd_tmpSmokeGenerated() throws IOException {
- createSkill("foo", "name: foo\n");
- Path genDir = mkSubDir("foo", "tmp_smoke_generated");
- write(genDir, "SKILL.md", "name: smoke\n");
- syncer.syncTo(targetRoot);
- assertThat(targetRoot.resolve("foo/tmp_smoke_generated")).doesNotExist();
- assertThat(targetRoot.resolve("foo/SKILL.md")).exists();
- }
- @Test
- void skipsNestedDirContainingSkillMd_atDeeperLevel() throws IOException {
- // 结构: foo/subdir/nested/SKILL.md,subdir 自身不含 SKILL.md
- // 期望:跳过 subdir/nested/(nested 整树),但 subdir/ 自身被创建为目录
- createSkill("foo", "name: foo\n");
- Path nested = mkSubDir("foo", "subdir/nested");
- write(nested, "SKILL.md", "name: nested\n");
- write(nested, "aux.txt", "aux");
- syncer.syncTo(targetRoot);
- // nested/ 整树被跳过
- assertThat(targetRoot.resolve("foo/subdir/nested")).doesNotExist();
- // subdir/ 本身仍创建(因为 preVisitDirectory 在 subdir 处返回 CONTINUE)
- assertThat(targetRoot.resolve("foo/subdir")).exists().isDirectory();
- // 技能根 SKILL.md 仍正常
- assertThat(targetRoot.resolve("foo/SKILL.md")).exists();
- }
- @Test
- void regularSubdirWithoutSkillMdIsCopied() throws IOException {
- createSkill("foo", "name: foo\n");
- Path sub = mkSubDir("foo", "docs");
- write(sub, "guide.md", "guide");
- write(sub, "examples.py", "print('hi')");
- syncer.syncTo(targetRoot);
- assertThat(targetRoot.resolve("foo/docs/guide.md")).exists();
- assertThat(targetRoot.resolve("foo/docs/examples.py")).exists();
- }
- @Test
- void cleansOrphanedTargetDirs() throws IOException {
- // 目标侧已存在一个源侧没有的目录,同步后应被清除
- Path orphan = targetRoot.resolve("deleted-skill");
- Files.createDirectories(orphan);
- Files.writeString(orphan.resolve("SKILL.md"), "name: deleted\n");
- createSkill("foo", "name: foo\n");
- syncer.syncTo(targetRoot);
- assertThat(orphan).doesNotExist();
- assertThat(targetRoot.resolve("foo/SKILL.md")).exists();
- }
- @Test
- void mirrorsSourceFileDeletionWithinSkill() throws IOException {
- // 首次同步:foo 含 SKILL.md + obsolete.md
- createSkill("foo", "name: foo\n");
- write(sourceRoot.resolve("foo"), "obsolete.md", "will be deleted");
- syncer.syncTo(targetRoot);
- assertThat(targetRoot.resolve("foo/obsolete.md")).exists();
- // 删除源侧 obsolete.md,再次同步
- Files.delete(sourceRoot.resolve("foo").resolve("obsolete.md"));
- syncer.syncTo(targetRoot);
- // 目标侧 obsolete.md 应被镜像清除(先删后拷语义)
- assertThat(targetRoot.resolve("foo/obsolete.md")).doesNotExist();
- assertThat(targetRoot.resolve("foo/SKILL.md")).exists();
- }
- @Test
- void multipleSkillsAllChecked() throws IOException {
- createSkill("foo", "name: foo\n");
- createSkill("bar", "name: bar\n");
- // foo 下有残留
- Path genFoo = mkSubDir("foo", "generated_skills");
- write(genFoo, "SKILL.md", "name: gen\n");
- // bar 下正常
- Path docsBar = mkSubDir("bar", "docs");
- write(docsBar, "x.md", "x");
- syncer.syncTo(targetRoot);
- assertThat(targetRoot.resolve("foo/SKILL.md")).exists();
- assertThat(targetRoot.resolve("foo/generated_skills")).doesNotExist();
- assertThat(targetRoot.resolve("bar/SKILL.md")).exists();
- assertThat(targetRoot.resolve("bar/docs/x.md")).exists();
- }
- @Test
- void returnsZeroWhenSourceMissing() throws IOException {
- // sourceRoot 不存在(删掉)
- sourceRoot.toFile().delete();
- int copied = syncer.syncTo(targetRoot);
- assertThat(copied).isZero();
- }
- }
|