Skip to content

Commit 94cf189

Browse files
alexeyvclaude
andcommitted
test(quick-dev): add renderer smoke test with TOML override
New test/test-quick-dev-renderer.js spins up a temp project with base _bmad/config.toml and a _bmad/custom/config.user.toml override, runs render.py, and asserts the override wins in rendered workflow.md and that sprint_status is rooted at an absolute path in the temp project. Registered as test:renderer in package.json and chained into the npm test script. Part of plan-quick-dev-python-config-hardening.md (F7). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 1580a47 commit 94cf189

2 files changed

Lines changed: 178 additions & 2 deletions

File tree

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,12 @@
3939
"lint:fix": "eslint . --ext .js,.cjs,.mjs,.yaml --fix",
4040
"lint:md": "markdownlint-cli2 \"**/*.md\"",
4141
"prepare": "command -v husky >/dev/null 2>&1 && husky || exit 0",
42-
"quality": "npm run format:check && npm run lint && npm run lint:md && npm run docs:build && npm run test:install && npm run validate:refs && npm run validate:skills",
42+
"quality": "npm run format:check && npm run lint && npm run lint:md && npm run docs:build && npm run test:install && npm run test:renderer && npm run validate:refs && npm run validate:skills",
4343
"rebundle": "node tools/installer/bundlers/bundle-web.js rebundle",
44-
"test": "npm run test:refs && npm run test:install && npm run lint && npm run lint:md && npm run format:check",
44+
"test": "npm run test:refs && npm run test:install && npm run test:renderer && npm run lint && npm run lint:md && npm run format:check",
4545
"test:install": "node test/test-installation-components.js",
4646
"test:refs": "node test/test-file-refs-csv.js",
47+
"test:renderer": "node test/test-quick-dev-renderer.js",
4748
"validate:refs": "node tools/validate-file-refs.js --strict",
4849
"validate:skills": "node tools/validate-skills.js --strict"
4950
},

