ma-agents 3.5.6 → 3.6.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/.ma-agents.json +10 -0
- package/AGENTS.md +97 -0
- package/MANIFEST.yaml +3 -0
- package/README.md +17 -0
- package/_bmad-output/implementation-artifacts/21-10-profile-reconfigure.md +30 -6
- package/_bmad-output/implementation-artifacts/21-11-profile-uninstall.md +2 -1
- package/_bmad-output/implementation-artifacts/21-2-universal-instruction-block-expansion.md +217 -62
- package/_bmad-output/implementation-artifacts/21-3-roomodes-template-bmad-modes.md +196 -73
- package/_bmad-output/implementation-artifacts/21-4-agents-md-template-opencode.md +242 -53
- package/_bmad-output/implementation-artifacts/21-5-clinerules-template-extension.md +180 -41
- package/_bmad-output/implementation-artifacts/21-6-onprem-layered-guardrails.md +250 -75
- package/_bmad-output/implementation-artifacts/21-7-bmad-persona-phase-prefix.md +221 -89
- package/_bmad-output/implementation-artifacts/21-8-vllm-reference-doc-readme.md +121 -63
- package/_bmad-output/implementation-artifacts/21-9-tests-validation.md +332 -61
- package/_bmad-output/implementation-artifacts/bug-bmad-recompile-fails-on-airgapped-network.md +112 -0
- package/_bmad-output/implementation-artifacts/sprint-status.yaml +3 -2
- package/bin/cli.js +59 -0
- package/docs/deployment/vllm-nemotron.md +130 -0
- package/lib/agents.js +17 -2
- package/lib/bmad-customize/bmm-analyst.customize.yaml +8 -0
- package/lib/bmad-customize/bmm-architect.customize.yaml +2 -0
- package/lib/bmad-customize/bmm-dev.customize.yaml +2 -0
- package/lib/bmad-customize/bmm-pm.customize.yaml +2 -0
- package/lib/bmad-customize/bmm-qa.customize.yaml +2 -0
- package/lib/bmad-customize/bmm-quick-flow-solo-dev.customize.yaml +8 -0
- package/lib/bmad-customize/bmm-sm.customize.yaml +2 -0
- package/lib/bmad-customize/bmm-tech-writer.customize.yaml +2 -0
- package/lib/bmad-customize/bmm-ux-designer.customize.yaml +2 -0
- package/lib/bmad.js +293 -1
- package/lib/installer.js +617 -43
- package/lib/merge/roomodes.js +125 -0
- package/lib/profile.js +25 -2
- package/lib/reconfigure.js +334 -0
- package/lib/templates/agents-md.template.md +67 -0
- package/lib/templates/clinerules.template.md +13 -0
- package/lib/templates/instruction-block-onprem.template.md +86 -0
- package/lib/templates/instruction-block-universal.template.md +29 -0
- package/lib/templates/roomodes.template.yaml +96 -0
- package/lib/uninstall.js +314 -0
- package/package.json +4 -3
- package/test/agents-md.test.js +398 -0
- package/test/bmad-extension.test.js +2 -2
- package/test/bmad-persona-phase-prefix.test.js +271 -0
- package/test/clinerules.test.js +339 -0
- package/test/instruction-block.test.js +388 -0
- package/test/integration-verification.test.js +2 -2
- package/test/migration-validation.test.js +2 -2
- package/test/offline-recompile.test.js +237 -0
- package/test/onprem-injection.test.js +425 -32
- package/test/onprem-layer.test.js +419 -0
- package/test/reconfigure.test.js +436 -0
- package/test/roomodes.test.js +343 -0
- package/test/uninstall.test.js +402 -0
|
@@ -1,48 +1,441 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* Story 21.9 — End-to-end
|
|
3
|
+
* Story 21.9 — End-to-end NFR44/NFR46/NFR47 integration tests.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
5
|
+
* AC coverage:
|
|
6
|
+
* (a) NFR44 — standard profile: no /no_think, str_replace_editor, ~/.claude/ anywhere
|
|
7
|
+
* (b) NFR44 — on-prem profile: the three literals ARE present in instruction files
|
|
8
|
+
* (c) NFR46 — two consecutive on-prem installs → byte-identical marker-block content
|
|
9
|
+
* (d) slug-collision — bmad-dev overwritten; my-custom-mode preserved
|
|
10
|
+
* (e) NFR47 — .roomodes fileRegex patterns enforce BMAD mode file restrictions
|
|
11
|
+
* (f) AC #6 — profile round-trip: standard → on-prem → standard CLAUDE.md identical
|
|
12
|
+
* (g) persona — on-prem: bmm-pm critical_actions[0] contains /no_think (applyPersonaPhasePrefix)
|
|
13
|
+
* (h) docs — vllm deployment doc NOT stamped into target projects
|
|
11
14
|
*/
|
|
12
15
|
'use strict';
|
|
13
16
|
|
|
14
|
-
const
|
|
17
|
+
const assert = require('assert');
|
|
15
18
|
const path = require('path');
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const os = require('os');
|
|
21
|
+
const yaml = require('js-yaml');
|
|
16
22
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const
|
|
23
|
+
let passed = 0;
|
|
24
|
+
let failed = 0;
|
|
25
|
+
const errors = [];
|
|
20
26
|
|
|
21
|
-
function
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
27
|
+
async function test(name, fn) {
|
|
28
|
+
try {
|
|
29
|
+
await fn();
|
|
30
|
+
console.log(` \u2713 ${name}`);
|
|
31
|
+
passed++;
|
|
32
|
+
} catch (err) {
|
|
33
|
+
console.error(` \u2717 ${name}: ${err.stack || err.message}`);
|
|
34
|
+
failed++;
|
|
35
|
+
errors.push({ name, error: err.message });
|
|
36
|
+
}
|
|
25
37
|
}
|
|
26
38
|
|
|
27
|
-
|
|
28
|
-
const
|
|
39
|
+
// ── Imports ──────────────────────────────────────────────────────────────────
|
|
40
|
+
const {
|
|
41
|
+
_testUpdateAgentInstructions: updateAgentInstructions,
|
|
42
|
+
stampExtraInstructionTemplates,
|
|
43
|
+
composeInstructionBlock,
|
|
44
|
+
} = require('../lib/installer');
|
|
45
|
+
const { setProfile, getProfile } = require('../lib/profile');
|
|
46
|
+
const { applyPersonaPhasePrefix } = require('../lib/bmad');
|
|
47
|
+
const agents = require('../lib/agents');
|
|
29
48
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
console.log(' Exiting 0 (scaffold only).');
|
|
34
|
-
process.exit(0);
|
|
49
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
50
|
+
function mktemp() {
|
|
51
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), 'ma-agents-21-9-'));
|
|
35
52
|
}
|
|
36
53
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
54
|
+
function cleanup(dir) {
|
|
55
|
+
try { fs.rmSync(dir, { recursive: true, force: true }); } catch {}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Extract the MA-AGENTS-START…MA-AGENTS-END block (inclusive) from text. */
|
|
59
|
+
function extractMarkerBlock(text) {
|
|
60
|
+
const start = text.indexOf('<!-- MA-AGENTS-START -->');
|
|
61
|
+
const end = text.indexOf('<!-- MA-AGENTS-END -->');
|
|
62
|
+
if (start < 0 || end < 0) return null;
|
|
63
|
+
return text.slice(start, end + '<!-- MA-AGENTS-END -->'.length);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Run a minimal install: inject claude-code instruction block + roo-code
|
|
68
|
+
* (which has extraInstructionTemplates that produce .roomodes).
|
|
69
|
+
* cwd is temporarily changed to projectRoot so relative-path resolution inside
|
|
70
|
+
* installer helpers stays consistent with the real CLI invocation.
|
|
71
|
+
*/
|
|
72
|
+
async function runMinimalInstall(projectRoot, opts = {}) {
|
|
73
|
+
const cwdOriginal = process.cwd();
|
|
74
|
+
process.chdir(projectRoot);
|
|
75
|
+
try {
|
|
76
|
+
const claudeAgent = agents.getAgent('claude-code');
|
|
77
|
+
if (claudeAgent) {
|
|
78
|
+
await updateAgentInstructions(claudeAgent, projectRoot, opts);
|
|
79
|
+
}
|
|
80
|
+
const rooAgent = agents.getAgent('roo-code');
|
|
81
|
+
if (rooAgent) {
|
|
82
|
+
await updateAgentInstructions(rooAgent, projectRoot, opts);
|
|
83
|
+
if (Array.isArray(rooAgent.extraInstructionTemplates) && rooAgent.extraInstructionTemplates.length > 0) {
|
|
84
|
+
await stampExtraInstructionTemplates(rooAgent, projectRoot, opts);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
} finally {
|
|
88
|
+
process.chdir(cwdOriginal);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ── Test cases ────────────────────────────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
console.log('\n story 21.9 — end-to-end NFR44/NFR46/NFR47 integration tests\n');
|
|
95
|
+
|
|
96
|
+
async function runAll() {
|
|
97
|
+
// ── (a) Standard profile: no on-prem strings ─────────────────────────────
|
|
98
|
+
|
|
99
|
+
await test('standard profile: generated files contain no /no_think, str_replace_editor, or ~/.claude/', async () => {
|
|
100
|
+
const dir = mktemp();
|
|
101
|
+
try {
|
|
102
|
+
// Standard is the default — no setProfile call needed (--yes resolves to standard)
|
|
103
|
+
setProfile(dir, 'standard');
|
|
104
|
+
await runMinimalInstall(dir, { yesMode: true });
|
|
105
|
+
|
|
106
|
+
const claudeMd = path.join(dir, '.claude', 'CLAUDE.md');
|
|
107
|
+
assert.ok(fs.existsSync(claudeMd), 'CLAUDE.md must be created by standard install');
|
|
108
|
+
const content = fs.readFileSync(claudeMd, 'utf-8');
|
|
109
|
+
|
|
110
|
+
assert.ok(!content.includes('/no_think'), 'CLAUDE.md must not contain /no_think in standard profile (NFR44)');
|
|
111
|
+
assert.ok(!content.includes('str_replace_editor'), 'CLAUDE.md must not contain str_replace_editor in standard profile (NFR44)');
|
|
112
|
+
assert.ok(!content.includes('~/.claude/'), 'CLAUDE.md must not contain ~/.claude/ in standard profile (NFR44)');
|
|
113
|
+
|
|
114
|
+
// Also check .roomodes if it was created
|
|
115
|
+
const roomodes = path.join(dir, '.roomodes');
|
|
116
|
+
if (fs.existsSync(roomodes)) {
|
|
117
|
+
const rm = fs.readFileSync(roomodes, 'utf-8');
|
|
118
|
+
assert.ok(!rm.includes('/no_think'), '.roomodes must not contain /no_think in standard profile (NFR44)');
|
|
119
|
+
assert.ok(!rm.includes('str_replace_editor'), '.roomodes must not contain str_replace_editor in standard profile (NFR44)');
|
|
120
|
+
assert.ok(!rm.includes('~/.claude/'), '.roomodes must not contain ~/.claude/ in standard profile (NFR44)');
|
|
121
|
+
}
|
|
122
|
+
} finally { cleanup(dir); }
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// ── (b) On-prem profile: expected on-prem strings present ─────────────────
|
|
126
|
+
|
|
127
|
+
await test('on-prem profile: instruction files contain on-prem guardrail strings', async () => {
|
|
128
|
+
const dir = mktemp();
|
|
129
|
+
try {
|
|
130
|
+
setProfile(dir, 'on-prem');
|
|
131
|
+
await runMinimalInstall(dir, { yesMode: true });
|
|
132
|
+
|
|
133
|
+
const claudeMd = path.join(dir, '.claude', 'CLAUDE.md');
|
|
134
|
+
assert.ok(fs.existsSync(claudeMd), 'CLAUDE.md must be created by on-prem install');
|
|
135
|
+
const content = fs.readFileSync(claudeMd, 'utf-8');
|
|
136
|
+
|
|
137
|
+
assert.ok(content.includes('/no_think'), 'CLAUDE.md must contain /no_think in on-prem profile');
|
|
138
|
+
assert.ok(content.includes('str_replace_editor'), 'CLAUDE.md must contain str_replace_editor in on-prem profile');
|
|
139
|
+
assert.ok(content.includes('~/.claude/'), 'CLAUDE.md must contain ~/.claude/ prohibition in on-prem profile');
|
|
140
|
+
} finally { cleanup(dir); }
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// ── (c) NFR46: Idempotency — two consecutive installs produce byte-identical marker blocks ──
|
|
144
|
+
|
|
145
|
+
await test('NFR46: two consecutive installs with same profile produce byte-identical marker-block content', async () => {
|
|
146
|
+
const dir = mktemp();
|
|
147
|
+
try {
|
|
148
|
+
setProfile(dir, 'on-prem');
|
|
149
|
+
await runMinimalInstall(dir, { yesMode: true });
|
|
150
|
+
|
|
151
|
+
const claudeMd = path.join(dir, '.claude', 'CLAUDE.md');
|
|
152
|
+
assert.ok(fs.existsSync(claudeMd), 'CLAUDE.md created on first install');
|
|
153
|
+
const content1 = fs.readFileSync(claudeMd, 'utf-8');
|
|
154
|
+
const block1 = extractMarkerBlock(content1);
|
|
155
|
+
assert.ok(block1 !== null, 'marker block must be present after first install');
|
|
156
|
+
|
|
157
|
+
// Second install
|
|
158
|
+
await runMinimalInstall(dir, { yesMode: true });
|
|
159
|
+
const content2 = fs.readFileSync(claudeMd, 'utf-8');
|
|
160
|
+
const block2 = extractMarkerBlock(content2);
|
|
161
|
+
assert.ok(block2 !== null, 'marker block must be present after second install');
|
|
162
|
+
|
|
163
|
+
assert.strictEqual(block2, block1, 'marker-block content must be byte-identical across two consecutive installs (NFR46)');
|
|
164
|
+
|
|
165
|
+
// Also verify .roomodes if present
|
|
166
|
+
const roomodes = path.join(dir, '.roomodes');
|
|
167
|
+
if (fs.existsSync(roomodes)) {
|
|
168
|
+
const rm1 = fs.readFileSync(roomodes, 'utf-8');
|
|
169
|
+
await runMinimalInstall(dir, { yesMode: true });
|
|
170
|
+
const rm2 = fs.readFileSync(roomodes, 'utf-8');
|
|
171
|
+
assert.strictEqual(rm2, rm1, '.roomodes must be byte-identical across consecutive installs (NFR46)');
|
|
172
|
+
}
|
|
173
|
+
} finally { cleanup(dir); }
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// ── (d) .roomodes slug-collision: ma-agents slugs overwrite; user slugs preserved ──
|
|
177
|
+
|
|
178
|
+
await test('slug-collision: ma-agents slugs overwrite; user slugs preserved', async () => {
|
|
179
|
+
const dir = mktemp();
|
|
180
|
+
try {
|
|
181
|
+
setProfile(dir, 'standard');
|
|
182
|
+
|
|
183
|
+
// Seed a .roomodes with one colliding slug and one non-colliding slug
|
|
184
|
+
const existingRoomodes = yaml.dump({
|
|
185
|
+
customModes: [
|
|
186
|
+
{
|
|
187
|
+
slug: 'bmad-dev',
|
|
188
|
+
name: 'User Custom Dev (should be overwritten)',
|
|
189
|
+
roleDefinition: 'user-defined role for dev',
|
|
190
|
+
groups: ['read']
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
slug: 'my-custom-mode',
|
|
194
|
+
name: 'My Custom Mode',
|
|
195
|
+
roleDefinition: 'a project-specific mode',
|
|
196
|
+
groups: ['read', 'edit']
|
|
197
|
+
}
|
|
198
|
+
]
|
|
199
|
+
});
|
|
200
|
+
fs.writeFileSync(path.join(dir, '.roomodes'), existingRoomodes, 'utf-8');
|
|
201
|
+
|
|
202
|
+
const cwdOriginal = process.cwd();
|
|
203
|
+
process.chdir(dir);
|
|
204
|
+
try {
|
|
205
|
+
const rooAgent = agents.getAgent('roo-code');
|
|
206
|
+
if (rooAgent && Array.isArray(rooAgent.extraInstructionTemplates) && rooAgent.extraInstructionTemplates.length > 0) {
|
|
207
|
+
await stampExtraInstructionTemplates(rooAgent, dir);
|
|
208
|
+
}
|
|
209
|
+
} finally {
|
|
210
|
+
process.chdir(cwdOriginal);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const merged = yaml.load(fs.readFileSync(path.join(dir, '.roomodes'), 'utf-8'));
|
|
214
|
+
assert.ok(Array.isArray(merged.customModes), 'customModes must be an array');
|
|
215
|
+
|
|
216
|
+
const devMode = merged.customModes.find(m => m.slug === 'bmad-dev');
|
|
217
|
+
assert.ok(devMode, 'bmad-dev slug must be present');
|
|
218
|
+
assert.notStrictEqual(
|
|
219
|
+
devMode.name,
|
|
220
|
+
'User Custom Dev (should be overwritten)',
|
|
221
|
+
'bmad-dev must be overwritten with the ma-agents version'
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
const customMode = merged.customModes.find(m => m.slug === 'my-custom-mode');
|
|
225
|
+
assert.ok(customMode, 'my-custom-mode slug must be preserved');
|
|
226
|
+
assert.strictEqual(customMode.name, 'My Custom Mode', 'user slug name must be preserved byte-for-byte');
|
|
227
|
+
assert.strictEqual(customMode.roleDefinition, 'a project-specific mode', 'user slug roleDefinition must be preserved');
|
|
228
|
+
} finally { cleanup(dir); }
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// ── (e) NFR47: .roomodes fileRegex matrix ─────────────────────────────────
|
|
232
|
+
|
|
233
|
+
await test('NFR47: .roomodes fileRegex patterns enforce BMAD mode file restrictions', async () => {
|
|
234
|
+
const dir = mktemp();
|
|
235
|
+
try {
|
|
236
|
+
setProfile(dir, 'standard');
|
|
237
|
+
|
|
238
|
+
const cwdOriginal = process.cwd();
|
|
239
|
+
process.chdir(dir);
|
|
240
|
+
try {
|
|
241
|
+
const rooAgent = agents.getAgent('roo-code');
|
|
242
|
+
if (rooAgent) {
|
|
243
|
+
await updateAgentInstructions(rooAgent, dir);
|
|
244
|
+
if (Array.isArray(rooAgent.extraInstructionTemplates) && rooAgent.extraInstructionTemplates.length > 0) {
|
|
245
|
+
await stampExtraInstructionTemplates(rooAgent, dir);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
} finally {
|
|
249
|
+
process.chdir(cwdOriginal);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const roomodesPath = path.join(dir, '.roomodes');
|
|
253
|
+
assert.ok(fs.existsSync(roomodesPath), '.roomodes must be created by roo-code install');
|
|
254
|
+
const parsed = yaml.load(fs.readFileSync(roomodesPath, 'utf-8'));
|
|
255
|
+
assert.ok(Array.isArray(parsed.customModes), 'customModes must be an array');
|
|
256
|
+
|
|
257
|
+
const bySlug = Object.fromEntries(parsed.customModes.map(m => [m.slug, m]));
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Extract the fileRegex from the 'edit' group entry if it has a config object.
|
|
261
|
+
* Returns a RegExp if found, null if bare 'edit' string (unrestricted).
|
|
262
|
+
*/
|
|
263
|
+
function getEditFileRegex(mode) {
|
|
264
|
+
if (!mode || !Array.isArray(mode.groups)) return null;
|
|
265
|
+
for (const g of mode.groups) {
|
|
266
|
+
if (Array.isArray(g) && g[0] === 'edit' && g[1] && typeof g[1].fileRegex === 'string') {
|
|
267
|
+
return new RegExp(g[1].fileRegex);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// bmad-pm: accepts .md, rejects .ts / .py / .js
|
|
274
|
+
const pmRegex = getEditFileRegex(bySlug['bmad-pm']);
|
|
275
|
+
assert.ok(pmRegex !== null, 'bmad-pm must have a fileRegex on edit group (NFR47)');
|
|
276
|
+
assert.ok(pmRegex.test('plan.md'), 'bmad-pm fileRegex must accept .md');
|
|
277
|
+
assert.ok(!pmRegex.test('app.ts'), 'bmad-pm fileRegex must reject .ts');
|
|
278
|
+
assert.ok(!pmRegex.test('script.py'), 'bmad-pm fileRegex must reject .py');
|
|
279
|
+
assert.ok(!pmRegex.test('index.js'), 'bmad-pm fileRegex must reject .js');
|
|
280
|
+
|
|
281
|
+
// bmad-architect: accepts .md / .xml / .drawio, rejects .ts / .py / .js
|
|
282
|
+
const archRegex = getEditFileRegex(bySlug['bmad-architect']);
|
|
283
|
+
assert.ok(archRegex !== null, 'bmad-architect must have a fileRegex on edit group (NFR47)');
|
|
284
|
+
assert.ok(archRegex.test('arch.md'), 'bmad-architect fileRegex must accept .md');
|
|
285
|
+
assert.ok(archRegex.test('diagram.drawio'), 'bmad-architect fileRegex must accept .drawio');
|
|
286
|
+
assert.ok(archRegex.test('model.xml'), 'bmad-architect fileRegex must accept .xml');
|
|
287
|
+
assert.ok(!archRegex.test('app.ts'), 'bmad-architect fileRegex must reject .ts');
|
|
288
|
+
assert.ok(!archRegex.test('script.py'), 'bmad-architect fileRegex must reject .py');
|
|
289
|
+
|
|
290
|
+
// bmad-techlead: accepts .md / .json / .yaml, rejects .ts / .py
|
|
291
|
+
const tlRegex = getEditFileRegex(bySlug['bmad-techlead']);
|
|
292
|
+
assert.ok(tlRegex !== null, 'bmad-techlead must have a fileRegex on edit group (NFR47)');
|
|
293
|
+
assert.ok(tlRegex.test('notes.md'), 'bmad-techlead fileRegex must accept .md');
|
|
294
|
+
assert.ok(tlRegex.test('package.json'), 'bmad-techlead fileRegex must accept .json');
|
|
295
|
+
assert.ok(tlRegex.test('config.yaml'), 'bmad-techlead fileRegex must accept .yaml');
|
|
296
|
+
assert.ok(!tlRegex.test('app.ts'), 'bmad-techlead fileRegex must reject .ts');
|
|
297
|
+
assert.ok(!tlRegex.test('script.py'), 'bmad-techlead fileRegex must reject .py');
|
|
298
|
+
|
|
299
|
+
// bmad-dev: no fileRegex restriction (full access — bare "edit" string in groups)
|
|
300
|
+
const devRegex = getEditFileRegex(bySlug['bmad-dev']);
|
|
301
|
+
assert.strictEqual(devRegex, null, 'bmad-dev must have NO fileRegex restriction (full edit access)');
|
|
302
|
+
const devGroups = bySlug['bmad-dev'].groups;
|
|
303
|
+
assert.ok(devGroups.includes('edit'), 'bmad-dev must have bare "edit" in groups');
|
|
304
|
+
} finally { cleanup(dir); }
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// ── (f) AC #6: Profile switch round-trip ─────────────────────────────────
|
|
308
|
+
|
|
309
|
+
await test('AC #6 profile round-trip: standard → on-prem → standard produces byte-identical first and last CLAUDE.md', async () => {
|
|
310
|
+
const dir = mktemp();
|
|
311
|
+
try {
|
|
312
|
+
// Install 1: standard profile
|
|
313
|
+
setProfile(dir, 'standard');
|
|
314
|
+
await runMinimalInstall(dir, { yesMode: true });
|
|
315
|
+
const claudeMd = path.join(dir, '.claude', 'CLAUDE.md');
|
|
316
|
+
assert.ok(fs.existsSync(claudeMd), 'CLAUDE.md must exist after first install');
|
|
317
|
+
const content1 = fs.readFileSync(claudeMd, 'utf-8');
|
|
318
|
+
|
|
319
|
+
// Install 2: switch to on-prem
|
|
320
|
+
setProfile(dir, 'on-prem');
|
|
321
|
+
assert.strictEqual(getProfile(dir), 'on-prem', 'profile must be persisted as on-prem');
|
|
322
|
+
await runMinimalInstall(dir, { yesMode: true });
|
|
323
|
+
const content2 = fs.readFileSync(claudeMd, 'utf-8');
|
|
324
|
+
assert.ok(content2.includes('/no_think'), 'on-prem install must stamp /no_think into CLAUDE.md');
|
|
325
|
+
|
|
326
|
+
// Install 3: switch back to standard
|
|
327
|
+
setProfile(dir, 'standard');
|
|
328
|
+
assert.strictEqual(getProfile(dir), 'standard', 'profile must be restored to standard');
|
|
329
|
+
await runMinimalInstall(dir, { yesMode: true });
|
|
330
|
+
const content3 = fs.readFileSync(claudeMd, 'utf-8');
|
|
331
|
+
|
|
332
|
+
// Install 1 and Install 3 must be byte-identical
|
|
333
|
+
assert.strictEqual(
|
|
334
|
+
content3,
|
|
335
|
+
content1,
|
|
336
|
+
'standard reinstall after on-prem must produce byte-identical CLAUDE.md to original standard install'
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
// Install 2 must have contained on-prem content
|
|
340
|
+
assert.ok(!content3.includes('/no_think'), 'final standard install must not contain /no_think');
|
|
341
|
+
} finally { cleanup(dir); }
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
// ── (g) On-prem persona: planning persona has /no_think prefix in critical_actions ──
|
|
345
|
+
|
|
346
|
+
await test('on-prem install: planning persona has /no_think prefix in critical_actions[0]', async () => {
|
|
347
|
+
const dir = mktemp();
|
|
348
|
+
try {
|
|
349
|
+
const CUSTOMIZE_SOURCE = path.join(__dirname, '..', 'lib', 'bmad-customize');
|
|
350
|
+
const jsYaml = require('js-yaml');
|
|
351
|
+
|
|
352
|
+
// Copy all customize yaml files into a temp "deployed" dir, simulating post-BMAD deploy
|
|
353
|
+
const deployedDir = path.join(dir, '_config');
|
|
354
|
+
fs.mkdirSync(deployedDir, { recursive: true });
|
|
355
|
+
const files = fs.readdirSync(CUSTOMIZE_SOURCE).filter(f => f.endsWith('.customize.yaml'));
|
|
356
|
+
assert.ok(files.length > 0, 'bmad-customize directory must contain at least one .customize.yaml file');
|
|
357
|
+
for (const file of files) {
|
|
358
|
+
fs.copyFileSync(path.join(CUSTOMIZE_SOURCE, file), path.join(deployedDir, file));
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Apply the on-prem phase prefix
|
|
362
|
+
await applyPersonaPhasePrefix(CUSTOMIZE_SOURCE, deployedDir, 'on-prem');
|
|
363
|
+
|
|
364
|
+
// Verify bmm-pm: planning persona — critical_actions[0] must contain /no_think
|
|
365
|
+
const pmPath = path.join(deployedDir, 'bmm-pm.customize.yaml');
|
|
366
|
+
assert.ok(fs.existsSync(pmPath), 'bmm-pm.customize.yaml must exist in deployed dir');
|
|
367
|
+
const pmDoc = jsYaml.load(fs.readFileSync(pmPath, 'utf-8'));
|
|
368
|
+
assert.ok(Array.isArray(pmDoc.critical_actions) && pmDoc.critical_actions.length > 0,
|
|
369
|
+
'bmm-pm critical_actions must be non-empty after on-prem deploy');
|
|
370
|
+
assert.ok(
|
|
371
|
+
pmDoc.critical_actions[0].includes('/no_think'),
|
|
372
|
+
`bmm-pm critical_actions[0] must contain /no_think in on-prem profile; got: ${pmDoc.critical_actions[0]}`
|
|
373
|
+
);
|
|
374
|
+
|
|
375
|
+
// On-prem deployment must strip the phase: and on_prem_phase_prefix: keys
|
|
376
|
+
assert.ok(!Object.prototype.hasOwnProperty.call(pmDoc, 'phase'),
|
|
377
|
+
'deployed bmm-pm must not contain phase: key (AC #8 Story 21.7)');
|
|
378
|
+
assert.ok(!Object.prototype.hasOwnProperty.call(pmDoc, 'on_prem_phase_prefix'),
|
|
379
|
+
'deployed bmm-pm must not contain on_prem_phase_prefix: key (AC #8 Story 21.7)');
|
|
380
|
+
} finally { cleanup(dir); }
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
// ── (h) Installer non-regression: vllm deployment doc not stamped ─────────
|
|
384
|
+
|
|
385
|
+
await test('vllm deployment doc is not stamped into target projects', async () => {
|
|
386
|
+
const dir = mktemp();
|
|
387
|
+
try {
|
|
388
|
+
setProfile(dir, 'standard');
|
|
389
|
+
await runMinimalInstall(dir, { yesMode: true });
|
|
390
|
+
|
|
391
|
+
// Walk the entire temp dir and look for vllm artefacts
|
|
392
|
+
function walkDir(dirPath) {
|
|
393
|
+
const entries = [];
|
|
394
|
+
const items = fs.readdirSync(dirPath);
|
|
395
|
+
for (const item of items) {
|
|
396
|
+
const full = path.join(dirPath, item);
|
|
397
|
+
const stat = fs.statSync(full);
|
|
398
|
+
if (stat.isDirectory()) {
|
|
399
|
+
entries.push(...walkDir(full));
|
|
400
|
+
} else {
|
|
401
|
+
entries.push(full);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
return entries;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const allFiles = walkDir(dir);
|
|
408
|
+
|
|
409
|
+
// No file matching *vllm-nemotron* should exist anywhere in the project
|
|
410
|
+
const vllmFiles = allFiles.filter(f => path.basename(f).includes('vllm-nemotron'));
|
|
411
|
+
assert.strictEqual(vllmFiles.length, 0,
|
|
412
|
+
`vllm-nemotron docs must not be stamped into target projects; found: ${vllmFiles.join(', ')}`);
|
|
413
|
+
|
|
414
|
+
// No file should contain 'vllm serve' content
|
|
415
|
+
for (const filePath of allFiles) {
|
|
416
|
+
try {
|
|
417
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
418
|
+
assert.ok(
|
|
419
|
+
!content.includes('vllm serve'),
|
|
420
|
+
`File ${path.relative(dir, filePath)} must not contain "vllm serve" command`
|
|
421
|
+
);
|
|
422
|
+
} catch {
|
|
423
|
+
// binary files — skip
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
} finally { cleanup(dir); }
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
// ── Summary ───────────────────────────────────────────────────────────────
|
|
430
|
+
|
|
431
|
+
console.log(`\n ${passed} passed, ${failed} failed\n`);
|
|
432
|
+
if (failed > 0) {
|
|
433
|
+
errors.forEach(e => console.error(` \u2717 ${e.name}: ${e.error}`));
|
|
434
|
+
process.exit(1);
|
|
435
|
+
}
|
|
43
436
|
}
|
|
44
437
|
|
|
45
|
-
|
|
46
|
-
console.
|
|
47
|
-
|
|
48
|
-
|
|
438
|
+
runAll().catch(err => {
|
|
439
|
+
console.error('Unhandled error in test harness:', err);
|
|
440
|
+
process.exit(1);
|
|
441
|
+
});
|