dsh-skill-hub 0.3.18 → 0.3.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -695,6 +695,125 @@ describe('skill-hub routes', () => {
695
695
  }
696
696
  })
697
697
 
698
+ // ------------------------------------ 导入去重 vs 已禁用占位(issue #16)
699
+ /** 造一个「已禁用同名占位」:目录还在,SKILL.md 被改名成 SKILL.md.disabled。 */
700
+ async function seedDisabledPlaceholder(name: string) {
701
+ const dir = join(home, 'skills', name)
702
+ await mkdir(dir, { recursive: true })
703
+ await writeFile(join(dir, 'SKILL.md.disabled'), `---\nname: ${name}\ndescription: stub\n---\n`, 'utf8')
704
+ await store.addDisabled({
705
+ name,
706
+ description: 'stub',
707
+ path: join(dir, 'SKILL.md.disabled'),
708
+ root: 'user-dsh',
709
+ disabledAt: Date.now(),
710
+ })
711
+ return dir
712
+ }
713
+
714
+ it('does not flag a hub-disabled placeholder as existing when discovering', async () => {
715
+ await seedDisabledPlaceholder('code-review')
716
+ stubFetch([
717
+ ['repos/example/repo/releases/latest', jsonResponse({ tag_name: 'v1.0.0' })],
718
+ ['repos/example/repo/git/trees/v1.0.0', jsonResponse({ tree: [
719
+ { path: 'skills/code-review/SKILL.md', type: 'blob', size: 60 },
720
+ ] })],
721
+ ['repos/example/repo', jsonResponse({ default_branch: 'main' })],
722
+ ])
723
+ try {
724
+ const res = new FakeResponse()
725
+ await routeFor(SKILL_HUB_API.repo).handler(fakeReq('GET', `${SKILL_HUB_API.repo}?repo=example/repo`), res as never)
726
+ expect(res.status).toBe(200)
727
+ const body = res.json() as import('./protocol.ts').RepoDiscoverResponse
728
+ expect(body.ref).toBe('v1.0.0')
729
+ // 禁用只该影响启用态;被算成 existing 会让勾选框直接 disabled,用户无从下手。
730
+ expect(body.entries).toHaveLength(1)
731
+ expect(body.entries[0]).toMatchObject({ name: 'code-review', existing: false })
732
+ } finally {
733
+ vi.unstubAllGlobals()
734
+ }
735
+ })
736
+
737
+ it('imports over a hub-disabled placeholder instead of skipping it', async () => {
738
+ const dir = await seedDisabledPlaceholder('code-review')
739
+ const skillMd = '---\nname: code-review\ndescription: Reviews code\n---\n\nbody'
740
+ stubFetch([
741
+ ['repos/example/repo/git/trees/main', jsonResponse({ tree: [
742
+ { path: 'skills/code-review/SKILL.md', type: 'blob', size: 60 },
743
+ ] })],
744
+ ['repos/example/repo/commits', jsonResponse({ sha: 'abc123', commit: { tree: { sha: 'tree1' } } })],
745
+ ['repos/example/repo', jsonResponse({ default_branch: 'main' })],
746
+ ['raw.githubusercontent.com/example/repo/main/skills/code-review/SKILL.md', new Response(skillMd, { status: 200 })],
747
+ ])
748
+ try {
749
+ const res = new FakeResponse()
750
+ await routeFor(SKILL_HUB_API.repoImport).handler(fakeReq('POST', SKILL_HUB_API.repoImport, { repo: 'example/repo', paths: ['skills/code-review/SKILL.md'] }), res as never)
751
+ expect(res.status).toBe(200)
752
+ const created = res.json() as import('./protocol.ts').RepoImportResponse
753
+ let prog: import('./protocol.ts').RepoImportProgressResponse | undefined
754
+ for (let i = 0; i < 20; i++) {
755
+ await new Promise((r) => setTimeout(r, 50))
756
+ const progRes = new FakeResponse()
757
+ await routeFor(SKILL_HUB_API.repoImportProgress).handler(fakeReq('GET', `${SKILL_HUB_API.repoImportProgress}?jobId=${created.jobId}`), progRes as never)
758
+ prog = progRes.json() as import('./protocol.ts').RepoImportProgressResponse
759
+ if (prog.status !== 'running') break
760
+ }
761
+ expect(prog!.status).toBe('done')
762
+ // 关键:不再被「已存在」跳掉,真技能落地。
763
+ expect(prog!.skipped).toEqual([])
764
+ expect(prog!.imported.map((r) => r.name)).toEqual(['code-review'])
765
+ expect(prog!.replaced).toEqual(['code-review'])
766
+ await expect(readFile(join(dir, 'SKILL.md'), 'utf8')).resolves.toContain('name: code-review')
767
+ // 旧占位进回收站(可恢复),禁用记录被清掉,不然目录会留个幽灵禁用项。
768
+ await expect(access(join(dir, 'SKILL.md.disabled'))).rejects.toThrow()
769
+ expect(await store.listDisabled()).toEqual([])
770
+ const trash = await store.listTrash()
771
+ expect(trash).toHaveLength(1)
772
+ expect(trash[0].name).toBe('code-review')
773
+ expect(trash[0].path.startsWith(join(home, 'skills', '.trash', 'code-review-'))).toBe(true)
774
+ expect(trash[0].sourcePath).toBe(dir)
775
+ expect(await store.listSources()).toHaveLength(1)
776
+ } finally {
777
+ vi.unstubAllGlobals()
778
+ }
779
+ })
780
+
781
+ it('still skips a same-name directory that is not a disabled placeholder', async () => {
782
+ // 同名目录里还有真实 SKILL.md、且没有对应的禁用记录:不是占位,不许动。
783
+ const dir = join(home, 'skills', 'code-review')
784
+ await mkdir(dir, { recursive: true })
785
+ await writeFile(join(dir, 'SKILL.md'), '---\nname: code-review\ndescription: real\n---\n', 'utf8')
786
+ stubFetch([
787
+ ['repos/example/repo/git/trees/main', jsonResponse({ tree: [
788
+ { path: 'skills/code-review/SKILL.md', type: 'blob', size: 60 },
789
+ ] })],
790
+ ['repos/example/repo/commits', jsonResponse({ sha: 'abc123', commit: { tree: { sha: 'tree1' } } })],
791
+ ['repos/example/repo', jsonResponse({ default_branch: 'main' })],
792
+ ])
793
+ try {
794
+ const res = new FakeResponse()
795
+ await routeFor(SKILL_HUB_API.repoImport).handler(fakeReq('POST', SKILL_HUB_API.repoImport, { repo: 'example/repo', paths: ['skills/code-review/SKILL.md'] }), res as never)
796
+ const created = res.json() as import('./protocol.ts').RepoImportResponse
797
+ let prog: import('./protocol.ts').RepoImportProgressResponse | undefined
798
+ for (let i = 0; i < 20; i++) {
799
+ await new Promise((r) => setTimeout(r, 50))
800
+ const progRes = new FakeResponse()
801
+ await routeFor(SKILL_HUB_API.repoImportProgress).handler(fakeReq('GET', `${SKILL_HUB_API.repoImportProgress}?jobId=${created.jobId}`), progRes as never)
802
+ prog = progRes.json() as import('./protocol.ts').RepoImportProgressResponse
803
+ if (prog.status !== 'running') break
804
+ }
805
+ expect(prog!.status).toBe('done')
806
+ expect(prog!.skipped).toEqual([{ name: 'code-review', reason: 'exists' }])
807
+ expect(prog!.imported).toEqual([])
808
+ expect(prog!.replaced ?? []).toEqual([])
809
+ // 原目录原封不动,也没进回收站。
810
+ await expect(readFile(join(dir, 'SKILL.md'), 'utf8')).resolves.toContain('description: real')
811
+ expect(await store.listTrash()).toEqual([])
812
+ } finally {
813
+ vi.unstubAllGlobals()
814
+ }
815
+ })
816
+
698
817
  // -------------------------------------------------------------- groups
699
818
  it('serves groups: user tags + system collections + origins', async () => {
700
819
  const tag = await store.saveTag({ name: 'web' })