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>
This commit is contained in:
parent
38b2ffe53d
commit
550807f8ec
|
|
@ -39,12 +39,13 @@
|
||||||
"lint:fix": "eslint . --ext .js,.cjs,.mjs,.yaml --fix",
|
"lint:fix": "eslint . --ext .js,.cjs,.mjs,.yaml --fix",
|
||||||
"lint:md": "markdownlint-cli2 \"**/*.md\"",
|
"lint:md": "markdownlint-cli2 \"**/*.md\"",
|
||||||
"prepare": "command -v husky >/dev/null 2>&1 && husky || exit 0",
|
"prepare": "command -v husky >/dev/null 2>&1 && husky || exit 0",
|
||||||
"quality": "npm run format:check && npm run lint && npm run lint:md && npm run docs:build && npm run test:install && npm run test:urls && npm run validate:refs && npm run validate:skills",
|
"quality": "npm run format:check && npm run lint && npm run lint:md && npm run docs:build && npm run test:install && npm run test:urls && npm run test:renderer && npm run validate:refs && npm run validate:skills",
|
||||||
"rebundle": "node tools/installer/bundlers/bundle-web.js rebundle",
|
"rebundle": "node tools/installer/bundlers/bundle-web.js rebundle",
|
||||||
"test": "npm run test:refs && npm run test:install && npm run test:urls && npm run test:channels && npm run lint && npm run lint:md && npm run format:check",
|
"test": "npm run test:refs && npm run test:install && npm run test:urls && npm run test:channels && npm run test:renderer && npm run lint && npm run lint:md && npm run format:check",
|
||||||
"test:channels": "node test/test-installer-channels.js",
|
"test:channels": "node test/test-installer-channels.js",
|
||||||
"test:install": "node test/test-installation-components.js",
|
"test:install": "node test/test-installation-components.js",
|
||||||
"test:refs": "node test/test-file-refs-csv.js",
|
"test:refs": "node test/test-file-refs-csv.js",
|
||||||
|
"test:renderer": "node test/test-quick-dev-renderer.js",
|
||||||
"test:urls": "node test/test-parse-source-urls.js",
|
"test:urls": "node test/test-parse-source-urls.js",
|
||||||
"validate:refs": "node tools/validate-file-refs.js --strict",
|
"validate:refs": "node tools/validate-file-refs.js --strict",
|
||||||
"validate:skills": "node tools/validate-skills.js --strict"
|
"validate:skills": "node tools/validate-skills.js --strict"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,175 @@
|
||||||
|
/**
|
||||||
|
* Smoke test for bmad-quick-dev render.py
|
||||||
|
*
|
||||||
|
* Sets up a temp project with a base _bmad/config.toml and an override
|
||||||
|
* _bmad/custom/config.user.toml, runs render.py, and asserts:
|
||||||
|
* 1. The override wins (workflow.md contains "Japanese").
|
||||||
|
* 2. sprint_status is an absolute path rooted at the temp project dir.
|
||||||
|
*
|
||||||
|
* Usage: node test/test-quick-dev-renderer.js
|
||||||
|
* Exit codes: 0 = all tests pass, 1 = test failures
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const os = require('node:os');
|
||||||
|
const path = require('node:path');
|
||||||
|
const { spawnSync } = require('node:child_process');
|
||||||
|
|
||||||
|
// ANSI color codes (same as other test files)
|
||||||
|
const colors = {
|
||||||
|
reset: '\u001B[0m',
|
||||||
|
green: '\u001B[32m',
|
||||||
|
red: '\u001B[31m',
|
||||||
|
cyan: '\u001B[36m',
|
||||||
|
};
|
||||||
|
|
||||||
|
let totalTests = 0;
|
||||||
|
let passedTests = 0;
|
||||||
|
const failures = [];
|
||||||
|
|
||||||
|
function test(name, fn) {
|
||||||
|
totalTests++;
|
||||||
|
try {
|
||||||
|
fn();
|
||||||
|
passedTests++;
|
||||||
|
console.log(` ${colors.green}\u2713${colors.reset} ${name}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(` ${colors.red}\u2717${colors.reset} ${name} ${colors.red}${error.message}${colors.reset}`);
|
||||||
|
failures.push({ name, message: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function assert(condition, message) {
|
||||||
|
if (!condition) throw new Error(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const SKILL_SRC = path.join(__dirname, '..', 'src', 'bmm-skills', '4-implementation', 'bmad-quick-dev');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively copy a directory (stdlib only, no fs.cp to stay >=20 compat).
|
||||||
|
*/
|
||||||
|
function copyDirSync(src, dst) {
|
||||||
|
fs.mkdirSync(dst, { recursive: true });
|
||||||
|
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
||||||
|
const srcPath = path.join(src, entry.name);
|
||||||
|
const dstPath = path.join(dst, entry.name);
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
copyDirSync(srcPath, dstPath);
|
||||||
|
} else {
|
||||||
|
fs.copyFileSync(srcPath, dstPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Test fixture setup
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'bmad-renderer-test-'));
|
||||||
|
|
||||||
|
try {
|
||||||
|
// _bmad/config.toml — base layer
|
||||||
|
fs.mkdirSync(path.join(tmpDir, '_bmad'), { recursive: true });
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(tmpDir, '_bmad', 'config.toml'),
|
||||||
|
[
|
||||||
|
'[core]',
|
||||||
|
'communication_language = "French"',
|
||||||
|
'',
|
||||||
|
'[modules.bmm]',
|
||||||
|
'planning_artifacts = "{project-root}/plan"',
|
||||||
|
'implementation_artifacts = "{project-root}/impl"',
|
||||||
|
].join('\n'),
|
||||||
|
'utf-8',
|
||||||
|
);
|
||||||
|
|
||||||
|
// _bmad/custom/config.user.toml — override layer (should win)
|
||||||
|
fs.mkdirSync(path.join(tmpDir, '_bmad', 'custom'), { recursive: true });
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(tmpDir, '_bmad', 'custom', 'config.user.toml'),
|
||||||
|
['[core]', 'communication_language = "Japanese"'].join('\n'),
|
||||||
|
'utf-8',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Copy skill dir into <tmpDir>/bmad-quick-dev/ so find_project_root() walks
|
||||||
|
// up and finds <tmpDir>/_bmad/, and os.path.basename(script_dir) resolves
|
||||||
|
// to the real skill name so the render output lands at
|
||||||
|
// _bmad/render/bmad-quick-dev/workflow.md.
|
||||||
|
const skillDst = path.join(tmpDir, 'bmad-quick-dev');
|
||||||
|
copyDirSync(SKILL_SRC, skillDst);
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Run render.py
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
console.log(`\n${colors.cyan}Quick-dev renderer smoke tests${colors.reset}\n`);
|
||||||
|
|
||||||
|
const result = spawnSync('python3', [path.join(skillDst, 'render.py')], {
|
||||||
|
cwd: skillDst,
|
||||||
|
encoding: 'utf-8',
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Tests
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
test('render.py exits with code 0', () => {
|
||||||
|
assert(result.status === 0, `exit code ${result.status}\nstdout: ${result.stdout}\nstderr: ${result.stderr}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('workflow.md exists in render output', () => {
|
||||||
|
const rendered = path.join(tmpDir, '_bmad', 'render', 'bmad-quick-dev', 'workflow.md');
|
||||||
|
assert(fs.existsSync(rendered), `workflow.md not found at ${rendered}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('custom override wins — workflow.md contains "Japanese"', () => {
|
||||||
|
const rendered = path.join(tmpDir, '_bmad', 'render', 'bmad-quick-dev', 'workflow.md');
|
||||||
|
const content = fs.readFileSync(rendered, 'utf-8');
|
||||||
|
assert(content.includes('Japanese'), `"Japanese" not found in workflow.md (communication_language override did not win)`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('sprint_status is an absolute path rooted at temp project dir', () => {
|
||||||
|
const rendered = path.join(tmpDir, '_bmad', 'render', 'bmad-quick-dev', 'workflow.md');
|
||||||
|
const content = fs.readFileSync(rendered, 'utf-8');
|
||||||
|
// Normalize to forward slashes for cross-platform matching
|
||||||
|
const normalizedTmp = tmpDir.replaceAll('\\', '/');
|
||||||
|
// sprint_status should appear as <tmpDir>/impl/sprint-status.yaml
|
||||||
|
const expected = `${normalizedTmp}/impl/sprint-status.yaml`;
|
||||||
|
assert(
|
||||||
|
content.includes(expected),
|
||||||
|
`sprint_status path not found.\nExpected substring: ${expected}\n` +
|
||||||
|
`workflow.md excerpt (first 2000 chars):\n${content.slice(0, 2000)}`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Summary
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
console.log(`\n${colors.cyan}${'═'.repeat(55)}${colors.reset}`);
|
||||||
|
console.log(`${colors.cyan}Test Results:${colors.reset}`);
|
||||||
|
console.log(` Total: ${totalTests}`);
|
||||||
|
console.log(` Passed: ${colors.green}${passedTests}${colors.reset}`);
|
||||||
|
console.log(` Failed: ${passedTests === totalTests ? colors.green : colors.red}${totalTests - passedTests}${colors.reset}`);
|
||||||
|
console.log(`${colors.cyan}${'═'.repeat(55)}${colors.reset}\n`);
|
||||||
|
|
||||||
|
if (failures.length > 0) {
|
||||||
|
console.log(`${colors.red}FAILED TESTS:${colors.reset}\n`);
|
||||||
|
for (const failure of failures) {
|
||||||
|
console.log(`${colors.red}\u2717${colors.reset} ${failure.name}`);
|
||||||
|
console.log(` ${failure.message}\n`);
|
||||||
|
}
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`${colors.green}All tests passed!${colors.reset}\n`);
|
||||||
|
process.exit(0);
|
||||||
Loading…
Reference in New Issue