@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,19 +4,129 @@ import { describe, expect, it } from 'vitest'
4
4
 
5
5
  import { 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('resolvePromptAssetSelection', () => {
10
- it('prefers project prompt assets over plugin assets with the same identifier', async () => {
10
+ it('embeds only alwaysApply rules and keeps optional rules as summaries', async () => {
11
11
  const workspace = await createWorkspace()
12
12
 
13
+ await writeDocument(
14
+ join(workspace, '.ai/rules/base.md'),
15
+ [
16
+ '---',
17
+ 'alwaysApply: true',
18
+ 'description: 基础约束',
19
+ '---',
20
+ '始终检查公共边界。'
21
+ ].join('\n')
22
+ )
23
+ await writeDocument(
24
+ join(workspace, '.ai/rules/optional.md'),
25
+ [
26
+ '---',
27
+ 'alwaysApply: false',
28
+ 'description: 按需参考规则',
29
+ '---',
30
+ '只有在特定场景才需要展开。'
31
+ ].join('\n')
32
+ )
33
+
34
+ const bundle = await resolveWorkspaceAssetBundle({
35
+ cwd: workspace,
36
+ useDefaultVibeForgeMcpServer: false
37
+ })
38
+ const [, options] = await resolvePromptAssetSelection({
39
+ bundle,
40
+ type: undefined
41
+ })
42
+
43
+ expect(options.systemPrompt).toContain('# base')
44
+ expect(options.systemPrompt).toContain('> 始终检查公共边界。')
45
+ expect(options.systemPrompt).toContain('# optional')
46
+ expect(options.systemPrompt).toContain('> Use when: 按需参考规则')
47
+ expect(options.systemPrompt).toContain('> Rule file path: .ai/rules/optional.md')
48
+ expect(options.systemPrompt).toContain('> Only read this rule file when the task matches the scenario above.')
49
+ expect(options.systemPrompt).not.toContain('> 只有在特定场景才需要展开。')
50
+ })
51
+
52
+ it('selects local assets by short name and scoped plugin assets explicitly', async () => {
53
+ const workspace = await createWorkspace()
54
+
55
+ await installPluginPackage(workspace, '@vibe-forge/plugin-demo', {
56
+ 'package.json': JSON.stringify(
57
+ {
58
+ name: '@vibe-forge/plugin-demo',
59
+ version: '1.0.0'
60
+ },
61
+ null,
62
+ 2
63
+ ),
64
+ 'specs/release/index.md': '---\ndescription: 插件发布流程\n---\n执行插件发布'
65
+ })
13
66
  await writeDocument(
14
67
  join(workspace, '.ai/specs/release.md'),
15
68
  '---\ndescription: 项目发布流程\n---\n执行项目发布'
16
69
  )
70
+
71
+ const bundle = await resolveWorkspaceAssetBundle({
72
+ cwd: workspace,
73
+ configs: [{
74
+ plugins: [
75
+ { id: 'demo', scope: 'demo' }
76
+ ]
77
+ }, undefined],
78
+ useDefaultVibeForgeMcpServer: false
79
+ })
80
+ const [localData, localOptions] = await resolvePromptAssetSelection({
81
+ bundle,
82
+ type: 'spec',
83
+ name: 'release'
84
+ })
85
+ const [pluginData, pluginOptions] = await resolvePromptAssetSelection({
86
+ bundle,
87
+ type: 'spec',
88
+ name: 'demo/release'
89
+ })
90
+ const localSpecId = bundle.specs.find(asset => asset.origin === 'workspace' && asset.name === 'release')?.id
91
+ const pluginSpecId = bundle.specs.find(asset => asset.origin === 'plugin' && asset.displayName === 'demo/release')
92
+ ?.id
93
+
94
+ expect(localData.targetBody).toContain('执行项目发布')
95
+ expect(localData.targetBody).not.toContain('执行插件发布')
96
+ expect(localOptions.systemPrompt).toContain('项目发布流程')
97
+ expect(localOptions.systemPrompt).toContain('demo/release')
98
+ expect(localOptions.promptAssetIds).toEqual(expect.arrayContaining([localSpecId]))
99
+
100
+ expect(pluginData.targetBody).toContain('执行插件发布')
101
+ expect(pluginOptions.systemPrompt).toContain('插件发布流程')
102
+ expect(pluginOptions.promptAssetIds).toEqual(expect.arrayContaining([pluginSpecId]))
103
+ })
104
+
105
+ it('formats rules as markdown headings and blockquotes', async () => {
106
+ const workspace = await createWorkspace()
107
+
108
+ await writeDocument(
109
+ join(workspace, '.ai/rules/required.md'),
110
+ [
111
+ '---',
112
+ 'description: 必须执行的规则',
113
+ 'alwaysApply: true',
114
+ '---',
115
+ '# 标题',
116
+ '',
117
+ '正文第一行',
118
+ '正文第二行'
119
+ ].join('\n')
120
+ )
17
121
  await writeDocument(
18
- join(workspace, '.ai/plugins/demo/specs/release/index.md'),
19
- '---\ndescription: 插件发布流程\n---\n执行插件发布'
122
+ join(workspace, '.ai/rules/summary-only.md'),
123
+ [
124
+ '---',
125
+ 'description: 只展示摘要',
126
+ 'alwaysApply: false',
127
+ '---',
128
+ '不应该出现在引用正文里'
129
+ ].join('\n')
20
130
  )
21
131
 
22
132
  const bundle = await resolveWorkspaceAssetBundle({
@@ -24,21 +134,259 @@ describe('resolvePromptAssetSelection', () => {
24
134
  configs: [undefined, undefined],
25
135
  useDefaultVibeForgeMcpServer: false
26
136
  })
27
- const [data, resolvedOptions] = await resolvePromptAssetSelection({
137
+ const [, resolvedOptions] = await resolvePromptAssetSelection({
138
+ bundle,
139
+ type: undefined
140
+ })
141
+
142
+ expect(resolvedOptions.systemPrompt).toContain('# required')
143
+ expect(resolvedOptions.systemPrompt).toContain('> # 标题')
144
+ expect(resolvedOptions.systemPrompt).toContain('> 正文第一行')
145
+ expect(resolvedOptions.systemPrompt).toContain('> 正文第二行')
146
+ expect(resolvedOptions.systemPrompt).toContain('# summary-only')
147
+ expect(resolvedOptions.systemPrompt).toContain('> Use when: 只展示摘要')
148
+ expect(resolvedOptions.systemPrompt).toContain('> Rule file path: .ai/rules/summary-only.md')
149
+ expect(resolvedOptions.systemPrompt).not.toContain('> 不应该出现在引用正文里')
150
+ expect(resolvedOptions.systemPrompt).not.toContain('--------------------')
151
+ })
152
+
153
+ it('keeps skills as route-only guidance unless the target spec references them', async () => {
154
+ const workspace = await createWorkspace()
155
+
156
+ await writeDocument(
157
+ join(workspace, '.ai/skills/research/SKILL.md'),
158
+ [
159
+ '---',
160
+ 'description: 检索项目信息',
161
+ '---',
162
+ '先读 README.md'
163
+ ].join('\n')
164
+ )
165
+ await writeDocument(
166
+ join(workspace, '.ai/skills/review/SKILL.md'),
167
+ [
168
+ '---',
169
+ 'description: 评审代码改动',
170
+ '---',
171
+ '检查回归风险'
172
+ ].join('\n')
173
+ )
174
+ await writeDocument(
175
+ join(workspace, '.ai/specs/release/index.md'),
176
+ [
177
+ '---',
178
+ 'description: 发布流程',
179
+ 'skills:',
180
+ ' - research',
181
+ '---',
182
+ '执行发布'
183
+ ].join('\n')
184
+ )
185
+
186
+ const bundle = await resolveWorkspaceAssetBundle({
187
+ cwd: workspace,
188
+ useDefaultVibeForgeMcpServer: false
189
+ })
190
+ const [data, options] = await resolvePromptAssetSelection({
191
+ bundle,
192
+ type: 'spec',
193
+ name: 'release'
194
+ })
195
+
196
+ expect(data.targetSkills.map(skill => skill.resolvedName ?? skill.attributes.name)).toEqual(['research'])
197
+ expect(options.systemPrompt).toContain('The following skill modules are loaded for the project')
198
+ expect(options.systemPrompt).toContain('# research')
199
+ expect(options.systemPrompt).toContain('> Skill file path: .ai/skills/research/SKILL.md')
200
+ expect(options.systemPrompt).toContain('<skill-content>')
201
+ expect(options.systemPrompt).toContain('先读 README.md')
202
+ expect(options.systemPrompt).toContain('# review')
203
+ expect(options.systemPrompt).toContain('> Skill file path: .ai/skills/review/SKILL.md')
204
+ expect(options.systemPrompt).toContain(
205
+ '> Do not preload the body by default; read the corresponding skill file only when the task clearly requires it.'
206
+ )
207
+ expect(options.systemPrompt).not.toContain('<skill-content>\n检查回归风险\n</skill-content>')
208
+ })
209
+
210
+ it('keeps skills as route-only guidance in normal mode', async () => {
211
+ const workspace = await createWorkspace()
212
+
213
+ await writeDocument(
214
+ join(workspace, '.ai/skills/research/SKILL.md'),
215
+ [
216
+ '---',
217
+ 'description: 检索项目信息',
218
+ '---',
219
+ '先读 README.md'
220
+ ].join('\n')
221
+ )
222
+
223
+ const bundle = await resolveWorkspaceAssetBundle({
224
+ cwd: workspace,
225
+ useDefaultVibeForgeMcpServer: false
226
+ })
227
+ const [data, options] = await resolvePromptAssetSelection({
228
+ bundle,
229
+ type: undefined
230
+ })
231
+
232
+ expect(data.targetSkills).toEqual([])
233
+ expect(options.systemPrompt).not.toContain('The following skill modules are loaded for the project')
234
+ expect(options.systemPrompt).toContain('<skills>')
235
+ expect(options.systemPrompt).toContain('# research')
236
+ expect(options.systemPrompt).toContain('> Skill file path: .ai/skills/research/SKILL.md')
237
+ expect(options.systemPrompt).toContain(
238
+ '> Do not preload the body by default; read the corresponding skill file only when the task clearly requires it.'
239
+ )
240
+ expect(options.systemPrompt).not.toContain('<skill-content>')
241
+ expect(options.systemPrompt).not.toContain('先读 README.md')
242
+ })
243
+
244
+ it('keeps spec route guidance without default identity in normal mode', async () => {
245
+ const workspace = await createWorkspace()
246
+
247
+ await writeDocument(
248
+ join(workspace, '.ai/specs/release/index.md'),
249
+ [
250
+ '---',
251
+ 'description: 发布流程',
252
+ '---',
253
+ '执行发布'
254
+ ].join('\n')
255
+ )
256
+
257
+ const bundle = await resolveWorkspaceAssetBundle({
258
+ cwd: workspace,
259
+ useDefaultVibeForgeMcpServer: false
260
+ })
261
+ const [, options] = await resolvePromptAssetSelection({
262
+ bundle,
263
+ type: undefined
264
+ })
265
+
266
+ expect(options.systemPrompt).toContain('The project includes the following workflows')
267
+ expect(options.systemPrompt).toContain('Workflow name: release')
268
+ expect(options.systemPrompt).not.toContain('professional project execution manager')
269
+ })
270
+
271
+ it('injects spec identity guidance when a spec is actively selected', async () => {
272
+ const workspace = await createWorkspace()
273
+
274
+ await writeDocument(
275
+ join(workspace, '.ai/specs/release/index.md'),
276
+ [
277
+ '---',
278
+ 'description: 发布流程',
279
+ '---',
280
+ '执行发布'
281
+ ].join('\n')
282
+ )
283
+
284
+ const bundle = await resolveWorkspaceAssetBundle({
285
+ cwd: workspace,
286
+ useDefaultVibeForgeMcpServer: false
287
+ })
288
+ const [, options] = await resolvePromptAssetSelection({
28
289
  bundle,
29
290
  type: 'spec',
30
291
  name: 'release'
31
292
  })
32
293
 
33
- expect(data.targetBody).toContain('执行项目发布')
34
- expect(data.targetBody).not.toContain('执行插件发布')
35
- expect(data.specs).toHaveLength(1)
36
- expect(resolvedOptions.systemPrompt).toContain('项目发布流程')
37
- expect(resolvedOptions.systemPrompt).not.toContain('插件发布流程')
38
- expect(resolvedOptions.promptAssetIds).toEqual(
39
- expect.arrayContaining([
40
- 'spec:.ai/specs/release.md'
41
- ])
294
+ expect(options.systemPrompt).toContain('professional project execution manager')
295
+ expect(options.systemPrompt).toContain('Never complete code development work alone')
296
+ expect(options.systemPrompt).toContain('Workflow name: release')
297
+ })
298
+
299
+ it('embeds referenced skills for entity mode and removes them from route guidance', async () => {
300
+ const workspace = await createWorkspace()
301
+
302
+ await writeDocument(
303
+ join(workspace, '.ai/skills/research/SKILL.md'),
304
+ [
305
+ '---',
306
+ 'description: 检索项目信息',
307
+ '---',
308
+ '先读 README.md'
309
+ ].join('\n')
310
+ )
311
+ await writeDocument(
312
+ join(workspace, '.ai/skills/review/SKILL.md'),
313
+ [
314
+ '---',
315
+ 'description: 评审代码改动',
316
+ '---',
317
+ '检查回归风险'
318
+ ].join('\n')
319
+ )
320
+ await writeDocument(
321
+ join(workspace, '.ai/entities/reviewer/README.md'),
322
+ [
323
+ '---',
324
+ 'description: 代码评审实体',
325
+ 'skills:',
326
+ ' - review',
327
+ '---',
328
+ '负责代码评审'
329
+ ].join('\n')
42
330
  )
331
+
332
+ const bundle = await resolveWorkspaceAssetBundle({
333
+ cwd: workspace,
334
+ useDefaultVibeForgeMcpServer: false
335
+ })
336
+ const [data, options] = await resolvePromptAssetSelection({
337
+ bundle,
338
+ type: 'entity',
339
+ name: 'reviewer'
340
+ })
341
+
342
+ expect(data.targetSkills.map(skill => skill.resolvedName ?? skill.attributes.name)).toEqual(['review'])
343
+ expect(options.systemPrompt).toContain('The following skill modules are loaded for the project')
344
+ expect(options.systemPrompt).toContain('# review')
345
+ expect(options.systemPrompt).toContain('<skill-content>')
346
+ expect(options.systemPrompt).toContain('检查回归风险')
347
+ expect(options.systemPrompt).not.toContain('<skills>\n# review')
348
+ expect(options.systemPrompt).toContain('<skills>')
349
+ expect(options.systemPrompt).toContain('# research')
350
+ expect(options.systemPrompt).toContain('> Skill file path: .ai/skills/research/SKILL.md')
351
+ expect(options.systemPrompt).not.toContain('<skill-content>\n先读 README.md\n</skill-content>')
352
+ })
353
+
354
+ it('does not preload all skills when the target entity omits skill references', async () => {
355
+ const workspace = await createWorkspace()
356
+
357
+ await writeDocument(
358
+ join(workspace, '.ai/skills/research/SKILL.md'),
359
+ [
360
+ '---',
361
+ 'description: 检索项目信息',
362
+ '---',
363
+ '先读 README.md'
364
+ ].join('\n')
365
+ )
366
+ await writeDocument(
367
+ join(workspace, '.ai/entities/reviewer/README.md'),
368
+ [
369
+ '---',
370
+ 'description: 代码评审实体',
371
+ '---',
372
+ '负责代码评审'
373
+ ].join('\n')
374
+ )
375
+
376
+ const bundle = await resolveWorkspaceAssetBundle({
377
+ cwd: workspace,
378
+ useDefaultVibeForgeMcpServer: false
379
+ })
380
+ const [data, options] = await resolvePromptAssetSelection({
381
+ bundle,
382
+ type: 'entity',
383
+ name: 'reviewer'
384
+ })
385
+
386
+ expect(data.targetSkills).toEqual([])
387
+ expect(options.systemPrompt).not.toContain('The following skill modules are loaded for the project')
388
+ expect(options.systemPrompt).toContain('# research')
389
+ expect(options.systemPrompt).toContain('> Skill file path: .ai/skills/research/SKILL.md')
390
+ expect(options.systemPrompt).not.toContain('先读 README.md')
43
391
  })
44
392
  })
@@ -0,0 +1,42 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import type { WorkspaceAssetBundle } from '@vibe-forge/types'
4
+
5
+ import { resolveSelectedMcpNames } from '#~/selection-internal.js'
6
+
7
+ const createBundle = (): WorkspaceAssetBundle => ({
8
+ cwd: '/tmp/workspace',
9
+ pluginConfigs: [],
10
+ pluginInstances: [],
11
+ assets: [],
12
+ rules: [],
13
+ specs: [],
14
+ entities: [],
15
+ skills: [],
16
+ hookPlugins: [],
17
+ opencodeOverlayAssets: [],
18
+ defaultIncludeMcpServers: [],
19
+ defaultExcludeMcpServers: ['TypeScriptLanguageService'],
20
+ mcpServers: {
21
+ TypeScriptLanguageService: {
22
+ kind: 'mcpServer',
23
+ id: 'mcp:TypeScriptLanguageService',
24
+ scope: 'workspace',
25
+ name: 'TypeScriptLanguageService',
26
+ displayName: 'TypeScriptLanguageService',
27
+ source: '/tmp/workspace/.ai.config.json',
28
+ command: 'node',
29
+ args: ['tslspmcp']
30
+ }
31
+ }
32
+ })
33
+
34
+ describe('resolveSelectedMcpNames', () => {
35
+ it('lets an explicit include override default excludes', () => {
36
+ const selected = resolveSelectedMcpNames(createBundle(), {
37
+ include: ['TypeScriptLanguageService']
38
+ })
39
+
40
+ expect(selected).toEqual(['TypeScriptLanguageService'])
41
+ })
42
+ })