@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.
- package/AGENTS.md +4 -1
- package/LICENSE +21 -0
- package/__tests__/__snapshots__/workspace-assets-rich.snapshot.json +360 -355
- package/__tests__/adapter-asset-plan.spec.ts +76 -38
- package/__tests__/bundle.spec.ts +195 -43
- package/__tests__/prompt-builders.spec.ts +206 -0
- package/__tests__/prompt-selection.spec.ts +362 -14
- package/__tests__/selection-internal.spec.ts +42 -0
- package/__tests__/snapshot.ts +78 -128
- package/__tests__/test-helpers.ts +13 -0
- package/__tests__/workspace-assets.snapshot.spec.ts +84 -103
- package/package.json +11 -10
- package/src/adapter-asset-plan.ts +92 -111
- package/src/bundle-internal.ts +548 -0
- package/src/bundle.ts +17 -166
- package/src/internal-types.ts +1 -39
- package/src/prompt-builders.ts +184 -0
- package/src/prompt-selection.ts +144 -104
- package/src/selection-internal.ts +275 -0
- package/src/document-assets.ts +0 -191
- package/src/helpers.ts +0 -35
- package/src/plugin-assets.ts +0 -175
|
@@ -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,
|
|
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
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
20
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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:
|
|
103
|
+
assetId: loggerHookPluginId
|
|
92
104
|
}),
|
|
93
105
|
expect.objectContaining({
|
|
94
106
|
adapter: 'codex',
|
|
95
107
|
status: 'translated',
|
|
96
|
-
assetId:
|
|
108
|
+
assetId: docsMcpId
|
|
97
109
|
}),
|
|
98
110
|
expect.objectContaining({
|
|
99
111
|
adapter: 'codex',
|
|
100
112
|
status: 'skipped',
|
|
101
|
-
assetId:
|
|
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: [
|
|
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
|
})
|
package/__tests__/bundle.spec.ts
CHANGED
|
@@ -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,
|
|
7
|
+
import { createWorkspace, installPluginPackage } from './test-helpers'
|
|
9
8
|
|
|
10
9
|
describe('resolveWorkspaceAssetBundle', () => {
|
|
11
|
-
it('
|
|
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
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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).
|
|
58
|
-
expect(bundle.rules).
|
|
59
|
-
expect(Object.keys(bundle.mcpServers)).
|
|
60
|
-
expect(bundle.hookPlugins).
|
|
61
|
-
|
|
62
|
-
|
|
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
|
+
})
|