BMAD-METHOD/test/test-path-utils.js

369 lines
9.6 KiB
JavaScript

/**
* Unit Tests for path-utils.js
*
* Tests the toDashPath function which converts hierarchical file paths
* to flat dash-separated naming for IDE skill registration.
*
* Fixes tested:
* - Deduplication of matching folder/file names (issue #1422)
* - Handling of .yaml extension in addition to .md
*
* Run: node test/test-path-utils.js
*/
const {
toDashPath,
toDashName,
parseDashName,
isDashFormat,
} = require('../tools/cli/installers/lib/ide/shared/path-utils.js');
console.log('Running path-utils unit tests...\n');
let passed = 0;
let failed = 0;
function test(name, actual, expected) {
if (actual === expected) {
console.log(`${name}`);
passed++;
} else {
console.log(`${name}`);
console.log(` Expected: ${expected}`);
console.log(` Actual: ${actual}`);
failed++;
}
}
function testObject(name, actual, expected) {
const actualStr = JSON.stringify(actual);
const expectedStr = JSON.stringify(expected);
if (actualStr === expectedStr) {
console.log(`${name}`);
passed++;
} else {
console.log(`${name}`);
console.log(` Expected: ${expectedStr}`);
console.log(` Actual: ${actualStr}`);
failed++;
}
}
// ============================================
// CORE FUNCTIONALITY - Flat Files
// ============================================
console.log('\n--- Flat file paths (no subdirectory) ---');
test(
'module agent: bmm/agents/dev.md',
toDashPath('bmm/agents/dev.md'),
'bmad-bmm-dev.agent.md'
);
test(
'module workflow: bmm/workflows/sprint-planning.md',
toDashPath('bmm/workflows/sprint-planning.md'),
'bmad-bmm-sprint-planning.md'
);
test(
'module task: bmm/tasks/create-doc.md',
toDashPath('bmm/tasks/create-doc.md'),
'bmad-bmm-create-doc.md'
);
test(
'core agent (no module prefix): core/agents/bmad-master.md',
toDashPath('core/agents/bmad-master.md'),
'bmad-bmad-master.agent.md'
);
test(
'core workflow: core/workflows/brainstorming.md',
toDashPath('core/workflows/brainstorming.md'),
'bmad-brainstorming.md'
);
test(
'hyphenated names preserved: bmm/agents/quick-flow-solo-dev.md',
toDashPath('bmm/agents/quick-flow-solo-dev.md'),
'bmad-bmm-quick-flow-solo-dev.agent.md'
);
// ============================================
// BUG FIX - Matching Folder/File Deduplication (Issue #1422)
// ============================================
console.log('\n--- Bug fix: Matching folder/file deduplication (issue #1422) ---');
test(
'tech-writer/tech-writer.md → tech-writer (not tech-writer-tech-writer)',
toDashPath('bmm/agents/tech-writer/tech-writer.md'),
'bmad-bmm-tech-writer.agent.md'
);
test(
'storyteller/storyteller.md → storyteller',
toDashPath('cis/agents/storyteller/storyteller.md'),
'bmad-cis-storyteller.agent.md'
);
test(
'workflow with matching folder/file: party-mode/party-mode.md',
toDashPath('bmm/workflows/party-mode/party-mode.md'),
'bmad-bmm-party-mode.md'
);
test(
'core agent with matching folder/file: master/master.md',
toDashPath('core/agents/master/master.md'),
'bmad-master.agent.md'
);
// ============================================
// NON-MATCHING Nested Paths (Should NOT Dedupe)
// ============================================
console.log('\n--- Non-matching nested paths (no deduplication) ---');
test(
'create-prd/workflow.md keeps both segments',
toDashPath('bmm/workflows/create-prd/workflow.md'),
'bmad-bmm-create-prd-workflow.md'
);
test(
'document-project/checklist.md keeps both segments',
toDashPath('bmm/workflows/document-project/checklist.md'),
'bmad-bmm-document-project-checklist.md'
);
test(
'tech-writer/sidecar.md keeps both segments',
toDashPath('bmm/agents/tech-writer/sidecar.md'),
'bmad-bmm-tech-writer-sidecar.agent.md'
);
test(
'partial match NOT deduplicated: tech-writer/tech-writers.md',
toDashPath('bmm/agents/tech-writer/tech-writers.md'),
'bmad-bmm-tech-writer-tech-writers.agent.md'
);
test(
'substring match NOT deduplicated: writer/tech-writer.md',
toDashPath('bmm/agents/writer/tech-writer.md'),
'bmad-bmm-writer-tech-writer.agent.md'
);
// ============================================
// DEEP NESTING (3+ Levels)
// ============================================
console.log('\n--- Deep nesting (multiple subdirectories) ---');
test(
'category/writer/writer.md deduplicates last two only',
toDashPath('bmm/agents/category/writer/writer.md'),
'bmad-bmm-category-writer.agent.md'
);
test(
'ui/forms/validator/validator.md deduplicates last two only',
toDashPath('bmm/agents/ui/forms/validator/validator.md'),
'bmad-bmm-ui-forms-validator.agent.md'
);
test(
'deep path with different last two: a/b/c/d.md',
toDashPath('bmm/agents/a/b/c/d.md'),
'bmad-bmm-a-b-c-d.agent.md'
);
test(
'three matching segments: foo/foo/foo.md - only last two dedupe',
toDashPath('bmm/agents/foo/foo/foo.md'),
'bmad-bmm-foo-foo.agent.md'
);
// ============================================
// CASE SENSITIVITY
// ============================================
console.log('\n--- Case sensitivity ---');
test(
'different case NOT deduplicated: Tech-Writer/tech-writer.md',
toDashPath('bmm/agents/Tech-Writer/tech-writer.md'),
'bmad-bmm-Tech-Writer-tech-writer.agent.md'
);
test(
'same case deduplicated: WRITER/WRITER.md',
toDashPath('bmm/agents/WRITER/WRITER.md'),
'bmad-bmm-WRITER.agent.md'
);
// ============================================
// SPECIAL CHARACTERS IN NAMES
// ============================================
console.log('\n--- Special characters in names ---');
test(
'names with numbers: v2-helper/v2-helper.md',
toDashPath('bmm/agents/v2-helper/v2-helper.md'),
'bmad-bmm-v2-helper.agent.md'
);
test(
'names with underscores: my_agent/my_agent.md',
toDashPath('bmm/agents/my_agent/my_agent.md'),
'bmad-bmm-my_agent.agent.md'
);
// ============================================
// INPUT VALIDATION & EDGE CASES
// ============================================
console.log('\n--- Input validation ---');
test('null returns fallback', toDashPath(null), 'bmad-unknown.md');
test('undefined returns fallback', toDashPath(undefined), 'bmad-unknown.md');
test('empty string returns fallback', toDashPath(''), 'bmad-unknown.md');
test('number returns fallback', toDashPath(123), 'bmad-unknown.md');
test('object returns fallback', toDashPath({}), 'bmad-unknown.md');
// ============================================
// WINDOWS PATH SEPARATORS
// ============================================
console.log('\n--- Windows path separators (backslashes) ---');
test(
'backslashes converted correctly',
toDashPath('bmm\\agents\\dev.md'),
'bmad-bmm-dev.agent.md'
);
test(
'nested Windows path with matching folder/file',
toDashPath('bmm\\agents\\tech-writer\\tech-writer.md'),
'bmad-bmm-tech-writer.agent.md'
);
test(
'mixed separators',
toDashPath('bmm/agents\\tech-writer/tech-writer.md'),
'bmad-bmm-tech-writer.agent.md'
);
// ============================================
// FILE EXTENSION HANDLING
// ============================================
console.log('\n--- File extension handling ---');
test(
'.md extension removed',
toDashPath('bmm/agents/dev.md'),
'bmad-bmm-dev.agent.md'
);
test(
'.yaml extension removed (new fix)',
toDashPath('bmm/agents/dev.yaml'),
'bmad-bmm-dev.agent.md'
);
test(
'.yaml with matching folder/file',
toDashPath('bmm/agents/tech-writer/tech-writer.yaml'),
'bmad-bmm-tech-writer.agent.md'
);
test(
'no extension handled',
toDashPath('bmm/agents/dev'),
'bmad-bmm-dev.agent.md'
);
// ============================================
// ROUNDTRIP TESTS
// ============================================
// Note: parseDashName has pre-existing issues with .agent.md extension handling
// that are outside the scope of issue #1422. The tests below verify that
// toDashPath output is structurally valid for parseDashName, even if the
// parsed values have known issues.
console.log('\n--- Roundtrip consistency (toDashPath → parseDashName) ---');
{
const dashName = toDashPath('bmm/agents/dev.md');
const parsed = parseDashName(dashName);
// Verify parseDashName returns an object (not null) for valid toDashPath output
if (parsed !== null && typeof parsed === 'object') {
console.log('✓ flat agent produces parseable output');
passed++;
} else {
console.log('✗ flat agent produces parseable output');
failed++;
}
}
{
const dashName = toDashPath('bmm/agents/tech-writer/tech-writer.md');
const parsed = parseDashName(dashName);
if (parsed !== null && typeof parsed === 'object') {
console.log('✓ nested deduplicated agent produces parseable output');
passed++;
} else {
console.log('✗ nested deduplicated agent produces parseable output');
failed++;
}
}
{
const dashName = toDashPath('core/agents/master.md');
const parsed = parseDashName(dashName);
if (parsed !== null && typeof parsed === 'object') {
console.log('✓ core agent produces parseable output');
passed++;
} else {
console.log('✗ core agent produces parseable output');
failed++;
}
}
// ============================================
// isDashFormat VALIDATION
// ============================================
console.log('\n--- isDashFormat validation ---');
test(
'valid dash format returns true',
isDashFormat('bmad-bmm-dev.agent.md'),
true
);
test('non-dash format returns false', isDashFormat('dev.md'), false);
// ============================================
// SUMMARY
// ============================================
console.log('\n========================================');
console.log(`Results: ${passed} passed, ${failed} failed`);
console.log('========================================\n');
if (failed > 0) {
process.exit(1);
}