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} 单元测试。 * *

覆盖镜像同步的跳过规则: *

*/ 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(); } }