@vibe-forge/workspace-assets 0.9.1-alpha.0 → 0.9.2-alpha.0

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.
@@ -4,43 +4,50 @@ import { describe, expect, it } from 'vitest'
4
4
 
5
5
  import { buildAdapterAssetPlan, resolvePromptAssetSelection, resolveWorkspaceAssetBundle } from '#~/index.js'
6
6
 
7
- import { createWorkspace, writeDocument } from './test-helpers'
7
+ import { createWorkspace, installPluginPackage, writeDocument } from './test-helpers'
8
8
 
9
9
  describe('buildAdapterAssetPlan', () => {
10
- it('builds codex diagnostics for prompt, mcp, native hooks, and unsupported claude native plugins', async () => {
10
+ it('builds codex diagnostics for prompt, mcp, hook plugins, and unsupported opencode assets', async () => {
11
11
  const workspace = await createWorkspace()
12
12
 
13
- await writeDocument(
14
- join(workspace, '.ai.config.json'),
15
- JSON.stringify({
16
- plugins: {
17
- logger: {}
13
+ await installPluginPackage(workspace, '@vibe-forge/plugin-logger', {
14
+ 'package.json': JSON.stringify(
15
+ {
16
+ name: '@vibe-forge/plugin-logger',
17
+ version: '1.0.0'
18
18
  },
19
- enabledPlugins: {
20
- logger: true
19
+ null,
20
+ 2
21
+ ),
22
+ 'hooks.js': 'module.exports = {}\n'
23
+ })
24
+ await installPluginPackage(workspace, '@vibe-forge/plugin-demo', {
25
+ 'package.json': JSON.stringify(
26
+ {
27
+ name: '@vibe-forge/plugin-demo',
28
+ version: '1.0.0'
21
29
  },
22
- mcpServers: {
23
- docs: {
24
- command: 'npx',
25
- args: ['docs-server']
26
- }
27
- }
28
- })
29
- )
30
+ null,
31
+ 2
32
+ ),
33
+ 'opencode/commands/review.md': '# review\n'
34
+ })
30
35
  await writeDocument(
31
36
  join(workspace, '.ai/skills/research/SKILL.md'),
32
37
  '---\ndescription: 检索资料\n---\n阅读 README.md'
33
38
  )
39
+ await writeDocument(
40
+ join(workspace, '.ai/skills/review/SKILL.md'),
41
+ '---\ndescription: 代码评审\n---\n检查风险'
42
+ )
34
43
 
35
44
  const bundle = await resolveWorkspaceAssetBundle({
36
45
  cwd: workspace,
37
46
  configs: [{
38
- plugins: {
39
- logger: {}
40
- },
41
- enabledPlugins: {
42
- logger: true
43
- },
47
+ plugins: [
48
+ { id: 'logger' },
49
+ { id: 'demo', scope: 'demo' }
50
+ ],
44
51
  mcpServers: {
45
52
  docs: {
46
53
  command: 'npx',
@@ -50,6 +57,17 @@ describe('buildAdapterAssetPlan', () => {
50
57
  }, undefined],
51
58
  useDefaultVibeForgeMcpServer: false
52
59
  })
60
+ const researchSkillId = bundle.skills.find(asset => asset.name === 'research')?.id
61
+ const reviewSkillId = bundle.skills.find(asset => asset.name === 'review')?.id
62
+ const loggerHookPluginId = bundle.hookPlugins.find(asset => asset.packageId === '@vibe-forge/plugin-logger')?.id
63
+ const demoCommandId = bundle.opencodeOverlayAssets.find(asset => asset.kind === 'command')?.id
64
+ const docsMcpId = bundle.mcpServers.docs?.id
65
+ expect(researchSkillId).toBeDefined()
66
+ expect(reviewSkillId).toBeDefined()
67
+ expect(loggerHookPluginId).toBeDefined()
68
+ expect(demoCommandId).toBeDefined()
69
+ expect(docsMcpId).toBeDefined()
70
+
53
71
  const [, resolvedOptions] = await resolvePromptAssetSelection({
54
72
  bundle,
55
73
  type: undefined,
@@ -73,32 +91,33 @@ describe('buildAdapterAssetPlan', () => {
73
91
  })
74
92
 
75
93
  expect(plan.mcpServers).toHaveProperty('docs')
76
- expect(plan.native.codexHooks?.supportedEvents).toEqual([
77
- 'SessionStart',
78
- 'UserPromptSubmit',
79
- 'PreToolUse',
80
- 'PostToolUse',
81
- 'Stop'
82
- ])
83
94
  expect(plan.diagnostics).toEqual(expect.arrayContaining([
84
95
  expect.objectContaining({
96
+ assetId: researchSkillId,
85
97
  adapter: 'codex',
86
98
  status: 'prompt'
87
99
  }),
88
100
  expect.objectContaining({
89
101
  adapter: 'codex',
90
102
  status: 'native',
91
- assetId: 'hookPlugin:project:logger'
103
+ assetId: loggerHookPluginId
92
104
  }),
93
105
  expect.objectContaining({
94
106
  adapter: 'codex',
95
107
  status: 'translated',
96
- assetId: 'mcpServer:project:docs'
108
+ assetId: docsMcpId
97
109
  }),
98
110
  expect.objectContaining({
99
111
  adapter: 'codex',
100
112
  status: 'skipped',
101
- assetId: 'nativePlugin:claude-code:logger'
113
+ assetId: demoCommandId
114
+ })
115
+ ]))
116
+ expect(plan.diagnostics).not.toEqual(expect.arrayContaining([
117
+ expect.objectContaining({
118
+ assetId: reviewSkillId,
119
+ adapter: 'codex',
120
+ status: 'prompt'
102
121
  })
103
122
  ]))
104
123
  })
@@ -106,18 +125,29 @@ describe('buildAdapterAssetPlan', () => {
106
125
  it('builds opencode overlays for skills and native commands', async () => {
107
126
  const workspace = await createWorkspace()
108
127
 
128
+ await installPluginPackage(workspace, '@vibe-forge/plugin-demo', {
129
+ 'package.json': JSON.stringify(
130
+ {
131
+ name: '@vibe-forge/plugin-demo',
132
+ version: '1.0.0'
133
+ },
134
+ null,
135
+ 2
136
+ ),
137
+ 'opencode/commands/review.md': '# review\n'
138
+ })
109
139
  await writeDocument(
110
140
  join(workspace, '.ai/skills/research/SKILL.md'),
111
141
  '---\ndescription: 检索资料\n---\n阅读 README.md'
112
142
  )
113
- await writeDocument(
114
- join(workspace, '.ai/plugins/demo/opencode/commands/review.md'),
115
- '# review'
116
- )
117
143
 
118
144
  const bundle = await resolveWorkspaceAssetBundle({
119
145
  cwd: workspace,
120
- configs: [undefined, undefined],
146
+ configs: [{
147
+ plugins: [
148
+ { id: 'demo', scope: 'demo' }
149
+ ]
150
+ }, undefined],
121
151
  useDefaultVibeForgeMcpServer: false
122
152
  })
123
153
  const plan = buildAdapterAssetPlan({
@@ -129,6 +159,7 @@ describe('buildAdapterAssetPlan', () => {
129
159
  }
130
160
  }
131
161
  })
162
+ const commandAsset = bundle.opencodeOverlayAssets.find(asset => asset.kind === 'command')
132
163
 
133
164
  expect(plan.overlays).toEqual(expect.arrayContaining([
134
165
  expect.objectContaining({
@@ -140,5 +171,12 @@ describe('buildAdapterAssetPlan', () => {
140
171
  targetPath: 'commands/review.md'
141
172
  })
142
173
  ]))
174
+ expect(plan.diagnostics).toEqual(expect.arrayContaining([
175
+ expect.objectContaining({
176
+ assetId: commandAsset?.id,
177
+ adapter: 'opencode',
178
+ status: 'native'
179
+ })
180
+ ]))
143
181
  })
144
182
  })
@@ -1,65 +1,70 @@
1
- import { join } from 'node:path'
2
1
  import process from 'node:process'
3
2
 
4
3
  import { describe, expect, it } from 'vitest'
5
4
 
6
5
  import { resolveWorkspaceAssetBundle } from '#~/index.js'
7
6
 
8
- import { createWorkspace, writeDocument } from './test-helpers'
7
+ import { createWorkspace, installPluginPackage } from './test-helpers'
9
8
 
10
9
  describe('resolveWorkspaceAssetBundle', () => {
11
- it('treats enabledPlugins as a global asset switch', async () => {
10
+ it('loads npm plugin assets via the package-id fallback and exposes OpenCode overlays', async () => {
12
11
  const workspace = await createWorkspace()
13
12
 
14
- await writeDocument(
15
- join(workspace, '.ai.config.json'),
16
- JSON.stringify({
17
- plugins: {
18
- logger: {}
13
+ await installPluginPackage(workspace, '@vibe-forge/plugin-demo', {
14
+ 'package.json': JSON.stringify(
15
+ {
16
+ name: '@vibe-forge/plugin-demo',
17
+ version: '1.0.0'
19
18
  },
20
- enabledPlugins: {
21
- logger: false,
22
- demo: false
23
- }
24
- })
25
- )
26
- await writeDocument(
27
- join(workspace, '.ai/plugins/demo/skills/research/SKILL.md'),
28
- '---\ndescription: 检索资料\n---\n阅读 README.md'
29
- )
30
- await writeDocument(
31
- join(workspace, '.ai/plugins/demo/rules/review.md'),
32
- '---\ndescription: 评审规则\n---\n必须检查风险'
33
- )
34
- await writeDocument(
35
- join(workspace, '.ai/plugins/demo/mcp/browser.json'),
36
- JSON.stringify({ command: 'npx', args: ['browser-server'] })
37
- )
38
- await writeDocument(
39
- join(workspace, '.ai/plugins/demo/opencode/commands/review.md'),
40
- '# review'
41
- )
19
+ null,
20
+ 2
21
+ ),
22
+ 'skills/research/SKILL.md': '---\ndescription: 检索资料\n---\n阅读 README.md',
23
+ 'rules/review.md': '---\ndescription: 评审规则\n---\n必须检查风险',
24
+ 'mcp/browser.json': JSON.stringify({ command: 'npx', args: ['browser-server'] }, null, 2),
25
+ 'opencode/commands/review.md': '# review\n'
26
+ })
27
+ await installPluginPackage(workspace, '@vibe-forge/plugin-logger', {
28
+ 'package.json': JSON.stringify(
29
+ {
30
+ name: '@vibe-forge/plugin-logger',
31
+ version: '1.0.0'
32
+ },
33
+ null,
34
+ 2
35
+ ),
36
+ 'hooks.js': 'module.exports = {}\n'
37
+ })
42
38
 
43
39
  const bundle = await resolveWorkspaceAssetBundle({
44
40
  cwd: workspace,
45
41
  configs: [{
46
- plugins: {
47
- logger: {}
48
- },
49
- enabledPlugins: {
50
- logger: false,
51
- demo: false
52
- }
42
+ plugins: [
43
+ { id: 'demo', scope: 'demo' },
44
+ { id: 'logger' }
45
+ ]
53
46
  }, undefined],
54
47
  useDefaultVibeForgeMcpServer: false
55
48
  })
56
49
 
57
- expect(bundle.skills).toHaveLength(0)
58
- expect(bundle.rules).toHaveLength(0)
59
- expect(Object.keys(bundle.mcpServers)).toHaveLength(0)
60
- expect(bundle.hookPlugins).toHaveLength(0)
61
- expect(bundle.assets.some((asset: (typeof bundle.assets)[number]) => asset.pluginId === 'demo' && asset.enabled))
62
- .toBe(false)
50
+ expect(bundle.skills.map(asset => asset.displayName)).toEqual(['demo/research'])
51
+ expect(bundle.rules.map(asset => asset.displayName)).toEqual(['demo/review'])
52
+ expect(Object.keys(bundle.mcpServers)).toEqual(['demo/browser'])
53
+ expect(bundle.hookPlugins).toEqual(expect.arrayContaining([
54
+ expect.objectContaining({
55
+ packageId: '@vibe-forge/plugin-logger'
56
+ })
57
+ ]))
58
+ expect(bundle.hookPlugins).toHaveLength(1)
59
+ expect(bundle.opencodeOverlayAssets).toEqual(expect.arrayContaining([
60
+ expect.objectContaining({
61
+ kind: 'command',
62
+ sourcePath: expect.stringContaining('/node_modules/@vibe-forge/plugin-demo/opencode/commands/review.md'),
63
+ payload: expect.objectContaining({
64
+ targetSubpath: 'commands/review.md'
65
+ })
66
+ })
67
+ ]))
63
68
  })
64
69
 
65
70
  it('adds the built-in Vibe Forge MCP server when enabled and omits it when disabled', async () => {
@@ -85,4 +90,151 @@ describe('resolveWorkspaceAssetBundle', () => {
85
90
 
86
91
  expect(disabledBundle.mcpServers).not.toHaveProperty('vibe-forge')
87
92
  })
93
+
94
+ it('skips disabled plugin instances and lets disabled child overrides suppress default child activation', async () => {
95
+ const workspace = await createWorkspace()
96
+
97
+ await installPluginPackage(workspace, '@vibe-forge/plugin-demo', {
98
+ 'package.json': JSON.stringify(
99
+ {
100
+ name: '@vibe-forge/plugin-demo',
101
+ version: '1.0.0'
102
+ },
103
+ null,
104
+ 2
105
+ ),
106
+ 'skills/research/SKILL.md': '---\ndescription: 检索资料\n---\n阅读 README.md'
107
+ })
108
+ await installPluginPackage(workspace, '@vibe-forge/plugin-bundle', {
109
+ 'package.json': JSON.stringify(
110
+ {
111
+ name: '@vibe-forge/plugin-bundle',
112
+ version: '1.0.0'
113
+ },
114
+ null,
115
+ 2
116
+ ),
117
+ 'index.js': [
118
+ 'module.exports = {',
119
+ ' __vibeForgePluginManifest: true,',
120
+ ' children: {',
121
+ ' review: {',
122
+ ' source: { type: "package", id: "@vibe-forge/plugin-review" },',
123
+ ' activation: "default"',
124
+ ' }',
125
+ ' }',
126
+ '}',
127
+ ''
128
+ ].join('\n')
129
+ })
130
+ await installPluginPackage(workspace, '@vibe-forge/plugin-review', {
131
+ 'package.json': JSON.stringify(
132
+ {
133
+ name: '@vibe-forge/plugin-review',
134
+ version: '1.0.0'
135
+ },
136
+ null,
137
+ 2
138
+ ),
139
+ 'skills/audit/SKILL.md': '---\ndescription: 代码审计\n---\n检查 child plugin 是否启用'
140
+ })
141
+
142
+ const bundle = await resolveWorkspaceAssetBundle({
143
+ cwd: workspace,
144
+ configs: [{
145
+ plugins: [
146
+ { id: 'demo', scope: 'demo', enabled: false },
147
+ {
148
+ id: 'bundle',
149
+ scope: 'bundle',
150
+ children: [
151
+ { id: 'review', enabled: false }
152
+ ]
153
+ }
154
+ ]
155
+ }, undefined],
156
+ useDefaultVibeForgeMcpServer: false
157
+ })
158
+
159
+ expect(bundle.skills).toEqual([])
160
+ expect(bundle.pluginInstances.map(instance => instance.packageId)).toEqual(['@vibe-forge/plugin-bundle'])
161
+ })
162
+
163
+ it('lets later config layers disable matching plugin instances by id and scope', async () => {
164
+ const workspace = await createWorkspace()
165
+
166
+ await installPluginPackage(workspace, '@vibe-forge/plugin-logger', {
167
+ 'package.json': JSON.stringify(
168
+ {
169
+ name: '@vibe-forge/plugin-logger',
170
+ version: '1.0.0'
171
+ },
172
+ null,
173
+ 2
174
+ ),
175
+ 'hooks.js': 'module.exports = {}\n'
176
+ })
177
+
178
+ const bundle = await resolveWorkspaceAssetBundle({
179
+ cwd: workspace,
180
+ configs: [
181
+ {
182
+ plugins: [
183
+ { id: 'logger' }
184
+ ]
185
+ },
186
+ {
187
+ plugins: [
188
+ { id: 'logger', enabled: false }
189
+ ]
190
+ }
191
+ ],
192
+ useDefaultVibeForgeMcpServer: false
193
+ })
194
+
195
+ expect(bundle.pluginConfigs).toEqual([
196
+ { id: 'logger', enabled: false }
197
+ ])
198
+ expect(bundle.pluginInstances).toEqual([])
199
+ expect(bundle.hookPlugins).toEqual([])
200
+ })
201
+
202
+ it('surfaces invalid plugin manifests instead of silently falling back to directory scanning', async () => {
203
+ const workspace = await createWorkspace()
204
+
205
+ await installPluginPackage(workspace, '@vibe-forge/plugin-bad-manifest', {
206
+ 'package.json': JSON.stringify(
207
+ {
208
+ name: '@vibe-forge/plugin-bad-manifest',
209
+ version: '1.0.0',
210
+ exports: {
211
+ '.': './index.js'
212
+ }
213
+ },
214
+ null,
215
+ 2
216
+ ),
217
+ 'index.js': [
218
+ 'module.exports = {',
219
+ ' __vibeForgePluginManifest: true,',
220
+ ' scope: "bad",',
221
+ ' assets: {',
222
+ ' skills: "./custom-skills"',
223
+ ' }',
224
+ '}',
225
+ ''
226
+ ].join('\n'),
227
+ 'custom-skills/research/SKILL.md': '---\ndescription: 检索资料\n---\n阅读 README.md'
228
+ })
229
+
230
+ await expect(resolveWorkspaceAssetBundle({
231
+ cwd: workspace,
232
+ configs: [{
233
+ plugins: [
234
+ { id: '@vibe-forge/plugin-bad-manifest' }
235
+ ]
236
+ }, undefined],
237
+ useDefaultVibeForgeMcpServer: false
238
+ })).rejects.toThrow('Failed to load plugin manifest for @vibe-forge/plugin-bad-manifest')
239
+ })
88
240
  })
@@ -0,0 +1,206 @@
1
+ import { join } from 'node:path'
2
+
3
+ import { describe, expect, it } from 'vitest'
4
+
5
+ import {
6
+ generateEntitiesRoutePrompt,
7
+ generateRulesPrompt,
8
+ generateSkillsPrompt,
9
+ generateSkillsRoutePrompt,
10
+ generateSpecRoutePrompt
11
+ } from '#~/prompt-builders.js'
12
+
13
+ describe('workspace asset prompt builders', () => {
14
+ it('builds skill prompts with stable names, descriptions, and relative paths', () => {
15
+ const cwd = '/tmp/project'
16
+
17
+ const prompt = generateSkillsPrompt(cwd, [
18
+ {
19
+ path: join(cwd, '.ai/skills/research/SKILL.md'),
20
+ body: '阅读 README.md\n',
21
+ attributes: {
22
+ description: '检索项目信息'
23
+ }
24
+ }
25
+ ])
26
+
27
+ expect(prompt).toContain('The following skill modules are loaded for the project')
28
+ expect(prompt).toContain('# research')
29
+ expect(prompt).toContain('> Skill description: 检索项目信息')
30
+ expect(prompt).toContain('> Skill file path: .ai/skills/research/SKILL.md')
31
+ expect(prompt).toContain('<skill-content>')
32
+ expect(prompt).not.toContain('/tmp/project/.ai/skills/research/SKILL.md')
33
+ })
34
+
35
+ it('builds rules prompts with embedded always rules and summary-only optional rules', () => {
36
+ const cwd = '/tmp/project'
37
+
38
+ const prompt = generateRulesPrompt(cwd, [
39
+ {
40
+ path: join(cwd, '.ai/rules/base.md'),
41
+ body: '始终检查公共边界。',
42
+ attributes: {
43
+ alwaysApply: true
44
+ }
45
+ },
46
+ {
47
+ path: join(cwd, '.ai/rules/optional.md'),
48
+ body: '仅在需要时展开。',
49
+ attributes: {
50
+ description: '按需规则',
51
+ alwaysApply: false
52
+ }
53
+ }
54
+ ])
55
+
56
+ expect(prompt).toContain('# base')
57
+ expect(prompt).toContain('> 始终检查公共边界。')
58
+ expect(prompt).toContain('> Use when: 按需规则')
59
+ expect(prompt).toContain('> Rule file path: .ai/rules/optional.md')
60
+ expect(prompt).not.toContain('> 仅在需要时展开。')
61
+ })
62
+
63
+ it('builds rule prompts with markdown headings and blockquotes', () => {
64
+ const cwd = '/tmp/project'
65
+
66
+ const prompt = generateRulesPrompt(cwd, [
67
+ {
68
+ path: join(cwd, '.ai/rules/required.md'),
69
+ body: '# 标题\n\n正文',
70
+ attributes: {
71
+ alwaysApply: true
72
+ }
73
+ },
74
+ {
75
+ path: join(cwd, '.ai/rules/summary-only.md'),
76
+ body: '不应该内联',
77
+ attributes: {
78
+ description: '只展示摘要',
79
+ alwaysApply: false
80
+ }
81
+ }
82
+ ])
83
+
84
+ expect(prompt).toContain('# required')
85
+ expect(prompt).toContain('> # 标题')
86
+ expect(prompt).toContain('> 正文')
87
+ expect(prompt).toContain('# summary-only')
88
+ expect(prompt).toContain('> Use when: 只展示摘要')
89
+ expect(prompt).toContain('> Rule file path: .ai/rules/summary-only.md')
90
+ expect(prompt).not.toContain('> 不应该内联')
91
+ expect(prompt).not.toContain('--------------------')
92
+ })
93
+
94
+ it('builds spec route prompts with logical identifiers and active identity guidance', () => {
95
+ const cwd = '/tmp/project'
96
+
97
+ const prompt = generateSpecRoutePrompt([
98
+ {
99
+ path: join(cwd, '.ai/specs/release/index.md'),
100
+ body: '发布流程',
101
+ attributes: {
102
+ params: [
103
+ {
104
+ name: 'version',
105
+ description: '版本号'
106
+ }
107
+ ]
108
+ }
109
+ }
110
+ ], { active: true })
111
+
112
+ expect(prompt).toContain('professional project execution manager')
113
+ expect(prompt).toContain('Workflow name: release')
114
+ expect(prompt).toContain('Identifier: release')
115
+ expect(prompt).toContain(' - version: 版本号')
116
+ expect(prompt).toContain('use the workflow identifier to locate and load the corresponding definition')
117
+ expect(prompt).not.toContain('load-spec')
118
+ })
119
+
120
+ it('builds spec route prompts without exposing file paths', () => {
121
+ const cwd = '/tmp/project'
122
+
123
+ const prompt = generateSpecRoutePrompt([
124
+ {
125
+ path: join(cwd, '.ai/specs/release/index.md'),
126
+ body: '发布流程\n执行发布任务',
127
+ attributes: {
128
+ params: [
129
+ {
130
+ name: 'version',
131
+ description: '版本号'
132
+ }
133
+ ]
134
+ }
135
+ },
136
+ {
137
+ path: join(cwd, '.ai/specs/internal.md'),
138
+ body: '内部流程',
139
+ attributes: {
140
+ always: false
141
+ }
142
+ }
143
+ ])
144
+
145
+ expect(prompt).toContain('Workflow name: release')
146
+ expect(prompt).toContain('Description: 发布流程')
147
+ expect(prompt).toContain('Identifier: release')
148
+ expect(prompt).toContain(' - version: 版本号')
149
+ expect(prompt).toContain('use the workflow identifier to locate and load the corresponding definition')
150
+ expect(prompt).not.toContain('load-spec')
151
+ expect(prompt).not.toContain('professional project execution manager')
152
+ expect(prompt).not.toContain('.ai/specs/release/index.md')
153
+ expect(prompt).not.toContain('internal')
154
+ })
155
+
156
+ it('builds entity routes from summaries instead of full bodies', () => {
157
+ const cwd = '/tmp/project'
158
+
159
+ const prompt = generateEntitiesRoutePrompt([
160
+ {
161
+ path: join(cwd, '.ai/entities/reviewer/README.md'),
162
+ body: '负责代码审查\n需要关注变更风险',
163
+ attributes: {}
164
+ },
165
+ {
166
+ path: join(cwd, '.ai/entities/hidden.md'),
167
+ body: '不应暴露',
168
+ attributes: {
169
+ name: 'hidden',
170
+ always: false
171
+ }
172
+ }
173
+ ])
174
+
175
+ expect(prompt).toContain('reviewer: 负责代码审查')
176
+ expect(prompt).toContain('`vibe-forge.StartTasks`')
177
+ expect(prompt).toContain('`GetTaskInfo`')
178
+ expect(prompt).toContain('`wait`')
179
+ expect(prompt).not.toContain('run-tasks')
180
+ expect(prompt).not.toContain('需要关注变更风险')
181
+ expect(prompt).not.toContain('hidden')
182
+ })
183
+
184
+ it('builds skill route prompts without preloading content', () => {
185
+ const cwd = '/tmp/project'
186
+
187
+ const prompt = generateSkillsRoutePrompt(cwd, [
188
+ {
189
+ path: join(cwd, '.ai/skills/research/SKILL.md'),
190
+ body: '阅读 README.md\n',
191
+ attributes: {
192
+ description: '检索项目信息'
193
+ }
194
+ }
195
+ ])
196
+
197
+ expect(prompt).toContain('# research')
198
+ expect(prompt).toContain('> Skill description: 检索项目信息')
199
+ expect(prompt).toContain('> Skill file path: .ai/skills/research/SKILL.md')
200
+ expect(prompt).toContain(
201
+ '> Do not preload the body by default; read the corresponding skill file only when the task clearly requires it.'
202
+ )
203
+ expect(prompt).not.toContain('<skill-content>')
204
+ expect(prompt).not.toContain('阅读 README.md')
205
+ })
206
+ })