ma-agents 3.0.1 → 3.2.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/.opencode/skills/.ma-agents.json +99 -99
- package/README.md +48 -2
- package/bin/cli.js +546 -2
- package/lib/bmad-extension/module-help.csv +8 -4
- package/lib/bmad-extension/skills/add-sprint/SKILL.md +126 -40
- package/lib/bmad-extension/skills/add-to-sprint/SKILL.md +116 -142
- package/lib/bmad-extension/skills/cleanup-done/.gitkeep +0 -0
- package/lib/bmad-extension/skills/cleanup-done/SKILL.md +159 -0
- package/lib/bmad-extension/skills/cleanup-done/bmad-skill-manifest.yaml +3 -0
- package/lib/bmad-extension/skills/create-bug-story/SKILL.md +75 -7
- package/lib/bmad-extension/skills/generate-backlog/SKILL.md +183 -0
- package/lib/bmad-extension/skills/generate-backlog/bmad-skill-manifest.yaml +3 -0
- package/lib/bmad-extension/skills/modify-sprint/SKILL.md +63 -0
- package/lib/bmad-extension/skills/prioritize-backlog/.gitkeep +0 -0
- package/lib/bmad-extension/skills/prioritize-backlog/SKILL.md +195 -0
- package/lib/bmad-extension/skills/prioritize-backlog/bmad-skill-manifest.yaml +3 -0
- package/lib/bmad-extension/skills/remove-from-sprint/.gitkeep +0 -0
- package/lib/bmad-extension/skills/remove-from-sprint/SKILL.md +163 -0
- package/lib/bmad-extension/skills/remove-from-sprint/bmad-skill-manifest.yaml +3 -0
- package/lib/bmad-extension/skills/sprint-status-view/SKILL.md +199 -138
- package/lib/bmad-extension/workflows/add-sprint/workflow.md +129 -39
- package/lib/bmad-extension/workflows/add-to-sprint/workflow.md +3 -205
- package/lib/bmad-extension/workflows/modify-sprint/workflow.md +5 -0
- package/lib/bmad-extension/workflows/sprint-status-view/workflow.md +3 -192
- package/lib/installer.js +109 -2
- package/lib/templates/project-context.template.md +1 -1
- package/package.json +2 -2
- package/test/cicd-remote-mode.test.js +224 -0
- package/test/config-layout.test.js +230 -0
- package/test/config-lost-on-update.test.js +363 -0
- package/test/config-storage.test.js +275 -0
- package/test/cross-repo-validation.test.js +201 -0
- package/test/generate-project-context.test.js +148 -2
- package/test/portable-paths.test.js +268 -0
- package/test/repo-layout.test.js +246 -0
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Tests for Story 16.1: Repository Layout Wizard
|
|
4
|
+
* Tests collectRepoLayout() function and its sub-flows
|
|
5
|
+
*/
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
const assert = require('assert');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const os = require('os');
|
|
12
|
+
|
|
13
|
+
let passed = 0;
|
|
14
|
+
let failed = 0;
|
|
15
|
+
const errors = [];
|
|
16
|
+
|
|
17
|
+
function test(name, fn) {
|
|
18
|
+
try {
|
|
19
|
+
fn();
|
|
20
|
+
console.log(` \u2713 ${name}`);
|
|
21
|
+
passed++;
|
|
22
|
+
} catch (err) {
|
|
23
|
+
console.error(` \u2717 ${name}: ${err.message}`);
|
|
24
|
+
failed++;
|
|
25
|
+
errors.push({ name, error: err.message });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function testAsync(name, fn) {
|
|
30
|
+
try {
|
|
31
|
+
await fn();
|
|
32
|
+
console.log(` \u2713 ${name}`);
|
|
33
|
+
passed++;
|
|
34
|
+
} catch (err) {
|
|
35
|
+
console.error(` \u2717 ${name}: ${err.message}`);
|
|
36
|
+
failed++;
|
|
37
|
+
errors.push({ name, error: err.message });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ─── Load module ─────────────────────────────────────────────────────────────
|
|
42
|
+
let collectRepoLayout;
|
|
43
|
+
try {
|
|
44
|
+
({ collectRepoLayout } = require('../bin/cli.js'));
|
|
45
|
+
if (typeof collectRepoLayout !== 'function') {
|
|
46
|
+
throw new Error('collectRepoLayout is not exported from cli.js');
|
|
47
|
+
}
|
|
48
|
+
} catch (e) {
|
|
49
|
+
console.error('\n FATAL: Cannot load collectRepoLayout from cli.js');
|
|
50
|
+
console.error(' ', e.message);
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ─── Task 5.1: --yes flag returns both as same mode ─────────────────────────
|
|
55
|
+
console.log('\nTask 5.1 — collectRepoLayout with --yes flag (no env vars)');
|
|
56
|
+
|
|
57
|
+
async function runTests() {
|
|
58
|
+
await testAsync('--yes returns both concerns as same mode', async () => {
|
|
59
|
+
// Clear env vars to ensure clean test
|
|
60
|
+
const origKb = process.env.MA_KNOWLEDGEBASE_PATH;
|
|
61
|
+
const origSp = process.env.MA_SPRINT_PATH;
|
|
62
|
+
delete process.env.MA_KNOWLEDGEBASE_PATH;
|
|
63
|
+
delete process.env.MA_SPRINT_PATH;
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const result = await collectRepoLayout({ yes: true });
|
|
67
|
+
assert.deepStrictEqual(result.knowledgebase, { mode: 'same', path: '.' });
|
|
68
|
+
assert.deepStrictEqual(result.sprintManagement, { mode: 'same', path: '.' });
|
|
69
|
+
} finally {
|
|
70
|
+
if (origKb !== undefined) process.env.MA_KNOWLEDGEBASE_PATH = origKb;
|
|
71
|
+
if (origSp !== undefined) process.env.MA_SPRINT_PATH = origSp;
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// ─── Task 5.2: --yes + env vars returns local mode with resolved paths ────
|
|
76
|
+
console.log('\nTask 5.2 — collectRepoLayout with --yes + env vars');
|
|
77
|
+
|
|
78
|
+
await testAsync('--yes + MA_KNOWLEDGEBASE_PATH returns local mode with resolved path', async () => {
|
|
79
|
+
const origKb = process.env.MA_KNOWLEDGEBASE_PATH;
|
|
80
|
+
const origSp = process.env.MA_SPRINT_PATH;
|
|
81
|
+
process.env.MA_KNOWLEDGEBASE_PATH = '../kb-repo';
|
|
82
|
+
delete process.env.MA_SPRINT_PATH;
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
const result = await collectRepoLayout({ yes: true });
|
|
86
|
+
assert.strictEqual(result.knowledgebase.mode, 'local');
|
|
87
|
+
assert.strictEqual(result.knowledgebase.path, path.resolve('../kb-repo'));
|
|
88
|
+
assert.deepStrictEqual(result.sprintManagement, { mode: 'same', path: '.' });
|
|
89
|
+
} finally {
|
|
90
|
+
if (origKb !== undefined) process.env.MA_KNOWLEDGEBASE_PATH = origKb;
|
|
91
|
+
else delete process.env.MA_KNOWLEDGEBASE_PATH;
|
|
92
|
+
if (origSp !== undefined) process.env.MA_SPRINT_PATH = origSp;
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
await testAsync('--yes + MA_SPRINT_PATH returns local mode with resolved path', async () => {
|
|
97
|
+
const origKb = process.env.MA_KNOWLEDGEBASE_PATH;
|
|
98
|
+
const origSp = process.env.MA_SPRINT_PATH;
|
|
99
|
+
delete process.env.MA_KNOWLEDGEBASE_PATH;
|
|
100
|
+
process.env.MA_SPRINT_PATH = '/absolute/sprint/path';
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
const result = await collectRepoLayout({ yes: true });
|
|
104
|
+
assert.deepStrictEqual(result.knowledgebase, { mode: 'same', path: '.' });
|
|
105
|
+
assert.strictEqual(result.sprintManagement.mode, 'local');
|
|
106
|
+
assert.strictEqual(result.sprintManagement.path, path.resolve('/absolute/sprint/path'));
|
|
107
|
+
} finally {
|
|
108
|
+
if (origKb !== undefined) process.env.MA_KNOWLEDGEBASE_PATH = origKb;
|
|
109
|
+
if (origSp !== undefined) process.env.MA_SPRINT_PATH = origSp;
|
|
110
|
+
else delete process.env.MA_SPRINT_PATH;
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
await testAsync('--yes + both env vars returns local mode for both', async () => {
|
|
115
|
+
const origKb = process.env.MA_KNOWLEDGEBASE_PATH;
|
|
116
|
+
const origSp = process.env.MA_SPRINT_PATH;
|
|
117
|
+
process.env.MA_KNOWLEDGEBASE_PATH = '../shared-docs';
|
|
118
|
+
process.env.MA_SPRINT_PATH = '../shared-sprint';
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
const result = await collectRepoLayout({ yes: true });
|
|
122
|
+
assert.strictEqual(result.knowledgebase.mode, 'local');
|
|
123
|
+
assert.strictEqual(result.knowledgebase.path, path.resolve('../shared-docs'));
|
|
124
|
+
assert.strictEqual(result.sprintManagement.mode, 'local');
|
|
125
|
+
assert.strictEqual(result.sprintManagement.path, path.resolve('../shared-sprint'));
|
|
126
|
+
} finally {
|
|
127
|
+
if (origKb !== undefined) process.env.MA_KNOWLEDGEBASE_PATH = origKb;
|
|
128
|
+
else delete process.env.MA_KNOWLEDGEBASE_PATH;
|
|
129
|
+
if (origSp !== undefined) process.env.MA_SPRINT_PATH = origSp;
|
|
130
|
+
else delete process.env.MA_SPRINT_PATH;
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// ─── Task 5.3: Local path validation ──────────────────────────────────────
|
|
135
|
+
console.log('\nTask 5.3 — Local path validation (integration via spawnSync)');
|
|
136
|
+
|
|
137
|
+
const { spawnSync } = require('child_process');
|
|
138
|
+
const CLI_PATH = path.join(__dirname, '..', 'bin', 'cli.js');
|
|
139
|
+
|
|
140
|
+
// These tests verify behavior by checking the collectRepoLayout function directly
|
|
141
|
+
// The local path and remote flows require interactive prompts, so we test them
|
|
142
|
+
// through the --yes code path and validate the logic structurally.
|
|
143
|
+
|
|
144
|
+
test('collectRepoLayout exists and is a function', () => {
|
|
145
|
+
assert.strictEqual(typeof collectRepoLayout, 'function');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test('collectRepoLayout returns object with knowledgebase and sprintManagement keys', async () => {
|
|
149
|
+
delete process.env.MA_KNOWLEDGEBASE_PATH;
|
|
150
|
+
delete process.env.MA_SPRINT_PATH;
|
|
151
|
+
const result = await collectRepoLayout({ yes: true });
|
|
152
|
+
assert.ok(result.knowledgebase, 'should have knowledgebase key');
|
|
153
|
+
assert.ok(result.sprintManagement, 'should have sprintManagement key');
|
|
154
|
+
assert.ok(result.knowledgebase.mode, 'knowledgebase should have mode');
|
|
155
|
+
assert.ok(result.knowledgebase.path !== undefined, 'knowledgebase should have path');
|
|
156
|
+
assert.ok(result.sprintManagement.mode, 'sprintManagement should have mode');
|
|
157
|
+
assert.ok(result.sprintManagement.path !== undefined, 'sprintManagement should have path');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// ─── Task 5.4: execFileSync called with correct args (structural check) ───
|
|
161
|
+
console.log('\nTask 5.4 — Remote flow uses execFileSync with array args');
|
|
162
|
+
|
|
163
|
+
test('collectRemotePath function exists in cli.js source', () => {
|
|
164
|
+
const src = fs.readFileSync(path.join(__dirname, '..', 'bin', 'cli.js'), 'utf-8');
|
|
165
|
+
assert.ok(src.includes("execFileSync('git', ['clone',"), 'Should use execFileSync with array args for git clone');
|
|
166
|
+
assert.ok(!src.includes('execSync(`git clone'), 'Should NOT use execSync with template literal for git clone');
|
|
167
|
+
assert.ok(!src.includes("execSync('git clone"), 'Should NOT use execSync with string concat for git clone');
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// ─── Task 5.5: Clone failure cleanup (structural check) ──────────────────
|
|
171
|
+
console.log('\nTask 5.5 — Clone failure: cleanup logic present');
|
|
172
|
+
|
|
173
|
+
test('clone failure cleanup removes partial directory', () => {
|
|
174
|
+
const src = fs.readFileSync(path.join(__dirname, '..', 'bin', 'cli.js'), 'utf-8');
|
|
175
|
+
assert.ok(src.includes('rmSync(destPath, { recursive: true, force: true })'),
|
|
176
|
+
'Should clean up partial clone directory on failure');
|
|
177
|
+
assert.ok(src.includes('existedBefore'),
|
|
178
|
+
'Should check if directory existed before clone attempt');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// ─── Task 5.6: Empty/whitespace input rejection (structural check) ────────
|
|
182
|
+
console.log('\nTask 5.6 — Empty/whitespace input rejection');
|
|
183
|
+
|
|
184
|
+
test('local path flow rejects empty input', () => {
|
|
185
|
+
const src = fs.readFileSync(path.join(__dirname, '..', 'bin', 'cli.js'), 'utf-8');
|
|
186
|
+
assert.ok(src.includes('Path cannot be empty'),
|
|
187
|
+
'Should have empty path rejection message');
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test('remote flow rejects empty git URL', () => {
|
|
191
|
+
const src = fs.readFileSync(path.join(__dirname, '..', 'bin', 'cli.js'), 'utf-8');
|
|
192
|
+
assert.ok(src.includes('Git URL cannot be empty'),
|
|
193
|
+
'Should have empty git URL rejection message');
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test('remote flow rejects empty destination path', () => {
|
|
197
|
+
const src = fs.readFileSync(path.join(__dirname, '..', 'bin', 'cli.js'), 'utf-8');
|
|
198
|
+
assert.ok(src.includes('Destination path cannot be empty'),
|
|
199
|
+
'Should have empty destination path rejection message');
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// ─── Task 5.7: Nested-repo warning (structural check) ────────────────────
|
|
203
|
+
console.log('\nTask 5.7 — Nested-repo warning');
|
|
204
|
+
|
|
205
|
+
test('warns when path is inside cwd', () => {
|
|
206
|
+
const src = fs.readFileSync(path.join(__dirname, '..', 'bin', 'cli.js'), 'utf-8');
|
|
207
|
+
assert.ok(src.includes('Nested repos may cause git confusion'),
|
|
208
|
+
'Should warn about nested repos');
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// ─── Integration: --yes mode with env vars via spawnSync ──────────────────
|
|
212
|
+
console.log('\nIntegration — --yes mode processes repo layout without prompts');
|
|
213
|
+
|
|
214
|
+
test('--yes mode completes without hanging (repo layout integrated)', () => {
|
|
215
|
+
// This test verifies the wizard completes with --yes flag
|
|
216
|
+
// which exercises the collectRepoLayout CI/CD path
|
|
217
|
+
const result = spawnSync(process.execPath, [CLI_PATH, 'install', '--yes'], {
|
|
218
|
+
timeout: 600000,
|
|
219
|
+
encoding: 'utf-8',
|
|
220
|
+
input: '',
|
|
221
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
222
|
+
cwd: path.join(__dirname, '..')
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
if (result.status === null) {
|
|
226
|
+
console.log(' SKIP: process timed out');
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
assert.strictEqual(
|
|
231
|
+
result.status,
|
|
232
|
+
0,
|
|
233
|
+
`Expected exit 0, got ${result.status}.\nstdout: ${(result.stdout || '').slice(0, 500)}\nstderr: ${(result.stderr || '').slice(0, 500)}`
|
|
234
|
+
);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// ─── Summary ──────────────────────────────────────────────────────────────
|
|
238
|
+
console.log(`\n${passed} passed, ${failed} failed`);
|
|
239
|
+
if (errors.length > 0) {
|
|
240
|
+
console.log('\nFailed tests:');
|
|
241
|
+
errors.forEach(e => console.log(` - ${e.name}: ${e.error}`));
|
|
242
|
+
}
|
|
243
|
+
if (failed > 0) process.exit(1);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
runTests();
|