返回 oh-my-ppt
skill-initializer.test.ts
根目录 / tests / unit / skills / skill-initializer.test.ts
1 import { mkdir, mkdtemp, readFile, writeFile } from 'node:fs/promises'
2 import os from 'node:os'
3 import path from 'node:path'
4 import { CompositeBackend, FilesystemBackend } from 'deepagents'
5 import { describe, expect, it } from 'vitest'
6 import { attachProductSkillsBackend } from '../../../src/main/agent-runtime/skills'
7 import { compareVersion, initializeSkills } from '../../../src/main/product-skills/initializer'
8 import {
9 CHART_SKILL_NAME,
10 DATA_ANIM_SKILL_NAME,
11 LAYOUT_SKILL_NAME,
12 PRODUCT_SKILLS_ROUTE,
13 RED_LAYOUT_SKILL_NAME,
14 REQUIRED_PRODUCT_SKILL_NAMES,
15 SOURCE_READING_SKILL_NAME,
16 SQUARE_1_1_LAYOUT_SKILL_NAME,
17 STANDARD_4_3_LAYOUT_SKILL_NAME,
18 SYSTEM_SKILLS_SOURCE_PATH,
19 VERTICAL_3_4_LAYOUT_SKILL_NAME,
20 VERTICAL_9_16_LAYOUT_SKILL_NAME
21 } from '../../../src/main/product-skills/contract'
22 import { setSkillsRuntime } from '../../../src/main/product-skills/runtime-state'
23
24 async function makeSkill(root: string, name: string, version: string, body = '# Skill\n'): Promise<void> {
25 const skillPath = path.join(root, name)
26 await mkdir(skillPath, { recursive: true })
27 await writeFile(
28 path.join(skillPath, 'skill.json'),
29 `${JSON.stringify({ name, version, source: 'builtin' }, null, 2)}\n`,
30 'utf8'
31 )
32 await writeFile(
33 path.join(skillPath, 'SKILL.md'),
34 `---\nname: ${name}\ndescription: Test skill\n---\n\n${body}`,
35 'utf8'
36 )
37 }
38
39 describe('initializeSkills', () => {
40 it('compares numeric semver segments', () => {
41 expect(compareVersion('1.10.0', '1.2.0')).toBeGreaterThan(0)
42 expect(compareVersion('2.0.0', '10.0.0')).toBeLessThan(0)
43 expect(compareVersion('1.0', '1.0.0')).toBe(0)
44 })
45
46 it('installs bundled system skills into installed root and writes manifest', async () => {
47 const tmp = await mkdtemp(path.join(os.tmpdir(), 'ohmyppt-skills-'))
48 const bundled = path.join(tmp, 'bundled')
49 const installed = path.join(tmp, 'installed')
50 await makeSkill(bundled, 'oh-my-ppt-data-anim', '1.0.0')
51
52 const result = await initializeSkills({
53 builtinSourcePath: bundled,
54 installedRootPath: installed,
55 })
56
57 expect(result).toMatchObject({
58 builtinCount: 1,
59 copiedCount: 1,
60 skippedCount: 0,
61 failedCount: 0,
62 })
63 const manifest = JSON.parse(
64 await readFile(path.join(installed, 'system', '.manifest.json'), 'utf8')
65 )
66 expect(manifest.skills['oh-my-ppt-data-anim']).toMatchObject({
67 version: '1.0.0',
68 source: 'builtin',
69 })
70 await expect(
71 readFile(path.join(installed, 'system', 'oh-my-ppt-data-anim', 'SKILL.md'), 'utf8')
72 ).resolves.toContain('Test skill')
73 })
74
75 it('keeps installed skills readable through the DeepAgents composite route', async () => {
76 const tmp = await mkdtemp(path.join(os.tmpdir(), 'ohmyppt-skills-route-'))
77 const bundled = path.join(tmp, 'bundled')
78 const installed = path.join(tmp, 'installed')
79 await makeSkill(bundled, 'oh-my-ppt-data-anim', '1.0.0', '# Routed Skill\n')
80 await makeSkill(bundled, 'oh-my-ppt-chart', '1.0.0', '# Routed Chart Skill\n')
81 await initializeSkills({
82 builtinSourcePath: bundled,
83 installedRootPath: installed,
84 })
85
86 const backend = new CompositeBackend(
87 new FilesystemBackend({ rootDir: tmp, virtualMode: true }),
88 {
89 [PRODUCT_SKILLS_ROUTE]: new FilesystemBackend({
90 rootDir: installed,
91 virtualMode: true,
92 }),
93 }
94 )
95
96 const skillSource = `${PRODUCT_SKILLS_ROUTE}${SYSTEM_SKILLS_SOURCE_PATH.replace(/^\//, '')}`
97 const dataAnimSkillPath = `${skillSource}oh-my-ppt-data-anim/SKILL.md`
98 const chartSkillPath = `${skillSource}oh-my-ppt-chart/SKILL.md`
99 const listed = await backend.ls(skillSource)
100 expect(listed.error).toBeUndefined()
101 expect(listed.files?.map((file) => file.path)).toContain(
102 `${skillSource}oh-my-ppt-data-anim/`
103 )
104 expect(listed.files?.map((file) => file.path)).toContain(
105 `${skillSource}oh-my-ppt-chart/`
106 )
107
108 const read = await backend.read(
109 dataAnimSkillPath,
110 0,
111 20
112 )
113 expect(read.error).toBeUndefined()
114 expect(read.content).toContain('Routed Skill')
115
116 const downloads = await backend.downloadFiles([
117 dataAnimSkillPath,
118 chartSkillPath,
119 ])
120 expect(downloads.map((download) => download.error ?? null)).toEqual([null, null])
121 expect(new TextDecoder().decode(downloads[1].content)).toContain('Routed Chart Skill')
122 })
123
124 it('upgrades system skills by numeric version and marks removed bundled skills', async () => {
125 const tmp = await mkdtemp(path.join(os.tmpdir(), 'ohmyppt-skills-upgrade-'))
126 const bundled = path.join(tmp, 'bundled')
127 const installed = path.join(tmp, 'installed')
128 await makeSkill(bundled, 'oh-my-ppt-data-anim', '1.0.0', 'old body\n')
129 await makeSkill(bundled, 'legacy-skill', '1.0.0', 'legacy body\n')
130
131 await initializeSkills({
132 builtinSourcePath: bundled,
133 installedRootPath: installed,
134 })
135
136 const upgradedBundled = path.join(tmp, 'bundled-upgraded')
137 await makeSkill(upgradedBundled, 'oh-my-ppt-data-anim', '1.10.0', 'new body\n')
138 const upgraded = await initializeSkills({
139 builtinSourcePath: upgradedBundled,
140 installedRootPath: installed,
141 })
142
143 expect(upgraded.copiedCount).toBe(1)
144 expect(upgraded.manifest.skills['oh-my-ppt-data-anim']).toMatchObject({
145 version: '1.10.0',
146 })
147 await expect(
148 readFile(path.join(installed, 'system', 'oh-my-ppt-data-anim', 'SKILL.md'), 'utf8')
149 ).resolves.toContain('new body')
150 expect(upgraded.manifest.skills['legacy-skill'].missingFromBundle).toBe(true)
151 })
152
153 it('exposes only the selected canvas product skills through the DeepAgents source', async () => {
154 const tmp = await mkdtemp(path.join(os.tmpdir(), 'ohmyppt-filtered-skills-'))
155 const bundled = path.join(tmp, 'bundled')
156 const installed = path.join(tmp, 'installed')
157 await makeSkill(bundled, VERTICAL_9_16_LAYOUT_SKILL_NAME, '1.0.0', '# Portrait Layout\n')
158 await makeSkill(bundled, STANDARD_4_3_LAYOUT_SKILL_NAME, '1.0.0', '# Standard Layout\n')
159 await makeSkill(bundled, DATA_ANIM_SKILL_NAME, '1.0.0', '# Data Anim\n')
160 await makeSkill(bundled, CHART_SKILL_NAME, '1.0.0', '# Chart\n')
161 await makeSkill(bundled, SOURCE_READING_SKILL_NAME, '1.0.0', '# Source Reading\n')
162 await initializeSkills({
163 builtinSourcePath: bundled,
164 installedRootPath: installed
165 })
166 setSkillsRuntime({ installedSkillsPath: installed, ready: Promise.resolve(null) })
167
168 const agentBackend = attachProductSkillsBackend(
169 new FilesystemBackend({ rootDir: tmp, virtualMode: true }),
170 'session-deck',
171 [
172 VERTICAL_9_16_LAYOUT_SKILL_NAME,
173 DATA_ANIM_SKILL_NAME,
174 CHART_SKILL_NAME,
175 SOURCE_READING_SKILL_NAME
176 ]
177 )
178
179 const listed = await agentBackend.backend.ls(agentBackend.skillSource)
180 expect(listed.error).toBeUndefined()
181 expect(listed.files?.map((file) => file.path).sort()).toEqual([
182 `${agentBackend.skillSource}${CHART_SKILL_NAME}/`,
183 `${agentBackend.skillSource}${DATA_ANIM_SKILL_NAME}/`,
184 `${agentBackend.skillSource}${SOURCE_READING_SKILL_NAME}/`,
185 `${agentBackend.skillSource}${VERTICAL_9_16_LAYOUT_SKILL_NAME}/`
186 ])
187
188 const allowedRead = await agentBackend.backend.read(
189 `${agentBackend.skillSource}${VERTICAL_9_16_LAYOUT_SKILL_NAME}/SKILL.md`
190 )
191 expect(allowedRead.error).toBeUndefined()
192 expect(allowedRead.content).toContain('Portrait Layout')
193
194 const blockedRead = await agentBackend.backend.read(
195 `${agentBackend.skillSource}${STANDARD_4_3_LAYOUT_SKILL_NAME}/SKILL.md`
196 )
197 expect(blockedRead.error).toContain('Product skill is not enabled for this canvas')
198 })
199
200 it('bundles dedicated layout skills for every supported canvas', async () => {
201 for (const skillName of [
202 LAYOUT_SKILL_NAME,
203 VERTICAL_9_16_LAYOUT_SKILL_NAME,
204 STANDARD_4_3_LAYOUT_SKILL_NAME,
205 SQUARE_1_1_LAYOUT_SKILL_NAME,
206 VERTICAL_3_4_LAYOUT_SKILL_NAME,
207 RED_LAYOUT_SKILL_NAME
208 ]) {
209 expect(REQUIRED_PRODUCT_SKILL_NAMES).toContain(skillName)
210 const raw = await readFile(
211 path.join(process.cwd(), 'resources', 'skills', skillName, 'skill.json'),
212 'utf8'
213 )
214 expect(JSON.parse(raw)).toMatchObject({ name: skillName, source: 'builtin' })
215 await expect(
216 readFile(path.join(process.cwd(), 'resources', 'skills', skillName, 'SKILL.md'), 'utf8')
217 ).resolves.toContain(`name: ${skillName}`)
218 }
219 })
220 })
221
221 lines TYPESCRIPT