@vibe-forge/workspace-assets 1.0.0 → 2.0.1

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.
@@ -1,12 +1,35 @@
1
+ /* eslint-disable import/first, max-lines -- bundle coverage keeps related fixture scenarios in one file */
1
2
  import { join } from 'node:path'
2
3
  import process from 'node:process'
3
4
 
4
- import { describe, expect, it } from 'vitest'
5
+ import { readFile } from 'node:fs/promises'
6
+ import { afterEach, describe, expect, it, vi } from 'vitest'
5
7
 
6
- import { resolveWorkspaceAssetBundle } from '#~/index.js'
8
+ const skillsCliMocks = vi.hoisted(() => ({
9
+ findSkillsCli: vi.fn(),
10
+ installSkillsCliRefToTemp: vi.fn(),
11
+ installSkillsCliSkillToTemp: vi.fn()
12
+ }))
13
+
14
+ vi.mock('@vibe-forge/utils/skills-cli', async () => {
15
+ const actual = await vi.importActual<typeof import('@vibe-forge/utils/skills-cli')>('@vibe-forge/utils/skills-cli')
16
+ return {
17
+ ...actual,
18
+ findSkillsCli: skillsCliMocks.findSkillsCli,
19
+ installSkillsCliRefToTemp: skillsCliMocks.installSkillsCliRefToTemp,
20
+ installSkillsCliSkillToTemp: skillsCliMocks.installSkillsCliSkillToTemp
21
+ }
22
+ })
23
+
24
+ import { buildAdapterAssetPlan, resolveWorkspaceAssetBundle } from '#~/index.js'
7
25
 
8
26
  import { createWorkspace, installPluginPackage, writeDocument } from './test-helpers'
9
27
 
