Merge c035b2f247 into 4ded43d707
This commit is contained in:
commit
b08909ccf4
|
|
@ -0,0 +1,368 @@
|
|||
/**
|
||||
* 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);
|
||||
}
|
||||
|
|
@ -48,6 +48,7 @@ function toDashName(module, type, name) {
|
|||
* Converts: 'bmm/agents/pm.md' → 'bmad-bmm-pm.agent.md'
|
||||
* Converts: 'bmm/workflows/correct-course.md' → 'bmad-bmm-correct-course.md'
|
||||
* Converts: 'core/agents/brainstorming.md' → 'bmad-brainstorming.agent.md' (core items skip module prefix)
|
||||
* Converts: 'bmm/agents/tech-writer/tech-writer.md' → 'bmad-bmm-tech-writer.agent.md' (deduplicates matching folder/file)
|
||||
*
|
||||
* @param {string} relativePath - Path like 'bmm/agents/pm.md'
|
||||
* @returns {string} Flat filename like 'bmad-bmm-pm.agent.md' or 'bmad-brainstorming.md'
|
||||
|
|
@ -58,12 +59,28 @@ function toDashPath(relativePath) {
|
|||
return 'bmad-unknown.md';
|
||||
}
|
||||
|
||||
const withoutExt = relativePath.replace('.md', '');
|
||||
// Handle both .md and .yaml extensions
|
||||
const withoutExt = relativePath.replace(/\.(md|yaml)$/, '');
|
||||
const parts = withoutExt.split(/[/\\]/);
|
||||
|
||||
const module = parts[0];
|
||||
const type = parts[1];
|
||||
const name = parts.slice(2).join('-');
|
||||
|
||||
// Get name parts (everything after module/type)
|
||||
let nameParts = parts.slice(2);
|
||||
|
||||
// Deduplicate when folder name matches filename
|
||||
// e.g., ['tech-writer', 'tech-writer'] → ['tech-writer']
|
||||
// This prevents redundant skill names like 'bmad-bmm-tech-writer-tech-writer'
|
||||
// Fixes: https://github.com/bmad-code-org/BMAD-METHOD/issues/1422
|
||||
if (nameParts.length >= 2) {
|
||||
const lastTwo = nameParts.slice(-2);
|
||||
if (lastTwo[0] === lastTwo[1]) {
|
||||
nameParts = [...nameParts.slice(0, -2), lastTwo[0]];
|
||||
}
|
||||
}
|
||||
|
||||
const name = nameParts.join('-');
|
||||
|
||||
return toDashName(module, type, name);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue