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
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Story 21.3 — Unit + integration tests for .roomodes template and merger.
|
|
4
|
+
*
|
|
5
|
+
* AC coverage (tests → ACs):
|
|
6
|
+
* 5.1 Empty existingYaml → output = four ma-agents-owned modes in canonical order (AC #7, #5, #10)
|
|
7
|
+
* 5.2 Two non-colliding user entries preserved in order, then ma-agents modes (AC #5, #8, #10)
|
|
8
|
+
* 5.3 Colliding slug dropped + exactly one warning naming the slug (AC #6)
|
|
9
|
+
* 5.4 Non-customModes top-level keys preserved (AC #5, FR176)
|
|
10
|
+
* 5.5 NFR44 — standard profile output lacks /no_think, str_replace_editor, ~/.claude/ (AC #11)
|
|
11
|
+
* 5.6 NFR46 — two consecutive merges return byte-identical strings (AC #10)
|
|
12
|
+
* 5.7 NFR47 — each mode's fileRegex accepts/rejects expected file paths (AC #9)
|
|
13
|
+
* 5.8 MA_AGENTS_OWNED_SLUGS exported with canonical order and immutability (AC #5, downstream 21.10/21.11)
|
|
14
|
+
* 5.9 Fresh install via stampExtraInstructionTemplates creates .roomodes with four modes (AC #4, #7)
|
|
15
|
+
* 5.10 Re-install preserves user entry and is byte-identical on second run (AC #8, #10)
|
|
16
|
+
* 5.11 Agent without extraInstructionTemplates is a no-op (AC #13)
|
|
17
|
+
* 5.12 Raw template contains exactly four {{UNIVERSAL_BLOCK}} sentinels and no other placeholders (AC #1, #2)
|
|
18
|
+
*/
|
|
19
|
+
'use strict';
|
|
20
|
+
|
|
21
|
+
const assert = require('assert');
|
|
22
|
+
const path = require('path');
|
|
23
|
+
const fs = require('fs');
|
|
24
|
+
const os = require('os');
|
|
25
|
+
const yaml = require('js-yaml');
|
|
26
|
+
|
|
27
|
+
let passed = 0;
|
|
28
|
+
let failed = 0;
|
|
29
|
+
const errors = [];
|
|
30
|
+
|
|
31
|
+
async function test(name, fn) {
|
|
32
|
+
try {
|
|
33
|
+
await fn();
|
|
34
|
+
console.log(` \u2713 ${name}`);
|
|
35
|
+
passed++;
|
|
36
|
+
} catch (err) {
|
|
37
|
+
console.error(` \u2717 ${name}: ${err.stack || err.message}`);
|
|
38
|
+
failed++;
|
|
39
|
+
errors.push({ name, error: err.message });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const { mergeRoomodes, MA_AGENTS_OWNED_SLUGS } = require('../lib/merge/roomodes');
|
|
44
|
+
const { stampExtraInstructionTemplates, composeInstructionBlock } = require('../lib/installer');
|
|
45
|
+
const { setProfile } = require('../lib/profile');
|
|
46
|
+
const { getAgent } = require('../lib/agents');
|
|
47
|
+
|
|
48
|
+
const ROOMODES_TEMPLATE_PATH = path.join(__dirname, '..', 'lib', 'templates', 'roomodes.template.yaml');
|
|
49
|
+
|
|
50
|
+
function mktemp() {
|
|
51
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), 'ma-agents-roomodes-test-'));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function cleanup(dir) {
|
|
55
|
+
try { fs.rmSync(dir, { recursive: true, force: true }); } catch {}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Load the raw template and expand {{UNIVERSAL_BLOCK}} + {{MANIFEST_PATH}}
|
|
60
|
+
* against a project root with a persisted profile — mirrors what the stamper
|
|
61
|
+
* does before calling mergeRoomodes.
|
|
62
|
+
*/
|
|
63
|
+
function loadComposedTemplate(projectRoot, profile = 'standard') {
|
|
64
|
+
setProfile(projectRoot, profile);
|
|
65
|
+
const raw = fs.readFileSync(ROOMODES_TEMPLATE_PATH, 'utf-8');
|
|
66
|
+
const universal = composeInstructionBlock({ profile, projectRoot })
|
|
67
|
+
.replace(/\{\{MANIFEST_PATH\}\}/g, '.roo/skills/MANIFEST.yaml');
|
|
68
|
+
// Mirror the stamper's indent-preserving substitution so tests stay in sync.
|
|
69
|
+
return raw.replace(
|
|
70
|
+
/^([ \t]*)\{\{UNIVERSAL_BLOCK\}\}/gm,
|
|
71
|
+
(_m, indent) => universal
|
|
72
|
+
.replace(/\s+$/, '')
|
|
73
|
+
.split('\n')
|
|
74
|
+
.map(l => indent + l)
|
|
75
|
+
.join('\n')
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
console.log('\n roomodes merger + stamper tests\n');
|
|
80
|
+
|
|
81
|
+
async function runAll() {
|
|
82
|
+
// 5.1 — empty existingYaml: four modes in canonical order
|
|
83
|
+
await test('5.1 empty existingYaml → four ma-agents modes in canonical order', () => {
|
|
84
|
+
const dir = mktemp();
|
|
85
|
+
try {
|
|
86
|
+
const composed = loadComposedTemplate(dir);
|
|
87
|
+
const out = mergeRoomodes('', composed);
|
|
88
|
+
const parsed = yaml.load(out);
|
|
89
|
+
assert.ok(Array.isArray(parsed.customModes), 'customModes must be an array');
|
|
90
|
+
assert.deepStrictEqual(
|
|
91
|
+
parsed.customModes.map(m => m.slug),
|
|
92
|
+
['bmad-pm', 'bmad-architect', 'bmad-techlead', 'bmad-dev']
|
|
93
|
+
);
|
|
94
|
+
} finally { cleanup(dir); }
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// 5.2 — user entries preserved before ma-agents entries
|
|
98
|
+
await test('5.2 user entries preserved in order, then ma-agents modes', () => {
|
|
99
|
+
const dir = mktemp();
|
|
100
|
+
try {
|
|
101
|
+
const composed = loadComposedTemplate(dir);
|
|
102
|
+
const existing = yaml.dump({
|
|
103
|
+
customModes: [
|
|
104
|
+
{ slug: 'my-custom-mode', name: 'Custom', roleDefinition: 'r', groups: ['read'] },
|
|
105
|
+
{ slug: 'another-mode', name: 'Another', roleDefinition: 'r2', groups: ['read'] }
|
|
106
|
+
]
|
|
107
|
+
});
|
|
108
|
+
const out = mergeRoomodes(existing, composed);
|
|
109
|
+
const parsed = yaml.load(out);
|
|
110
|
+
assert.deepStrictEqual(
|
|
111
|
+
parsed.customModes.map(m => m.slug),
|
|
112
|
+
['my-custom-mode', 'another-mode', 'bmad-pm', 'bmad-architect', 'bmad-techlead', 'bmad-dev']
|
|
113
|
+
);
|
|
114
|
+
} finally { cleanup(dir); }
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// 5.3 — collision: user entry dropped, exactly one warning naming the slug
|
|
118
|
+
await test('5.3 colliding slug dropped + exactly one warning naming the slug', () => {
|
|
119
|
+
const dir = mktemp();
|
|
120
|
+
try {
|
|
121
|
+
const composed = loadComposedTemplate(dir);
|
|
122
|
+
const existing = yaml.dump({
|
|
123
|
+
customModes: [
|
|
124
|
+
{ slug: 'keep-me', name: 'Keep', roleDefinition: 'r', groups: ['read'] },
|
|
125
|
+
{ slug: 'bmad-architect', name: 'User Architect Override', roleDefinition: 'r2', groups: ['read'] }
|
|
126
|
+
]
|
|
127
|
+
});
|
|
128
|
+
const warnings = [];
|
|
129
|
+
const out = mergeRoomodes(existing, composed, { warn: (msg) => warnings.push(msg) });
|
|
130
|
+
const parsed = yaml.load(out);
|
|
131
|
+
assert.strictEqual(warnings.length, 1, `expected 1 warning, got ${warnings.length}`);
|
|
132
|
+
assert.ok(
|
|
133
|
+
warnings[0].includes('"bmad-architect"'),
|
|
134
|
+
`warning must cite slug verbatim; got: ${warnings[0]}`
|
|
135
|
+
);
|
|
136
|
+
const slugs = parsed.customModes.map(m => m.slug);
|
|
137
|
+
assert.deepStrictEqual(slugs, ['keep-me', 'bmad-pm', 'bmad-architect', 'bmad-techlead', 'bmad-dev']);
|
|
138
|
+
// And the bmad-architect entry is the ma-agents version, NOT the user's.
|
|
139
|
+
const arch = parsed.customModes.find(m => m.slug === 'bmad-architect');
|
|
140
|
+
assert.notStrictEqual(arch.name, 'User Architect Override');
|
|
141
|
+
} finally { cleanup(dir); }
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// 5.4 — non-customModes top-level keys preserved (FR176)
|
|
145
|
+
await test('5.4 non-customModes top-level keys preserved (FR176)', () => {
|
|
146
|
+
const dir = mktemp();
|
|
147
|
+
try {
|
|
148
|
+
const composed = loadComposedTemplate(dir);
|
|
149
|
+
const existing = yaml.dump({
|
|
150
|
+
version: 1,
|
|
151
|
+
settings: { foo: 'bar' },
|
|
152
|
+
customModes: []
|
|
153
|
+
});
|
|
154
|
+
const out = mergeRoomodes(existing, composed);
|
|
155
|
+
const parsed = yaml.load(out);
|
|
156
|
+
assert.strictEqual(parsed.version, 1);
|
|
157
|
+
assert.deepStrictEqual(parsed.settings, { foo: 'bar' });
|
|
158
|
+
} finally { cleanup(dir); }
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// 5.5 — NFR44: standard profile output has no on-prem literals
|
|
162
|
+
await test('5.5 NFR44 — standard-profile output lacks /no_think, str_replace_editor, ~/.claude/', () => {
|
|
163
|
+
const dir = mktemp();
|
|
164
|
+
try {
|
|
165
|
+
const composed = loadComposedTemplate(dir, 'standard');
|
|
166
|
+
const out = mergeRoomodes('', composed);
|
|
167
|
+
assert.ok(!out.includes('/no_think'), 'output must not contain /no_think');
|
|
168
|
+
assert.ok(!out.includes('str_replace_editor'), 'output must not contain str_replace_editor');
|
|
169
|
+
assert.ok(!out.includes('~/.claude/'), 'output must not contain ~/.claude/');
|
|
170
|
+
} finally { cleanup(dir); }
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// 5.6 — NFR46: idempotency — two merges byte-identical
|
|
174
|
+
await test('5.6 NFR46 — two consecutive merges byte-identical', () => {
|
|
175
|
+
const dir = mktemp();
|
|
176
|
+
try {
|
|
177
|
+
const composed = loadComposedTemplate(dir);
|
|
178
|
+
const existing = yaml.dump({
|
|
179
|
+
customModes: [{ slug: 'u1', name: 'U1', roleDefinition: 'r', groups: ['read'] }]
|
|
180
|
+
});
|
|
181
|
+
const out1 = mergeRoomodes(existing, composed);
|
|
182
|
+
const out2 = mergeRoomodes(existing, composed);
|
|
183
|
+
assert.strictEqual(out1, out2, 'mergeRoomodes must be byte-deterministic');
|
|
184
|
+
// Also: merging the previous output against the template produces the same output (fixed point).
|
|
185
|
+
const out3 = mergeRoomodes(out1, composed);
|
|
186
|
+
assert.strictEqual(out1, out3, 'merging the output as input must be a fixed point');
|
|
187
|
+
} finally { cleanup(dir); }
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// 5.7 — NFR47: fileRegex contract per mode
|
|
191
|
+
await test('5.7 NFR47 — each mode fileRegex accepts/rejects expected paths', () => {
|
|
192
|
+
const dir = mktemp();
|
|
193
|
+
try {
|
|
194
|
+
const composed = loadComposedTemplate(dir);
|
|
195
|
+
const out = mergeRoomodes('', composed);
|
|
196
|
+
const parsed = yaml.load(out);
|
|
197
|
+
const byslug = Object.fromEntries(parsed.customModes.map(m => [m.slug, m]));
|
|
198
|
+
|
|
199
|
+
function editFileRegex(mode) {
|
|
200
|
+
// groups is: [ "read", [ "edit", { fileRegex, description } ], ... ]
|
|
201
|
+
for (const g of mode.groups) {
|
|
202
|
+
if (Array.isArray(g) && g[0] === 'edit' && g[1] && typeof g[1].fileRegex === 'string') {
|
|
203
|
+
return new RegExp(g[1].fileRegex);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const pm = editFileRegex(byslug['bmad-pm']);
|
|
210
|
+
assert.ok(pm.test('notes.md'), 'bmad-pm must accept .md');
|
|
211
|
+
assert.ok(!pm.test('app.ts'), 'bmad-pm must reject .ts');
|
|
212
|
+
assert.ok(!pm.test('script.py'), 'bmad-pm must reject .py');
|
|
213
|
+
|
|
214
|
+
const arch = editFileRegex(byslug['bmad-architect']);
|
|
215
|
+
assert.ok(arch.test('arch.md'));
|
|
216
|
+
assert.ok(arch.test('diagram.drawio'));
|
|
217
|
+
assert.ok(arch.test('model.xml'));
|
|
218
|
+
assert.ok(!arch.test('app.ts'));
|
|
219
|
+
assert.ok(!arch.test('script.py'));
|
|
220
|
+
|
|
221
|
+
const tl = editFileRegex(byslug['bmad-techlead']);
|
|
222
|
+
assert.ok(tl.test('config.yaml'));
|
|
223
|
+
assert.ok(tl.test('package.json'));
|
|
224
|
+
assert.ok(tl.test('notes.md'));
|
|
225
|
+
assert.ok(!tl.test('app.ts'));
|
|
226
|
+
assert.ok(!tl.test('script.py'));
|
|
227
|
+
|
|
228
|
+
// bmad-dev: no fileRegex on edit → unrestricted
|
|
229
|
+
assert.strictEqual(editFileRegex(byslug['bmad-dev']), null, 'bmad-dev must have no edit-level fileRegex');
|
|
230
|
+
const devGroups = byslug['bmad-dev'].groups;
|
|
231
|
+
assert.ok(devGroups.includes('edit'), 'bmad-dev groups must include bare "edit"');
|
|
232
|
+
assert.ok(devGroups.includes('command'), 'bmad-dev groups must include "command"');
|
|
233
|
+
} finally { cleanup(dir); }
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// 5.8 — MA_AGENTS_OWNED_SLUGS export
|
|
237
|
+
await test('5.8 MA_AGENTS_OWNED_SLUGS exported with canonical order + immutable', () => {
|
|
238
|
+
assert.deepStrictEqual(
|
|
239
|
+
Array.from(MA_AGENTS_OWNED_SLUGS),
|
|
240
|
+
['bmad-pm', 'bmad-architect', 'bmad-techlead', 'bmad-dev']
|
|
241
|
+
);
|
|
242
|
+
assert.throws(() => { MA_AGENTS_OWNED_SLUGS[0] = 'x'; }, 'MA_AGENTS_OWNED_SLUGS must be frozen');
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// 5.9 — integration: fresh install writes four-mode .roomodes
|
|
246
|
+
await test('5.9 stampExtraInstructionTemplates fresh install writes four-mode .roomodes', async () => {
|
|
247
|
+
const dir = mktemp();
|
|
248
|
+
try {
|
|
249
|
+
setProfile(dir, 'standard');
|
|
250
|
+
const roo = getAgent('roo-code');
|
|
251
|
+
// Override getProjectPath to point into the tmp root so the MANIFEST.yaml
|
|
252
|
+
// relative-path computation is deterministic and doesn't leak cwd.
|
|
253
|
+
const agent = Object.assign({}, roo, {
|
|
254
|
+
getProjectPath: () => path.join(dir, '.roo', 'skills')
|
|
255
|
+
});
|
|
256
|
+
await stampExtraInstructionTemplates(agent, dir);
|
|
257
|
+
const target = path.join(dir, '.roomodes');
|
|
258
|
+
assert.ok(fs.existsSync(target), '.roomodes must be created');
|
|
259
|
+
const parsed = yaml.load(fs.readFileSync(target, 'utf-8'));
|
|
260
|
+
assert.deepStrictEqual(
|
|
261
|
+
parsed.customModes.map(m => m.slug),
|
|
262
|
+
['bmad-pm', 'bmad-architect', 'bmad-techlead', 'bmad-dev']
|
|
263
|
+
);
|
|
264
|
+
// {{UNIVERSAL_BLOCK}} must have been expanded (no sentinel remains).
|
|
265
|
+
const raw = fs.readFileSync(target, 'utf-8');
|
|
266
|
+
assert.ok(!raw.includes('{{UNIVERSAL_BLOCK}}'), 'no placeholder may remain after stamp');
|
|
267
|
+
assert.ok(!raw.includes('{{MANIFEST_PATH}}'), 'no manifest placeholder may remain');
|
|
268
|
+
} finally { cleanup(dir); }
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// 5.10 — re-install preserves user entry + byte-identical on second run
|
|
272
|
+
await test('5.10 re-install preserves user entry + byte-identical on second run', async () => {
|
|
273
|
+
const dir = mktemp();
|
|
274
|
+
try {
|
|
275
|
+
setProfile(dir, 'standard');
|
|
276
|
+
// Seed with a user entry
|
|
277
|
+
const seed = yaml.dump({
|
|
278
|
+
customModes: [
|
|
279
|
+
{ slug: 'my-custom-mode', name: 'Custom', roleDefinition: 'r', groups: ['read'] }
|
|
280
|
+
]
|
|
281
|
+
});
|
|
282
|
+
fs.writeFileSync(path.join(dir, '.roomodes'), seed, 'utf-8');
|
|
283
|
+
|
|
284
|
+
const roo = getAgent('roo-code');
|
|
285
|
+
const agent = Object.assign({}, roo, {
|
|
286
|
+
getProjectPath: () => path.join(dir, '.roo', 'skills')
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
await stampExtraInstructionTemplates(agent, dir);
|
|
290
|
+
const after1 = fs.readFileSync(path.join(dir, '.roomodes'), 'utf-8');
|
|
291
|
+
const parsed = yaml.load(after1);
|
|
292
|
+
assert.deepStrictEqual(
|
|
293
|
+
parsed.customModes.map(m => m.slug),
|
|
294
|
+
['my-custom-mode', 'bmad-pm', 'bmad-architect', 'bmad-techlead', 'bmad-dev']
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
await stampExtraInstructionTemplates(agent, dir);
|
|
298
|
+
const after2 = fs.readFileSync(path.join(dir, '.roomodes'), 'utf-8');
|
|
299
|
+
assert.strictEqual(after1, after2, 'second install must produce byte-identical .roomodes (NFR46)');
|
|
300
|
+
} finally { cleanup(dir); }
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
// 5.11 — agent without extraInstructionTemplates is a no-op
|
|
304
|
+
await test('5.11 agent without extraInstructionTemplates is a no-op (AC #13)', async () => {
|
|
305
|
+
const dir = mktemp();
|
|
306
|
+
try {
|
|
307
|
+
setProfile(dir, 'standard');
|
|
308
|
+
const claude = getAgent('claude-code');
|
|
309
|
+
// claude-code has no extraInstructionTemplates — must not create .roomodes
|
|
310
|
+
await stampExtraInstructionTemplates(claude, dir);
|
|
311
|
+
assert.ok(!fs.existsSync(path.join(dir, '.roomodes')), '.roomodes must NOT be created for agents lacking the field');
|
|
312
|
+
} finally { cleanup(dir); }
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
// 5.12 — template shape: four sentinels, no other placeholders
|
|
316
|
+
await test('5.12 template contains {{UNIVERSAL_BLOCK}} × 4 and no other placeholders', () => {
|
|
317
|
+
const raw = fs.readFileSync(ROOMODES_TEMPLATE_PATH, 'utf-8');
|
|
318
|
+
const matches = raw.match(/\{\{UNIVERSAL_BLOCK\}\}/g) || [];
|
|
319
|
+
assert.strictEqual(matches.length, 4, `expected 4 UNIVERSAL_BLOCK sentinels, got ${matches.length}`);
|
|
320
|
+
// No other {{...}} placeholders permitted (per AC #1.2).
|
|
321
|
+
const leftover = raw.replace(/\{\{UNIVERSAL_BLOCK\}\}/g, '').match(/\{\{[^}]+\}\}/g);
|
|
322
|
+
assert.ok(!leftover, `template must contain no other {{...}} placeholders; found: ${leftover}`);
|
|
323
|
+
// NFR44 — raw template must not contain on-prem literals.
|
|
324
|
+
assert.ok(!raw.includes('/no_think'));
|
|
325
|
+
assert.ok(!raw.includes('str_replace_editor'));
|
|
326
|
+
assert.ok(!raw.includes('~/.claude/'));
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
// Summary
|
|
330
|
+
console.log('');
|
|
331
|
+
if (failed > 0) {
|
|
332
|
+
console.error(` ${failed} test(s) failed (${passed} passed)`);
|
|
333
|
+
for (const e of errors) console.error(` - ${e.name}: ${e.error}`);
|
|
334
|
+
process.exit(1);
|
|
335
|
+
} else {
|
|
336
|
+
console.log(` All ${passed} tests passed.`);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
runAll().catch((err) => {
|
|
341
|
+
console.error('Test harness error:', err);
|
|
342
|
+
process.exit(1);
|
|
343
|
+
});
|