test/test-quick-dev-renderer.js

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
/**
2+
* Smoke test for bmad-quick-dev render.py
3+
*
4+
* Sets up a temp project with a base _bmad/config.toml and an override
5+
* _bmad/custom/config.user.toml, runs render.py, and asserts:
6+
* 1. The override wins (workflow.md contains "Japanese").
7+
* 2. sprint_status is an absolute path rooted at the temp project dir.
8+
*
9+
* Usage: node test/test-quick-dev-renderer.js
10+
* Exit codes: 0 = all tests pass, 1 = test failures
11+
*/
12+
13+
'use strict';
14+
15+
const fs = require('node:fs');
16+
const os = require('node:os');
17+
const path = require('node:path');
18+
const { spawnSync } = require('node:child_process');
19+
20+
// ANSI color codes (same as other test files)
21+
const colors = {
22+
reset: '\u001B[0m',
23+
green: '\u001B[32m',
24+
red: '\u001B[31m',
25+
cyan: '\u001B[36m',
26+
};
27+
28+
let totalTests = 0;
29+
let passedTests = 0;
30+
const failures = [];
31+
32+
function test(name, fn) {
33+
totalTests++;
34+
try {
35+
fn();
36+
passedTests++;
37+
console.log(` ${colors.green}\u2713${colors.reset} ${name}`);
38+
} catch (error) {
39+
console.log(` ${colors.red}\u2717${colors.reset} ${name} ${colors.red}${error.message}${colors.reset}`);
40+
failures.push({ name, message: error.message });
41+
}
42+
}
43+
44+
function assert(condition, message) {
45+
if (!condition) throw new Error(message);
46+
}
47+
48+
// ---------------------------------------------------------------------------
49+
// Helpers
50+
// ---------------------------------------------------------------------------
51+
52+
const SKILL_SRC = path.join(__dirname, '..', 'src', 'bmm-skills', '4-implementation', 'bmad-quick-dev');
53+
54+
/**
55+
* Recursively copy a directory (stdlib only, no fs.cp to stay >=20 compat).
56+
*/
57+
function copyDirSync(src, dst) {
58+
fs.mkdirSync(dst, { recursive: true });
59+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
60+
const srcPath = path.join(src, entry.name);
61+
const dstPath = path.join(dst, entry.name);
62+
if (entry.isDirectory()) {
63+
copyDirSync(srcPath, dstPath);
64+
} else {
65+
fs.copyFileSync(srcPath, dstPath);
66+
}
67+
}
68+
}
69+
70+
// ---------------------------------------------------------------------------
71+
// Test fixture setup
72+
// ---------------------------------------------------------------------------
73+
74+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'bmad-renderer-test-'));
75+
76+
try {
77+
// _bmad/config.toml — base layer
78+
fs.mkdirSync(path.join(tmpDir, '_bmad'), { recursive: true });
79+
fs.writeFileSync(
80+
path.join(tmpDir, '_bmad', 'config.toml'),
81+
[
82+
'[core]',
83+
'communication_language = "French"',
84+
'',
85+
'[modules.bmm]',
86+
'planning_artifacts = "{project-root}/plan"',
87+
'implementation_artifacts = "{project-root}/impl"',
88+
].join('\n'),
89+
'utf-8',
90+
);
91+
92+
// _bmad/custom/config.user.toml — override layer (should win)
93+
fs.mkdirSync(path.join(tmpDir, '_bmad', 'custom'), { recursive: true });
94+
fs.writeFileSync(
95+
path.join(tmpDir, '_bmad', 'custom', 'config.user.toml'),
96+
['[core]', 'communication_language = "Japanese"'].join('\n'),
97+
'utf-8',
98+
);
99+
100+
// Copy skill dir into <tmpDir>/bmad-quick-dev/ so find_project_root() walks
101+
// up and finds <tmpDir>/_bmad/, and os.path.basename(script_dir) resolves
102+
// to the real skill name so the render output lands at
103+
// _bmad/render/bmad-quick-dev/workflow.md.
104+
const skillDst = path.join(tmpDir, 'bmad-quick-dev');
105+
copyDirSync(SKILL_SRC, skillDst);
106+
107+
// ---------------------------------------------------------------------------
108+
// Run render.py
109+
// ---------------------------------------------------------------------------
110+
111+
console.log(`\n${colors.cyan}Quick-dev renderer smoke tests${colors.reset}\n`);
112+
113+
const result = spawnSync('python3', [path.join(skillDst, 'render.py')], {
114+
cwd: skillDst,
115+
encoding: 'utf-8',
116+
});
117+
118+
// ---------------------------------------------------------------------------
119+
// Tests
120+
// ---------------------------------------------------------------------------
121+
122+
test('render.py exits with code 0', () => {
123+
assert(result.status === 0, `exit code ${result.status}\nstdout: ${result.stdout}\nstderr: ${result.stderr}`);
124+
});
125+
126+
test('workflow.md exists in render output', () => {
127+
const rendered = path.join(tmpDir, '_bmad', 'render', 'bmad-quick-dev', 'workflow.md');
128+
assert(fs.existsSync(rendered), `workflow.md not found at ${rendered}`);
129+
});
130+
131+
test('custom override wins — workflow.md contains "Japanese"', () => {
132+
const rendered = path.join(tmpDir, '_bmad', 'render', 'bmad-quick-dev', 'workflow.md');
133+
const content = fs.readFileSync(rendered, 'utf-8');
134+
assert(content.includes('Japanese'), `"Japanese" not found in workflow.md (communication_language override did not win)`);
135+
});
136+
137+
test('sprint_status is an absolute path rooted at temp project dir', () => {
138+
const rendered = path.join(tmpDir, '_bmad', 'render', 'bmad-quick-dev', 'workflow.md');
139+
const content = fs.readFileSync(rendered, 'utf-8');
140+
// Normalize to forward slashes for cross-platform matching
141+
const normalizedTmp = tmpDir.replaceAll('\\', '/');
142+
// sprint_status should appear as <tmpDir>/impl/sprint-status.yaml
143+
const expected = `${normalizedTmp}/impl/sprint-status.yaml`;
144+
assert(
145+
content.includes(expected),
146+
`sprint_status path not found.\nExpected substring: ${expected}\n` +
147+
`workflow.md excerpt (first 2000 chars):\n${content.slice(0, 2000)}`,
148+
);
149+
});
150+
} finally {
151+
fs.rmSync(tmpDir, { recursive: true, force: true });
152+
}
153+
154+
// ---------------------------------------------------------------------------
155+
// Summary
156+
// ---------------------------------------------------------------------------
157+
158+
console.log(`\n${colors.cyan}${'═'.repeat(55)}${colors.reset}`);
159+
console.log(`${colors.cyan}Test Results:${colors.reset}`);
160+
console.log(` Total: ${totalTests}`);
161+
console.log(` Passed: ${colors.green}${passedTests}${colors.reset}`);
162+
console.log(` Failed: ${passedTests === totalTests ? colors.green : colors.red}${totalTests - passedTests}${colors.reset}`);
163+
console.log(`${colors.cyan}${'═'.repeat(55)}${colors.reset}\n`);
164+
165+
if (failures.length > 0) {
166+
console.log(`${colors.red}FAILED TESTS:${colors.reset}\n`);
167+
for (const failure of failures) {
168+
console.log(`${colors.red}\u2717${colors.reset} ${failure.name}`);
169+
console.log(` ${failure.message}\n`);
170+
}
171+
process.exit(1);
172+
}
173+
174+
console.log(`${colors.green}All tests passed!${colors.reset}\n`);
175+
process.exit(0);

0 commit comments

Comments
 (0)