ma-agents 2.20.3 → 2.22.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 +241 -0
- package/.opencode/skills/MANIFEST.yaml +254 -0
- package/.opencode/skills/ai-audit-trail/SKILL.md +23 -0
- package/.opencode/skills/auto-bug-detection/SKILL.md +169 -0
- package/.opencode/skills/cmake-best-practices/SKILL.md +64 -0
- package/.opencode/skills/cmake-best-practices/examples/cmake.md +59 -0
- package/.opencode/skills/code-documentation/SKILL.md +57 -0
- package/.opencode/skills/code-documentation/examples/cpp.md +29 -0
- package/.opencode/skills/code-documentation/examples/csharp.md +28 -0
- package/.opencode/skills/code-documentation/examples/javascript_typescript.md +28 -0
- package/.opencode/skills/code-documentation/examples/python.md +57 -0
- package/.opencode/skills/code-review/SKILL.md +43 -0
- package/.opencode/skills/commit-message/SKILL.md +79 -0
- package/.opencode/skills/cpp-best-practices/SKILL.md +234 -0
- package/.opencode/skills/cpp-best-practices/examples/modern-idioms.md +189 -0
- package/.opencode/skills/cpp-best-practices/examples/naming-and-organization.md +102 -0
- package/.opencode/skills/cpp-concurrency-safety/SKILL.md +60 -0
- package/.opencode/skills/cpp-concurrency-safety/examples/concurrency.md +73 -0
- package/.opencode/skills/cpp-const-correctness/SKILL.md +63 -0
- package/.opencode/skills/cpp-const-correctness/examples/const_correctness.md +54 -0
- package/.opencode/skills/cpp-memory-handling/SKILL.md +42 -0
- package/.opencode/skills/cpp-memory-handling/examples/modern-cpp.md +49 -0
- package/.opencode/skills/cpp-memory-handling/examples/smart-pointers.md +46 -0
- package/.opencode/skills/cpp-modern-composition/SKILL.md +64 -0
- package/.opencode/skills/cpp-modern-composition/examples/composition.md +51 -0
- package/.opencode/skills/cpp-robust-interfaces/SKILL.md +55 -0
- package/.opencode/skills/cpp-robust-interfaces/examples/interfaces.md +56 -0
- package/.opencode/skills/create-hardened-docker-skill/SKILL.md +637 -0
- package/.opencode/skills/create-hardened-docker-skill/scripts/create-all.sh +489 -0
- package/.opencode/skills/csharp-best-practices/SKILL.md +278 -0
- package/.opencode/skills/docker-hardening-verification/SKILL.md +28 -0
- package/.opencode/skills/docker-hardening-verification/scripts/verify-hardening.sh +39 -0
- package/.opencode/skills/docker-image-signing/SKILL.md +28 -0
- package/.opencode/skills/docker-image-signing/scripts/sign-image.sh +33 -0
- package/.opencode/skills/document-revision-history/SKILL.md +104 -0
- package/.opencode/skills/git-workflow-skill/SKILL.md +194 -0
- package/.opencode/skills/git-workflow-skill/hooks/commit-msg +61 -0
- package/.opencode/skills/git-workflow-skill/hooks/pre-commit +38 -0
- package/.opencode/skills/git-workflow-skill/hooks/prepare-commit-msg +56 -0
- package/.opencode/skills/git-workflow-skill/scripts/finish-feature.sh +192 -0
- package/.opencode/skills/git-workflow-skill/scripts/install-hooks.sh +55 -0
- package/.opencode/skills/git-workflow-skill/scripts/start-feature.sh +110 -0
- package/.opencode/skills/git-workflow-skill/scripts/validate-workflow.sh +229 -0
- package/.opencode/skills/js-ts-dependency-mgmt/SKILL.md +49 -0
- package/.opencode/skills/js-ts-dependency-mgmt/examples/dependency_mgmt.md +60 -0
- package/.opencode/skills/js-ts-security-skill/SKILL.md +64 -0
- package/.opencode/skills/js-ts-security-skill/scripts/verify-security.sh +136 -0
- package/.opencode/skills/logging-best-practices/SKILL.md +50 -0
- package/.opencode/skills/logging-best-practices/examples/cpp.md +36 -0
- package/.opencode/skills/logging-best-practices/examples/csharp.md +49 -0
- package/.opencode/skills/logging-best-practices/examples/javascript.md +77 -0
- package/.opencode/skills/logging-best-practices/examples/python.md +57 -0
- package/.opencode/skills/logging-best-practices/references/logging-standards.md +29 -0
- package/.opencode/skills/open-presentation/SKILL.md +35 -0
- package/.opencode/skills/opentelemetry-best-practices/SKILL.md +34 -0
- package/.opencode/skills/opentelemetry-best-practices/examples/go.md +32 -0
- package/.opencode/skills/opentelemetry-best-practices/examples/javascript.md +58 -0
- package/.opencode/skills/opentelemetry-best-practices/examples/python.md +37 -0
- package/.opencode/skills/opentelemetry-best-practices/references/otel-standards.md +37 -0
- package/.opencode/skills/python-best-practices/SKILL.md +385 -0
- package/.opencode/skills/python-dependency-mgmt/SKILL.md +42 -0
- package/.opencode/skills/python-dependency-mgmt/examples/dependency_mgmt.md +67 -0
- package/.opencode/skills/python-security-skill/SKILL.md +56 -0
- package/.opencode/skills/python-security-skill/examples/security.md +56 -0
- package/.opencode/skills/self-signed-cert/SKILL.md +42 -0
- package/.opencode/skills/self-signed-cert/scripts/generate-cert.ps1 +45 -0
- package/.opencode/skills/self-signed-cert/scripts/generate-cert.sh +43 -0
- package/.opencode/skills/skill-creator/SKILL.md +196 -0
- package/.opencode/skills/skill-creator/references/output-patterns.md +82 -0
- package/.opencode/skills/skill-creator/references/workflows.md +28 -0
- package/.opencode/skills/skill-creator/scripts/init_skill.py +208 -0
- package/.opencode/skills/skill-creator/scripts/package_skill.py +99 -0
- package/.opencode/skills/skill-creator/scripts/quick_validate.py +113 -0
- package/.opencode/skills/story-status-lookup/SKILL.md +78 -0
- package/.opencode/skills/test-accompanied-development/SKILL.md +50 -0
- package/.opencode/skills/test-generator/SKILL.md +65 -0
- package/.opencode/skills/vercel-react-best-practices/SKILL.md +109 -0
- package/.opencode/skills/verify-hardened-docker-skill/SKILL.md +442 -0
- package/.opencode/skills/verify-hardened-docker-skill/scripts/verify-docker-hardening.sh +439 -0
- package/AiAudit.md +5 -0
- package/QUICK_START.md +11 -5
- package/README.md +52 -1
- package/bin/cli.js +31 -4
- package/docs/BMAD_AI_Development_Training.pptx +0 -0
- package/docs/technical-notes/context-persistence-research.md +434 -0
- package/docs/technical-notes/enforcement-hooks-research.md +415 -0
- package/lib/agents.js +34 -0
- package/lib/bmad-extension/agents/bmm-architect.customize.yaml +5 -0
- package/lib/bmad-extension/agents/bmm-bmad-master.customize.yaml +5 -0
- package/lib/bmad-extension/agents/bmm-cyber.customize.yaml +30 -0
- package/lib/bmad-extension/agents/bmm-dev.customize.yaml +5 -0
- package/lib/bmad-extension/agents/bmm-devops.customize.yaml +30 -0
- package/lib/bmad-extension/agents/bmm-mil498.customize.yaml +42 -0
- package/lib/bmad-extension/agents/bmm-pm.customize.yaml +5 -0
- package/lib/bmad-extension/agents/bmm-qa.customize.yaml +5 -0
- package/lib/bmad-extension/agents/bmm-sm.customize.yaml +5 -0
- package/lib/bmad-extension/agents/bmm-sre.customize.yaml +30 -0
- package/lib/bmad-extension/agents/bmm-tech-writer.customize.yaml +5 -0
- package/lib/bmad-extension/agents/bmm-ux-designer.customize.yaml +5 -0
- package/lib/bmad-extension/module-help.csv +7 -0
- package/lib/bmad-extension/module.yaml +3 -0
- package/lib/bmad-extension/workflows/add-sprint/workflow.md +112 -0
- package/lib/bmad-extension/workflows/add-to-sprint/workflow.md +206 -0
- package/lib/bmad-extension/workflows/create-bug-story/workflow.md +186 -0
- package/lib/bmad-extension/workflows/modify-sprint/workflow.md +250 -0
- package/lib/bmad-extension/workflows/project-context-expansion/workflow.md +229 -0
- package/lib/bmad-extension/workflows/sprint-status-view/workflow.md +193 -0
- package/lib/bmad.js +168 -36
- package/lib/hooks/claude-code/verify-manifest.js +56 -0
- package/lib/installer.js +282 -1
- package/lib/methodology/BMAD_AI_Development_Training.pptx +0 -0
- package/lib/methodology/version.json +7 -0
- package/lib/skill-authoring.js +732 -0
- package/lib/templates/project-context.template.md +47 -0
- package/opencode.json +8 -0
- package/package.json +2 -2
- package/skills/auto-bug-detection/SKILL.md +165 -0
- package/skills/auto-bug-detection/skill.json +8 -0
- package/skills/code-review/SKILL.md +40 -0
- package/skills/cpp-best-practices/SKILL.md +230 -0
- package/skills/cpp-best-practices/examples/modern-idioms.md +189 -0
- package/skills/cpp-best-practices/examples/naming-and-organization.md +102 -0
- package/skills/cpp-best-practices/skill.json +25 -0
- package/skills/csharp-best-practices/SKILL.md +274 -0
- package/skills/csharp-best-practices/skill.json +23 -0
- package/skills/git-workflow-skill/skill.json +1 -1
- package/skills/open-presentation/SKILL.md +31 -0
- package/skills/open-presentation/skill.json +11 -0
- package/skills/python-best-practices/SKILL.md +381 -0
- package/skills/python-best-practices/skill.json +26 -0
- package/skills/story-status-lookup/SKILL.md +74 -0
- package/skills/story-status-lookup/skill.json +8 -0
- package/test/agent-injection-strategy.test.js +13 -7
- package/test/bmad-extension.test.js +237 -0
- package/test/bmad-output-policy.test.js +119 -0
- package/test/build-bmad-args.test.js +361 -0
- package/test/create-agent.test.js +232 -0
- package/test/enforcement-hooks.test.js +324 -0
- package/test/generate-project-context.test.js +337 -0
- package/test/integration-verification.test.js +402 -0
- package/test/opencode-agent.test.js +150 -0
- package/test/opencode-json-error.test.js +260 -0
- package/test/opencode-json-injection.test.js +256 -0
- package/test/opencode-json-merge.test.js +299 -0
- package/test/skill-authoring.test.js +272 -0
- package/test/skill-customize-agent.test.js +253 -0
- package/test/skill-mandatory.test.js +235 -0
- package/test/skill-validation.test.js +378 -0
- package/test/yes-flag.test.js +1 -1
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Tests for customize-agent command implementation
|
|
4
|
+
* Story 3.4: BMAD Persona Customization Tooling
|
|
5
|
+
*
|
|
6
|
+
* Task 5.1: customize-agent bmm-sre runs and generates full file
|
|
7
|
+
* Task 5.2: customize-agent bmm-pm generates critical_actions-only file
|
|
8
|
+
* Task 5.3: Existing file loaded as defaults
|
|
9
|
+
* Task 5.4: Generated YAML is valid structure
|
|
10
|
+
*/
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
const assert = require('assert');
|
|
14
|
+
const { spawnSync } = require('child_process');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const os = require('os');
|
|
18
|
+
|
|
19
|
+
const CLI_PATH = path.join(__dirname, '..', 'bin', 'cli.js');
|
|
20
|
+
const PROJECT_ROOT = path.join(__dirname, '..');
|
|
21
|
+
const AGENTS_DIR = path.join(PROJECT_ROOT, 'lib', 'bmad-extension', 'agents');
|
|
22
|
+
|
|
23
|
+
let passed = 0;
|
|
24
|
+
let failed = 0;
|
|
25
|
+
const errors = [];
|
|
26
|
+
|
|
27
|
+
function test(name, fn) {
|
|
28
|
+
try {
|
|
29
|
+
fn();
|
|
30
|
+
console.log(` ✓ ${name}`);
|
|
31
|
+
passed++;
|
|
32
|
+
} catch (err) {
|
|
33
|
+
console.error(` ✗ ${name}: ${err.message}`);
|
|
34
|
+
failed++;
|
|
35
|
+
errors.push({ name, error: err.message });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ─── Module imports ────────────────────────────────────────────────────────────
|
|
40
|
+
let validateBmadAgent, generateCustomizeYaml, writeCustomizeFile, loadExistingCustomization;
|
|
41
|
+
try {
|
|
42
|
+
({ validateBmadAgent, generateCustomizeYaml, writeCustomizeFile, loadExistingCustomization } = require('../lib/skill-authoring'));
|
|
43
|
+
} catch (e) {
|
|
44
|
+
console.error('\n FATAL: Cannot load skill-authoring module');
|
|
45
|
+
console.error(' ', e.message);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ─── Unit tests: validateBmadAgent() ─────────────────────────────────────────
|
|
50
|
+
console.log('\n validateBmadAgent unit tests\n');
|
|
51
|
+
|
|
52
|
+
test('validates custom agents (full customization)', () => {
|
|
53
|
+
const customAgents = ['bmm-sre', 'bmm-devops', 'bmm-cyber', 'bmm-mil498'];
|
|
54
|
+
for (const agent of customAgents) {
|
|
55
|
+
const result = validateBmadAgent(agent);
|
|
56
|
+
assert.strictEqual(result.valid, true, `Expected ${agent} to be valid`);
|
|
57
|
+
assert.strictEqual(result.type, 'custom', `Expected ${agent} to be type custom`);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('validates builtin agents (critical_actions only)', () => {
|
|
62
|
+
const builtinAgents = ['bmm-pm', 'bmm-architect', 'bmm-dev', 'bmm-qa', 'bmm-sm', 'bmm-tech-writer', 'bmm-ux-designer'];
|
|
63
|
+
for (const agent of builtinAgents) {
|
|
64
|
+
const result = validateBmadAgent(agent);
|
|
65
|
+
assert.strictEqual(result.valid, true, `Expected ${agent} to be valid`);
|
|
66
|
+
assert.strictEqual(result.type, 'builtin', `Expected ${agent} to be type builtin`);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('rejects unknown agents', () => {
|
|
71
|
+
const result = validateBmadAgent('bmm-unknown');
|
|
72
|
+
assert.strictEqual(result.valid, false);
|
|
73
|
+
assert.ok(result.hint.length > 0, 'Expected hint with available agents');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('rejects empty agent name', () => {
|
|
77
|
+
const result = validateBmadAgent('');
|
|
78
|
+
assert.strictEqual(result.valid, false);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// ─── Unit tests: generateCustomizeYaml() ─────────────────────────────────────
|
|
82
|
+
console.log('\n generateCustomizeYaml unit tests\n');
|
|
83
|
+
|
|
84
|
+
const MANDATORY_ACTIONS = [
|
|
85
|
+
'Read the skills MANIFEST at skills/MANIFEST.yaml (relative to project root)',
|
|
86
|
+
'For each skill marked always_load: true, read the skill file completely',
|
|
87
|
+
'Follow all skill directives during this session'
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
test('generateCustomizeYaml: builtin agent produces critical_actions only', () => {
|
|
91
|
+
const yaml = generateCustomizeYaml('bmm-pm', {}, 'builtin');
|
|
92
|
+
assert.ok(yaml.includes('critical_actions'), 'Expected critical_actions');
|
|
93
|
+
assert.ok(!yaml.includes('persona:'), 'Builtin must not include persona section');
|
|
94
|
+
assert.ok(!yaml.includes('menu:'), 'Builtin must not include menu section');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('generateCustomizeYaml: builtin always includes mandatory actions 1-3', () => {
|
|
98
|
+
const yaml = generateCustomizeYaml('bmm-pm', {}, 'builtin');
|
|
99
|
+
assert.ok(yaml.includes('1:'), 'Expected action 1');
|
|
100
|
+
assert.ok(yaml.includes('2:'), 'Expected action 2');
|
|
101
|
+
assert.ok(yaml.includes('3:'), 'Expected action 3');
|
|
102
|
+
assert.ok(yaml.includes('MANIFEST'), 'Expected MANIFEST reference in action 1');
|
|
103
|
+
assert.ok(yaml.includes('always_load'), 'Expected always_load reference in action 2');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('generateCustomizeYaml: custom agent with persona includes persona section', () => {
|
|
107
|
+
const data = {
|
|
108
|
+
persona: {
|
|
109
|
+
role: 'Site Reliability Engineer',
|
|
110
|
+
identity: 'Expert in system reliability.',
|
|
111
|
+
communication_style: 'Calm and data-driven.',
|
|
112
|
+
principles: ['Automate everything', 'Monitor all the things']
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
const yaml = generateCustomizeYaml('bmm-sre', data, 'custom');
|
|
116
|
+
assert.ok(yaml.includes('persona:'), 'Expected persona section');
|
|
117
|
+
assert.ok(yaml.includes('role:'), 'Expected role field');
|
|
118
|
+
assert.ok(yaml.includes('Site Reliability Engineer'), 'Expected role value');
|
|
119
|
+
assert.ok(yaml.includes('critical_actions:'), 'Expected critical_actions section');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('generateCustomizeYaml: custom agent with menu includes menu section', () => {
|
|
123
|
+
const data = {
|
|
124
|
+
menu: [
|
|
125
|
+
{ trigger: 'bmad-sre-health', workflow: 'workflows/health.md', description: 'Health Check' }
|
|
126
|
+
]
|
|
127
|
+
};
|
|
128
|
+
const yaml = generateCustomizeYaml('bmm-sre', data, 'custom');
|
|
129
|
+
assert.ok(yaml.includes('menu:'), 'Expected menu section');
|
|
130
|
+
assert.ok(yaml.includes('trigger:'), 'Expected trigger field');
|
|
131
|
+
assert.ok(yaml.includes('bmad-sre-health'), 'Expected trigger value');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('generateCustomizeYaml: mandatory actions 1-3 not overridable', () => {
|
|
135
|
+
// Even if caller passes custom actions, 1-3 must come from the hardcoded set
|
|
136
|
+
const data = {
|
|
137
|
+
extraActions: { 4: 'Custom action 4' }
|
|
138
|
+
};
|
|
139
|
+
const yaml = generateCustomizeYaml('bmm-sre', data, 'custom');
|
|
140
|
+
assert.ok(yaml.includes('MANIFEST'), 'Expected mandatory action 1 content (MANIFEST)');
|
|
141
|
+
assert.ok(yaml.includes('always_load'), 'Expected mandatory action 2 content');
|
|
142
|
+
assert.ok(yaml.includes('Follow all skill directives'), 'Expected mandatory action 3 content');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test('generateCustomizeYaml: extra actions appended starting at 4', () => {
|
|
146
|
+
const data = {
|
|
147
|
+
extraActions: { 4: 'Read the project context file' }
|
|
148
|
+
};
|
|
149
|
+
const yaml = generateCustomizeYaml('bmm-sre', data, 'custom');
|
|
150
|
+
assert.ok(yaml.includes('4:'), 'Expected action 4');
|
|
151
|
+
assert.ok(yaml.includes('Read the project context file'), 'Expected action 4 content');
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test('generateCustomizeYaml: includes header comment', () => {
|
|
155
|
+
const yaml = generateCustomizeYaml('bmm-pm', {}, 'builtin');
|
|
156
|
+
assert.ok(yaml.startsWith('#'), 'Expected YAML to start with comment');
|
|
157
|
+
assert.ok(yaml.includes('bmm-pm'), 'Expected agent name in header');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// ─── Unit tests: writeCustomizeFile() / loadExistingCustomization() ───────────
|
|
161
|
+
console.log('\n writeCustomizeFile / loadExistingCustomization unit tests\n');
|
|
162
|
+
|
|
163
|
+
test('writeCustomizeFile: writes file to specified directory', () => {
|
|
164
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ma-customize-test-'));
|
|
165
|
+
const agentsDir = path.join(tmpDir, 'agents');
|
|
166
|
+
fs.mkdirSync(agentsDir, { recursive: true });
|
|
167
|
+
try {
|
|
168
|
+
const yaml = generateCustomizeYaml('bmm-pm', {}, 'builtin');
|
|
169
|
+
writeCustomizeFile('bmm-pm', yaml, agentsDir);
|
|
170
|
+
const filePath = path.join(agentsDir, 'bmm-pm.customize.yaml');
|
|
171
|
+
assert.ok(fs.existsSync(filePath), 'Expected file to be written');
|
|
172
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
173
|
+
assert.ok(content.includes('critical_actions'), 'Expected content to be written');
|
|
174
|
+
} finally {
|
|
175
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test('loadExistingCustomization: returns null for non-existent file', () => {
|
|
180
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ma-customize-load-'));
|
|
181
|
+
try {
|
|
182
|
+
const result = loadExistingCustomization('bmm-pm', tmpDir);
|
|
183
|
+
assert.strictEqual(result, null, 'Expected null for missing file');
|
|
184
|
+
} finally {
|
|
185
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test('loadExistingCustomization: returns raw content for existing file', () => {
|
|
190
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ma-customize-load2-'));
|
|
191
|
+
const filePath = path.join(tmpDir, 'bmm-pm.customize.yaml');
|
|
192
|
+
try {
|
|
193
|
+
fs.writeFileSync(filePath, 'critical_actions:\n 1: "Test"\n', 'utf8');
|
|
194
|
+
const result = loadExistingCustomization('bmm-pm', tmpDir);
|
|
195
|
+
assert.ok(result !== null, 'Expected non-null for existing file');
|
|
196
|
+
assert.ok(typeof result === 'string', 'Expected string content');
|
|
197
|
+
assert.ok(result.includes('critical_actions'), 'Expected file content');
|
|
198
|
+
} finally {
|
|
199
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// ─── CLI integration tests ─────────────────────────────────────────────────────
|
|
204
|
+
console.log('\n customize-agent CLI integration tests\n');
|
|
205
|
+
|
|
206
|
+
// Unknown agent
|
|
207
|
+
test('customize-agent: rejects unknown agent with exit 1', () => {
|
|
208
|
+
const result = spawnSync('node', [CLI_PATH, 'customize-agent', 'bmm-unknown'], {
|
|
209
|
+
encoding: 'utf8', cwd: PROJECT_ROOT
|
|
210
|
+
});
|
|
211
|
+
assert.strictEqual(result.status, 1, 'Expected exit 1 for unknown agent');
|
|
212
|
+
const output = result.stdout + result.stderr;
|
|
213
|
+
assert.ok(output.includes('Error') || output.includes('Unknown'), 'Expected error message');
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// No agent name
|
|
217
|
+
test('customize-agent: no agent name exits 1', () => {
|
|
218
|
+
const result = spawnSync('node', [CLI_PATH, 'customize-agent'], {
|
|
219
|
+
encoding: 'utf8', cwd: PROJECT_ROOT
|
|
220
|
+
});
|
|
221
|
+
assert.strictEqual(result.status, 1, 'Expected exit 1 for missing agent name');
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// --yes mode skips (non-interactive)
|
|
225
|
+
test('customize-agent: --yes flag skips interactive wizard with exit 0', () => {
|
|
226
|
+
const srePath = path.join(AGENTS_DIR, 'bmm-sre.customize.yaml');
|
|
227
|
+
const originalContent = fs.existsSync(srePath) ? fs.readFileSync(srePath, 'utf8') : null;
|
|
228
|
+
try {
|
|
229
|
+
const result = spawnSync('node', [CLI_PATH, 'customize-agent', 'bmm-sre', '--yes'], {
|
|
230
|
+
encoding: 'utf8', cwd: PROJECT_ROOT, input: ''
|
|
231
|
+
});
|
|
232
|
+
// Should skip gracefully (exit 0) or generate minimal file
|
|
233
|
+
assert.ok(result.status === 0 || result.status === null, `Expected exit 0 or null, got: ${result.status}. stderr: ${result.stderr}`);
|
|
234
|
+
assert.ok(
|
|
235
|
+
result.stdout.includes('Skipping') || result.stdout.includes('non-interactive') || result.stdout.includes('generated'),
|
|
236
|
+
`Expected skip or generate message: ${result.stdout}`
|
|
237
|
+
);
|
|
238
|
+
} finally {
|
|
239
|
+
// Restore original file to avoid polluting real agent configs
|
|
240
|
+
if (originalContent !== null) {
|
|
241
|
+
fs.writeFileSync(srePath, originalContent, 'utf8');
|
|
242
|
+
} else if (fs.existsSync(srePath)) {
|
|
243
|
+
fs.rmSync(srePath);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// ─── Results ──────────────────────────────────────────────────────────────────
|
|
249
|
+
console.log(`\n ${passed} passed, ${failed} failed\n`);
|
|
250
|
+
if (failed > 0) {
|
|
251
|
+
errors.forEach(e => console.error(` ✗ ${e.name}: ${e.error}`));
|
|
252
|
+
process.exit(1);
|
|
253
|
+
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Tests for set-mandatory command and list --mandatory filter
|
|
4
|
+
* Story 3.3: Mandatory Skill Designation Tooling
|
|
5
|
+
*
|
|
6
|
+
* Task 5.1: set-mandatory enables always_load
|
|
7
|
+
* Task 5.2: set-mandatory --off disables always_load
|
|
8
|
+
* Task 5.3: list --mandatory filters correctly
|
|
9
|
+
* Task 5.4: MANIFEST.yaml reflects changes
|
|
10
|
+
*/
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
const assert = require('assert');
|
|
14
|
+
const { spawnSync } = require('child_process');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const os = require('os');
|
|
18
|
+
|
|
19
|
+
const CLI_PATH = path.join(__dirname, '..', 'bin', 'cli.js');
|
|
20
|
+
const PROJECT_ROOT = path.join(__dirname, '..');
|
|
21
|
+
|
|
22
|
+
let passed = 0;
|
|
23
|
+
let failed = 0;
|
|
24
|
+
const errors = [];
|
|
25
|
+
|
|
26
|
+
function test(name, fn) {
|
|
27
|
+
try {
|
|
28
|
+
fn();
|
|
29
|
+
console.log(` ✓ ${name}`);
|
|
30
|
+
passed++;
|
|
31
|
+
} catch (err) {
|
|
32
|
+
console.error(` ✗ ${name}: ${err.message}`);
|
|
33
|
+
failed++;
|
|
34
|
+
errors.push({ name, error: err.message });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ─── Module imports ────────────────────────────────────────────────────────────
|
|
39
|
+
let setMandatory;
|
|
40
|
+
try {
|
|
41
|
+
({ setMandatory } = require('../lib/skill-authoring'));
|
|
42
|
+
} catch (e) {
|
|
43
|
+
console.error('\n FATAL: Cannot load skill-authoring module');
|
|
44
|
+
console.error(' ', e.message);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ─── Helper: build a temporary skill with a real skill.json ──────────────────
|
|
49
|
+
function makeTmpSkill(alwaysLoad = false, extraFields = {}) {
|
|
50
|
+
const tmpBase = fs.mkdtempSync(path.join(os.tmpdir(), 'ma-mandatory-test-'));
|
|
51
|
+
const skillName = 'test-mandatory-skill';
|
|
52
|
+
const skillDir = path.join(tmpBase, skillName);
|
|
53
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
54
|
+
|
|
55
|
+
const json = Object.assign({
|
|
56
|
+
name: 'Test Mandatory Skill',
|
|
57
|
+
description: 'Test skill',
|
|
58
|
+
version: '1.0.0',
|
|
59
|
+
category: 'workflow',
|
|
60
|
+
author: 'ma-agents',
|
|
61
|
+
tags: ['test'],
|
|
62
|
+
always_load: alwaysLoad
|
|
63
|
+
}, extraFields);
|
|
64
|
+
|
|
65
|
+
fs.writeFileSync(path.join(skillDir, 'skill.json'), JSON.stringify(json, null, 2) + '\n', 'utf8');
|
|
66
|
+
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), '# Test Mandatory Skill\n', 'utf8');
|
|
67
|
+
|
|
68
|
+
return { tmpBase, skillDir, skillName };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ─── Unit tests: setMandatory() ───────────────────────────────────────────────
|
|
72
|
+
console.log('\n setMandatory unit tests\n');
|
|
73
|
+
|
|
74
|
+
// AC #1: Enable
|
|
75
|
+
test('setMandatory: sets always_load=true in skill.json', () => {
|
|
76
|
+
const { tmpBase, skillName } = makeTmpSkill(false);
|
|
77
|
+
try {
|
|
78
|
+
const result = setMandatory(skillName, true, tmpBase);
|
|
79
|
+
assert.strictEqual(result.success, true, `Expected success, got: ${JSON.stringify(result)}`);
|
|
80
|
+
|
|
81
|
+
const updated = JSON.parse(fs.readFileSync(path.join(tmpBase, skillName, 'skill.json'), 'utf8'));
|
|
82
|
+
assert.strictEqual(updated.always_load, true, 'Expected always_load=true');
|
|
83
|
+
} finally {
|
|
84
|
+
fs.rmSync(tmpBase, { recursive: true, force: true });
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// AC #2: Disable
|
|
89
|
+
test('setMandatory: sets always_load=false with enable=false', () => {
|
|
90
|
+
const { tmpBase, skillName } = makeTmpSkill(true);
|
|
91
|
+
try {
|
|
92
|
+
const result = setMandatory(skillName, false, tmpBase);
|
|
93
|
+
assert.strictEqual(result.success, true);
|
|
94
|
+
|
|
95
|
+
const updated = JSON.parse(fs.readFileSync(path.join(tmpBase, skillName, 'skill.json'), 'utf8'));
|
|
96
|
+
assert.strictEqual(updated.always_load, false, 'Expected always_load=false');
|
|
97
|
+
} finally {
|
|
98
|
+
fs.rmSync(tmpBase, { recursive: true, force: true });
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('setMandatory: preserves all other fields in skill.json', () => {
|
|
103
|
+
const { tmpBase, skillName } = makeTmpSkill(false, { custom_field: 'preserved' });
|
|
104
|
+
try {
|
|
105
|
+
setMandatory(skillName, true, tmpBase);
|
|
106
|
+
const updated = JSON.parse(fs.readFileSync(path.join(tmpBase, skillName, 'skill.json'), 'utf8'));
|
|
107
|
+
assert.strictEqual(updated.name, 'Test Mandatory Skill', 'Expected name preserved');
|
|
108
|
+
assert.strictEqual(updated.version, '1.0.0', 'Expected version preserved');
|
|
109
|
+
assert.deepStrictEqual(updated.tags, ['test'], 'Expected tags preserved');
|
|
110
|
+
assert.strictEqual(updated.custom_field, 'preserved', 'Expected custom_field preserved');
|
|
111
|
+
} finally {
|
|
112
|
+
fs.rmSync(tmpBase, { recursive: true, force: true });
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('setMandatory: returns error for non-existent skill', () => {
|
|
117
|
+
const tmpBase = fs.mkdtempSync(path.join(os.tmpdir(), 'ma-mandatory-empty-'));
|
|
118
|
+
try {
|
|
119
|
+
const result = setMandatory('no-such-skill', true, tmpBase);
|
|
120
|
+
assert.strictEqual(result.success, false);
|
|
121
|
+
assert.ok(result.error.includes('not found') || result.error.includes('no-such-skill'),
|
|
122
|
+
`Expected not-found error, got: ${result.error}`);
|
|
123
|
+
} finally {
|
|
124
|
+
fs.rmSync(tmpBase, { recursive: true, force: true });
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('setMandatory: error and hint are label-free data', () => {
|
|
129
|
+
const tmpBase = fs.mkdtempSync(path.join(os.tmpdir(), 'ma-mandatory-empty2-'));
|
|
130
|
+
try {
|
|
131
|
+
const result = setMandatory('no-such-skill', true, tmpBase);
|
|
132
|
+
assert.ok(!result.error.startsWith('Error:'), 'error field should not start with "Error:"');
|
|
133
|
+
assert.ok(!result.hint.startsWith('Hint:'), 'hint field should not start with "Hint:"');
|
|
134
|
+
} finally {
|
|
135
|
+
fs.rmSync(tmpBase, { recursive: true, force: true });
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// ─── CLI integration tests: set-mandatory ────────────────────────────────────
|
|
140
|
+
console.log('\n set-mandatory CLI integration tests\n');
|
|
141
|
+
|
|
142
|
+
// Use a real skill in the project's skills/ dir for CLI tests
|
|
143
|
+
const TEST_SKILL = 'git-workflow-skill';
|
|
144
|
+
const SKILL_JSON_PATH = path.join(PROJECT_ROOT, 'skills', TEST_SKILL, 'skill.json');
|
|
145
|
+
|
|
146
|
+
// Save and restore original always_load value
|
|
147
|
+
let originalAlwaysLoad;
|
|
148
|
+
try {
|
|
149
|
+
const original = JSON.parse(fs.readFileSync(SKILL_JSON_PATH, 'utf8'));
|
|
150
|
+
originalAlwaysLoad = original.always_load;
|
|
151
|
+
} catch (_) {
|
|
152
|
+
originalAlwaysLoad = true;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// AC #1: Enable
|
|
156
|
+
test('set-mandatory: sets always_load=true with exit 0', () => {
|
|
157
|
+
const result = spawnSync('node', [CLI_PATH, 'set-mandatory', TEST_SKILL], {
|
|
158
|
+
encoding: 'utf8', cwd: PROJECT_ROOT
|
|
159
|
+
});
|
|
160
|
+
assert.strictEqual(result.status, 0, `Expected exit 0. stderr: ${result.stderr}`);
|
|
161
|
+
assert.ok(result.stdout.includes('mandatory') || result.stdout.includes('always_load'),
|
|
162
|
+
`Expected confirmation in output: ${result.stdout}`);
|
|
163
|
+
|
|
164
|
+
const updated = JSON.parse(fs.readFileSync(SKILL_JSON_PATH, 'utf8'));
|
|
165
|
+
assert.strictEqual(updated.always_load, true, 'Expected always_load=true in skill.json');
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// AC #2: Disable
|
|
169
|
+
test('set-mandatory --off: sets always_load=false with exit 0', () => {
|
|
170
|
+
const result = spawnSync('node', [CLI_PATH, 'set-mandatory', TEST_SKILL, '--off'], {
|
|
171
|
+
encoding: 'utf8', cwd: PROJECT_ROOT
|
|
172
|
+
});
|
|
173
|
+
assert.strictEqual(result.status, 0, `Expected exit 0. stderr: ${result.stderr}`);
|
|
174
|
+
|
|
175
|
+
const updated = JSON.parse(fs.readFileSync(SKILL_JSON_PATH, 'utf8'));
|
|
176
|
+
assert.strictEqual(updated.always_load, false, 'Expected always_load=false in skill.json');
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// Restore original value
|
|
180
|
+
try {
|
|
181
|
+
const current = JSON.parse(fs.readFileSync(SKILL_JSON_PATH, 'utf8'));
|
|
182
|
+
current.always_load = originalAlwaysLoad;
|
|
183
|
+
fs.writeFileSync(SKILL_JSON_PATH, JSON.stringify(current, null, 2) + '\n', 'utf8');
|
|
184
|
+
} catch (_) {}
|
|
185
|
+
|
|
186
|
+
// Missing skill name
|
|
187
|
+
test('set-mandatory: no skill name exits 1', () => {
|
|
188
|
+
const result = spawnSync('node', [CLI_PATH, 'set-mandatory'], {
|
|
189
|
+
encoding: 'utf8', cwd: PROJECT_ROOT
|
|
190
|
+
});
|
|
191
|
+
assert.strictEqual(result.status, 1, 'Expected exit 1');
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// Unknown skill
|
|
195
|
+
test('set-mandatory: unknown skill exits 1 with error', () => {
|
|
196
|
+
const result = spawnSync('node', [CLI_PATH, 'set-mandatory', 'no-such-skill'], {
|
|
197
|
+
encoding: 'utf8', cwd: PROJECT_ROOT
|
|
198
|
+
});
|
|
199
|
+
assert.strictEqual(result.status, 1, 'Expected exit 1 for missing skill');
|
|
200
|
+
const output = result.stdout + result.stderr;
|
|
201
|
+
assert.ok(output.includes('Error') || output.includes('not found'), 'Expected error message');
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// ─── CLI integration tests: list --mandatory ──────────────────────────────────
|
|
205
|
+
console.log('\n list --mandatory CLI integration tests\n');
|
|
206
|
+
|
|
207
|
+
// AC #3: Filter mandatory skills
|
|
208
|
+
test('list --mandatory: exits 0', () => {
|
|
209
|
+
const result = spawnSync('node', [CLI_PATH, 'list', '--mandatory'], {
|
|
210
|
+
encoding: 'utf8', cwd: PROJECT_ROOT
|
|
211
|
+
});
|
|
212
|
+
assert.strictEqual(result.status, 0, `Expected exit 0. stderr: ${result.stderr}`);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test('list --mandatory: only shows skills with always_load=true', () => {
|
|
216
|
+
const result = spawnSync('node', [CLI_PATH, 'list', '--mandatory'], {
|
|
217
|
+
encoding: 'utf8', cwd: PROJECT_ROOT
|
|
218
|
+
});
|
|
219
|
+
// git-workflow-skill has always_load: true — should appear
|
|
220
|
+
assert.ok(result.stdout.includes('git-workflow-skill'), 'Expected git-workflow-skill in mandatory list');
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test('list --mandatory: output differs from full list', () => {
|
|
224
|
+
const full = spawnSync('node', [CLI_PATH, 'list'], { encoding: 'utf8', cwd: PROJECT_ROOT });
|
|
225
|
+
const filtered = spawnSync('node', [CLI_PATH, 'list', '--mandatory'], { encoding: 'utf8', cwd: PROJECT_ROOT });
|
|
226
|
+
// Mandatory list should be shorter or equal (never longer)
|
|
227
|
+
assert.ok(filtered.stdout.length <= full.stdout.length, 'Mandatory list should not be longer than full list');
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// ─── Results ──────────────────────────────────────────────────────────────────
|
|
231
|
+
console.log(`\n ${passed} passed, ${failed} failed\n`);
|
|
232
|
+
if (failed > 0) {
|
|
233
|
+
errors.forEach(e => console.error(` ✗ ${e.name}: ${e.error}`));
|
|
234
|
+
process.exit(1);
|
|
235
|
+
}
|