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,436 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Story 21.10 — Tests for the `reconfigure` subcommand orchestrator.
|
|
4
|
+
*
|
|
5
|
+
* AC coverage (one-to-one with acceptance criteria 1–11):
|
|
6
|
+
* 8.1 --yes rejected with pinned message (AC #6)
|
|
7
|
+
* 8.2 missing .ma-agents.json → named error (AC #1)
|
|
8
|
+
* 8.3 same-value → "Profile unchanged" and no writes (AC #4)
|
|
9
|
+
* 8.4 profile flip standard → on-prem re-stamps injection files + .roomodes (AC #5)
|
|
10
|
+
* 8.5 two-step "Continue?" → no aborts with zero writes (AC #7)
|
|
11
|
+
* 8.6 backup files written at <target>.backup-<ISO> (AC #8)
|
|
12
|
+
* 8.7 slug divergence raises RoomodesSlugDivergenceError; --force bypasses (AC #9)
|
|
13
|
+
* 8.8 dual-file drift raises ClinerulesDualFileDriftError (AC #10)
|
|
14
|
+
* 8.9 profileHistory appends; cap at 20; first call creates field (AC #11)
|
|
15
|
+
* 8.10 full round-trip standard → on-prem → standard (NFR46)
|
|
16
|
+
* 8.11 Current profile appears in the prompt message (AC #2)
|
|
17
|
+
*/
|
|
18
|
+
'use strict';
|
|
19
|
+
|
|
20
|
+
const assert = require('assert');
|
|
21
|
+
const path = require('path');
|
|
22
|
+
const fs = require('fs');
|
|
23
|
+
const os = require('os');
|
|
24
|
+
|
|
25
|
+
let passed = 0;
|
|
26
|
+
let failed = 0;
|
|
27
|
+
const errors = [];
|
|
28
|
+
|
|
29
|
+
async function test(name, fn) {
|
|
30
|
+
try {
|
|
31
|
+
await fn();
|
|
32
|
+
console.log(` \u2713 ${name}`);
|
|
33
|
+
passed++;
|
|
34
|
+
} catch (err) {
|
|
35
|
+
console.error(` \u2717 ${name}: ${err.stack || err.message}`);
|
|
36
|
+
failed++;
|
|
37
|
+
errors.push({ name, error: err.message });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const {
|
|
42
|
+
reconfigure,
|
|
43
|
+
RoomodesSlugDivergenceError,
|
|
44
|
+
ManifestNotFoundError,
|
|
45
|
+
ReconfigureYesRejectedError,
|
|
46
|
+
YES_REJECT_MESSAGE,
|
|
47
|
+
PROFILE_HISTORY_CAP,
|
|
48
|
+
listTouchedFiles,
|
|
49
|
+
appendProfileHistory
|
|
50
|
+
} = require('../lib/reconfigure');
|
|
51
|
+
const { setProfile, getProfile } = require('../lib/profile');
|
|
52
|
+
const installer = require('../lib/installer');
|
|
53
|
+
const { getAgent } = require('../lib/agents');
|
|
54
|
+
|
|
55
|
+
function mktemp() {
|
|
56
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), 'ma-agents-reconfigure-test-'));
|
|
57
|
+
}
|
|
58
|
+
function cleanup(dir) { try { fs.rmSync(dir, { recursive: true, force: true }); } catch {} }
|
|
59
|
+
|
|
60
|
+
function silenceLogs(fn) {
|
|
61
|
+
return async (...args) => {
|
|
62
|
+
const origLog = console.log;
|
|
63
|
+
const origErr = console.error;
|
|
64
|
+
const origWarn = console.warn;
|
|
65
|
+
console.log = () => {};
|
|
66
|
+
console.error = () => {};
|
|
67
|
+
console.warn = () => {};
|
|
68
|
+
try { return await fn(...args); }
|
|
69
|
+
finally {
|
|
70
|
+
console.log = origLog;
|
|
71
|
+
console.error = origErr;
|
|
72
|
+
console.warn = origWarn;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Build a prompts() stub that returns the given sequence of responses,
|
|
79
|
+
* one per prompt call. Throws if the caller asks for more prompts than
|
|
80
|
+
* responses were scripted.
|
|
81
|
+
*/
|
|
82
|
+
function scriptedPrompts(responses) {
|
|
83
|
+
let i = 0;
|
|
84
|
+
const stub = async () => {
|
|
85
|
+
if (i >= responses.length) {
|
|
86
|
+
throw new Error(`scriptedPrompts exhausted after ${responses.length} calls`);
|
|
87
|
+
}
|
|
88
|
+
return responses[i++];
|
|
89
|
+
};
|
|
90
|
+
stub.calls = () => i;
|
|
91
|
+
stub.messages = [];
|
|
92
|
+
return stub;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Install a minimal project scaffold: .ma-agents.json with a given profile and
|
|
97
|
+
* agent list. Does NOT run the real installer (tests isolate the reconfigure
|
|
98
|
+
* orchestration). Returns the tmp dir.
|
|
99
|
+
*/
|
|
100
|
+
function setupProject({ profile = 'standard', agents = ['claude-code'] } = {}) {
|
|
101
|
+
const dir = mktemp();
|
|
102
|
+
const manifest = {
|
|
103
|
+
manifestVersion: '1.2.0',
|
|
104
|
+
agent: agents[0],
|
|
105
|
+
agents,
|
|
106
|
+
scope: 'project',
|
|
107
|
+
profile,
|
|
108
|
+
skills: {}
|
|
109
|
+
};
|
|
110
|
+
fs.writeFileSync(
|
|
111
|
+
path.join(dir, '.ma-agents.json'),
|
|
112
|
+
JSON.stringify(manifest, null, 2) + '\n',
|
|
113
|
+
'utf-8'
|
|
114
|
+
);
|
|
115
|
+
return dir;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
console.log('\n reconfigure tests (Story 21.10)\n');
|
|
119
|
+
|
|
120
|
+
async function runAll() {
|
|
121
|
+
// 8.1 --yes rejected
|
|
122
|
+
await test('8.1 --yes rejected with pinned message and ReconfigureYesRejectedError (AC #6)', silenceLogs(async () => {
|
|
123
|
+
const dir = setupProject();
|
|
124
|
+
try {
|
|
125
|
+
await assert.rejects(
|
|
126
|
+
() => reconfigure({ projectRoot: dir, argv: ['--yes'], promptsLib: scriptedPrompts([]) }),
|
|
127
|
+
(err) => err instanceof ReconfigureYesRejectedError && err.message === YES_REJECT_MESSAGE
|
|
128
|
+
);
|
|
129
|
+
// Manifest should be untouched.
|
|
130
|
+
const json = JSON.parse(fs.readFileSync(path.join(dir, '.ma-agents.json'), 'utf-8'));
|
|
131
|
+
assert.strictEqual(json.profile, 'standard');
|
|
132
|
+
assert.strictEqual(json.profileHistory, undefined);
|
|
133
|
+
} finally { cleanup(dir); }
|
|
134
|
+
}));
|
|
135
|
+
|
|
136
|
+
// 8.2 missing manifest
|
|
137
|
+
await test('8.2 missing .ma-agents.json → ManifestNotFoundError (AC #1)', silenceLogs(async () => {
|
|
138
|
+
const dir = mktemp();
|
|
139
|
+
try {
|
|
140
|
+
await assert.rejects(
|
|
141
|
+
() => reconfigure({ projectRoot: dir, argv: [], promptsLib: scriptedPrompts([]) }),
|
|
142
|
+
(err) => err instanceof ManifestNotFoundError
|
|
143
|
+
);
|
|
144
|
+
} finally { cleanup(dir); }
|
|
145
|
+
}));
|
|
146
|
+
|
|
147
|
+
// 8.3 same-value short-circuit
|
|
148
|
+
await test('8.3 same-value exits with "Profile unchanged:" and no writes (AC #4)', silenceLogs(async () => {
|
|
149
|
+
const dir = setupProject({ profile: 'standard' });
|
|
150
|
+
try {
|
|
151
|
+
const manifestPath = path.join(dir, '.ma-agents.json');
|
|
152
|
+
const originalBytes = fs.readFileSync(manifestPath);
|
|
153
|
+
const result = await reconfigure({
|
|
154
|
+
projectRoot: dir,
|
|
155
|
+
argv: [],
|
|
156
|
+
promptsLib: scriptedPrompts([{ chosenProfile: 'standard' }])
|
|
157
|
+
});
|
|
158
|
+
assert.strictEqual(result.status, 'unchanged');
|
|
159
|
+
// No writes → byte-identical manifest.
|
|
160
|
+
assert.deepStrictEqual(fs.readFileSync(manifestPath), originalBytes);
|
|
161
|
+
} finally { cleanup(dir); }
|
|
162
|
+
}));
|
|
163
|
+
|
|
164
|
+
// 8.4 profile flip re-stamps injection files + .roomodes
|
|
165
|
+
await test('8.4 profile flip standard→on-prem re-stamps injection files (AC #5)', silenceLogs(async () => {
|
|
166
|
+
const dir = setupProject({ profile: 'standard', agents: ['claude-code'] });
|
|
167
|
+
try {
|
|
168
|
+
// Pre-stamp the .claude/CLAUDE.md with standard content via the installer
|
|
169
|
+
// helper so we have a realistic starting state.
|
|
170
|
+
const agent = getAgent('claude-code');
|
|
171
|
+
await installer._testUpdateAgentInstructions(agent, dir, { yesMode: true });
|
|
172
|
+
const claudePath = path.join(dir, '.claude', 'CLAUDE.md');
|
|
173
|
+
const before = fs.readFileSync(claudePath, 'utf-8');
|
|
174
|
+
|
|
175
|
+
const result = await reconfigure({
|
|
176
|
+
projectRoot: dir,
|
|
177
|
+
argv: [],
|
|
178
|
+
promptsLib: scriptedPrompts([
|
|
179
|
+
{ chosenProfile: 'on-prem' }, // profile choice
|
|
180
|
+
{ proceed: true } // Continue?
|
|
181
|
+
])
|
|
182
|
+
});
|
|
183
|
+
assert.strictEqual(result.status, 'reconfigured');
|
|
184
|
+
const after = fs.readFileSync(claudePath, 'utf-8');
|
|
185
|
+
assert.notStrictEqual(after, before, 'file must change');
|
|
186
|
+
// On-prem content ships onprem-specific strings (e.g. /no_think).
|
|
187
|
+
assert.match(after, /\/no_think|str_replace_editor|~\/\.claude\/|Local-LLM|Nemotron|on-prem/i,
|
|
188
|
+
'on-prem profile output should mention a local-LLM-specific token');
|
|
189
|
+
assert.strictEqual(getProfile(dir), 'on-prem');
|
|
190
|
+
} finally { cleanup(dir); }
|
|
191
|
+
}));
|
|
192
|
+
|
|
193
|
+
// 8.5 two-step confirmation decline
|
|
194
|
+
await test('8.5 declining "Continue?" aborts with zero writes (AC #7)', silenceLogs(async () => {
|
|
195
|
+
const dir = setupProject({ profile: 'standard', agents: ['claude-code'] });
|
|
196
|
+
try {
|
|
197
|
+
// Seed .claude/CLAUDE.md with standard content so a real re-stamp would
|
|
198
|
+
// leave a visible diff.
|
|
199
|
+
const agent = getAgent('claude-code');
|
|
200
|
+
await installer._testUpdateAgentInstructions(agent, dir, { yesMode: true });
|
|
201
|
+
const claudePath = path.join(dir, '.claude', 'CLAUDE.md');
|
|
202
|
+
const before = fs.readFileSync(claudePath);
|
|
203
|
+
const manifestBefore = fs.readFileSync(path.join(dir, '.ma-agents.json'));
|
|
204
|
+
|
|
205
|
+
const result = await reconfigure({
|
|
206
|
+
projectRoot: dir,
|
|
207
|
+
argv: [],
|
|
208
|
+
promptsLib: scriptedPrompts([
|
|
209
|
+
{ chosenProfile: 'on-prem' },
|
|
210
|
+
{ proceed: false }
|
|
211
|
+
])
|
|
212
|
+
});
|
|
213
|
+
assert.strictEqual(result.status, 'aborted');
|
|
214
|
+
assert.deepStrictEqual(fs.readFileSync(claudePath), before, 'CLAUDE.md must be untouched');
|
|
215
|
+
assert.deepStrictEqual(
|
|
216
|
+
fs.readFileSync(path.join(dir, '.ma-agents.json')),
|
|
217
|
+
manifestBefore,
|
|
218
|
+
'manifest must be untouched when user declines'
|
|
219
|
+
);
|
|
220
|
+
} finally { cleanup(dir); }
|
|
221
|
+
}));
|
|
222
|
+
|
|
223
|
+
// 8.6 backup files are written
|
|
224
|
+
await test('8.6 backup files created at <target>.backup-<timestamp> (AC #8)', silenceLogs(async () => {
|
|
225
|
+
const dir = setupProject({ profile: 'standard', agents: ['claude-code'] });
|
|
226
|
+
try {
|
|
227
|
+
const agent = getAgent('claude-code');
|
|
228
|
+
await installer._testUpdateAgentInstructions(agent, dir, { yesMode: true });
|
|
229
|
+
await reconfigure({
|
|
230
|
+
projectRoot: dir,
|
|
231
|
+
argv: [],
|
|
232
|
+
promptsLib: scriptedPrompts([
|
|
233
|
+
{ chosenProfile: 'on-prem' },
|
|
234
|
+
{ proceed: true }
|
|
235
|
+
])
|
|
236
|
+
});
|
|
237
|
+
const claudeDir = path.join(dir, '.claude');
|
|
238
|
+
const backups = fs.readdirSync(claudeDir).filter(f => /CLAUDE\.md\.backup-/.test(f));
|
|
239
|
+
assert.ok(backups.length >= 1, `expected at least one backup file, got ${backups.join(',')}`);
|
|
240
|
+
const backupName = backups[0];
|
|
241
|
+
// Canonical format: <target>.backup-<YYYY-MM-DDTHH-mm-ssZ>
|
|
242
|
+
assert.match(backupName, /^CLAUDE\.md\.backup-\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}Z(\.\d+)?$/);
|
|
243
|
+
} finally { cleanup(dir); }
|
|
244
|
+
}));
|
|
245
|
+
|
|
246
|
+
// 8.7 slug divergence
|
|
247
|
+
await test('8.7 RoomodesSlugDivergenceError raised when user edited owned slug; --force bypasses (AC #9)', silenceLogs(async () => {
|
|
248
|
+
const dir = setupProject({ profile: 'standard', agents: ['roo-code'] });
|
|
249
|
+
try {
|
|
250
|
+
// Write a .roomodes whose `bmad-architect` entry has a fabricated
|
|
251
|
+
// `whenToUse` that doesn't match the shipped template.
|
|
252
|
+
const yaml = require('js-yaml');
|
|
253
|
+
const divergent = {
|
|
254
|
+
customModes: [
|
|
255
|
+
{ slug: 'bmad-architect', name: 'Hacked', whenToUse: 'different', roleDefinition: 'x', groups: ['read'], customInstructions: 'x' }
|
|
256
|
+
]
|
|
257
|
+
};
|
|
258
|
+
fs.writeFileSync(path.join(dir, '.roomodes'), yaml.dump(divergent), 'utf-8');
|
|
259
|
+
|
|
260
|
+
await assert.rejects(
|
|
261
|
+
() => reconfigure({
|
|
262
|
+
projectRoot: dir,
|
|
263
|
+
argv: [],
|
|
264
|
+
promptsLib: scriptedPrompts([{ chosenProfile: 'on-prem' }])
|
|
265
|
+
}),
|
|
266
|
+
(err) => err instanceof RoomodesSlugDivergenceError && err.divergentSlugs.includes('bmad-architect')
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
// With --force-roomodes-overwrite, proceed through (confirm continue).
|
|
270
|
+
const result = await reconfigure({
|
|
271
|
+
projectRoot: dir,
|
|
272
|
+
argv: ['--force-roomodes-overwrite'],
|
|
273
|
+
promptsLib: scriptedPrompts([
|
|
274
|
+
{ chosenProfile: 'on-prem' },
|
|
275
|
+
{ proceed: true }
|
|
276
|
+
])
|
|
277
|
+
});
|
|
278
|
+
assert.strictEqual(result.status, 'reconfigured');
|
|
279
|
+
} finally { cleanup(dir); }
|
|
280
|
+
}));
|
|
281
|
+
|
|
282
|
+
// 8.8 dual-file drift (Cline)
|
|
283
|
+
await test('8.8 ClinerulesDualFileDriftError raised; no --force override (AC #10)', silenceLogs(async () => {
|
|
284
|
+
const dir = setupProject({ profile: 'standard', agents: ['cline'] });
|
|
285
|
+
try {
|
|
286
|
+
// Seed with identical content first.
|
|
287
|
+
const agent = getAgent('cline');
|
|
288
|
+
await installer._testUpdateAgentInstructions(agent, dir, { yesMode: true });
|
|
289
|
+
// Mutate ONE of the two files inside the markers to induce drift.
|
|
290
|
+
const fileA = path.join(dir, '.cline', 'clinerules.md');
|
|
291
|
+
const content = fs.readFileSync(fileA, 'utf-8');
|
|
292
|
+
const mutated = content.replace(
|
|
293
|
+
/<!-- MA-AGENTS-START -->[\s\S]*?<!-- MA-AGENTS-END -->/,
|
|
294
|
+
'<!-- MA-AGENTS-START -->\nCLI-NO-MATCHY\n<!-- MA-AGENTS-END -->'
|
|
295
|
+
);
|
|
296
|
+
fs.writeFileSync(fileA, mutated, 'utf-8');
|
|
297
|
+
|
|
298
|
+
await assert.rejects(
|
|
299
|
+
() => reconfigure({
|
|
300
|
+
projectRoot: dir,
|
|
301
|
+
argv: [],
|
|
302
|
+
promptsLib: scriptedPrompts([{ chosenProfile: 'on-prem' }])
|
|
303
|
+
}),
|
|
304
|
+
(err) => err && err.name === 'ClinerulesDualFileDriftError'
|
|
305
|
+
);
|
|
306
|
+
} finally { cleanup(dir); }
|
|
307
|
+
}));
|
|
308
|
+
|
|
309
|
+
// 8.9 profileHistory append + cap
|
|
310
|
+
await test('8.9 profileHistory appends; missing-field creation; 20-cap evicts oldest (AC #11)', silenceLogs(async () => {
|
|
311
|
+
const dir = setupProject({ profile: 'standard', agents: ['claude-code'] });
|
|
312
|
+
try {
|
|
313
|
+
// First reconfigure creates the field.
|
|
314
|
+
await reconfigure({
|
|
315
|
+
projectRoot: dir,
|
|
316
|
+
argv: [],
|
|
317
|
+
promptsLib: scriptedPrompts([
|
|
318
|
+
{ chosenProfile: 'on-prem' },
|
|
319
|
+
{ proceed: true }
|
|
320
|
+
])
|
|
321
|
+
});
|
|
322
|
+
let manifest = JSON.parse(fs.readFileSync(path.join(dir, '.ma-agents.json'), 'utf-8'));
|
|
323
|
+
assert.ok(Array.isArray(manifest.profileHistory));
|
|
324
|
+
assert.strictEqual(manifest.profileHistory.length, 1);
|
|
325
|
+
assert.deepStrictEqual(
|
|
326
|
+
{ from: manifest.profileHistory[0].from, to: manifest.profileHistory[0].to, source: manifest.profileHistory[0].source },
|
|
327
|
+
{ from: 'standard', to: 'on-prem', source: 'reconfigure' }
|
|
328
|
+
);
|
|
329
|
+
assert.ok(typeof manifest.profileHistory[0].date === 'string');
|
|
330
|
+
|
|
331
|
+
// Now directly invoke appendProfileHistory 25 more times to exercise the cap.
|
|
332
|
+
for (let i = 0; i < 25; i++) {
|
|
333
|
+
appendProfileHistory(dir, {
|
|
334
|
+
date: new Date(2027, 0, i + 1).toISOString(),
|
|
335
|
+
from: 'standard',
|
|
336
|
+
to: 'on-prem',
|
|
337
|
+
source: 'reconfigure'
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
manifest = JSON.parse(fs.readFileSync(path.join(dir, '.ma-agents.json'), 'utf-8'));
|
|
341
|
+
assert.strictEqual(
|
|
342
|
+
manifest.profileHistory.length,
|
|
343
|
+
PROFILE_HISTORY_CAP,
|
|
344
|
+
`history should be capped at ${PROFILE_HISTORY_CAP}`
|
|
345
|
+
);
|
|
346
|
+
// Oldest-first eviction: the first entry we wrote (the real reconfigure
|
|
347
|
+
// result) must be gone. The newest entry's date must survive.
|
|
348
|
+
const dates = manifest.profileHistory.map(e => e.date);
|
|
349
|
+
assert.ok(!dates.includes(manifest.profileHistory[0].date === manifest.profileHistory[0].date && undefined));
|
|
350
|
+
const newest = new Date(2027, 0, 25).toISOString();
|
|
351
|
+
assert.ok(dates.includes(newest), 'newest entry must be preserved');
|
|
352
|
+
} finally { cleanup(dir); }
|
|
353
|
+
}));
|
|
354
|
+
|
|
355
|
+
// 8.10 round-trip standard → on-prem → standard
|
|
356
|
+
await test('8.10 round-trip standard → on-prem → standard lands on standard content (NFR46)', silenceLogs(async () => {
|
|
357
|
+
const dir = setupProject({ profile: 'standard', agents: ['claude-code'] });
|
|
358
|
+
try {
|
|
359
|
+
const agent = getAgent('claude-code');
|
|
360
|
+
await installer._testUpdateAgentInstructions(agent, dir, { yesMode: true });
|
|
361
|
+
const baselineStandard = fs.readFileSync(path.join(dir, '.claude', 'CLAUDE.md'), 'utf-8');
|
|
362
|
+
|
|
363
|
+
await reconfigure({
|
|
364
|
+
projectRoot: dir,
|
|
365
|
+
argv: [],
|
|
366
|
+
promptsLib: scriptedPrompts([
|
|
367
|
+
{ chosenProfile: 'on-prem' },
|
|
368
|
+
{ proceed: true }
|
|
369
|
+
])
|
|
370
|
+
});
|
|
371
|
+
assert.strictEqual(getProfile(dir), 'on-prem');
|
|
372
|
+
|
|
373
|
+
await reconfigure({
|
|
374
|
+
projectRoot: dir,
|
|
375
|
+
argv: [],
|
|
376
|
+
promptsLib: scriptedPrompts([
|
|
377
|
+
{ chosenProfile: 'standard' },
|
|
378
|
+
{ proceed: true }
|
|
379
|
+
])
|
|
380
|
+
});
|
|
381
|
+
assert.strictEqual(getProfile(dir), 'standard');
|
|
382
|
+
const after = fs.readFileSync(path.join(dir, '.claude', 'CLAUDE.md'), 'utf-8');
|
|
383
|
+
// Compare MARKER block content only, since outside-markers content may
|
|
384
|
+
// accumulate nothing here but we want a robust assertion.
|
|
385
|
+
const extract = (s) => {
|
|
386
|
+
const m = s.match(/<!-- MA-AGENTS-START -->([\s\S]*?)<!-- MA-AGENTS-END -->/);
|
|
387
|
+
return m ? m[1] : null;
|
|
388
|
+
};
|
|
389
|
+
assert.strictEqual(extract(after), extract(baselineStandard),
|
|
390
|
+
'post round-trip marker block must equal the original standard-profile stamp');
|
|
391
|
+
|
|
392
|
+
// Two history entries.
|
|
393
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(dir, '.ma-agents.json'), 'utf-8'));
|
|
394
|
+
assert.strictEqual(manifest.profileHistory.length, 2);
|
|
395
|
+
assert.strictEqual(manifest.profileHistory[0].to, 'on-prem');
|
|
396
|
+
assert.strictEqual(manifest.profileHistory[1].to, 'standard');
|
|
397
|
+
} finally { cleanup(dir); }
|
|
398
|
+
}));
|
|
399
|
+
|
|
400
|
+
// 8.11 prompt text references current value
|
|
401
|
+
await test('8.11 prompt message references current profile value (AC #2)', silenceLogs(async () => {
|
|
402
|
+
const dir = setupProject({ profile: 'on-prem', agents: ['claude-code'] });
|
|
403
|
+
try {
|
|
404
|
+
let seenMessage = null;
|
|
405
|
+
let seenInitial = null;
|
|
406
|
+
const stub = async (opts) => {
|
|
407
|
+
if (opts && opts.type === 'select' && !seenMessage) {
|
|
408
|
+
seenMessage = opts.message;
|
|
409
|
+
seenInitial = opts.initial;
|
|
410
|
+
return { chosenProfile: 'on-prem' }; // same-value → early exit
|
|
411
|
+
}
|
|
412
|
+
return { proceed: false };
|
|
413
|
+
};
|
|
414
|
+
await reconfigure({ projectRoot: dir, argv: [], promptsLib: stub });
|
|
415
|
+
assert.match(seenMessage || '', /Current profile: on-prem\. Change to\?/);
|
|
416
|
+
// AC: default-highlighted option is the persisted value → initial index 0 (on-prem row).
|
|
417
|
+
assert.strictEqual(seenInitial, 0);
|
|
418
|
+
} finally { cleanup(dir); }
|
|
419
|
+
}));
|
|
420
|
+
|
|
421
|
+
// listTouchedFiles smoke
|
|
422
|
+
await test('listTouchedFiles returns instructionFiles + extraInstructionTemplates, sorted', () => {
|
|
423
|
+
const files = listTouchedFiles('/tmp/x', [getAgent('roo-code')]);
|
|
424
|
+
assert.ok(files.includes('.roomodes'));
|
|
425
|
+
assert.ok(files.includes('.roo/rules/00-ma-agents.md'));
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
runAll().then(() => {
|
|
430
|
+
console.log(`\n ${passed} passed, ${failed} failed\n`);
|
|
431
|
+
if (failed > 0) {
|
|
432
|
+
console.error('FAILURES:');
|
|
433
|
+
errors.forEach(e => console.error(` - ${e.name}: ${e.error}`));
|
|
434
|
+
process.exit(1);
|
|
435
|
+
}
|
|
436
|
+
});
|