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,260 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Tests for Story 9.4: JSON Injection — Handle Invalid opencode.json Gracefully
|
|
4
|
+
*
|
|
5
|
+
* Validates that updateAgentInstructions() recovers non-fatally when
|
|
6
|
+
* opencode.json contains invalid JSON: logs an error, leaves the file
|
|
7
|
+
* byte-identical, and continues processing subsequent agents.
|
|
8
|
+
*/
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const assert = require('assert');
|
|
12
|
+
const fs = require('fs-extra');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const os = require('os');
|
|
15
|
+
|
|
16
|
+
let passed = 0;
|
|
17
|
+
let failed = 0;
|
|
18
|
+
const errors = [];
|
|
19
|
+
|
|
20
|
+
async function asyncTest(name, fn) {
|
|
21
|
+
try {
|
|
22
|
+
await fn();
|
|
23
|
+
console.log(` ✓ ${name}`);
|
|
24
|
+
passed++;
|
|
25
|
+
} catch (err) {
|
|
26
|
+
console.error(` ✗ ${name}: ${err.message}`);
|
|
27
|
+
failed++;
|
|
28
|
+
errors.push({ name, error: err.message });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Load functions under test
|
|
33
|
+
const {
|
|
34
|
+
_testUpdateAgentInstructions: updateFn,
|
|
35
|
+
_MA_AGENTS_SOURCE: MA_AGENTS_SOURCE
|
|
36
|
+
} = require('../lib/installer');
|
|
37
|
+
|
|
38
|
+
// Helper: create a temporary directory, run fn, then clean up
|
|
39
|
+
async function withTempDir(fn) {
|
|
40
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'ma-agents-json-error-test-'));
|
|
41
|
+
try {
|
|
42
|
+
await fn(tmpDir);
|
|
43
|
+
} finally {
|
|
44
|
+
await fs.remove(tmpDir);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Helper: build a mock OpenCode agent pointing to a given projectRoot
|
|
49
|
+
function createOpenCodeAgent(projectRoot) {
|
|
50
|
+
return {
|
|
51
|
+
id: 'opencode',
|
|
52
|
+
name: 'OpenCode',
|
|
53
|
+
category: 'ide',
|
|
54
|
+
instructionFiles: ['opencode.json'],
|
|
55
|
+
injectionStrategy: { position: 'json-merge', targetKey: 'instructions' },
|
|
56
|
+
getProjectPath: () => path.join(projectRoot, '.opencode', 'skills'),
|
|
57
|
+
getGlobalPath: () => path.join(os.homedir(), '.opencode', 'skills')
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Helper: build a generic markdown agent (simulates "someOtherAgent")
|
|
62
|
+
function createMarkdownAgent(projectRoot, fileName) {
|
|
63
|
+
return {
|
|
64
|
+
id: 'claude-code',
|
|
65
|
+
name: 'Claude Code',
|
|
66
|
+
category: 'ide',
|
|
67
|
+
instructionFiles: [fileName],
|
|
68
|
+
injectionStrategy: { position: 'top', skipPatterns: [] },
|
|
69
|
+
getProjectPath: () => path.join(projectRoot, '.claude', 'skills'),
|
|
70
|
+
getGlobalPath: () => path.join(os.homedir(), '.claude', 'skills')
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// The corrupt JSON fixture content
|
|
75
|
+
const CORRUPT_JSON = 'not valid json {{{';
|
|
76
|
+
|
|
77
|
+
// ─── Fixture A: corrupt opencode.json ─────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
console.log('\nStory 9.4 — JSON error handling: corrupt opencode.json');
|
|
80
|
+
|
|
81
|
+
(async () => {
|
|
82
|
+
await asyncTest('9.4.1: file is byte-identical after run with corrupt JSON (not modified)', async () => {
|
|
83
|
+
await withTempDir(async (tmpDir) => {
|
|
84
|
+
const agent = createOpenCodeAgent(tmpDir);
|
|
85
|
+
const filePath = path.join(tmpDir, 'opencode.json');
|
|
86
|
+
|
|
87
|
+
// Write the corrupt fixture
|
|
88
|
+
await fs.writeFile(filePath, CORRUPT_JSON, 'utf-8');
|
|
89
|
+
const beforeBytes = fs.readFileSync(filePath);
|
|
90
|
+
|
|
91
|
+
await updateFn(agent, tmpDir);
|
|
92
|
+
|
|
93
|
+
const afterBytes = fs.readFileSync(filePath);
|
|
94
|
+
assert.ok(
|
|
95
|
+
beforeBytes.equals(afterBytes),
|
|
96
|
+
`File content changed after corrupt-JSON run.\nBefore: ${beforeBytes.toString()}\nAfter: ${afterBytes.toString()}`
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
await asyncTest('9.4.2: console.error is called with the file path on parse failure', async () => {
|
|
102
|
+
await withTempDir(async (tmpDir) => {
|
|
103
|
+
const agent = createOpenCodeAgent(tmpDir);
|
|
104
|
+
const filePath = path.join(tmpDir, 'opencode.json');
|
|
105
|
+
await fs.writeFile(filePath, CORRUPT_JSON, 'utf-8');
|
|
106
|
+
|
|
107
|
+
const errorMessages = [];
|
|
108
|
+
const origError = console.error;
|
|
109
|
+
console.error = (...args) => errorMessages.push(args.join(' '));
|
|
110
|
+
try {
|
|
111
|
+
await updateFn(agent, tmpDir);
|
|
112
|
+
} finally {
|
|
113
|
+
console.error = origError;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
assert.ok(errorMessages.length > 0, 'console.error should have been called');
|
|
117
|
+
const combined = errorMessages.join('\n');
|
|
118
|
+
assert.ok(
|
|
119
|
+
combined.includes(filePath),
|
|
120
|
+
`Error message should contain the file path "${filePath}".\nActual messages: ${combined}`
|
|
121
|
+
);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
await asyncTest('9.4.3: console.error message describes the parse failure', async () => {
|
|
126
|
+
await withTempDir(async (tmpDir) => {
|
|
127
|
+
const agent = createOpenCodeAgent(tmpDir);
|
|
128
|
+
const filePath = path.join(tmpDir, 'opencode.json');
|
|
129
|
+
await fs.writeFile(filePath, CORRUPT_JSON, 'utf-8');
|
|
130
|
+
|
|
131
|
+
const errorMessages = [];
|
|
132
|
+
const origError = console.error;
|
|
133
|
+
console.error = (...args) => errorMessages.push(args.join(' '));
|
|
134
|
+
try {
|
|
135
|
+
await updateFn(agent, tmpDir);
|
|
136
|
+
} finally {
|
|
137
|
+
console.error = origError;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const combined = errorMessages.join('\n');
|
|
141
|
+
// The message must mention that file was not modified
|
|
142
|
+
assert.ok(
|
|
143
|
+
combined.toLowerCase().includes('not modified') || combined.toLowerCase().includes('file not modified'),
|
|
144
|
+
`Error message should state the file was not modified.\nActual messages: ${combined}`
|
|
145
|
+
);
|
|
146
|
+
// The message must include a parse/error description (err.message from JSON.parse)
|
|
147
|
+
assert.ok(
|
|
148
|
+
combined.toLowerCase().includes('parse') ||
|
|
149
|
+
combined.toLowerCase().includes('token') ||
|
|
150
|
+
combined.toLowerCase().includes('json') ||
|
|
151
|
+
combined.toLowerCase().includes('unexpected'),
|
|
152
|
+
`Error message should describe the parse failure.\nActual messages: ${combined}`
|
|
153
|
+
);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
await asyncTest('9.4.4: no .tmp file is left on disk after parse failure', async () => {
|
|
158
|
+
await withTempDir(async (tmpDir) => {
|
|
159
|
+
const agent = createOpenCodeAgent(tmpDir);
|
|
160
|
+
const filePath = path.join(tmpDir, 'opencode.json');
|
|
161
|
+
await fs.writeFile(filePath, CORRUPT_JSON, 'utf-8');
|
|
162
|
+
|
|
163
|
+
const origError = console.error;
|
|
164
|
+
console.error = () => {};
|
|
165
|
+
try {
|
|
166
|
+
await updateFn(agent, tmpDir);
|
|
167
|
+
} finally {
|
|
168
|
+
console.error = origError;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const tmpPath = filePath + '.tmp';
|
|
172
|
+
assert.strictEqual(
|
|
173
|
+
fs.existsSync(tmpPath),
|
|
174
|
+
false,
|
|
175
|
+
'.tmp file should not exist after a parse-failure run'
|
|
176
|
+
);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// ─── Fixture B: continuation test (CRITICAL) ────────────────────────────────
|
|
181
|
+
|
|
182
|
+
console.log('\nStory 9.4 — JSON error handling: continuation after error (Fixture B)');
|
|
183
|
+
|
|
184
|
+
await asyncTest('9.4.5: subsequent agent instruction file IS updated after OpenCode parse failure', async () => {
|
|
185
|
+
await withTempDir(async (tmpDir) => {
|
|
186
|
+
// 1. Set up OpenCode with corrupt JSON
|
|
187
|
+
const opencodeAgent = createOpenCodeAgent(tmpDir);
|
|
188
|
+
const opencodeFilePath = path.join(tmpDir, 'opencode.json');
|
|
189
|
+
await fs.writeFile(opencodeFilePath, CORRUPT_JSON, 'utf-8');
|
|
190
|
+
|
|
191
|
+
// 2. Set up a second markdown agent whose file does not yet exist
|
|
192
|
+
const otherFileName = '.claude/CLAUDE.md';
|
|
193
|
+
const otherAgent = createMarkdownAgent(tmpDir, otherFileName);
|
|
194
|
+
const otherFilePath = path.join(tmpDir, otherFileName);
|
|
195
|
+
|
|
196
|
+
// Suppress console.error during this test
|
|
197
|
+
const origError = console.error;
|
|
198
|
+
console.error = () => {};
|
|
199
|
+
try {
|
|
200
|
+
// 3. Run both agents in sequence — simulating what the install loop does
|
|
201
|
+
await updateFn(opencodeAgent, tmpDir);
|
|
202
|
+
await updateFn(otherAgent, tmpDir);
|
|
203
|
+
} finally {
|
|
204
|
+
console.error = origError;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// 4. OpenCode file must be unchanged (still corrupt)
|
|
208
|
+
const opencodeBytes = fs.readFileSync(opencodeFilePath, 'utf-8');
|
|
209
|
+
assert.strictEqual(
|
|
210
|
+
opencodeBytes,
|
|
211
|
+
CORRUPT_JSON,
|
|
212
|
+
'opencode.json should remain byte-identical (corrupt) after error'
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
// 5. The other agent's instruction file MUST have been created/updated
|
|
216
|
+
assert.ok(
|
|
217
|
+
fs.existsSync(otherFilePath),
|
|
218
|
+
`Second agent's instruction file "${otherFileName}" should have been created after OpenCode error`
|
|
219
|
+
);
|
|
220
|
+
const otherContent = fs.readFileSync(otherFilePath, 'utf-8');
|
|
221
|
+
assert.ok(
|
|
222
|
+
otherContent.includes('MA-AGENTS-START') || otherContent.includes('MANIFEST.yaml'),
|
|
223
|
+
`Second agent's instruction file should contain injected content.\nActual: ${otherContent}`
|
|
224
|
+
);
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
await asyncTest('9.4.6: updateAgentInstructions does not throw on corrupt JSON (non-fatal)', async () => {
|
|
229
|
+
await withTempDir(async (tmpDir) => {
|
|
230
|
+
const agent = createOpenCodeAgent(tmpDir);
|
|
231
|
+
const filePath = path.join(tmpDir, 'opencode.json');
|
|
232
|
+
await fs.writeFile(filePath, CORRUPT_JSON, 'utf-8');
|
|
233
|
+
|
|
234
|
+
const origError = console.error;
|
|
235
|
+
console.error = () => {};
|
|
236
|
+
let threw = false;
|
|
237
|
+
try {
|
|
238
|
+
await updateFn(agent, tmpDir);
|
|
239
|
+
} catch (err) {
|
|
240
|
+
threw = true;
|
|
241
|
+
} finally {
|
|
242
|
+
console.error = origError;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
assert.strictEqual(
|
|
246
|
+
threw,
|
|
247
|
+
false,
|
|
248
|
+
'updateAgentInstructions() must NOT throw when JSON.parse fails — error must be non-fatal'
|
|
249
|
+
);
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// Print summary
|
|
254
|
+
console.log(`\n${passed} passed, ${failed} failed`);
|
|
255
|
+
if (errors.length > 0) {
|
|
256
|
+
console.log('\nFailed tests:');
|
|
257
|
+
errors.forEach(e => console.log(` - ${e.name}: ${e.error}`));
|
|
258
|
+
}
|
|
259
|
+
if (failed > 0) process.exit(1);
|
|
260
|
+
})();
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Tests for Story 9.2: JSON Injection — Create opencode.json When Absent
|
|
4
|
+
*
|
|
5
|
+
* Validates that updateAgentInstructions() creates a valid opencode.json
|
|
6
|
+
* from scratch when the file does not yet exist, using the json-merge strategy.
|
|
7
|
+
*/
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
const assert = require('assert');
|
|
11
|
+
const fs = require('fs-extra');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const os = require('os');
|
|
14
|
+
|
|
15
|
+
let passed = 0;
|
|
16
|
+
let failed = 0;
|
|
17
|
+
const errors = [];
|
|
18
|
+
|
|
19
|
+
function test(name, fn) {
|
|
20
|
+
try {
|
|
21
|
+
fn();
|
|
22
|
+
console.log(` ✓ ${name}`);
|
|
23
|
+
passed++;
|
|
24
|
+
} catch (err) {
|
|
25
|
+
console.error(` ✗ ${name}: ${err.message}`);
|
|
26
|
+
failed++;
|
|
27
|
+
errors.push({ name, error: err.message });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function asyncTest(name, fn) {
|
|
32
|
+
try {
|
|
33
|
+
await fn();
|
|
34
|
+
console.log(` ✓ ${name}`);
|
|
35
|
+
passed++;
|
|
36
|
+
} catch (err) {
|
|
37
|
+
console.error(` ✗ ${name}: ${err.message}`);
|
|
38
|
+
failed++;
|
|
39
|
+
errors.push({ name, error: err.message });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Load functions under test
|
|
44
|
+
const {
|
|
45
|
+
_testUpdateAgentInstructions: updateFn,
|
|
46
|
+
_MA_AGENTS_SOURCE: MA_AGENTS_SOURCE
|
|
47
|
+
} = require('../lib/installer');
|
|
48
|
+
|
|
49
|
+
// Helper: create a temporary directory, run fn, then clean up
|
|
50
|
+
async function withTempDir(fn) {
|
|
51
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'ma-agents-json-test-'));
|
|
52
|
+
try {
|
|
53
|
+
await fn(tmpDir);
|
|
54
|
+
} finally {
|
|
55
|
+
await fs.remove(tmpDir);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Helper: build a mock OpenCode agent pointing to a given projectRoot
|
|
60
|
+
function createOpenCodeAgent(projectRoot) {
|
|
61
|
+
return {
|
|
62
|
+
id: 'opencode',
|
|
63
|
+
name: 'OpenCode',
|
|
64
|
+
category: 'ide',
|
|
65
|
+
instructionFiles: ['opencode.json'],
|
|
66
|
+
injectionStrategy: { position: 'json-merge', targetKey: 'instructions' },
|
|
67
|
+
getProjectPath: () => path.join(projectRoot, '.opencode', 'skills'),
|
|
68
|
+
getGlobalPath: () => path.join(os.homedir(), '.opencode', 'skills')
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ─── Creation path tests (file absent) ───────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
console.log('\nStory 9.2 — JSON injection: file-absent creation path');
|
|
75
|
+
|
|
76
|
+
(async () => {
|
|
77
|
+
await asyncTest('9.2.1: creates opencode.json when file does not exist', async () => {
|
|
78
|
+
await withTempDir(async (tmpDir) => {
|
|
79
|
+
const agent = createOpenCodeAgent(tmpDir);
|
|
80
|
+
const filePath = path.join(tmpDir, 'opencode.json');
|
|
81
|
+
|
|
82
|
+
assert.strictEqual(fs.existsSync(filePath), false, 'Precondition: file must not exist');
|
|
83
|
+
await updateFn(agent, tmpDir);
|
|
84
|
+
assert.ok(fs.existsSync(filePath), 'opencode.json should have been created');
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
await asyncTest('9.2.2: created file is valid JSON', async () => {
|
|
89
|
+
await withTempDir(async (tmpDir) => {
|
|
90
|
+
const agent = createOpenCodeAgent(tmpDir);
|
|
91
|
+
const filePath = path.join(tmpDir, 'opencode.json');
|
|
92
|
+
|
|
93
|
+
await updateFn(agent, tmpDir);
|
|
94
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
95
|
+
let parsed;
|
|
96
|
+
try {
|
|
97
|
+
parsed = JSON.parse(raw);
|
|
98
|
+
} catch (e) {
|
|
99
|
+
throw new Error(`File content is not valid JSON: ${e.message}\nContent: ${raw}`);
|
|
100
|
+
}
|
|
101
|
+
assert.ok(parsed !== null && typeof parsed === 'object', 'Parsed result should be an object');
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
await asyncTest('9.2.3: created file contains an "instructions" array', async () => {
|
|
106
|
+
await withTempDir(async (tmpDir) => {
|
|
107
|
+
const agent = createOpenCodeAgent(tmpDir);
|
|
108
|
+
const filePath = path.join(tmpDir, 'opencode.json');
|
|
109
|
+
|
|
110
|
+
await updateFn(agent, tmpDir);
|
|
111
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
112
|
+
assert.ok(Array.isArray(parsed.instructions),
|
|
113
|
+
`Expected "instructions" to be an array, got: ${typeof parsed.instructions}`);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
await asyncTest('9.2.4: instructions array is non-empty', async () => {
|
|
118
|
+
await withTempDir(async (tmpDir) => {
|
|
119
|
+
const agent = createOpenCodeAgent(tmpDir);
|
|
120
|
+
const filePath = path.join(tmpDir, 'opencode.json');
|
|
121
|
+
|
|
122
|
+
await updateFn(agent, tmpDir);
|
|
123
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
124
|
+
assert.ok(parsed.instructions.length > 0,
|
|
125
|
+
'instructions array should have at least one entry');
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
await asyncTest('9.2.5: every entry has _source === MA_AGENTS_SOURCE', async () => {
|
|
130
|
+
await withTempDir(async (tmpDir) => {
|
|
131
|
+
const agent = createOpenCodeAgent(tmpDir);
|
|
132
|
+
const filePath = path.join(tmpDir, 'opencode.json');
|
|
133
|
+
|
|
134
|
+
await updateFn(agent, tmpDir);
|
|
135
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
136
|
+
for (const entry of parsed.instructions) {
|
|
137
|
+
assert.strictEqual(entry._source, MA_AGENTS_SOURCE,
|
|
138
|
+
`Entry _source should be "${MA_AGENTS_SOURCE}", got: "${entry._source}"`);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
await asyncTest('9.2.6: every entry has a non-empty "text" string', async () => {
|
|
144
|
+
await withTempDir(async (tmpDir) => {
|
|
145
|
+
const agent = createOpenCodeAgent(tmpDir);
|
|
146
|
+
const filePath = path.join(tmpDir, 'opencode.json');
|
|
147
|
+
|
|
148
|
+
await updateFn(agent, tmpDir);
|
|
149
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
150
|
+
for (const entry of parsed.instructions) {
|
|
151
|
+
assert.strictEqual(typeof entry.text, 'string',
|
|
152
|
+
`Entry "text" should be a string, got: ${typeof entry.text}`);
|
|
153
|
+
assert.ok(entry.text.length > 0,
|
|
154
|
+
'Entry "text" should not be empty');
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
await asyncTest('9.2.7: no extra top-level keys in the written JSON', async () => {
|
|
160
|
+
await withTempDir(async (tmpDir) => {
|
|
161
|
+
const agent = createOpenCodeAgent(tmpDir);
|
|
162
|
+
const filePath = path.join(tmpDir, 'opencode.json');
|
|
163
|
+
|
|
164
|
+
await updateFn(agent, tmpDir);
|
|
165
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
166
|
+
const keys = Object.keys(parsed);
|
|
167
|
+
assert.deepStrictEqual(keys, ['instructions'],
|
|
168
|
+
`Expected only ["instructions"] as top-level keys, got: ${JSON.stringify(keys)}`);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
await asyncTest('9.2.8: file uses 2-space indentation', async () => {
|
|
173
|
+
await withTempDir(async (tmpDir) => {
|
|
174
|
+
const agent = createOpenCodeAgent(tmpDir);
|
|
175
|
+
const filePath = path.join(tmpDir, 'opencode.json');
|
|
176
|
+
|
|
177
|
+
await updateFn(agent, tmpDir);
|
|
178
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
179
|
+
// JSON.stringify(data, null, 2) produces lines starting with " " (2 spaces) for first level
|
|
180
|
+
assert.ok(raw.includes(' "instructions"'),
|
|
181
|
+
'File should use 2-space indentation (line should start with " \\"instructions\\"")\nActual content:\n' + raw);
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
await asyncTest('9.2.9: instruction text contains skill manifest reference', async () => {
|
|
186
|
+
await withTempDir(async (tmpDir) => {
|
|
187
|
+
const agent = createOpenCodeAgent(tmpDir);
|
|
188
|
+
const filePath = path.join(tmpDir, 'opencode.json');
|
|
189
|
+
|
|
190
|
+
await updateFn(agent, tmpDir);
|
|
191
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
192
|
+
const allText = parsed.instructions.map(e => e.text).join('\n');
|
|
193
|
+
assert.ok(allText.includes('MANIFEST.yaml'),
|
|
194
|
+
'Instruction text should reference MANIFEST.yaml');
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
await asyncTest('9.2.10: MA_AGENTS_SOURCE constant is "ma-agents"', () => {
|
|
199
|
+
assert.strictEqual(MA_AGENTS_SOURCE, 'ma-agents',
|
|
200
|
+
`MA_AGENTS_SOURCE should equal "ma-agents", got: "${MA_AGENTS_SOURCE}"`);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
await asyncTest('9.2.11: non-json-merge agents are not affected (falls through to markdown logic)', async () => {
|
|
204
|
+
await withTempDir(async (tmpDir) => {
|
|
205
|
+
// A regular markdown agent — should not produce a JSON file
|
|
206
|
+
const markdownAgent = {
|
|
207
|
+
id: 'claude-code',
|
|
208
|
+
name: 'Claude Code',
|
|
209
|
+
category: 'ide',
|
|
210
|
+
instructionFiles: ['.claude/CLAUDE.md'],
|
|
211
|
+
injectionStrategy: { position: 'top', skipPatterns: [] },
|
|
212
|
+
getProjectPath: () => path.join(tmpDir, '.claude', 'skills'),
|
|
213
|
+
getGlobalPath: () => path.join(os.homedir(), '.claude', 'skills')
|
|
214
|
+
};
|
|
215
|
+
await updateFn(markdownAgent, tmpDir);
|
|
216
|
+
// Should NOT create opencode.json
|
|
217
|
+
assert.strictEqual(fs.existsSync(path.join(tmpDir, 'opencode.json')), false,
|
|
218
|
+
'Should not create opencode.json for non-json-merge agents');
|
|
219
|
+
// Should create the markdown file instead
|
|
220
|
+
assert.ok(fs.existsSync(path.join(tmpDir, '.claude', 'CLAUDE.md')),
|
|
221
|
+
'Markdown agent file should be created');
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
await asyncTest('9.2.12: file present — user entries preserved, fresh ma-agents entries appended (Story 9.3)', async () => {
|
|
226
|
+
await withTempDir(async (tmpDir) => {
|
|
227
|
+
const agent = createOpenCodeAgent(tmpDir);
|
|
228
|
+
const filePath = path.join(tmpDir, 'opencode.json');
|
|
229
|
+
|
|
230
|
+
// Pre-create a file with a single user entry
|
|
231
|
+
const existingContent = { instructions: [{ _source: 'user', text: 'user instruction' }] };
|
|
232
|
+
await fs.writeFile(filePath, JSON.stringify(existingContent, null, 2), 'utf-8');
|
|
233
|
+
|
|
234
|
+
await updateFn(agent, tmpDir);
|
|
235
|
+
|
|
236
|
+
// Story 9.3 merge: user entry preserved, fresh ma-agents entries appended
|
|
237
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
238
|
+
const userEntries = parsed.instructions.filter(e => e._source !== MA_AGENTS_SOURCE);
|
|
239
|
+
const maEntries = parsed.instructions.filter(e => e._source === MA_AGENTS_SOURCE);
|
|
240
|
+
assert.strictEqual(userEntries.length, 1,
|
|
241
|
+
'Existing user entry should be preserved');
|
|
242
|
+
assert.strictEqual(userEntries[0]._source, 'user',
|
|
243
|
+
'User entry _source should remain "user"');
|
|
244
|
+
assert.ok(maEntries.length > 0,
|
|
245
|
+
'Fresh ma-agents entries should be appended');
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// Print summary
|
|
250
|
+
console.log(`\n${passed} passed, ${failed} failed`);
|
|
251
|
+
if (errors.length > 0) {
|
|
252
|
+
console.log('\nFailed tests:');
|
|
253
|
+
errors.forEach(e => console.log(` - ${e.name}: ${e.error}`));
|
|
254
|
+
}
|
|
255
|
+
if (failed > 0) process.exit(1);
|
|
256
|
+
})();
|