28
+ afterEach(() => {
29
+ vi.clearAllMocks()
30
+ vi.unstubAllGlobals()
31
+ })
32
+
10
33
  describe('resolveWorkspaceAssetBundle', () => {
11
34
  it('loads npm plugin assets via the package-id fallback and exposes OpenCode overlays', async () => {
12
35
  const workspace = await createWorkspace()
@@ -97,6 +120,620 @@ describe('resolveWorkspaceAssetBundle', () => {
97
120
  }
98
121
  })
99
122
 
123
+ it('loads local and dev rule files as workspace rules', async () => {
124
+ const workspace = await createWorkspace()
125
+
126
+ await writeDocument(
127
+ join(workspace, '.ai/rules/team.md'),
128
+ '---\ndescription: 团队规则\n---\n团队共享约束'
129
+ )
130
+ await writeDocument(
131
+ join(workspace, '.ai/rules/preference.local.md'),
132
+ '---\ndescription: 本地偏好\nalwaysApply: true\n---\n使用当前用户偏好的输出风格'
133
+ )
134
+ await writeDocument(
135
+ join(workspace, '.ai/rules/debug.dev.md'),
136
+ '---\ndescription: 本地调试\nalwaysApply: true\n---\n优先保留调试证据'
137
+ )
138
+
139
+ const bundle = await resolveWorkspaceAssetBundle({
140
+ cwd: workspace,
141
+ configs: [undefined, undefined],
142
+ useDefaultVibeForgeMcpServer: false
143
+ })
144
+
145
+ expect(bundle.rules.map(asset => asset.displayName).sort()).toEqual(['debug.dev', 'preference.local', 'team'])
146
+ expect(bundle.rules.find(asset => asset.displayName === 'preference.local')?.payload.definition.body)
147
+ .toContain('当前用户偏好')
148
+ expect(bundle.rules.find(asset => asset.displayName === 'debug.dev')?.payload.definition.attributes.alwaysApply)
149
+ .toBe(true)
150
+ })
151
+
152
+ it('bridges supported home skill roots by default and keeps the first duplicate root', async () => {
153
+ const workspace = await createWorkspace()
154
+ const realHome = process.env.__VF_PROJECT_REAL_HOME__
155
+
156
+ await writeDocument(
157
+ join(realHome!, '.agents/skills/research/SKILL.md'),
158
+ '---\ndescription: 来自 agents root\n---\n阅读 README.md'
159
+ )
160
+ await writeDocument(
161
+ join(realHome!, '.claude/skills/research/SKILL.md'),
162
+ '---\ndescription: 来自 claude root\n---\n这份定义应被后面的 root 覆盖掉'
163
+ )
164
+ await writeDocument(
165
+ join(realHome!, '.config/opencode/skills/release/SKILL.md'),
166
+ '---\ndescription: 来自 opencode root\n---\n整理发布材料'
167
+ )
168
+
169
+ const bundle = await resolveWorkspaceAssetBundle({
170
+ cwd: workspace,
171
+ configs: [undefined, undefined],
172
+ useDefaultVibeForgeMcpServer: false
173
+ })
174
+
175
+ expect(bundle.skills.map(asset => asset.displayName)).toEqual(['research', 'release'])
176
+ expect(bundle.skills.find(asset => asset.name === 'research')).toEqual(expect.objectContaining({
177
+ origin: 'workspace',
178
+ resolvedBy: 'home-bridge',
179
+ sourcePath: join(realHome!, '.agents/skills/research/SKILL.md')
180
+ }))
181
+ expect(bundle.skills.find(asset => asset.name === 'release')).toEqual(expect.objectContaining({
182
+ origin: 'workspace',
183
+ resolvedBy: 'home-bridge',
184
+ sourcePath: join(realHome!, '.config/opencode/skills/release/SKILL.md')
185
+ }))
186
+ })
187
+
188
+ it('can disable the home skill bridge entirely', async () => {
189
+ const workspace = await createWorkspace()
190
+ const realHome = process.env.__VF_PROJECT_REAL_HOME__
191
+
192
+ await writeDocument(
193
+ join(realHome!, '.agents/skills/research/SKILL.md'),
194
+ '---\ndescription: 检索资料\n---\n阅读 README.md'
195
+ )
196
+
197
+ const bundle = await resolveWorkspaceAssetBundle({
198
+ cwd: workspace,
199
+ configs: [{
200
+ skills: {
201
+ homeBridge: {
202
+ enabled: false
203
+ }
204
+ }
205
+ }, undefined],
206
+ useDefaultVibeForgeMcpServer: false
207
+ })
208
+
209
+ expect(bundle.skills).toEqual([])
210
+ })
211
+
212
+ it('supports custom home skill roots with tilde expansion', async () => {
213
+ const workspace = await createWorkspace()
214
+ const realHome = process.env.__VF_PROJECT_REAL_HOME__
215
+
216
+ await writeDocument(
217
+ join(realHome!, '.agents/skills/ignored/SKILL.md'),
218
+ '---\ndescription: 默认目录\n---\n这份定义不应被加载'
219
+ )
220
+ await writeDocument(
221
+ join(realHome!, 'custom-skills/writer/SKILL.md'),
222
+ '---\ndescription: 自定义目录\n---\n产出说明文档'
223
+ )
224
+
225
+ const bundle = await resolveWorkspaceAssetBundle({
226
+ cwd: workspace,
227
+ configs: [{
228
+ skills: {
229
+ homeBridge: {
230
+ roots: '~/custom-skills'
231
+ }
232
+ }
233
+ }, undefined],
234
+ useDefaultVibeForgeMcpServer: false
235
+ })
236
+
237
+ expect(bundle.skills.map(asset => asset.displayName)).toEqual(['writer'])
238
+ expect(bundle.skills[0]?.sourcePath).toBe(join(realHome!, 'custom-skills/writer/SKILL.md'))
239
+ expect(bundle.skills[0]?.resolvedBy).toBe('home-bridge')
240
+ })
241
+
242
+ it('keeps the first matching skill when multiple homeBridge roots contain the same name', async () => {
243
+ const workspace = await createWorkspace()
244
+ const realHome = process.env.__VF_PROJECT_REAL_HOME__
245
+
246
+ await writeDocument(
247
+ join(realHome!, '.claude/skills/research/SKILL.md'),
248
+ '---\ndescription: 来自 claude root\n---\n优先保留这份定义'
249
+ )
250
+ await writeDocument(
251
+ join(realHome!, '.agents/skills/research/SKILL.md'),
252
+ '---\ndescription: 来自 agents root\n---\n这份定义应被后面的 root 跳过'
253
+ )
254
+
255
+ const bundle = await resolveWorkspaceAssetBundle({
256
+ cwd: workspace,
257
+ configs: [{
258
+ skills: {
259
+ homeBridge: {
260
+ roots: ['~/.claude/skills', '~/.agents/skills']
261
+ }
262
+ }
263
+ }, undefined],
264
+ useDefaultVibeForgeMcpServer: false
265
+ })
266
+
267
+ expect(bundle.skills.map(asset => asset.displayName)).toEqual(['research'])
268
+ expect(bundle.skills[0]).toEqual(expect.objectContaining({
269
+ origin: 'workspace',
270
+ resolvedBy: 'home-bridge',
271
+ sourcePath: join(realHome!, '.claude/skills/research/SKILL.md')
272
+ }))
273
+ })
274
+
275
+ it('warns once when a custom homeBridge root uses an unsupported relative path', async () => {
276
+ const workspace = await createWorkspace()
277
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined)
278
+
279
+ try {
280
+ const bundle = await resolveWorkspaceAssetBundle({
281
+ cwd: workspace,
282
+ configs: [{
283
+ skills: {
284
+ homeBridge: {
285
+ roots: ['./team-skills']
286
+ }
287
+ }
288
+ }, undefined],
289
+ useDefaultVibeForgeMcpServer: false
290
+ })
291
+
292
+ expect(bundle.skills).toEqual([])
293
+ expect(warnSpy).toHaveBeenCalledWith(
294
+ expect.stringContaining('Ignoring invalid skills.homeBridge root "./team-skills"')
295
+ )
296
+ } finally {
297
+ warnSpy.mockRestore()
298
+ }
299
+ })
300
+
301
+ it('lets project and plugin skills override matching home-bridged skills', async () => {
302
+ const workspace = await createWorkspace()
303
+ const realHome = process.env.__VF_PROJECT_REAL_HOME__
304
+
305
+ await writeDocument(
306
+ join(realHome!, '.agents/skills/research/SKILL.md'),
307
+ '---\ndescription: home research\n---\nhome research body'
308
+ )
309
+ await writeDocument(
310
+ join(realHome!, '.agents/skills/review/SKILL.md'),
311
+ '---\ndescription: home review\n---\nhome review body'
312
+ )
313
+ await writeDocument(
314
+ join(workspace, '.ai/skills/research/SKILL.md'),
315
+ '---\ndescription: project research\n---\nproject research body'
316
+ )
317
+ await installPluginPackage(workspace, '@vibe-forge/plugin-review', {
318
+ 'package.json': JSON.stringify(
319
+ {
320
+ name: '@vibe-forge/plugin-review',
321
+ version: '1.0.0'
322
+ },
323
+ null,
324
+ 2
325
+ ),
326
+ 'skills/review/SKILL.md': '---\ndescription: plugin review\n---\nplugin review body'
327
+ })
328
+
329
+ const bundle = await resolveWorkspaceAssetBundle({
330
+ cwd: workspace,
331
+ configs: [{
332
+ plugins: [
333
+ { id: 'review' }
334
+ ]
335
+ }, undefined],
336
+ useDefaultVibeForgeMcpServer: false
337
+ })
338
+
339
+ expect(bundle.skills.map(asset => asset.displayName).sort()).toEqual(['research', 'review'])
340
+ expect(bundle.skills.find(asset => asset.name === 'research')).toEqual(expect.objectContaining({
341
+ sourcePath: join(workspace, '.ai/skills/research/SKILL.md'),
342
+ resolvedBy: undefined
343
+ }))
344
+ expect(bundle.skills.find(asset => asset.name === 'review')).toEqual(expect.objectContaining({
345
+ origin: 'plugin',
346
+ sourcePath: expect.stringContaining('/node_modules/@vibe-forge/plugin-review/skills/review/SKILL.md')
347
+ }))
348
+ })
349
+
350
+ it('keeps scoped project skills alongside unscoped home skills with the same base name', async () => {
351
+ const workspace = await createWorkspace()
352
+ const realHome = process.env.__VF_PROJECT_REAL_HOME__
353
+
354
+ await writeDocument(
355
+ join(realHome!, '.agents/skills/research/SKILL.md'),
356
+ '---\ndescription: home research\n---\nhome research body'
357
+ )
358
+ await installPluginPackage(workspace, '@vibe-forge/plugin-team', {
359
+ 'package.json': JSON.stringify(
360
+ {
361
+ name: '@vibe-forge/plugin-team',
362
+ version: '1.0.0'
363
+ },
364
+ null,
365
+ 2
366
+ ),
367
+ 'skills/research/SKILL.md': '---\ndescription: scoped research\n---\nscoped research body'
368
+ })
369
+
370
+ const bundle = await resolveWorkspaceAssetBundle({
371
+ cwd: workspace,
372
+ configs: [{
373
+ plugins: [
374
+ { id: 'team', scope: 'team' }
375
+ ]
376
+ }, undefined],
377
+ useDefaultVibeForgeMcpServer: false
378
+ })
379
+
380
+ expect(bundle.skills.map(asset => asset.displayName).sort()).toEqual(['research', 'team/research'])
381
+ expect(bundle.skills.find(asset => asset.displayName === 'research')).toEqual(expect.objectContaining({
382
+ resolvedBy: 'home-bridge'
383
+ }))
384
+ expect(bundle.skills.find(asset => asset.displayName === 'team/research')).toEqual(expect.objectContaining({
385
+ origin: 'plugin'
386
+ }))
387
+ })
388
+
389
+ it('installs selected missing skill dependencies from the skills CLI cache', async () => {
390
+ const workspace = await createWorkspace()
391
+ const realHome = process.env.__VF_PROJECT_REAL_HOME__
392
+ const tempInstallDir = join(workspace, '.tmp-install-skills-cli')
393
+ const installedSkillDir = join(tempInstallDir, '.agents', 'skills', 'frontend-design')
394
+ await writeDocument(
395
+ join(installedSkillDir, 'SKILL.md'),
396
+ '---\nname: frontend-design\ndescription: UI design guidance\n---\nUse strong visual hierarchy.\n'
397
+ )
398
+ skillsCliMocks.installSkillsCliSkillToTemp.mockResolvedValue({
399
+ tempDir: tempInstallDir,
400
+ installedSkill: {
401
+ dirName: 'frontend-design',
402
+ name: 'frontend-design',
403
+ sourcePath: installedSkillDir
404
+ }
405
+ })
406
+
407
+ await writeDocument(
408
+ join(realHome!, '.agents/skills/frontend-design/SKILL.md'),
409
+ '---\ndescription: home frontend design\n---\nUse the home definition.'
410
+ )
411
+ await writeDocument(
412
+ join(workspace, '.ai/skills/app-builder/SKILL.md'),
413
+ [
414
+ '---',
415
+ 'name: app-builder',
416
+ 'description: Build apps',
417
+ 'dependencies:',
418
+ ' - anthropics/skills@frontend-design',
419
+ '---',
420
+ 'Build the app.'
421
+ ].join('\n')
422
+ )
423
+
424
+ const bundle = await resolveWorkspaceAssetBundle({
425
+ cwd: workspace,
426
+ configs: [undefined, undefined],
427
+ useDefaultVibeForgeMcpServer: false
428
+ })
429
+
430
+ expect(bundle.skills.map(asset => asset.name).sort()).toEqual(['app-builder', 'frontend-design'])
431
+ expect(bundle.skills.find(asset => asset.name === 'frontend-design')).toEqual(expect.objectContaining({
432
+ resolvedBy: 'home-bridge',
433
+ sourcePath: join(realHome!, '.agents/skills/frontend-design/SKILL.md')
434
+ }))
435
+
436
+ await buildAdapterAssetPlan({
437
+ adapter: 'opencode',
438
+ bundle,
439
+ options: {
440
+ skills: {
441
+ include: ['app-builder']
442
+ }
443
+ }
444
+ })
445
+
446
+ const dependency = bundle.skills.find(asset => asset.name === 'frontend-design')
447
+ expect(bundle.skills.map(asset => asset.name).sort()).toEqual(['app-builder', 'frontend-design'])
448
+ expect(dependency?.sourcePath).toContain(
449
+ '/.ai/caches/skill-dependencies/skills-cli/skills/latest/default/anthropics/skills/frontend-design/'
450
+ )
451
+ expect(bundle.skills.find(asset => (
452
+ asset.name === 'frontend-design' && asset.resolvedBy === 'home-bridge'
453
+ ))).toBeUndefined()
454
+ expect(skillsCliMocks.findSkillsCli).not.toHaveBeenCalled()
455
+ expect(skillsCliMocks.installSkillsCliSkillToTemp).toHaveBeenCalledWith({
456
+ config: undefined,
457
+ skill: 'frontend-design',
458
+ source: 'anthropics/skills'
459
+ })
460
+ })
461
+
462
+ it('installs configured project skills before bundle resolution and rewrites renamed skill names', async () => {
463
+ const workspace = await createWorkspace()
464
+ const tempInstallDir = join(workspace, '.tmp-configured-install')
465
+ const installedSkillDir = join(tempInstallDir, '.agents', 'skills', 'design-review')
466
+ await writeDocument(
467
+ join(installedSkillDir, 'SKILL.md'),
468
+ '---\nname: design-review\ndescription: Review design work\n---\nReview the UI implementation.\n'
469
+ )
470
+ skillsCliMocks.installSkillsCliSkillToTemp.mockResolvedValue({
471
+ tempDir: tempInstallDir,
472
+ installedSkill: {
473
+ dirName: 'design-review',
474
+ name: 'design-review',
475
+ sourcePath: installedSkillDir
476
+ }
477
+ })
478
+
479
+ const bundle = await resolveWorkspaceAssetBundle({
480
+ cwd: workspace,
481
+ configs: [{
482
+ skills: [
483
+ {
484
+ name: 'design-review',
485
+ source: 'example-source/default/public',
486
+ rename: 'internal-review'
487
+ }
488
+ ]
489
+ }, undefined],
490
+ syncConfiguredSkills: true,
491
+ useDefaultVibeForgeMcpServer: false
492
+ })
493
+
494
+ expect(bundle.skills.map(asset => asset.name)).toContain('internal-review')
495
+ expect(skillsCliMocks.installSkillsCliSkillToTemp).toHaveBeenCalledWith({
496
+ config: undefined,
497
+ skill: 'design-review',
498
+ source: 'example-source/default/public'
499
+ })
500
+ await expect(readFile(join(workspace, '.ai/skills/internal-review/SKILL.md'), 'utf8')).resolves.toContain(
501
+ 'name: internal-review'
502
+ )
503
+ })
504
+
505
+ it('skips configured skill reinstalls unless updateConfiguredSkills is enabled', async () => {
506
+ const workspace = await createWorkspace()
507
+ await writeDocument(
508
+ join(workspace, '.ai/skills/internal-review/SKILL.md'),
509
+ '---\nname: internal-review\ndescription: Existing skill\n---\nExisting content.\n'
510
+ )
511
+
512
+ const skippedBundle = await resolveWorkspaceAssetBundle({
513
+ cwd: workspace,
514
+ configs: [{
515
+ skills: [
516
+ {
517
+ name: 'design-review',
518
+ source: 'example-source/default/public',
519
+ rename: 'internal-review'
520
+ }
521
+ ]
522
+ }, undefined],
523
+ syncConfiguredSkills: true,
524
+ useDefaultVibeForgeMcpServer: false
525
+ })
526
+
527
+ expect(skippedBundle.skills.map(asset => asset.name)).toContain('internal-review')
528
+ expect(skillsCliMocks.installSkillsCliSkillToTemp).not.toHaveBeenCalled()
529
+
530
+ const tempInstallDir = join(workspace, '.tmp-configured-update')
531
+ const installedSkillDir = join(tempInstallDir, '.agents', 'skills', 'design-review')
532
+ await writeDocument(
533
+ join(installedSkillDir, 'SKILL.md'),
534
+ '---\nname: design-review\ndescription: Updated skill\n---\nUpdated content.\n'
535
+ )
536
+ skillsCliMocks.installSkillsCliSkillToTemp.mockResolvedValueOnce({
537
+ tempDir: tempInstallDir,
538
+ installedSkill: {
539
+ dirName: 'design-review',
540
+ name: 'design-review',
541
+ sourcePath: installedSkillDir
542
+ }
543
+ })
544
+
545
+ await resolveWorkspaceAssetBundle({
546
+ cwd: workspace,
547
+ configs: [{
548
+ skills: [
549
+ {
550
+ name: 'design-review',
551
+ source: 'example-source/default/public',
552
+ rename: 'internal-review'
553
+ }
554
+ ]
555
+ }, undefined],
556
+ syncConfiguredSkills: true,
557
+ updateConfiguredSkills: true,
558
+ useDefaultVibeForgeMcpServer: false
559
+ })
560
+
561
+ expect(skillsCliMocks.installSkillsCliSkillToTemp).toHaveBeenCalledTimes(1)
562
+ await expect(readFile(join(workspace, '.ai/skills/internal-review/SKILL.md'), 'utf8')).resolves.toContain(
563
+ 'Updated content.'
564
+ )
565
+ })
566
+
567
+ it('installs skill dependencies into the primary workspace shared cache', async () => {
568
+ const primary = await createWorkspace()
569
+ const worktree = await createWorkspace()
570
+ const previousPrimaryWorkspace = process.env.__VF_PROJECT_PRIMARY_WORKSPACE_FOLDER__
571
+ const tempInstallDir = join(worktree, '.tmp-install-skills-cli')
572
+ const installedSkillDir = join(tempInstallDir, '.agents', 'skills', 'frontend-design')
573
+ await writeDocument(
574
+ join(installedSkillDir, 'SKILL.md'),
575
+ '---\nname: frontend-design\ndescription: UI design guidance\n---\nUse primary cache.\n'
576
+ )
577
+ skillsCliMocks.installSkillsCliSkillToTemp.mockResolvedValue({
578
+ tempDir: tempInstallDir,
579
+ installedSkill: {
580
+ dirName: 'frontend-design',
581
+ name: 'frontend-design',
582
+ sourcePath: installedSkillDir
583
+ }
584
+ })
585
+
586
+ try {
587
+ process.env.__VF_PROJECT_PRIMARY_WORKSPACE_FOLDER__ = primary
588
+ await writeDocument(
589
+ join(worktree, '.ai/skills/app-builder/SKILL.md'),
590
+ [
591
+ '---',
592
+ 'name: app-builder',
593
+ 'description: Build apps',
594
+ 'dependencies:',
595
+ ' - anthropics/skills@frontend-design',
596
+ '---',
597
+ 'Build the app.'
598
+ ].join('\n')
599
+ )
600
+
601
+ const bundle = await resolveWorkspaceAssetBundle({
602
+ cwd: worktree,
603
+ configs: [undefined, undefined],
604
+ useDefaultVibeForgeMcpServer: false
605
+ })
606
+
607
+ await buildAdapterAssetPlan({
608
+ adapter: 'opencode',
609
+ bundle,
610
+ options: {
611
+ skills: {
612
+ include: ['app-builder']
613
+ }
614
+ }
615
+ })
616
+
617
+ const dependency = bundle.skills.find(asset => asset.name === 'frontend-design')
618
+ expect(dependency?.sourcePath).toContain(join(
619
+ primary,
620
+ '.ai/caches/skill-dependencies/skills-cli/skills/latest/default/anthropics/skills/frontend-design/'
621
+ ))
622
+ expect(dependency?.sourcePath).not.toContain(join(worktree, '.ai/caches'))
623
+ } finally {
624
+ if (previousPrimaryWorkspace == null) {
625
+ delete process.env.__VF_PROJECT_PRIMARY_WORKSPACE_FOLDER__
626
+ } else {
627
+ process.env.__VF_PROJECT_PRIMARY_WORKSPACE_FOLDER__ = previousPrimaryWorkspace
628
+ }
629
+ }
630
+ })
631
+
632
+ it('reuses complete skill dependency caches without deleting or downloading them again', async () => {
633
+ const workspace = await createWorkspace()
634
+
635
+ const cachedSkillPath = join(
636
+ workspace,
637
+ '.ai/caches/skill-dependencies/skills-cli/skills/latest/default/anthropics/skills/frontend-design/SKILL.md'
638
+ )
639
+ await writeDocument(
640
+ cachedSkillPath,
641
+ '---\nname: frontend-design\ndescription: Cached UI guidance\n---\nUse the cached copy.\n'
642
+ )
643
+ await writeDocument(
644
+ join(workspace, '.ai/skills/app-builder/SKILL.md'),
645
+ [
646
+ '---',
647
+ 'name: app-builder',
648
+ 'description: Build apps',
649
+ 'dependencies:',
650
+ ' - anthropics/skills@frontend-design',
651
+ '---',
652
+ 'Build the app.'
653
+ ].join('\n')
654
+ )
655
+
656
+ const bundle = await resolveWorkspaceAssetBundle({
657
+ cwd: workspace,
658
+ configs: [undefined, undefined],
659
+ useDefaultVibeForgeMcpServer: false
660
+ })
661
+
662
+ await buildAdapterAssetPlan({
663
+ adapter: 'opencode',
664
+ bundle,
665
+ options: {
666
+ skills: {
667
+ include: ['app-builder']
668
+ }
669
+ }
670
+ })
671
+
672
+ expect(skillsCliMocks.findSkillsCli).not.toHaveBeenCalled()
673
+ expect(skillsCliMocks.installSkillsCliSkillToTemp).not.toHaveBeenCalled()
674
+ expect(await readFile(cachedSkillPath, 'utf8')).toContain('Use the cached copy.')
675
+ expect(bundle.skills.map(asset => asset.name).sort()).toEqual(['app-builder', 'frontend-design'])
676
+ })
677
+
678
+ it('parses multi-segment source dependencies and forwards skillsCli runtime config', async () => {
679
+ const workspace = await createWorkspace()
680
+ const tempInstallDir = join(workspace, '.tmp-install-skills-cli')
681
+ const installedSkillDir = join(tempInstallDir, '.agents', 'skills', 'frontend-design')
682
+ await writeDocument(
683
+ join(installedSkillDir, 'SKILL.md'),
684
+ '---\nname: frontend-design\ndescription: UI design guidance\n---\nUse internal design tokens.\n'
685
+ )
686
+ skillsCliMocks.installSkillsCliSkillToTemp.mockResolvedValue({
687
+ tempDir: tempInstallDir,
688
+ installedSkill: {
689
+ dirName: 'frontend-design',
690
+ name: 'frontend-design',
691
+ sourcePath: installedSkillDir
692
+ }
693
+ })
694
+
695
+ await writeDocument(
696
+ join(workspace, '.ai/skills/app-builder/SKILL.md'),
697
+ [
698
+ '---',
699
+ 'name: app-builder',
700
+ 'description: Build apps',
701
+ 'dependencies:',
702
+ ' - example-source/default/public/frontend-design',
703
+ '---',
704
+ 'Build the app.'
705
+ ].join('\n')
706
+ )
707
+
708
+ const bundle = await resolveWorkspaceAssetBundle({
709
+ cwd: workspace,
710
+ configs: [{
711
+ skillsCli: {
712
+ registry: 'https://registry.example.com'
713
+ }
714
+ }, undefined],
715
+ useDefaultVibeForgeMcpServer: false
716
+ })
717
+
718
+ await buildAdapterAssetPlan({
719
+ adapter: 'opencode',
720
+ bundle,
721
+ options: {
722
+ skills: {
723
+ include: ['app-builder']
724
+ }
725
+ }
726
+ })
727
+
728
+ expect(skillsCliMocks.installSkillsCliSkillToTemp).toHaveBeenCalledWith({
729
+ config: {
730
+ registry: 'https://registry.example.com'
731
+ },
732
+ skill: 'frontend-design',
733
+ source: 'example-source/default/public'
734
+ })
735
+ })
736
+
100
737
  it('loads workspace entities from the env-configured entities dir', async () => {
101
738
  const workspace = await createWorkspace()
102
739
  const previousEntitiesDir = process.env.__VF_PROJECT_AI_ENTITIES_DIR__
@@ -201,6 +838,44 @@ describe('resolveWorkspaceAssetBundle', () => {
201
838
  expect(disabledBundle.mcpServers).not.toHaveProperty('VibeForge')
202
839
  })
203
840
 
841
+ it('discovers configured workspaces from glob patterns and entries', async () => {
842
+ const workspace = await createWorkspace()
843
+
844
+ await writeDocument(join(workspace, 'services/billing/README.md'), '# billing\n')
845
+ await writeDocument(join(workspace, 'services/legacy/README.md'), '# legacy\n')
846
+ await writeDocument(join(workspace, 'docs/README.md'), '# docs\n')
847
+
848
+ const bundle = await resolveWorkspaceAssetBundle({
849
+ cwd: workspace,
850
+ configs: [{
851
+ workspaces: {
852
+ include: ['services/*'],
853
+ exclude: ['services/legacy'],
854
+ entries: {
855
+ docs: {
856
+ path: 'docs',
857
+ description: 'Documentation workspace'
858
+ }
859
+ }
860
+ }
861
+ }, undefined],
862
+ useDefaultVibeForgeMcpServer: false
863
+ })
864
+
865
+ expect(bundle.workspaces.map(asset => asset.displayName)).toEqual(['billing', 'docs'])
866
+ expect(bundle.workspaces.map(asset => asset.payload)).toEqual([
867
+ expect.objectContaining({
868
+ id: 'billing',
869
+ path: 'services/billing'
870
+ }),
871
+ expect.objectContaining({
872
+ id: 'docs',
873
+ path: 'docs',
874
+ description: 'Documentation workspace'
875
+ })
876
+ ])
877
+ })
878
+
204
879
  it('skips disabled plugin instances and lets disabled child overrides suppress default child activation', async () => {
205
880
  const workspace = await createWorkspace()
206
881