BMAD-METHOD/test/test-installation-component...

2818 lines
110 KiB
JavaScript

/**
* Installation Component Tests
*
* Tests individual installation components in isolation:
* - Agent YAML → XML compilation
* - Manifest generation
* - Path resolution
* - Customization merging
*
* These are deterministic unit tests that don't require full installation.
* Usage: node test/test-installation-components.js
*/
const path = require('node:path');
const os = require('node:os');
const fs = require('fs-extra');
const yaml = require('yaml');
const csv = require('csv-parse/sync');
const { YamlXmlBuilder } = require('../tools/cli/lib/yaml-xml-builder');
const { Installer } = require('../tools/cli/installers/lib/core/installer');
const { ManifestGenerator } = require('../tools/cli/installers/lib/core/manifest-generator');
const { TaskToolCommandGenerator } = require('../tools/cli/installers/lib/ide/shared/task-tool-command-generator');
const { GitHubCopilotSetup } = require('../tools/cli/installers/lib/ide/github-copilot');
const {
HELP_ALIAS_NORMALIZATION_ERROR_CODES,
LOCKED_EXEMPLAR_ALIAS_ROWS,
normalizeRawIdentityToTuple,
resolveAliasTupleFromRows,
resolveAliasTupleUsingCanonicalAliasCsv,
normalizeAndResolveExemplarAlias,
} = require('../tools/cli/installers/lib/core/help-alias-normalizer');
const {
HELP_SIDECAR_REQUIRED_FIELDS,
HELP_SIDECAR_ERROR_CODES,
validateHelpSidecarContractFile,
} = require('../tools/cli/installers/lib/core/sidecar-contract-validator');
const {
HELP_FRONTMATTER_MISMATCH_ERROR_CODES,
validateHelpAuthoritySplitAndPrecedence,
} = require('../tools/cli/installers/lib/core/help-authority-validator');
const {
HELP_CATALOG_GENERATION_ERROR_CODES,
EXEMPLAR_HELP_CATALOG_AUTHORITY_SOURCE_PATH,
EXEMPLAR_HELP_CATALOG_ISSUING_COMPONENT,
INSTALLER_HELP_CATALOG_MERGE_COMPONENT,
buildSidecarAwareExemplarHelpRow,
evaluateExemplarCommandLabelReportRows,
} = require('../tools/cli/installers/lib/core/help-catalog-generator');
const {
CodexSetup,
CODEX_EXPORT_DERIVATION_ERROR_CODES,
EXEMPLAR_HELP_EXPORT_DERIVATION_SOURCE_TYPE,
} = require('../tools/cli/installers/lib/ide/codex');
const {
PROJECTION_COMPATIBILITY_ERROR_CODES,
TASK_MANIFEST_COMPATIBILITY_PREFIX_COLUMNS,
TASK_MANIFEST_WAVE1_ADDITIVE_COLUMNS,
HELP_CATALOG_COMPATIBILITY_PREFIX_COLUMNS,
HELP_CATALOG_WAVE1_ADDITIVE_COLUMNS,
validateTaskManifestCompatibilitySurface,
validateTaskManifestLoaderEntries,
validateHelpCatalogCompatibilitySurface,
validateHelpCatalogLoaderEntries,
validateGithubCopilotHelpLoaderEntries,
} = require('../tools/cli/installers/lib/core/projection-compatibility-validator');
const {
WAVE1_VALIDATION_ERROR_CODES,
WAVE1_VALIDATION_ARTIFACT_REGISTRY,
Wave1ValidationHarness,
} = require('../tools/cli/installers/lib/core/wave-1-validation-harness');
// ANSI colors
const colors = {
reset: '\u001B[0m',
green: '\u001B[32m',
red: '\u001B[31m',
yellow: '\u001B[33m',
cyan: '\u001B[36m',
dim: '\u001B[2m',
};
let passed = 0;
let failed = 0;
/**
* Test helper: Assert condition
*/
function assert(condition, testName, errorMessage = '') {
if (condition) {
console.log(`${colors.green}${colors.reset} ${testName}`);
passed++;
} else {
console.log(`${colors.red}${colors.reset} ${testName}`);
if (errorMessage) {
console.log(` ${colors.dim}${errorMessage}${colors.reset}`);
}
failed++;
}
}
/**
* Test Suite
*/
async function runTests() {
console.log(`${colors.cyan}========================================`);
console.log('Installation Component Tests');
console.log(`========================================${colors.reset}\n`);
const projectRoot = path.join(__dirname, '..');
// ============================================================
// Test 1: YAML → XML Agent Compilation (In-Memory)
// ============================================================
console.log(`${colors.yellow}Test Suite 1: Agent Compilation${colors.reset}\n`);
try {
const builder = new YamlXmlBuilder();
const pmAgentPath = path.join(projectRoot, 'src/bmm/agents/pm.agent.yaml');
// Create temp output path
const tempOutput = path.join(__dirname, 'temp-pm-agent.md');
try {
const result = await builder.buildAgent(pmAgentPath, null, tempOutput, { includeMetadata: true });
assert(result && result.outputPath === tempOutput, 'Agent compilation returns result object with outputPath');
// Read the output
const compiled = await fs.readFile(tempOutput, 'utf8');
assert(compiled.includes('<agent'), 'Compiled agent contains <agent> tag');
assert(compiled.includes('<persona>'), 'Compiled agent contains <persona> tag');
assert(compiled.includes('<menu>'), 'Compiled agent contains <menu> tag');
assert(compiled.includes('Product Manager'), 'Compiled agent contains agent title');
// Cleanup
await fs.remove(tempOutput);
} catch (error) {
assert(false, 'Agent compilation succeeds', error.message);
}
} catch (error) {
assert(false, 'YamlXmlBuilder instantiates', error.message);
}
console.log('');
// ============================================================
// Test 2: Customization Merging
// ============================================================
console.log(`${colors.yellow}Test Suite 2: Customization Merging${colors.reset}\n`);
try {
const builder = new YamlXmlBuilder();
// Test deepMerge function
const base = {
agent: {
metadata: { name: 'John', title: 'PM' },
persona: { role: 'Product Manager', style: 'Analytical' },
},
};
const customize = {
agent: {
metadata: { name: 'Sarah' }, // Override name only
persona: { style: 'Concise' }, // Override style only
},
};
const merged = builder.deepMerge(base, customize);
assert(merged.agent.metadata.name === 'Sarah', 'Deep merge overrides customized name');
assert(merged.agent.metadata.title === 'PM', 'Deep merge preserves non-overridden title');
assert(merged.agent.persona.role === 'Product Manager', 'Deep merge preserves non-overridden role');
assert(merged.agent.persona.style === 'Concise', 'Deep merge overrides customized style');
} catch (error) {
assert(false, 'Customization merging works', error.message);
}
console.log('');
// ============================================================
// Test 3: Path Resolution
// ============================================================
console.log(`${colors.yellow}Test Suite 3: Path Variable Resolution${colors.reset}\n`);
try {
const builder = new YamlXmlBuilder();
// Test path resolution logic (if exposed)
// This would test {project-root}, {installed_path}, {config_source} resolution
const testPath = '{project-root}/bmad/bmm/config.yaml';
const expectedPattern = /\/bmad\/bmm\/config\.yaml$/;
assert(
true, // Placeholder - would test actual resolution
'Path variable resolution pattern matches expected format',
'Note: This test validates path resolution logic exists',
);
} catch (error) {
assert(false, 'Path resolution works', error.message);
}
console.log('');
// ============================================================
// Test 4: Exemplar Sidecar Contract Validation
// ============================================================
console.log(`${colors.yellow}Test Suite 4: Sidecar Contract Validation${colors.reset}\n`);
const validHelpSidecar = {
schemaVersion: 1,
canonicalId: 'bmad-help',
artifactType: 'task',
module: 'core',
sourcePath: 'bmad-fork/src/core/tasks/help.md',
displayName: 'help',
description: 'Help command',
dependencies: {
requires: [],
},
};
const tempSidecarRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-help-sidecar-'));
const tempSidecarPath = path.join(tempSidecarRoot, 'help.artifact.yaml');
const deterministicSourcePath = 'bmad-fork/src/core/tasks/help.artifact.yaml';
const expectedUnsupportedMajorDetail = 'sidecar schema major version is unsupported';
const expectedBasenameMismatchDetail = 'sidecar basename does not match sourcePath basename';
const writeTempSidecar = async (data) => {
await fs.writeFile(tempSidecarPath, yaml.stringify(data), 'utf8');
};
const expectValidationError = async (data, expectedCode, expectedFieldPath, testLabel, expectedDetail = null) => {
await writeTempSidecar(data);
try {
await validateHelpSidecarContractFile(tempSidecarPath, { errorSourcePath: deterministicSourcePath });
assert(false, testLabel, 'Expected validation error but validation passed');
} catch (error) {
assert(error.code === expectedCode, `${testLabel} returns expected error code`, `Expected ${expectedCode}, got ${error.code}`);
assert(
error.fieldPath === expectedFieldPath,
`${testLabel} returns expected field path`,
`Expected ${expectedFieldPath}, got ${error.fieldPath}`,
);
assert(
typeof error.message === 'string' &&
error.message.includes(expectedCode) &&
error.message.includes(expectedFieldPath) &&
error.message.includes(deterministicSourcePath),
`${testLabel} includes deterministic message context`,
);
if (expectedDetail !== null) {
assert(
error.detail === expectedDetail,
`${testLabel} returns locked detail string`,
`Expected "${expectedDetail}", got "${error.detail}"`,
);
}
}
};
try {
await writeTempSidecar(validHelpSidecar);
await validateHelpSidecarContractFile(tempSidecarPath, { errorSourcePath: deterministicSourcePath });
assert(true, 'Valid sidecar contract passes');
for (const requiredField of HELP_SIDECAR_REQUIRED_FIELDS.filter((field) => field !== 'dependencies')) {
const invalidSidecar = structuredClone(validHelpSidecar);
delete invalidSidecar[requiredField];
await expectValidationError(
invalidSidecar,
HELP_SIDECAR_ERROR_CODES.REQUIRED_FIELD_MISSING,
requiredField,
`Missing required field "${requiredField}"`,
);
}
await expectValidationError(
{ ...validHelpSidecar, artifactType: 'workflow' },
HELP_SIDECAR_ERROR_CODES.ARTIFACT_TYPE_INVALID,
'artifactType',
'Invalid artifactType',
);
await expectValidationError(
{ ...validHelpSidecar, module: 'bmm' },
HELP_SIDECAR_ERROR_CODES.MODULE_INVALID,
'module',
'Invalid module',
);
await expectValidationError(
{ ...validHelpSidecar, schemaVersion: 2 },
HELP_SIDECAR_ERROR_CODES.MAJOR_VERSION_UNSUPPORTED,
'schemaVersion',
'Unsupported sidecar major schema version',
expectedUnsupportedMajorDetail,
);
await expectValidationError(
{ ...validHelpSidecar, canonicalId: ' ' },
HELP_SIDECAR_ERROR_CODES.REQUIRED_FIELD_EMPTY,
'canonicalId',
'Empty canonicalId',
);
await expectValidationError(
{ ...validHelpSidecar, sourcePath: '' },
HELP_SIDECAR_ERROR_CODES.REQUIRED_FIELD_EMPTY,
'sourcePath',
'Empty sourcePath',
);
await expectValidationError(
{ ...validHelpSidecar, sourcePath: 'bmad-fork/src/core/tasks/not-help.md' },
HELP_SIDECAR_ERROR_CODES.SOURCEPATH_BASENAME_MISMATCH,
'sourcePath',
'Source path mismatch with exemplar contract',
expectedBasenameMismatchDetail,
);
const mismatchedBasenamePath = path.join(tempSidecarRoot, 'not-help.artifact.yaml');
await fs.writeFile(mismatchedBasenamePath, yaml.stringify(validHelpSidecar), 'utf8');
try {
await validateHelpSidecarContractFile(mismatchedBasenamePath, {
errorSourcePath: 'bmad-fork/src/core/tasks/not-help.artifact.yaml',
});
assert(false, 'Sidecar basename mismatch returns validation error', 'Expected validation error but validation passed');
} catch (error) {
assert(error.code === HELP_SIDECAR_ERROR_CODES.SOURCEPATH_BASENAME_MISMATCH, 'Sidecar basename mismatch returns expected error code');
assert(
error.fieldPath === 'sourcePath',
'Sidecar basename mismatch returns expected field path',
`Expected sourcePath, got ${error.fieldPath}`,
);
assert(
typeof error.message === 'string' &&
error.message.includes(HELP_SIDECAR_ERROR_CODES.SOURCEPATH_BASENAME_MISMATCH) &&
error.message.includes('bmad-fork/src/core/tasks/not-help.artifact.yaml'),
'Sidecar basename mismatch includes deterministic message context',
);
assert(
error.detail === expectedBasenameMismatchDetail,
'Sidecar basename mismatch returns locked detail string',
`Expected "${expectedBasenameMismatchDetail}", got "${error.detail}"`,
);
}
const missingDependencies = structuredClone(validHelpSidecar);
delete missingDependencies.dependencies;
await expectValidationError(
missingDependencies,
HELP_SIDECAR_ERROR_CODES.DEPENDENCIES_MISSING,
'dependencies',
'Missing dependencies block',
);
await expectValidationError(
{ ...validHelpSidecar, dependencies: { requires: 'skill:bmad-help' } },
HELP_SIDECAR_ERROR_CODES.DEPENDENCIES_REQUIRES_INVALID,
'dependencies.requires',
'Non-array dependencies.requires',
);
await expectValidationError(
{ ...validHelpSidecar, dependencies: { requires: ['skill:bmad-help'] } },
HELP_SIDECAR_ERROR_CODES.DEPENDENCIES_REQUIRES_NOT_EMPTY,
'dependencies.requires',
'Non-empty dependencies.requires',
);
} catch (error) {
assert(false, 'Sidecar validation suite setup', error.message);
} finally {
await fs.remove(tempSidecarRoot);
}
console.log('');
// ============================================================
// Test 5: Authority Split and Frontmatter Precedence
// ============================================================
console.log(`${colors.yellow}Test Suite 5: Authority Split and Precedence${colors.reset}\n`);
const tempAuthorityRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-help-authority-'));
const tempAuthoritySidecarPath = path.join(tempAuthorityRoot, 'help.artifact.yaml');
const tempAuthoritySourcePath = path.join(tempAuthorityRoot, 'help-source.md');
const tempAuthorityRuntimePath = path.join(tempAuthorityRoot, 'help-runtime.md');
const deterministicAuthorityPaths = {
sidecar: 'bmad-fork/src/core/tasks/help.artifact.yaml',
source: 'bmad-fork/src/core/tasks/help.md',
runtime: '_bmad/core/tasks/help.md',
};
const writeMarkdownWithFrontmatter = async (filePath, frontmatter) => {
const frontmatterBody = yaml.stringify(frontmatter).trimEnd();
await fs.writeFile(filePath, `---\n${frontmatterBody}\n---\n\n# Placeholder\n`, 'utf8');
};
const validAuthoritySidecar = {
schemaVersion: 1,
canonicalId: 'bmad-help',
artifactType: 'task',
module: 'core',
sourcePath: deterministicAuthorityPaths.source,
displayName: 'help',
description: 'Help command',
dependencies: {
requires: [],
},
};
const validAuthorityFrontmatter = {
name: 'help',
description: 'Help command',
canonicalId: 'bmad-help',
dependencies: {
requires: [],
},
};
const runAuthorityValidation = async () =>
validateHelpAuthoritySplitAndPrecedence({
sidecarPath: tempAuthoritySidecarPath,
sourceMarkdownPath: tempAuthoritySourcePath,
runtimeMarkdownPath: tempAuthorityRuntimePath,
sidecarSourcePath: deterministicAuthorityPaths.sidecar,
sourceMarkdownSourcePath: deterministicAuthorityPaths.source,
runtimeMarkdownSourcePath: deterministicAuthorityPaths.runtime,
});
const expectAuthorityValidationError = async (
sourceFrontmatter,
runtimeFrontmatter,
expectedCode,
expectedFieldPath,
expectedSourcePath,
testLabel,
) => {
await writeMarkdownWithFrontmatter(tempAuthoritySourcePath, sourceFrontmatter);
await writeMarkdownWithFrontmatter(tempAuthorityRuntimePath, runtimeFrontmatter);
try {
await runAuthorityValidation();
assert(false, testLabel, 'Expected authority validation error but validation passed');
} catch (error) {
assert(error.code === expectedCode, `${testLabel} returns expected error code`, `Expected ${expectedCode}, got ${error.code}`);
assert(
error.fieldPath === expectedFieldPath,
`${testLabel} returns expected field path`,
`Expected ${expectedFieldPath}, got ${error.fieldPath}`,
);
assert(
error.sourcePath === expectedSourcePath,
`${testLabel} returns expected source path`,
`Expected ${expectedSourcePath}, got ${error.sourcePath}`,
);
assert(
typeof error.message === 'string' &&
error.message.includes(expectedCode) &&
error.message.includes(expectedFieldPath) &&
error.message.includes(expectedSourcePath),
`${testLabel} includes deterministic message context`,
);
}
};
try {
await fs.writeFile(tempAuthoritySidecarPath, yaml.stringify(validAuthoritySidecar), 'utf8');
await writeMarkdownWithFrontmatter(tempAuthoritySourcePath, validAuthorityFrontmatter);
await writeMarkdownWithFrontmatter(tempAuthorityRuntimePath, validAuthorityFrontmatter);
const authorityValidation = await runAuthorityValidation();
assert(
authorityValidation.authoritativePresenceKey === 'capability:bmad-help',
'Authority validation returns shared authoritative presence key',
);
assert(
Array.isArray(authorityValidation.authoritativeRecords) && authorityValidation.authoritativeRecords.length === 2,
'Authority validation returns sidecar and source authority records',
);
const sidecarRecord = authorityValidation.authoritativeRecords.find((record) => record.authoritySourceType === 'sidecar');
const sourceRecord = authorityValidation.authoritativeRecords.find((record) => record.authoritySourceType === 'source-markdown');
assert(
sidecarRecord && sourceRecord && sidecarRecord.authoritativePresenceKey === sourceRecord.authoritativePresenceKey,
'Source markdown and sidecar records share one authoritative presence key',
);
assert(
sidecarRecord && sidecarRecord.authoritySourcePath === deterministicAuthorityPaths.sidecar,
'Sidecar authority record preserves truthful sidecar source path',
);
assert(
sourceRecord && sourceRecord.authoritySourcePath === deterministicAuthorityPaths.source,
'Source body authority record preserves truthful source markdown path',
);
const manifestGenerator = new ManifestGenerator();
manifestGenerator.modules = ['core'];
manifestGenerator.bmadDir = tempAuthorityRoot;
manifestGenerator.selectedIdes = [];
manifestGenerator.helpAuthorityRecords = authorityValidation.authoritativeRecords;
const tempManifestConfigDir = path.join(tempAuthorityRoot, '_config');
await fs.ensureDir(tempManifestConfigDir);
await manifestGenerator.writeMainManifest(tempManifestConfigDir);
const writtenManifestRaw = await fs.readFile(path.join(tempManifestConfigDir, 'manifest.yaml'), 'utf8');
const writtenManifest = yaml.parse(writtenManifestRaw);
assert(
writtenManifest.helpAuthority && Array.isArray(writtenManifest.helpAuthority.records),
'Manifest generation persists help authority records',
);
assert(
writtenManifest.helpAuthority && writtenManifest.helpAuthority.records && writtenManifest.helpAuthority.records.length === 2,
'Manifest generation persists both authority records',
);
assert(
writtenManifest.helpAuthority &&
writtenManifest.helpAuthority.records.some(
(record) => record.authoritySourceType === 'sidecar' && record.authoritySourcePath === deterministicAuthorityPaths.sidecar,
),
'Manifest generation preserves sidecar authority provenance',
);
assert(
writtenManifest.helpAuthority &&
writtenManifest.helpAuthority.records.some(
(record) => record.authoritySourceType === 'source-markdown' && record.authoritySourcePath === deterministicAuthorityPaths.source,
),
'Manifest generation preserves source-markdown authority provenance',
);
await expectAuthorityValidationError(
{ ...validAuthorityFrontmatter, canonicalId: 'legacy-help' },
validAuthorityFrontmatter,
HELP_FRONTMATTER_MISMATCH_ERROR_CODES.CANONICAL_ID_MISMATCH,
'canonicalId',
deterministicAuthorityPaths.source,
'Source canonicalId mismatch',
);
await expectAuthorityValidationError(
{ ...validAuthorityFrontmatter, name: 'BMAD Help' },
validAuthorityFrontmatter,
HELP_FRONTMATTER_MISMATCH_ERROR_CODES.DISPLAY_NAME_MISMATCH,
'name',
deterministicAuthorityPaths.source,
'Source display-name mismatch',
);
await expectAuthorityValidationError(
validAuthorityFrontmatter,
{ ...validAuthorityFrontmatter, description: 'Runtime override' },
HELP_FRONTMATTER_MISMATCH_ERROR_CODES.DESCRIPTION_MISMATCH,
'description',
deterministicAuthorityPaths.runtime,
'Runtime description mismatch',
);
await expectAuthorityValidationError(
{ ...validAuthorityFrontmatter, dependencies: { requires: ['skill:other'] } },
validAuthorityFrontmatter,
HELP_FRONTMATTER_MISMATCH_ERROR_CODES.DEPENDENCIES_REQUIRES_MISMATCH,
'dependencies.requires',
deterministicAuthorityPaths.source,
'Source dependencies.requires mismatch',
);
} catch (error) {
assert(false, 'Authority split and precedence suite setup', error.message);
} finally {
await fs.remove(tempAuthorityRoot);
}
console.log('');
// ============================================================
// Test 6: Installer Fail-Fast Pre-Generation
// ============================================================
console.log(`${colors.yellow}Test Suite 6: Installer Fail-Fast Pre-Generation${colors.reset}\n`);
const tempInstallerRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-installer-sidecar-failfast-'));
try {
const installer = new Installer();
let authorityValidationCalled = false;
let generateConfigsCalled = false;
let manifestGenerationCalled = false;
let helpCatalogGenerationCalled = false;
let successResultCount = 0;
installer.validateHelpSidecarContractFile = async () => {
const error = new Error(expectedUnsupportedMajorDetail);
error.code = HELP_SIDECAR_ERROR_CODES.MAJOR_VERSION_UNSUPPORTED;
error.fieldPath = 'schemaVersion';
error.detail = expectedUnsupportedMajorDetail;
throw error;
};
installer.validateHelpAuthoritySplitAndPrecedence = async () => {
authorityValidationCalled = true;
return {
authoritativeRecords: [],
authoritativePresenceKey: 'capability:bmad-help',
};
};
installer.generateModuleConfigs = async () => {
generateConfigsCalled = true;
};
installer.mergeModuleHelpCatalogs = async () => {
helpCatalogGenerationCalled = true;
};
installer.ManifestGenerator = class ManifestGeneratorStub {
async generateManifests() {
manifestGenerationCalled = true;
return {
workflows: 0,
agents: 0,
tasks: 0,
tools: 0,
};
}
};
try {
await installer.runConfigurationGenerationTask({
message: () => {},
bmadDir: tempInstallerRoot,
moduleConfigs: { core: {} },
config: { ides: [] },
allModules: ['core'],
addResult: () => {
successResultCount += 1;
},
});
assert(
false,
'Installer fail-fast blocks projection generation on sidecar validation failure',
'Expected sidecar validation failure but configuration generation completed',
);
} catch (error) {
assert(
error.code === HELP_SIDECAR_ERROR_CODES.MAJOR_VERSION_UNSUPPORTED,
'Installer fail-fast surfaces sidecar validation error code',
`Expected ${HELP_SIDECAR_ERROR_CODES.MAJOR_VERSION_UNSUPPORTED}, got ${error.code}`,
);
assert(
!authorityValidationCalled && !generateConfigsCalled && !manifestGenerationCalled && !helpCatalogGenerationCalled,
'Installer fail-fast prevents downstream authority/config/manifest/help generation',
);
assert(
successResultCount === 0,
'Installer fail-fast records no successful projection milestones',
`Expected 0, got ${successResultCount}`,
);
}
} catch (error) {
assert(false, 'Installer fail-fast test setup', error.message);
} finally {
await fs.remove(tempInstallerRoot);
}
console.log('');
// ============================================================
// Test 7: Canonical Alias Normalization Core
// ============================================================
console.log(`${colors.yellow}Test Suite 7: Canonical Alias Normalization Core${colors.reset}\n`);
const deterministicAliasTableSourcePath = '_bmad/_config/canonical-aliases.csv';
const expectAliasNormalizationError = async (
operation,
expectedCode,
expectedFieldPath,
expectedObservedValue,
testLabel,
expectedDetail = null,
) => {
try {
await Promise.resolve(operation());
assert(false, testLabel, 'Expected alias normalization error but operation succeeded');
} catch (error) {
assert(error.code === expectedCode, `${testLabel} returns expected error code`, `Expected ${expectedCode}, got ${error.code}`);
assert(
error.fieldPath === expectedFieldPath,
`${testLabel} returns expected field path`,
`Expected ${expectedFieldPath}, got ${error.fieldPath}`,
);
assert(
error.sourcePath === deterministicAliasTableSourcePath,
`${testLabel} returns expected source path`,
`Expected ${deterministicAliasTableSourcePath}, got ${error.sourcePath}`,
);
assert(
error.observedValue === expectedObservedValue,
`${testLabel} returns normalized offending value context`,
`Expected "${expectedObservedValue}", got "${error.observedValue}"`,
);
assert(
typeof error.message === 'string' &&
error.message.includes(expectedCode) &&
error.message.includes(expectedFieldPath) &&
error.message.includes(deterministicAliasTableSourcePath),
`${testLabel} includes deterministic message context`,
);
if (expectedDetail !== null) {
assert(
error.detail === expectedDetail,
`${testLabel} returns locked detail string`,
`Expected "${expectedDetail}", got "${error.detail}"`,
);
}
}
};
try {
const canonicalTuple = normalizeRawIdentityToTuple(' BMAD-HELP ', {
fieldPath: 'canonicalId',
sourcePath: deterministicAliasTableSourcePath,
});
assert(canonicalTuple.rawIdentityHasLeadingSlash === false, 'Canonical tuple sets rawIdentityHasLeadingSlash=false');
assert(canonicalTuple.preAliasNormalizedValue === 'bmad-help', 'Canonical tuple computes preAliasNormalizedValue=bmad-help');
assert(canonicalTuple.normalizedRawIdentity === 'bmad-help', 'Canonical tuple computes normalizedRawIdentity');
const canonicalResolution = resolveAliasTupleFromRows(canonicalTuple, LOCKED_EXEMPLAR_ALIAS_ROWS, {
sourcePath: deterministicAliasTableSourcePath,
});
assert(
canonicalResolution.aliasRowLocator === 'alias-row:bmad-help:canonical-id',
'Canonical tuple resolves to locked canonical-id row locator',
);
assert(canonicalResolution.postAliasCanonicalId === 'bmad-help', 'Canonical tuple resolves to locked canonicalId');
const legacyResolution = await normalizeAndResolveExemplarAlias(' HELP ', {
fieldPath: 'canonicalId',
sourcePath: deterministicAliasTableSourcePath,
});
assert(legacyResolution.rawIdentityHasLeadingSlash === false, 'Legacy tuple sets rawIdentityHasLeadingSlash=false');
assert(legacyResolution.preAliasNormalizedValue === 'help', 'Legacy tuple computes preAliasNormalizedValue=help');
assert(
legacyResolution.aliasRowLocator === 'alias-row:bmad-help:legacy-name',
'Legacy tuple resolves to locked legacy-name row locator',
);
assert(legacyResolution.postAliasCanonicalId === 'bmad-help', 'Legacy tuple resolves to locked canonicalId');
const slashResolution = await normalizeAndResolveExemplarAlias(' /BMAD-HELP ', {
fieldPath: 'canonicalId',
sourcePath: deterministicAliasTableSourcePath,
});
assert(slashResolution.rawIdentityHasLeadingSlash === true, 'Slash tuple sets rawIdentityHasLeadingSlash=true');
assert(slashResolution.preAliasNormalizedValue === 'bmad-help', 'Slash tuple computes preAliasNormalizedValue=bmad-help');
assert(
slashResolution.aliasRowLocator === 'alias-row:bmad-help:slash-command',
'Slash tuple resolves to locked slash-command row locator',
);
assert(slashResolution.postAliasCanonicalId === 'bmad-help', 'Slash tuple resolves to locked canonicalId');
const tempAliasAuthorityRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-alias-authority-'));
const tempAliasSidecarPath = path.join(tempAliasAuthorityRoot, 'help.artifact.yaml');
const tempAliasSourcePath = path.join(tempAliasAuthorityRoot, 'help-source.md');
const tempAliasRuntimePath = path.join(tempAliasAuthorityRoot, 'help-runtime.md');
const tempAliasConfigDir = path.join(tempAliasAuthorityRoot, '_config');
const tempAuthorityAliasTablePath = path.join(tempAliasConfigDir, 'canonical-aliases.csv');
const aliasAuthorityPaths = {
sidecar: 'bmad-fork/src/core/tasks/help.artifact.yaml',
source: 'bmad-fork/src/core/tasks/help.md',
runtime: '_bmad/core/tasks/help.md',
};
const aliasFrontmatter = {
name: 'help',
description: 'Help command',
canonicalId: 'help',
dependencies: {
requires: [],
},
};
try {
await fs.writeFile(
tempAliasSidecarPath,
yaml.stringify({
schemaVersion: 1,
canonicalId: 'help',
artifactType: 'task',
module: 'core',
sourcePath: aliasAuthorityPaths.source,
displayName: 'help',
description: 'Help command',
dependencies: {
requires: [],
},
}),
'utf8',
);
await fs.writeFile(tempAliasSourcePath, `---\n${yaml.stringify(aliasFrontmatter).trimEnd()}\n---\n\n# Help\n`, 'utf8');
await fs.writeFile(tempAliasRuntimePath, `---\n${yaml.stringify(aliasFrontmatter).trimEnd()}\n---\n\n# Help\n`, 'utf8');
const aliasAuthorityValidation = await validateHelpAuthoritySplitAndPrecedence({
sidecarPath: tempAliasSidecarPath,
sourceMarkdownPath: tempAliasSourcePath,
runtimeMarkdownPath: tempAliasRuntimePath,
sidecarSourcePath: aliasAuthorityPaths.sidecar,
sourceMarkdownSourcePath: aliasAuthorityPaths.source,
runtimeMarkdownSourcePath: aliasAuthorityPaths.runtime,
});
assert(
aliasAuthorityValidation.canonicalId === 'bmad-help',
'Authority validation normalizes legacy canonical identity to locked canonicalId',
);
assert(
aliasAuthorityValidation.authoritativePresenceKey === 'capability:bmad-help',
'Authority validation emits canonical presence key after alias resolution',
);
await fs.ensureDir(tempAliasConfigDir);
await fs.writeFile(
tempAuthorityAliasTablePath,
[
'rowIdentity,canonicalId,normalizedAliasValue,rawIdentityHasLeadingSlash',
'alias-row:bmad-help:legacy-name,bmad-help-csv,help,false',
].join('\n') + '\n',
'utf8',
);
const csvBackedAuthorityValidation = await validateHelpAuthoritySplitAndPrecedence({
sidecarPath: tempAliasSidecarPath,
sourceMarkdownPath: tempAliasSourcePath,
runtimeMarkdownPath: tempAliasRuntimePath,
sidecarSourcePath: aliasAuthorityPaths.sidecar,
sourceMarkdownSourcePath: aliasAuthorityPaths.source,
runtimeMarkdownSourcePath: aliasAuthorityPaths.runtime,
bmadDir: tempAliasAuthorityRoot,
});
assert(
csvBackedAuthorityValidation.canonicalId === 'bmad-help-csv',
'Authority validation prefers canonical alias CSV when available',
);
assert(
csvBackedAuthorityValidation.authoritativePresenceKey === 'capability:bmad-help-csv',
'Authority validation derives presence key from CSV-resolved canonical identity',
);
} finally {
await fs.remove(tempAliasAuthorityRoot);
}
const collapsedWhitespaceTuple = normalizeRawIdentityToTuple(' bmad\t\thelp ', {
fieldPath: 'canonicalId',
sourcePath: deterministicAliasTableSourcePath,
});
assert(
collapsedWhitespaceTuple.preAliasNormalizedValue === 'bmad help',
'Tuple normalization collapses internal whitespace runs deterministically',
);
await expectAliasNormalizationError(
() =>
normalizeRawIdentityToTuple(' \n\t ', {
fieldPath: 'canonicalId',
sourcePath: deterministicAliasTableSourcePath,
}),
HELP_ALIAS_NORMALIZATION_ERROR_CODES.EMPTY_INPUT,
'canonicalId',
'',
'Empty alias input',
'alias identity is empty after normalization',
);
await expectAliasNormalizationError(
() =>
normalizeRawIdentityToTuple('//bmad-help', {
fieldPath: 'canonicalId',
sourcePath: deterministicAliasTableSourcePath,
}),
HELP_ALIAS_NORMALIZATION_ERROR_CODES.MULTIPLE_LEADING_SLASHES,
'canonicalId',
'//bmad-help',
'Alias input with multiple leading slashes',
'alias identity contains multiple leading slashes',
);
await expectAliasNormalizationError(
() =>
normalizeRawIdentityToTuple('/ ', {
fieldPath: 'canonicalId',
sourcePath: deterministicAliasTableSourcePath,
}),
HELP_ALIAS_NORMALIZATION_ERROR_CODES.EMPTY_PREALIAS,
'preAliasNormalizedValue',
'/',
'Alias input with empty pre-alias value',
'alias preAliasNormalizedValue is empty after slash normalization',
);
await expectAliasNormalizationError(
() =>
normalizeAndResolveExemplarAlias('not-a-locked-alias', {
fieldPath: 'canonicalId',
sourcePath: deterministicAliasTableSourcePath,
}),
HELP_ALIAS_NORMALIZATION_ERROR_CODES.UNRESOLVED,
'preAliasNormalizedValue',
'not-a-locked-alias|leadingSlash:false',
'Unresolved alias tuple',
'alias tuple did not resolve to any canonical alias row',
);
const ambiguousAliasRows = [
{
rowIdentity: 'alias-row:a',
canonicalId: 'bmad-help',
normalizedAliasValue: 'help',
rawIdentityHasLeadingSlash: false,
},
{
rowIdentity: 'alias-row:b',
canonicalId: 'legacy-help',
normalizedAliasValue: 'help',
rawIdentityHasLeadingSlash: false,
},
];
const ambiguousTuple = normalizeRawIdentityToTuple('help', {
fieldPath: 'canonicalId',
sourcePath: deterministicAliasTableSourcePath,
});
await expectAliasNormalizationError(
() =>
resolveAliasTupleFromRows(ambiguousTuple, ambiguousAliasRows, {
sourcePath: deterministicAliasTableSourcePath,
}),
HELP_ALIAS_NORMALIZATION_ERROR_CODES.UNRESOLVED,
'preAliasNormalizedValue',
'help|leadingSlash:false',
'Ambiguous alias tuple resolution',
'alias tuple resolved ambiguously to multiple canonical alias rows',
);
const tempAliasTableRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-canonical-alias-table-'));
const tempAliasTablePath = path.join(tempAliasTableRoot, 'canonical-aliases.csv');
const csvRows = [
'rowIdentity,canonicalId,normalizedAliasValue,rawIdentityHasLeadingSlash',
'alias-row:bmad-help:canonical-id,bmad-help,bmad-help,false',
'alias-row:bmad-help:legacy-name,bmad-help,help,false',
'alias-row:bmad-help:slash-command,bmad-help,bmad-help,true',
];
try {
await fs.writeFile(tempAliasTablePath, `${csvRows.join('\n')}\n`, 'utf8');
const csvTuple = normalizeRawIdentityToTuple('/bmad-help', {
fieldPath: 'canonicalId',
sourcePath: deterministicAliasTableSourcePath,
});
const csvResolution = await resolveAliasTupleUsingCanonicalAliasCsv(csvTuple, tempAliasTablePath, {
sourcePath: deterministicAliasTableSourcePath,
});
assert(
csvResolution.aliasRowLocator === 'alias-row:bmad-help:slash-command',
'CSV-backed tuple resolution maps slash-command alias row locator',
);
assert(csvResolution.postAliasCanonicalId === 'bmad-help', 'CSV-backed tuple resolution maps canonicalId');
const manifestGenerator = new ManifestGenerator();
const normalizedHelpAuthorityRecords = await manifestGenerator.normalizeHelpAuthorityRecords([
{
recordType: 'metadata-authority',
canonicalId: 'help',
authoritativePresenceKey: 'capability:legacy-help',
authoritySourceType: 'sidecar',
authoritySourcePath: aliasAuthorityPaths.sidecar,
sourcePath: aliasAuthorityPaths.source,
},
]);
assert(
normalizedHelpAuthorityRecords.length === 1 && normalizedHelpAuthorityRecords[0].canonicalId === 'bmad-help',
'Manifest generator normalizes legacy canonical identities using alias tuple resolution',
);
assert(
normalizedHelpAuthorityRecords.length === 1 &&
normalizedHelpAuthorityRecords[0].authoritativePresenceKey === 'capability:bmad-help',
'Manifest generator canonicalizes authoritative presence key from normalized canonicalId',
);
await expectAliasNormalizationError(
() =>
manifestGenerator.normalizeHelpAuthorityRecords([
{
recordType: 'metadata-authority',
canonicalId: 'not-a-locked-alias',
authoritativePresenceKey: 'capability:not-a-locked-alias',
authoritySourceType: 'sidecar',
authoritySourcePath: aliasAuthorityPaths.sidecar,
sourcePath: aliasAuthorityPaths.source,
},
]),
HELP_ALIAS_NORMALIZATION_ERROR_CODES.UNRESOLVED,
'preAliasNormalizedValue',
'not-a-locked-alias|leadingSlash:false',
'Manifest generator fails unresolved canonical identity normalization',
'alias tuple did not resolve to any canonical alias row',
);
await expectAliasNormalizationError(
() =>
resolveAliasTupleUsingCanonicalAliasCsv(csvTuple, path.join(tempAliasTableRoot, 'missing.csv'), {
sourcePath: deterministicAliasTableSourcePath,
}),
HELP_ALIAS_NORMALIZATION_ERROR_CODES.UNRESOLVED,
'aliasTablePath',
path.join(tempAliasTableRoot, 'missing.csv'),
'CSV-backed alias resolution with missing table file',
'canonical alias table file was not found',
);
} finally {
await fs.remove(tempAliasTableRoot);
}
} catch (error) {
assert(false, 'Canonical alias normalization suite setup', error.message);
}
console.log('');
// ============================================================
// Test 8: Additive Task Manifest Projection
// ============================================================
console.log(`${colors.yellow}Test Suite 8: Additive Task Manifest Projection${colors.reset}\n`);
const tempTaskManifestRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-task-manifest-'));
try {
const manifestGenerator = new ManifestGenerator();
manifestGenerator.bmadDir = tempTaskManifestRoot;
manifestGenerator.bmadFolderName = '_bmad';
manifestGenerator.tasks = [
{
name: 'help',
displayName: 'help',
description: 'Help command',
module: 'core',
path: 'core/tasks/help.md',
standalone: true,
},
{
name: 'validate-workflow',
displayName: 'validate-workflow',
description: 'Validate workflow',
module: 'core',
path: 'core/tasks/validate-workflow.xml',
standalone: true,
},
];
manifestGenerator.helpAuthorityRecords = [
{
recordType: 'metadata-authority',
canonicalId: 'bmad-help',
authoritativePresenceKey: 'capability:bmad-help',
authoritySourceType: 'sidecar',
authoritySourcePath: 'bmad-fork/src/core/tasks/help.artifact.yaml',
sourcePath: 'bmad-fork/src/core/tasks/help.md',
},
];
const tempTaskManifestConfigDir = path.join(tempTaskManifestRoot, '_config');
await fs.ensureDir(tempTaskManifestConfigDir);
await manifestGenerator.writeTaskManifest(tempTaskManifestConfigDir);
const writtenTaskManifestRaw = await fs.readFile(path.join(tempTaskManifestConfigDir, 'task-manifest.csv'), 'utf8');
const writtenTaskManifestLines = writtenTaskManifestRaw.trim().split('\n');
const expectedHeader =
'name,displayName,description,module,path,standalone,legacyName,canonicalId,authoritySourceType,authoritySourcePath';
assert(
writtenTaskManifestLines[0] === expectedHeader,
'Task manifest writes compatibility-prefix columns with locked wave-1 appended column order',
);
const writtenTaskManifestRecords = csv.parse(writtenTaskManifestRaw, {
columns: true,
skip_empty_lines: true,
trim: true,
});
const helpTaskRow = writtenTaskManifestRecords.find((record) => record.module === 'core' && record.name === 'help');
const validateTaskRow = writtenTaskManifestRecords.find((record) => record.module === 'core' && record.name === 'validate-workflow');
assert(!!helpTaskRow, 'Task manifest includes exemplar help row');
assert(helpTaskRow && helpTaskRow.legacyName === 'help', 'Task manifest help row sets legacyName=help');
assert(helpTaskRow && helpTaskRow.canonicalId === 'bmad-help', 'Task manifest help row sets canonicalId=bmad-help');
assert(helpTaskRow && helpTaskRow.authoritySourceType === 'sidecar', 'Task manifest help row sets authoritySourceType=sidecar');
assert(
helpTaskRow && helpTaskRow.authoritySourcePath === 'bmad-fork/src/core/tasks/help.artifact.yaml',
'Task manifest help row sets authoritySourcePath to sidecar source path',
);
assert(!!validateTaskRow, 'Task manifest preserves non-exemplar rows');
assert(
validateTaskRow && validateTaskRow.legacyName === 'validate-workflow',
'Task manifest non-exemplar rows remain additive-compatible with default legacyName',
);
let capturedAuthorityValidationOptions = null;
let capturedManifestHelpAuthorityRecords = null;
let capturedInstalledFiles = null;
const installer = new Installer();
installer.validateHelpSidecarContractFile = async () => {};
installer.validateHelpAuthoritySplitAndPrecedence = async (options) => {
capturedAuthorityValidationOptions = options;
return {
authoritativePresenceKey: 'capability:bmad-help',
authoritativeRecords: [
{
recordType: 'metadata-authority',
canonicalId: 'bmad-help',
authoritativePresenceKey: 'capability:bmad-help',
authoritySourceType: 'sidecar',
authoritySourcePath: options.sidecarSourcePath,
sourcePath: options.sourceMarkdownSourcePath,
},
],
};
};
installer.generateModuleConfigs = async () => {};
installer.mergeModuleHelpCatalogs = async () => {};
installer.ManifestGenerator = class ManifestGeneratorStub {
async generateManifests(_bmadDir, _selectedModules, _installedFiles, options = {}) {
capturedInstalledFiles = _installedFiles;
capturedManifestHelpAuthorityRecords = options.helpAuthorityRecords;
return {
workflows: 0,
agents: 0,
tasks: 0,
tools: 0,
};
}
};
await installer.runConfigurationGenerationTask({
message: () => {},
bmadDir: tempTaskManifestRoot,
moduleConfigs: { core: {} },
config: { ides: [] },
allModules: ['core'],
addResult: () => {},
});
assert(
capturedAuthorityValidationOptions &&
capturedAuthorityValidationOptions.sidecarSourcePath === 'bmad-fork/src/core/tasks/help.artifact.yaml',
'Installer passes locked sidecar source path to authority validation',
);
assert(
capturedAuthorityValidationOptions &&
capturedAuthorityValidationOptions.sourceMarkdownSourcePath === 'bmad-fork/src/core/tasks/help.md',
'Installer passes locked source-markdown path to authority validation',
);
assert(
capturedAuthorityValidationOptions && capturedAuthorityValidationOptions.runtimeMarkdownSourcePath === '_bmad/core/tasks/help.md',
'Installer passes locked runtime markdown path to authority validation',
);
assert(
Array.isArray(capturedManifestHelpAuthorityRecords) &&
capturedManifestHelpAuthorityRecords[0] &&
capturedManifestHelpAuthorityRecords[0].authoritySourcePath === 'bmad-fork/src/core/tasks/help.artifact.yaml',
'Installer passes sidecar authority path into manifest generation options',
);
assert(
Array.isArray(capturedInstalledFiles) &&
capturedInstalledFiles.some((filePath) => filePath.endsWith('/_config/canonical-aliases.csv')),
'Installer pre-registers canonical-aliases.csv for files-manifest tracking',
);
} catch (error) {
assert(false, 'Additive task manifest projection suite setup', error.message);
} finally {
await fs.remove(tempTaskManifestRoot);
}
console.log('');
// ============================================================
// Test 9: Canonical Alias Table Projection
// ============================================================
console.log(`${colors.yellow}Test Suite 9: Canonical Alias Table Projection${colors.reset}\n`);
const tempCanonicalAliasRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-canonical-alias-projection-'));
try {
const manifestGenerator = new ManifestGenerator();
manifestGenerator.bmadDir = tempCanonicalAliasRoot;
manifestGenerator.bmadFolderName = '_bmad';
manifestGenerator.helpAuthorityRecords = [
{
recordType: 'metadata-authority',
canonicalId: 'bmad-help',
authoritativePresenceKey: 'capability:bmad-help',
authoritySourceType: 'sidecar',
authoritySourcePath: 'bmad-fork/src/core/tasks/help.artifact.yaml',
sourcePath: 'bmad-fork/src/core/tasks/help.md',
},
];
const tempCanonicalAliasConfigDir = path.join(tempCanonicalAliasRoot, '_config');
await fs.ensureDir(tempCanonicalAliasConfigDir);
const canonicalAliasPath = await manifestGenerator.writeCanonicalAliasManifest(tempCanonicalAliasConfigDir);
const canonicalAliasRaw = await fs.readFile(canonicalAliasPath, 'utf8');
const canonicalAliasLines = canonicalAliasRaw.trim().split('\n');
const expectedCanonicalAliasHeader =
'canonicalId,alias,aliasType,authoritySourceType,authoritySourcePath,rowIdentity,normalizedAliasValue,rawIdentityHasLeadingSlash,resolutionEligibility';
assert(
canonicalAliasLines[0] === expectedCanonicalAliasHeader,
'Canonical alias table writes locked compatibility-prefix plus tuple eligibility column order',
);
const canonicalAliasRows = csv.parse(canonicalAliasRaw, {
columns: true,
skip_empty_lines: true,
trim: true,
});
assert(canonicalAliasRows.length === 3, 'Canonical alias table emits exactly three exemplar rows');
assert(
canonicalAliasRows.map((row) => row.aliasType).join(',') === 'canonical-id,legacy-name,slash-command',
'Canonical alias table preserves locked deterministic row ordering',
);
const expectedRowsByType = new Map([
[
'canonical-id',
{
canonicalId: 'bmad-help',
alias: 'bmad-help',
rowIdentity: 'alias-row:bmad-help:canonical-id',
normalizedAliasValue: 'bmad-help',
rawIdentityHasLeadingSlash: 'false',
resolutionEligibility: 'canonical-id-only',
},
],
[
'legacy-name',
{
canonicalId: 'bmad-help',
alias: 'help',
rowIdentity: 'alias-row:bmad-help:legacy-name',
normalizedAliasValue: 'help',
rawIdentityHasLeadingSlash: 'false',
resolutionEligibility: 'legacy-name-only',
},
],
[
'slash-command',
{
canonicalId: 'bmad-help',
alias: '/bmad-help',
rowIdentity: 'alias-row:bmad-help:slash-command',
normalizedAliasValue: 'bmad-help',
rawIdentityHasLeadingSlash: 'true',
resolutionEligibility: 'slash-command-only',
},
],
]);
for (const [aliasType, expectedRow] of expectedRowsByType) {
const matchingRows = canonicalAliasRows.filter((row) => row.aliasType === aliasType);
assert(matchingRows.length === 1, `Canonical alias table emits exactly one ${aliasType} exemplar row`);
const row = matchingRows[0];
assert(
row && row.authoritySourceType === 'sidecar' && row.authoritySourcePath === 'bmad-fork/src/core/tasks/help.artifact.yaml',
`${aliasType} exemplar row uses sidecar provenance fields`,
);
assert(row && row.canonicalId === expectedRow.canonicalId, `${aliasType} exemplar row locks canonicalId contract`);
assert(row && row.alias === expectedRow.alias, `${aliasType} exemplar row locks alias contract`);
assert(row && row.rowIdentity === expectedRow.rowIdentity, `${aliasType} exemplar row locks rowIdentity contract`);
assert(
row && row.normalizedAliasValue === expectedRow.normalizedAliasValue,
`${aliasType} exemplar row locks normalizedAliasValue contract`,
);
assert(
row && row.rawIdentityHasLeadingSlash === expectedRow.rawIdentityHasLeadingSlash,
`${aliasType} exemplar row locks rawIdentityHasLeadingSlash contract`,
);
assert(
row && row.resolutionEligibility === expectedRow.resolutionEligibility,
`${aliasType} exemplar row locks resolutionEligibility contract`,
);
}
const validateLockedCanonicalAliasProjection = (rows) => {
for (const [aliasType, expectedRow] of expectedRowsByType) {
const matchingRows = rows.filter((row) => row.canonicalId === 'bmad-help' && row.aliasType === aliasType);
if (matchingRows.length === 0) {
return { valid: false, reason: `missing:${aliasType}` };
}
if (matchingRows.length > 1) {
return { valid: false, reason: `conflict:${aliasType}` };
}
const row = matchingRows[0];
if (
row.rowIdentity !== expectedRow.rowIdentity ||
row.normalizedAliasValue !== expectedRow.normalizedAliasValue ||
row.rawIdentityHasLeadingSlash !== expectedRow.rawIdentityHasLeadingSlash ||
row.resolutionEligibility !== expectedRow.resolutionEligibility
) {
return { valid: false, reason: `conflict:${aliasType}` };
}
}
if (rows.length !== expectedRowsByType.size) {
return { valid: false, reason: 'conflict:extra-rows' };
}
return { valid: true, reason: 'ok' };
};
const baselineProjectionValidation = validateLockedCanonicalAliasProjection(canonicalAliasRows);
assert(
baselineProjectionValidation.valid,
'Canonical alias projection validator passes when all required exemplar rows are present exactly once',
baselineProjectionValidation.reason,
);
const missingLegacyRows = canonicalAliasRows.filter((row) => row.aliasType !== 'legacy-name');
const missingLegacyValidation = validateLockedCanonicalAliasProjection(missingLegacyRows);
assert(
!missingLegacyValidation.valid && missingLegacyValidation.reason === 'missing:legacy-name',
'Canonical alias projection validator fails when required legacy-name row is missing',
);
const conflictingRows = [
...canonicalAliasRows,
{
...canonicalAliasRows.find((row) => row.aliasType === 'slash-command'),
rowIdentity: 'alias-row:bmad-help:slash-command:duplicate',
},
];
const conflictingValidation = validateLockedCanonicalAliasProjection(conflictingRows);
assert(
!conflictingValidation.valid && conflictingValidation.reason === 'conflict:slash-command',
'Canonical alias projection validator fails when conflicting duplicate exemplar rows appear',
);
const fallbackManifestGenerator = new ManifestGenerator();
fallbackManifestGenerator.bmadDir = tempCanonicalAliasRoot;
fallbackManifestGenerator.bmadFolderName = '_bmad';
fallbackManifestGenerator.helpAuthorityRecords = [];
const fallbackCanonicalAliasPath = await fallbackManifestGenerator.writeCanonicalAliasManifest(tempCanonicalAliasConfigDir);
const fallbackCanonicalAliasRaw = await fs.readFile(fallbackCanonicalAliasPath, 'utf8');
const fallbackCanonicalAliasRows = csv.parse(fallbackCanonicalAliasRaw, {
columns: true,
skip_empty_lines: true,
trim: true,
});
assert(
fallbackCanonicalAliasRows.every(
(row) => row.authoritySourceType === 'sidecar' && row.authoritySourcePath === 'bmad-fork/src/core/tasks/help.artifact.yaml',
),
'Canonical alias table falls back to locked sidecar provenance when authority records are unavailable',
);
const tempGeneratedBmadDir = path.join(tempCanonicalAliasRoot, '_bmad');
await fs.ensureDir(tempGeneratedBmadDir);
const manifestStats = await new ManifestGenerator().generateManifests(
tempGeneratedBmadDir,
[],
[path.join(tempGeneratedBmadDir, '_config', 'canonical-aliases.csv')],
{
ides: [],
preservedModules: [],
helpAuthorityRecords: manifestGenerator.helpAuthorityRecords,
},
);
assert(
Array.isArray(manifestStats.manifestFiles) &&
manifestStats.manifestFiles.some((filePath) => filePath.endsWith('/_config/canonical-aliases.csv')),
'Manifest generation includes canonical-aliases.csv in output sequencing',
);
const writtenFilesManifestRaw = await fs.readFile(path.join(tempGeneratedBmadDir, '_config', 'files-manifest.csv'), 'utf8');
assert(
writtenFilesManifestRaw.includes('"_config/canonical-aliases.csv"'),
'Files manifest tracks canonical-aliases.csv when pre-registered by installer flow',
);
} catch (error) {
assert(false, 'Canonical alias projection suite setup', error.message);
} finally {
await fs.remove(tempCanonicalAliasRoot);
}
console.log('');
// ============================================================
// Test 10: Help Catalog Projection + Command Label Contract
// ============================================================
console.log(`${colors.yellow}Test Suite 10: Help Catalog Projection + Command Label Contract${colors.reset}\n`);
const tempHelpCatalogRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-help-catalog-projection-'));
try {
const installer = new Installer();
installer.helpAuthorityRecords = [
{
recordType: 'metadata-authority',
canonicalId: 'bmad-help',
authoritativePresenceKey: 'capability:bmad-help',
authoritySourceType: 'sidecar',
authoritySourcePath: 'bmad-fork/src/core/tasks/help.artifact.yaml',
sourcePath: 'bmad-fork/src/core/tasks/help.md',
},
];
const sidecarAwareExemplar = await buildSidecarAwareExemplarHelpRow({
helpAuthorityRecords: installer.helpAuthorityRecords,
});
assert(
sidecarAwareExemplar.commandValue === 'bmad-help',
'Sidecar-aware exemplar help row derives raw command from canonical identity',
);
assert(
sidecarAwareExemplar.displayedCommandLabel === '/bmad-help',
'Sidecar-aware exemplar help row renders displayed label with exactly one leading slash',
);
assert(
sidecarAwareExemplar.authoritySourcePath === EXEMPLAR_HELP_CATALOG_AUTHORITY_SOURCE_PATH,
'Sidecar-aware exemplar help row locks authority source path to sidecar metadata file',
);
const legacySidecarPath = path.join(tempHelpCatalogRoot, 'legacy-help.artifact.yaml');
await fs.writeFile(
legacySidecarPath,
yaml.stringify({
schemaVersion: 1,
canonicalId: 'help',
artifactType: 'task',
module: 'core',
sourcePath: 'bmad-fork/src/core/tasks/help.md',
displayName: 'help',
description: 'Legacy exemplar alias canonical id',
dependencies: { requires: [] },
}),
'utf8',
);
const legacyIdentityExemplar = await buildSidecarAwareExemplarHelpRow({
sidecarPath: legacySidecarPath,
helpAuthorityRecords: installer.helpAuthorityRecords,
});
assert(
legacyIdentityExemplar.commandValue === 'bmad-help',
'Sidecar-aware exemplar help row normalizes legacy sidecar canonicalId to locked canonical identity',
);
await installer.mergeModuleHelpCatalogs(tempHelpCatalogRoot);
const generatedHelpPath = path.join(tempHelpCatalogRoot, '_config', 'bmad-help.csv');
const generatedCommandLabelReportPath = path.join(tempHelpCatalogRoot, '_config', 'bmad-help-command-label-report.csv');
const generatedPipelineReportPath = path.join(tempHelpCatalogRoot, '_config', 'bmad-help-catalog-pipeline.csv');
const generatedHelpRaw = await fs.readFile(generatedHelpPath, 'utf8');
const generatedHelpLines = generatedHelpRaw.trim().split('\n');
const expectedHelpHeader =
'module,phase,name,code,sequence,workflow-file,command,required,agent-name,agent-command,agent-display-name,agent-title,options,description,output-location,outputs';
assert(generatedHelpLines[0] === expectedHelpHeader, 'Help catalog header remains additive-compatible for existing consumers');
const generatedHelpRows = csv.parse(generatedHelpRaw, {
columns: true,
skip_empty_lines: true,
trim: true,
});
const exemplarRows = generatedHelpRows.filter((row) => row.command === 'bmad-help');
assert(exemplarRows.length === 1, 'Help catalog emits exactly one exemplar raw command row for bmad-help');
assert(
exemplarRows[0] && exemplarRows[0].name === 'bmad-help',
'Help catalog exemplar row preserves locked bmad-help workflow identity',
);
const sidecarRaw = await fs.readFile(path.join(projectRoot, 'src', 'core', 'tasks', 'help.artifact.yaml'), 'utf8');
const sidecarData = yaml.parse(sidecarRaw);
assert(
exemplarRows[0] && exemplarRows[0].description === sidecarData.description,
'Help catalog exemplar row description is sourced from sidecar metadata',
);
const commandLabelRows = installer.helpCatalogCommandLabelReportRows || [];
assert(commandLabelRows.length === 1, 'Installer emits one command-label report row for exemplar canonical id');
assert(
commandLabelRows[0] &&
commandLabelRows[0].rawCommandValue === 'bmad-help' &&
commandLabelRows[0].displayedCommandLabel === '/bmad-help',
'Command-label report locks raw and displayed command values for exemplar',
);
assert(
commandLabelRows[0] &&
commandLabelRows[0].authoritySourceType === 'sidecar' &&
commandLabelRows[0].authoritySourcePath === 'bmad-fork/src/core/tasks/help.artifact.yaml',
'Command-label report includes sidecar provenance linkage',
);
const generatedCommandLabelReportRaw = await fs.readFile(generatedCommandLabelReportPath, 'utf8');
const generatedCommandLabelReportRows = csv.parse(generatedCommandLabelReportRaw, {
columns: true,
skip_empty_lines: true,
trim: true,
});
assert(
generatedCommandLabelReportRows.length === 1 &&
generatedCommandLabelReportRows[0].displayedCommandLabel === '/bmad-help' &&
generatedCommandLabelReportRows[0].rowCountForCanonicalId === '1',
'Installer persists command-label report artifact with locked exemplar label contract values',
);
const baselineLabelContract = evaluateExemplarCommandLabelReportRows(commandLabelRows);
assert(
baselineLabelContract.valid,
'Command-label validator passes when exactly one exemplar /bmad-help displayed label row exists',
baselineLabelContract.reason,
);
const invalidLegacyLabelContract = evaluateExemplarCommandLabelReportRows([
{
...commandLabelRows[0],
displayedCommandLabel: 'help',
},
]);
assert(
!invalidLegacyLabelContract.valid && invalidLegacyLabelContract.reason === 'invalid-displayed-label:help',
'Command-label validator fails on alternate displayed label form "help"',
);
const invalidSlashHelpLabelContract = evaluateExemplarCommandLabelReportRows([
{
...commandLabelRows[0],
displayedCommandLabel: '/help',
},
]);
assert(
!invalidSlashHelpLabelContract.valid && invalidSlashHelpLabelContract.reason === 'invalid-displayed-label:/help',
'Command-label validator fails on alternate displayed label form "/help"',
);
const pipelineRows = installer.helpCatalogPipelineRows || [];
assert(pipelineRows.length === 2, 'Installer emits two stage rows for help catalog pipeline evidence linkage');
const installedStageRow = pipelineRows.find((row) => row.stage === 'installed-compatibility-row');
const mergedStageRow = pipelineRows.find((row) => row.stage === 'merged-config-row');
assert(
installedStageRow &&
installedStageRow.issuingComponent === EXEMPLAR_HELP_CATALOG_ISSUING_COMPONENT &&
installedStageRow.commandAuthoritySourceType === 'sidecar' &&
installedStageRow.commandAuthoritySourcePath === 'bmad-fork/src/core/tasks/help.artifact.yaml',
'Installed compatibility stage row preserves sidecar command provenance and issuing component linkage',
);
assert(
mergedStageRow &&
mergedStageRow.issuingComponent === INSTALLER_HELP_CATALOG_MERGE_COMPONENT &&
mergedStageRow.commandAuthoritySourceType === 'sidecar' &&
mergedStageRow.commandAuthoritySourcePath === 'bmad-fork/src/core/tasks/help.artifact.yaml',
'Merged config stage row preserves sidecar command provenance and merge issuing component linkage',
);
assert(
pipelineRows.every((row) => row.status === 'PASS' && typeof row.issuingComponentBindingEvidence === 'string'),
'Pipeline rows include deterministic PASS status and non-empty issuing-component evidence linkage',
);
const generatedPipelineReportRaw = await fs.readFile(generatedPipelineReportPath, 'utf8');
const generatedPipelineReportRows = csv.parse(generatedPipelineReportRaw, {
columns: true,
skip_empty_lines: true,
trim: true,
});
assert(
generatedPipelineReportRows.length === 2 &&
generatedPipelineReportRows.every(
(row) =>
row.commandAuthoritySourceType === 'sidecar' &&
row.commandAuthoritySourcePath === 'bmad-fork/src/core/tasks/help.artifact.yaml',
),
'Installer persists pipeline stage artifact with sidecar command provenance linkage for both stages',
);
const tempAltLabelRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-help-catalog-alt-label-'));
try {
const moduleDir = path.join(tempAltLabelRoot, 'modx');
await fs.ensureDir(moduleDir);
await fs.writeFile(
path.join(moduleDir, 'module-help.csv'),
[
'module,phase,name,code,sequence,workflow-file,command,required,agent,options,description,output-location,outputs',
'modx,anytime,alt-help,AH,,_bmad/core/tasks/help.md,/help,false,,,Alt help label,,,',
].join('\n') + '\n',
'utf8',
);
const alternateLabelInstaller = new Installer();
alternateLabelInstaller.helpAuthorityRecords = installer.helpAuthorityRecords;
try {
await alternateLabelInstaller.mergeModuleHelpCatalogs(tempAltLabelRoot);
assert(
false,
'Installer command-label contract rejects alternate rendered labels in merged help catalog',
'Expected command label contract failure for /help but merge succeeded',
);
} catch (error) {
assert(
error.code === HELP_CATALOG_GENERATION_ERROR_CODES.COMMAND_LABEL_CONTRACT_FAILED,
'Installer command-label contract returns deterministic failure code for alternate labels',
`Expected ${HELP_CATALOG_GENERATION_ERROR_CODES.COMMAND_LABEL_CONTRACT_FAILED}, got ${error.code}`,
);
}
} finally {
await fs.remove(tempAltLabelRoot);
}
} catch (error) {
assert(false, 'Help catalog projection suite setup', error.message);
} finally {
await fs.remove(tempHelpCatalogRoot);
}
console.log('');
// ============================================================
// Test 11: Export Projection from Sidecar Canonical ID
// ============================================================
console.log(`${colors.yellow}Test Suite 11: Export Projection from Sidecar Canonical ID${colors.reset}\n`);
const tempExportRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-export-projection-'));
try {
const codexSetup = new CodexSetup();
const skillsDir = path.join(tempExportRoot, '.agents', 'skills');
await fs.ensureDir(skillsDir);
await fs.ensureDir(path.join(tempExportRoot, 'bmad-fork', 'src', 'core', 'tasks'));
await fs.writeFile(
path.join(tempExportRoot, 'bmad-fork', 'src', 'core', 'tasks', 'help.artifact.yaml'),
yaml.stringify({
schemaVersion: 1,
canonicalId: 'bmad-help',
artifactType: 'task',
module: 'core',
sourcePath: 'bmad-fork/src/core/tasks/help.md',
displayName: 'help',
description: 'Help command',
dependencies: { requires: [] },
}),
'utf8',
);
const exemplarTaskArtifact = {
type: 'task',
name: 'help',
module: 'core',
sourcePath: path.join(tempExportRoot, '_bmad', 'core', 'tasks', 'help.md'),
relativePath: path.join('core', 'tasks', 'help.md'),
content: '---\nname: help\ndescription: Help command\ncanonicalId: bmad-help\n---\n\n# help\n',
};
const writtenCount = await codexSetup.writeSkillArtifacts(skillsDir, [exemplarTaskArtifact], 'task', {
projectDir: tempExportRoot,
});
assert(writtenCount === 1, 'Codex export writes one exemplar skill artifact');
const exemplarSkillPath = path.join(skillsDir, 'bmad-help', 'SKILL.md');
assert(await fs.pathExists(exemplarSkillPath), 'Codex export derives exemplar skill path from sidecar canonical identity');
const exemplarSkillRaw = await fs.readFile(exemplarSkillPath, 'utf8');
const exemplarFrontmatterMatch = exemplarSkillRaw.match(/^---\r?\n([\s\S]*?)\r?\n---/);
const exemplarFrontmatter = exemplarFrontmatterMatch ? yaml.parse(exemplarFrontmatterMatch[1]) : null;
assert(
exemplarFrontmatter && exemplarFrontmatter.name === 'bmad-help',
'Codex export frontmatter sets required name from sidecar canonical identity',
);
assert(
exemplarFrontmatter && Object.keys(exemplarFrontmatter).sort().join(',') === 'description,name',
'Codex export frontmatter remains constrained to required name plus optional description',
);
const exportDerivationRecord = codexSetup.exportDerivationRecords.find((row) => row.exportPath === '.agents/skills/bmad-help/SKILL.md');
assert(
exportDerivationRecord &&
exportDerivationRecord.exportIdDerivationSourceType === EXEMPLAR_HELP_EXPORT_DERIVATION_SOURCE_TYPE &&
exportDerivationRecord.exportIdDerivationSourcePath === 'bmad-fork/src/core/tasks/help.artifact.yaml',
'Codex export records exemplar derivation source metadata from sidecar canonical-id',
);
const tempSubmoduleRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-export-submodule-root-'));
try {
const submoduleRootSetup = new CodexSetup();
const submoduleSkillsDir = path.join(tempSubmoduleRoot, '.agents', 'skills');
await fs.ensureDir(submoduleSkillsDir);
await fs.ensureDir(path.join(tempSubmoduleRoot, 'src', 'core', 'tasks'));
await fs.writeFile(
path.join(tempSubmoduleRoot, 'src', 'core', 'tasks', 'help.artifact.yaml'),
yaml.stringify({
schemaVersion: 1,
canonicalId: 'bmad-help',
artifactType: 'task',
module: 'core',
sourcePath: 'bmad-fork/src/core/tasks/help.md',
displayName: 'help',
description: 'Help command',
dependencies: { requires: [] },
}),
'utf8',
);
await submoduleRootSetup.writeSkillArtifacts(submoduleSkillsDir, [exemplarTaskArtifact], 'task', {
projectDir: tempSubmoduleRoot,
});
const submoduleExportDerivationRecord = submoduleRootSetup.exportDerivationRecords.find(
(row) => row.exportPath === '.agents/skills/bmad-help/SKILL.md',
);
assert(
submoduleExportDerivationRecord &&
submoduleExportDerivationRecord.exportIdDerivationSourcePath === 'bmad-fork/src/core/tasks/help.artifact.yaml',
'Codex export locks exemplar derivation source-path contract when running from submodule root',
);
} finally {
await fs.remove(tempSubmoduleRoot);
}
const tempNoSidecarRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-export-missing-sidecar-'));
try {
const noSidecarSetup = new CodexSetup();
const noSidecarSkillDir = path.join(tempNoSidecarRoot, '.agents', 'skills');
await fs.ensureDir(noSidecarSkillDir);
try {
await noSidecarSetup.writeSkillArtifacts(noSidecarSkillDir, [exemplarTaskArtifact], 'task', {
projectDir: tempNoSidecarRoot,
});
assert(
false,
'Codex export fails when exemplar sidecar metadata is missing',
'Expected sidecar file-not-found failure but export succeeded',
);
} catch (error) {
assert(
error.code === CODEX_EXPORT_DERIVATION_ERROR_CODES.SIDECAR_FILE_NOT_FOUND,
'Codex export missing sidecar failure returns deterministic error code',
`Expected ${CODEX_EXPORT_DERIVATION_ERROR_CODES.SIDECAR_FILE_NOT_FOUND}, got ${error.code}`,
);
}
} finally {
await fs.remove(tempNoSidecarRoot);
}
const tempInferenceRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-export-no-inference-'));
try {
const noInferenceSetup = new CodexSetup();
const noInferenceSkillDir = path.join(tempInferenceRoot, '.agents', 'skills');
await fs.ensureDir(noInferenceSkillDir);
await fs.ensureDir(path.join(tempInferenceRoot, 'bmad-fork', 'src', 'core', 'tasks'));
await fs.writeFile(
path.join(tempInferenceRoot, 'bmad-fork', 'src', 'core', 'tasks', 'help.artifact.yaml'),
yaml.stringify({
schemaVersion: 1,
canonicalId: 'nonexistent-help-id',
artifactType: 'task',
module: 'core',
sourcePath: 'bmad-fork/src/core/tasks/help.md',
displayName: 'help',
description: 'Help command',
dependencies: { requires: [] },
}),
'utf8',
);
try {
await noInferenceSetup.writeSkillArtifacts(noInferenceSkillDir, [exemplarTaskArtifact], 'task', {
projectDir: tempInferenceRoot,
});
assert(
false,
'Codex export rejects path-inferred exemplar id when sidecar canonical-id derivation is unresolved',
'Expected canonical-id derivation failure but export succeeded',
);
} catch (error) {
assert(
error.code === CODEX_EXPORT_DERIVATION_ERROR_CODES.CANONICAL_ID_DERIVATION_FAILED,
'Codex export unresolved canonical-id derivation returns deterministic failure code',
`Expected ${CODEX_EXPORT_DERIVATION_ERROR_CODES.CANONICAL_ID_DERIVATION_FAILED}, got ${error.code}`,
);
}
} finally {
await fs.remove(tempInferenceRoot);
}
const compatibilitySetup = new CodexSetup();
const compatibilityIdentity = await compatibilitySetup.resolveSkillIdentityFromArtifact(
{
type: 'workflow-command',
name: 'create-story',
module: 'bmm',
relativePath: path.join('bmm', 'workflows', 'create-story.md'),
},
tempExportRoot,
);
assert(
compatibilityIdentity.skillName === 'bmad-bmm-create-story' && compatibilityIdentity.exportIdDerivationSourceType === 'path-derived',
'Codex export preserves non-exemplar path-derived skill identity behavior',
);
} catch (error) {
assert(false, 'Export projection suite setup', error.message);
} finally {
await fs.remove(tempExportRoot);
}
console.log('');
// ============================================================
// Test 12: QA Agent Compilation
// ============================================================
console.log(`${colors.yellow}Test Suite 12: QA Agent Compilation${colors.reset}\n`);
try {
const builder = new YamlXmlBuilder();
const qaAgentPath = path.join(projectRoot, 'src/bmm/agents/qa.agent.yaml');
const tempOutput = path.join(__dirname, 'temp-qa-agent.md');
try {
const result = await builder.buildAgent(qaAgentPath, null, tempOutput, { includeMetadata: true });
const compiled = await fs.readFile(tempOutput, 'utf8');
assert(compiled.includes('QA Engineer'), 'QA agent compilation includes agent title');
assert(compiled.includes('qa-generate-e2e-tests'), 'QA agent menu includes automate workflow');
// Cleanup
await fs.remove(tempOutput);
} catch (error) {
assert(false, 'QA agent compiles successfully', error.message);
}
} catch (error) {
assert(false, 'QA compilation test setup', error.message);
}
console.log('');
// ============================================================
// Test 13: Projection Consumer Compatibility Contracts
// ============================================================
console.log(`${colors.yellow}Test Suite 13: Projection Consumer Compatibility${colors.reset}\n`);
const tempCompatibilityRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-projection-compatibility-'));
try {
const tempCompatibilityConfigDir = path.join(tempCompatibilityRoot, '_config');
await fs.ensureDir(tempCompatibilityConfigDir);
const buildCsvLine = (columns, row) =>
columns
.map((column) => {
const value = String(row[column] ?? '');
return value.includes(',') ? `"${value.replaceAll('"', '""')}"` : value;
})
.join(',');
const taskManifestColumns = [
...TASK_MANIFEST_COMPATIBILITY_PREFIX_COLUMNS,
...TASK_MANIFEST_WAVE1_ADDITIVE_COLUMNS,
'futureAdditiveField',
];
const validTaskRows = [
{
name: 'help',
displayName: 'help',
description: 'Help command',
module: 'core',
path: '{project-root}/_bmad/core/tasks/help.md',
standalone: 'true',
legacyName: 'help',
canonicalId: 'bmad-help',
authoritySourceType: 'sidecar',
authoritySourcePath: 'bmad-fork/src/core/tasks/help.artifact.yaml',
futureAdditiveField: 'wave-1',
},
{
name: 'create-story',
displayName: 'Create Story',
description: 'Create a dedicated story file',
module: 'bmm',
path: '{project-root}/_bmad/bmm/workflows/2-creation/create-story/workflow.yaml',
standalone: 'true',
legacyName: 'create-story',
canonicalId: '',
authoritySourceType: '',
authoritySourcePath: '',
futureAdditiveField: 'wave-1',
},
];
const validTaskManifestCsv =
[taskManifestColumns.join(','), ...validTaskRows.map((row) => buildCsvLine(taskManifestColumns, row))].join('\n') + '\n';
await fs.writeFile(path.join(tempCompatibilityConfigDir, 'task-manifest.csv'), validTaskManifestCsv, 'utf8');
const validatedTaskSurface = validateTaskManifestCompatibilitySurface(validTaskManifestCsv, {
sourcePath: '_bmad/_config/task-manifest.csv',
});
assert(
validatedTaskSurface.headerColumns[0] === 'name' &&
validatedTaskSurface.headerColumns[TASK_MANIFEST_COMPATIBILITY_PREFIX_COLUMNS.length] === 'legacyName',
'Task-manifest compatibility validator enforces locked prefix plus additive wave-1 ordering',
);
assert(
validatedTaskSurface.headerColumns.at(-1) === 'futureAdditiveField',
'Task-manifest compatibility validator allows additive columns appended after locked wave-1 columns',
);
validateTaskManifestLoaderEntries(validatedTaskSurface.rows, {
sourcePath: '_bmad/_config/task-manifest.csv',
headerColumns: validatedTaskSurface.headerColumns,
});
assert(true, 'Task-manifest loader compatibility validator accepts known loader columns with additive fields');
const taskToolGenerator = new TaskToolCommandGenerator();
const loadedTaskRows = await taskToolGenerator.loadTaskManifest(tempCompatibilityRoot);
assert(
Array.isArray(loadedTaskRows) &&
loadedTaskRows.length === 2 &&
loadedTaskRows[0].name === 'help' &&
loadedTaskRows[1].name === 'create-story',
'Task-manifest loader remains parseable when additive columns are present',
);
const legacyTaskManifestColumns = [...TASK_MANIFEST_COMPATIBILITY_PREFIX_COLUMNS];
const legacyTaskManifestCsv =
[legacyTaskManifestColumns.join(','), buildCsvLine(legacyTaskManifestColumns, validTaskRows[0])].join('\n') + '\n';
const legacyTaskSurface = validateTaskManifestCompatibilitySurface(legacyTaskManifestCsv, {
sourcePath: '_bmad/_config/task-manifest.csv',
allowLegacyPrefixOnly: true,
});
assert(
legacyTaskSurface.isLegacyPrefixOnlyHeader === true,
'Task-manifest compatibility validator supports legacy prefix-only headers during migration reads',
);
try {
validateTaskManifestCompatibilitySurface(legacyTaskManifestCsv, {
sourcePath: '_bmad/_config/task-manifest.csv',
});
assert(false, 'Task-manifest strict validator rejects legacy prefix-only header without migration mode');
} catch (error) {
assert(
error.code === PROJECTION_COMPATIBILITY_ERROR_CODES.TASK_MANIFEST_HEADER_WAVE1_MISMATCH,
'Task-manifest strict validator emits deterministic wave-1 mismatch error for legacy prefix-only headers',
);
}
const reorderedTaskManifestColumns = [...taskManifestColumns];
[reorderedTaskManifestColumns[0], reorderedTaskManifestColumns[1]] = [reorderedTaskManifestColumns[1], reorderedTaskManifestColumns[0]];
const invalidTaskManifestCsv =
[reorderedTaskManifestColumns.join(','), buildCsvLine(reorderedTaskManifestColumns, validTaskRows[0])].join('\n') + '\n';
try {
validateTaskManifestCompatibilitySurface(invalidTaskManifestCsv, {
sourcePath: '_bmad/_config/task-manifest.csv',
});
assert(false, 'Task-manifest validator rejects non-additive reordered compatibility-prefix headers');
} catch (error) {
assert(
error.code === PROJECTION_COMPATIBILITY_ERROR_CODES.TASK_MANIFEST_HEADER_PREFIX_MISMATCH && error.fieldPath === 'header[0]',
'Task-manifest validator emits deterministic diagnostics for reordered compatibility-prefix headers',
);
}
const helpCatalogColumns = [
...HELP_CATALOG_COMPATIBILITY_PREFIX_COLUMNS,
...HELP_CATALOG_WAVE1_ADDITIVE_COLUMNS,
'futureAdditiveField',
];
const validHelpRows = [
{
module: 'core',
phase: 'anytime',
name: 'bmad-help',
code: 'BH',
sequence: '',
'workflow-file': '_bmad/core/tasks/help.md',
command: 'bmad-help',
required: 'false',
'agent-name': '',
'agent-command': '',
'agent-display-name': '',
'agent-title': '',
options: '',
description: 'Help command',
'output-location': '',
outputs: '',
futureAdditiveField: 'wave-1',
},
{
module: 'bmm',
phase: 'planning',
name: 'create-story',
code: 'CS',
sequence: '',
'workflow-file': '_bmad/bmm/workflows/2-creation/create-story/workflow.yaml',
command: 'bmad-bmm-create-story',
required: 'false',
'agent-name': 'sm',
'agent-command': 'bmad:agent:sm',
'agent-display-name': 'Scrum Master',
'agent-title': 'SM',
options: '',
description: 'Create next story',
'output-location': '',
outputs: '',
futureAdditiveField: 'wave-1',
},
];
const validHelpCatalogCsv =
[helpCatalogColumns.join(','), ...validHelpRows.map((row) => buildCsvLine(helpCatalogColumns, row))].join('\n') + '\n';
await fs.writeFile(path.join(tempCompatibilityConfigDir, 'bmad-help.csv'), validHelpCatalogCsv, 'utf8');
const validatedHelpSurface = validateHelpCatalogCompatibilitySurface(validHelpCatalogCsv, {
sourcePath: '_bmad/_config/bmad-help.csv',
});
assert(
validatedHelpSurface.headerColumns[5] === 'workflow-file' && validatedHelpSurface.headerColumns[6] === 'command',
'Help-catalog compatibility validator preserves workflow-file and command compatibility columns',
);
assert(
validatedHelpSurface.headerColumns.at(-1) === 'futureAdditiveField',
'Help-catalog compatibility validator allows additive columns appended after locked wave-1 columns',
);
validateHelpCatalogLoaderEntries(validatedHelpSurface.rows, {
sourcePath: '_bmad/_config/bmad-help.csv',
headerColumns: validatedHelpSurface.headerColumns,
});
validateGithubCopilotHelpLoaderEntries(validatedHelpSurface.rows, {
sourcePath: '_bmad/_config/bmad-help.csv',
headerColumns: validatedHelpSurface.headerColumns,
});
assert(true, 'Help-catalog and GitHub Copilot loader compatibility validators accept stable command/workflow-file contracts');
const githubCopilotSetup = new GitHubCopilotSetup();
const loadedHelpRows = await githubCopilotSetup.loadBmadHelp(tempCompatibilityRoot);
assert(
Array.isArray(loadedHelpRows) &&
loadedHelpRows.length === 2 &&
loadedHelpRows[0]['workflow-file'] === '_bmad/core/tasks/help.md' &&
loadedHelpRows[0].command === 'bmad-help',
'GitHub Copilot help loader remains parseable with additive help-catalog columns',
);
const reorderedHelpCatalogColumns = [...helpCatalogColumns];
[reorderedHelpCatalogColumns[5], reorderedHelpCatalogColumns[6]] = [reorderedHelpCatalogColumns[6], reorderedHelpCatalogColumns[5]];
const invalidHelpCatalogCsv =
[reorderedHelpCatalogColumns.join(','), buildCsvLine(reorderedHelpCatalogColumns, validHelpRows[0])].join('\n') + '\n';
try {
validateHelpCatalogCompatibilitySurface(invalidHelpCatalogCsv, {
sourcePath: '_bmad/_config/bmad-help.csv',
});
assert(false, 'Help-catalog validator rejects non-additive reordered compatibility headers');
} catch (error) {
assert(
error.code === PROJECTION_COMPATIBILITY_ERROR_CODES.HELP_CATALOG_HEADER_PREFIX_MISMATCH && error.fieldPath === 'header[5]',
'Help-catalog validator emits deterministic diagnostics for reordered compatibility headers',
);
}
const missingWorkflowFileRows = [
{
...validHelpRows[0],
'workflow-file': '',
command: 'bmad-help',
},
];
const missingWorkflowFileCsv =
[helpCatalogColumns.join(','), ...missingWorkflowFileRows.map((row) => buildCsvLine(helpCatalogColumns, row))].join('\n') + '\n';
await fs.writeFile(path.join(tempCompatibilityConfigDir, 'bmad-help.csv'), missingWorkflowFileCsv, 'utf8');
try {
await githubCopilotSetup.loadBmadHelp(tempCompatibilityRoot);
assert(false, 'GitHub Copilot help loader rejects rows that drop workflow-file while keeping command values');
} catch (error) {
assert(
error.code === PROJECTION_COMPATIBILITY_ERROR_CODES.GITHUB_COPILOT_WORKFLOW_FILE_MISSING &&
error.fieldPath === 'rows[0].workflow-file',
'GitHub Copilot help loader emits deterministic diagnostics for missing workflow-file compatibility breaks',
);
}
} catch (error) {
assert(false, 'Projection compatibility suite setup', error.message);
} finally {
await fs.remove(tempCompatibilityRoot);
}
console.log('');
// ============================================================
// Test 14: Deterministic Validation Artifact Suite
// ============================================================
console.log(`${colors.yellow}Test Suite 14: Deterministic Validation Artifact Suite${colors.reset}\n`);
const tempValidationHarnessRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-wave1-validation-suite-'));
try {
const tempProjectRoot = tempValidationHarnessRoot;
const tempBmadDir = path.join(tempProjectRoot, '_bmad');
const tempConfigDir = path.join(tempBmadDir, '_config');
const tempSourceTasksDir = path.join(tempProjectRoot, 'bmad-fork', 'src', 'core', 'tasks');
const tempSkillDir = path.join(tempProjectRoot, '.agents', 'skills', 'bmad-help');
await fs.ensureDir(tempConfigDir);
await fs.ensureDir(path.join(tempBmadDir, 'core', 'tasks'));
await fs.ensureDir(path.join(tempBmadDir, 'core'));
await fs.ensureDir(tempSourceTasksDir);
await fs.ensureDir(tempSkillDir);
const writeCsv = async (filePath, columns, rows) => {
const buildCsvLine = (values) =>
values
.map((value) => {
const text = String(value ?? '');
return text.includes(',') || text.includes('"') ? `"${text.replaceAll('"', '""')}"` : text;
})
.join(',');
const lines = [columns.join(','), ...rows.map((row) => buildCsvLine(columns.map((column) => row[column] ?? '')))];
await fs.writeFile(filePath, `${lines.join('\n')}\n`, 'utf8');
};
const sidecarFixture = {
schemaVersion: 1,
canonicalId: 'bmad-help',
artifactType: 'task',
module: 'core',
sourcePath: 'bmad-fork/src/core/tasks/help.md',
displayName: 'help',
description: 'Help command',
dependencies: {
requires: [],
},
};
await fs.writeFile(path.join(tempSourceTasksDir, 'help.artifact.yaml'), yaml.stringify(sidecarFixture), 'utf8');
await fs.writeFile(
path.join(tempSourceTasksDir, 'help.md'),
`---\n${yaml
.stringify({
name: 'help',
description: 'Help command',
canonicalId: 'bmad-help',
dependencies: { requires: [] },
})
.trimEnd()}\n---\n\n# Source Help\n`,
'utf8',
);
await fs.writeFile(
path.join(tempBmadDir, 'core', 'tasks', 'help.md'),
`---\n${yaml
.stringify({
name: 'help',
description: 'Help command',
canonicalId: 'bmad-help',
dependencies: { requires: [] },
})
.trimEnd()}\n---\n\n# Runtime Help\n`,
'utf8',
);
await fs.writeFile(
path.join(tempSkillDir, 'SKILL.md'),
`---\n${yaml.stringify({ name: 'bmad-help', description: 'Help command' }).trimEnd()}\n---\n\n# Skill\n`,
'utf8',
);
await writeCsv(
path.join(tempConfigDir, 'task-manifest.csv'),
[...TASK_MANIFEST_COMPATIBILITY_PREFIX_COLUMNS, ...TASK_MANIFEST_WAVE1_ADDITIVE_COLUMNS],
[
{
name: 'help',
displayName: 'help',
description: 'Help command',
module: 'core',
path: '_bmad/core/tasks/help.md',
standalone: 'true',
legacyName: 'help',
canonicalId: 'bmad-help',
authoritySourceType: 'sidecar',
authoritySourcePath: 'bmad-fork/src/core/tasks/help.artifact.yaml',
},
],
);
await writeCsv(
path.join(tempConfigDir, 'canonical-aliases.csv'),
[
'canonicalId',
'alias',
'aliasType',
'authoritySourceType',
'authoritySourcePath',
'rowIdentity',
'normalizedAliasValue',
'rawIdentityHasLeadingSlash',
'resolutionEligibility',
],
[
{
canonicalId: 'bmad-help',
alias: 'bmad-help',
aliasType: 'canonical-id',
authoritySourceType: 'sidecar',
authoritySourcePath: 'bmad-fork/src/core/tasks/help.artifact.yaml',
rowIdentity: 'alias-row:bmad-help:canonical-id',
normalizedAliasValue: 'bmad-help',
rawIdentityHasLeadingSlash: 'false',
resolutionEligibility: 'canonical-id-only',
},
{
canonicalId: 'bmad-help',
alias: 'help',
aliasType: 'legacy-name',
authoritySourceType: 'sidecar',
authoritySourcePath: 'bmad-fork/src/core/tasks/help.artifact.yaml',
rowIdentity: 'alias-row:bmad-help:legacy-name',
normalizedAliasValue: 'help',
rawIdentityHasLeadingSlash: 'false',
resolutionEligibility: 'legacy-name-only',
},
{
canonicalId: 'bmad-help',
alias: '/bmad-help',
aliasType: 'slash-command',
authoritySourceType: 'sidecar',
authoritySourcePath: 'bmad-fork/src/core/tasks/help.artifact.yaml',
rowIdentity: 'alias-row:bmad-help:slash-command',
normalizedAliasValue: 'bmad-help',
rawIdentityHasLeadingSlash: 'true',
resolutionEligibility: 'slash-command-only',
},
],
);
await writeCsv(
path.join(tempConfigDir, 'bmad-help.csv'),
[...HELP_CATALOG_COMPATIBILITY_PREFIX_COLUMNS, ...HELP_CATALOG_WAVE1_ADDITIVE_COLUMNS],
[
{
module: 'core',
phase: 'anytime',
name: 'bmad-help',
code: 'BH',
sequence: '',
'workflow-file': '_bmad/core/tasks/help.md',
command: 'bmad-help',
required: 'false',
'agent-name': '',
'agent-command': '',
'agent-display-name': '',
'agent-title': '',
options: '',
description: 'Help command',
'output-location': '',
outputs: '',
},
],
);
await writeCsv(
path.join(tempBmadDir, 'core', 'module-help.csv'),
[
'module',
'phase',
'name',
'code',
'sequence',
'workflow-file',
'command',
'required',
'agent',
'options',
'description',
'output-location',
'outputs',
],
[
{
module: 'core',
phase: 'anytime',
name: 'bmad-help',
code: 'BH',
sequence: '',
'workflow-file': '_bmad/core/tasks/help.md',
command: 'bmad-help',
required: 'false',
agent: '',
options: '',
description: 'Help command',
'output-location': '',
outputs: '',
},
],
);
await writeCsv(
path.join(tempConfigDir, 'bmad-help-catalog-pipeline.csv'),
[
'stage',
'artifactPath',
'rowIdentity',
'canonicalId',
'sourcePath',
'rowCountForStageCanonicalId',
'commandValue',
'expectedCommandValue',
'descriptionValue',
'expectedDescriptionValue',
'descriptionAuthoritySourceType',
'descriptionAuthoritySourcePath',
'commandAuthoritySourceType',
'commandAuthoritySourcePath',
'issuerOwnerClass',
'issuingComponent',
'issuingComponentBindingEvidence',
'stageStatus',
'status',
],
[
{
stage: 'installed-compatibility-row',
artifactPath: '_bmad/core/module-help.csv',
rowIdentity: 'module-help-row:bmad-help',
canonicalId: 'bmad-help',
sourcePath: 'bmad-fork/src/core/tasks/help.md',
rowCountForStageCanonicalId: '1',
commandValue: 'bmad-help',
expectedCommandValue: 'bmad-help',
descriptionValue: 'Help command',
expectedDescriptionValue: 'Help command',
descriptionAuthoritySourceType: 'sidecar',
descriptionAuthoritySourcePath: 'bmad-fork/src/core/tasks/help.artifact.yaml',
commandAuthoritySourceType: 'sidecar',
commandAuthoritySourcePath: 'bmad-fork/src/core/tasks/help.artifact.yaml',
issuerOwnerClass: 'installer',
issuingComponent: 'bmad-fork/tools/cli/installers/lib/core/help-catalog-generator.js::buildSidecarAwareExemplarHelpRow()',
issuingComponentBindingEvidence: 'deterministic',
stageStatus: 'PASS',
status: 'PASS',
},
{
stage: 'merged-config-row',
artifactPath: '_bmad/_config/bmad-help.csv',
rowIdentity: 'merged-help-row:bmad-help',
canonicalId: 'bmad-help',
sourcePath: 'bmad-fork/src/core/tasks/help.md',
rowCountForStageCanonicalId: '1',
commandValue: 'bmad-help',
expectedCommandValue: 'bmad-help',
descriptionValue: 'Help command',
expectedDescriptionValue: 'Help command',
descriptionAuthoritySourceType: 'sidecar',
descriptionAuthoritySourcePath: 'bmad-fork/src/core/tasks/help.artifact.yaml',
commandAuthoritySourceType: 'sidecar',
commandAuthoritySourcePath: 'bmad-fork/src/core/tasks/help.artifact.yaml',
issuerOwnerClass: 'installer',
issuingComponent: 'bmad-fork/tools/cli/installers/lib/core/installer.js::mergeModuleHelpCatalogs()',
issuingComponentBindingEvidence: 'deterministic',
stageStatus: 'PASS',
status: 'PASS',
},
],
);
await writeCsv(
path.join(tempConfigDir, 'bmad-help-command-label-report.csv'),
[
'surface',
'canonicalId',
'rawCommandValue',
'displayedCommandLabel',
'normalizedDisplayedLabel',
'rowCountForCanonicalId',
'authoritySourceType',
'authoritySourcePath',
'status',
'failureReason',
],
[
{
surface: '_bmad/_config/bmad-help.csv',
canonicalId: 'bmad-help',
rawCommandValue: 'bmad-help',
displayedCommandLabel: '/bmad-help',
normalizedDisplayedLabel: '/bmad-help',
rowCountForCanonicalId: '1',
authoritySourceType: 'sidecar',
authoritySourcePath: 'bmad-fork/src/core/tasks/help.artifact.yaml',
status: 'PASS',
failureReason: '',
},
],
);
const harness = new Wave1ValidationHarness();
const firstRun = await harness.generateAndValidate({
projectDir: tempProjectRoot,
bmadDir: tempBmadDir,
bmadFolderName: '_bmad',
sidecarPath: path.join(tempSourceTasksDir, 'help.artifact.yaml'),
sourceMarkdownPath: path.join(tempSourceTasksDir, 'help.md'),
});
assert(
firstRun.terminalStatus === 'PASS' && firstRun.generatedArtifactCount === WAVE1_VALIDATION_ARTIFACT_REGISTRY.length,
'Wave-1 validation harness generates and validates all required artifacts',
);
const artifactPathsById = new Map(
WAVE1_VALIDATION_ARTIFACT_REGISTRY.map((artifact) => [
artifact.artifactId,
path.join(tempProjectRoot, '_bmad-output', 'planning-artifacts', artifact.relativePath),
]),
);
for (const [artifactId, artifactPath] of artifactPathsById.entries()) {
assert(await fs.pathExists(artifactPath), `Wave-1 validation harness outputs artifact ${artifactId}`);
}
const artifactThreeBaselineRows = csv.parse(await fs.readFile(artifactPathsById.get(3), 'utf8'), {
columns: true,
skip_empty_lines: true,
});
const manifestProvenanceRow = artifactThreeBaselineRows.find((row) => row.artifactPath === '_bmad/_config/task-manifest.csv');
let manifestReplayEvidence = null;
try {
manifestReplayEvidence = JSON.parse(String(manifestProvenanceRow?.issuingComponentBindingEvidence || ''));
} catch {
manifestReplayEvidence = null;
}
assert(
manifestReplayEvidence &&
manifestReplayEvidence.evidenceVersion === 1 &&
manifestReplayEvidence.observationMethod === 'validator-observed-baseline-plus-isolated-single-component-perturbation' &&
typeof manifestReplayEvidence.baselineArtifactSha256 === 'string' &&
manifestReplayEvidence.baselineArtifactSha256.length === 64 &&
typeof manifestReplayEvidence.mutatedArtifactSha256 === 'string' &&
manifestReplayEvidence.mutatedArtifactSha256.length === 64 &&
manifestReplayEvidence.baselineArtifactSha256 !== manifestReplayEvidence.mutatedArtifactSha256 &&
manifestReplayEvidence.perturbationApplied === true &&
Number(manifestReplayEvidence.baselineTargetRowCount) > Number(manifestReplayEvidence.mutatedTargetRowCount) &&
manifestReplayEvidence.targetedRowLocator === manifestProvenanceRow.rowIdentity,
'Wave-1 validation harness emits validator-observed replay evidence with baseline/perturbation impact',
);
const firstArtifactContents = new Map();
for (const [artifactId, artifactPath] of artifactPathsById.entries()) {
firstArtifactContents.set(artifactId, await fs.readFile(artifactPath, 'utf8'));
}
await harness.generateAndValidate({
projectDir: tempProjectRoot,
bmadDir: tempBmadDir,
bmadFolderName: '_bmad',
sidecarPath: path.join(tempSourceTasksDir, 'help.artifact.yaml'),
sourceMarkdownPath: path.join(tempSourceTasksDir, 'help.md'),
});
let deterministicOutputs = true;
for (const [artifactId, artifactPath] of artifactPathsById.entries()) {
const rerunContent = await fs.readFile(artifactPath, 'utf8');
if (rerunContent !== firstArtifactContents.get(artifactId)) {
deterministicOutputs = false;
break;
}
}
assert(deterministicOutputs, 'Wave-1 validation harness outputs are byte-stable across unchanged repeated runs');
await fs.remove(path.join(tempSkillDir, 'SKILL.md'));
const noIdeInstaller = new Installer();
noIdeInstaller.codexExportDerivationRecords = [];
const noIdeValidationOptions = await noIdeInstaller.buildWave1ValidationOptions({
projectDir: tempProjectRoot,
bmadDir: tempBmadDir,
});
assert(
noIdeValidationOptions.requireExportSkillProjection === false,
'Installer wave-1 validation options disable export-surface requirement for no-IDE/non-Codex flow',
);
const noIdeRun = await harness.generateAndValidate({
...noIdeValidationOptions,
sidecarPath: path.join(tempSourceTasksDir, 'help.artifact.yaml'),
sourceMarkdownPath: path.join(tempSourceTasksDir, 'help.md'),
});
assert(
noIdeRun.terminalStatus === 'PASS',
'Wave-1 validation harness remains terminal-PASS for no-IDE/non-Codex flow when core projection surfaces are present',
);
const noIdeStandaloneValidation = await harness.validateGeneratedArtifacts({
projectDir: tempProjectRoot,
bmadFolderName: '_bmad',
});
assert(
noIdeStandaloneValidation.status === 'PASS',
'Wave-1 validation harness infers no-IDE export prerequisite context during standalone validation when options are omitted',
);
try {
await harness.buildObservedBindingEvidence({
artifactPath: '_bmad/_config/task-manifest.csv',
absolutePath: path.join(tempBmadDir, '_config', 'task-manifest.csv'),
componentPath: 'bmad-fork/tools/cli/installers/lib/core/manifest-generator.js',
rowIdentity: 'issued-artifact:missing-claim-row',
optionalSurface: false,
runtimeFolder: '_bmad',
});
assert(false, 'Wave-1 replay evidence generation rejects unmapped claimed rowIdentity');
} catch (error) {
assert(
error.code === WAVE1_VALIDATION_ERROR_CODES.REQUIRED_ROW_IDENTITY_MISSING,
'Wave-1 replay evidence generation emits deterministic missing-claimed-rowIdentity error code',
);
}
await fs.writeFile(
path.join(tempSkillDir, 'SKILL.md'),
`---\n${yaml.stringify({ name: 'bmad-help', description: 'Help command' }).trimEnd()}\n---\n\n# Skill\n`,
'utf8',
);
await fs.remove(path.join(tempConfigDir, 'task-manifest.csv'));
try {
await harness.generateAndValidate({
projectDir: tempProjectRoot,
bmadDir: tempBmadDir,
bmadFolderName: '_bmad',
sidecarPath: path.join(tempSourceTasksDir, 'help.artifact.yaml'),
sourceMarkdownPath: path.join(tempSourceTasksDir, 'help.md'),
});
assert(false, 'Wave-1 validation harness fails when required projection input surfaces are missing');
} catch (error) {
assert(
error.code === WAVE1_VALIDATION_ERROR_CODES.REQUIRED_ARTIFACT_MISSING,
'Wave-1 validation harness emits deterministic missing-input-surface error code',
);
}
await writeCsv(
path.join(tempConfigDir, 'task-manifest.csv'),
[...TASK_MANIFEST_COMPATIBILITY_PREFIX_COLUMNS, ...TASK_MANIFEST_WAVE1_ADDITIVE_COLUMNS],
[
{
name: 'help',
displayName: 'help',
description: 'Help command',
module: 'core',
path: '_bmad/core/tasks/help.md',
standalone: 'true',
legacyName: 'help',
canonicalId: 'bmad-help',
authoritySourceType: 'sidecar',
authoritySourcePath: 'bmad-fork/src/core/tasks/help.artifact.yaml',
},
],
);
await harness.generateAndValidate({
projectDir: tempProjectRoot,
bmadDir: tempBmadDir,
bmadFolderName: '_bmad',
sidecarPath: path.join(tempSourceTasksDir, 'help.artifact.yaml'),
sourceMarkdownPath: path.join(tempSourceTasksDir, 'help.md'),
});
await fs.remove(artifactPathsById.get(14));
try {
await harness.validateGeneratedArtifacts({ projectDir: tempProjectRoot });
assert(false, 'Wave-1 validation harness fails when a required artifact is missing');
} catch (error) {
assert(
error.code === WAVE1_VALIDATION_ERROR_CODES.REQUIRED_ARTIFACT_MISSING,
'Wave-1 validation harness emits deterministic missing-artifact error code',
);
}
await harness.generateAndValidate({
projectDir: tempProjectRoot,
bmadDir: tempBmadDir,
bmadFolderName: '_bmad',
sidecarPath: path.join(tempSourceTasksDir, 'help.artifact.yaml'),
sourceMarkdownPath: path.join(tempSourceTasksDir, 'help.md'),
});
const artifactTwoPath = artifactPathsById.get(2);
const artifactTwoContent = await fs.readFile(artifactTwoPath, 'utf8');
const artifactTwoLines = artifactTwoContent.split('\n');
artifactTwoLines[0] = artifactTwoLines[0].replace('surface', 'brokenSurface');
await fs.writeFile(artifactTwoPath, artifactTwoLines.join('\n'), 'utf8');
try {
await harness.validateGeneratedArtifacts({ projectDir: tempProjectRoot });
assert(false, 'Wave-1 validation harness rejects schema/header drift');
} catch (error) {
assert(
error.code === WAVE1_VALIDATION_ERROR_CODES.CSV_SCHEMA_MISMATCH,
'Wave-1 validation harness emits deterministic schema-mismatch error code',
);
}
await harness.generateAndValidate({
projectDir: tempProjectRoot,
bmadDir: tempBmadDir,
bmadFolderName: '_bmad',
sidecarPath: path.join(tempSourceTasksDir, 'help.artifact.yaml'),
sourceMarkdownPath: path.join(tempSourceTasksDir, 'help.md'),
});
const artifactNinePath = artifactPathsById.get(9);
const artifactNineHeader = (await fs.readFile(artifactNinePath, 'utf8')).split('\n')[0];
await fs.writeFile(artifactNinePath, `${artifactNineHeader}\n`, 'utf8');
try {
await harness.validateGeneratedArtifacts({ projectDir: tempProjectRoot });
assert(false, 'Wave-1 validation harness rejects header-only required-identity artifacts');
} catch (error) {
assert(
error.code === WAVE1_VALIDATION_ERROR_CODES.REQUIRED_ROW_IDENTITY_MISSING,
'Wave-1 validation harness emits deterministic missing-row error code for header-only artifacts',
);
}
await harness.generateAndValidate({
projectDir: tempProjectRoot,
bmadDir: tempBmadDir,
bmadFolderName: '_bmad',
sidecarPath: path.join(tempSourceTasksDir, 'help.artifact.yaml'),
sourceMarkdownPath: path.join(tempSourceTasksDir, 'help.md'),
});
const artifactThreePath = artifactPathsById.get(3);
const artifactThreeContent = await fs.readFile(artifactThreePath, 'utf8');
const artifactThreeRows = csv.parse(artifactThreeContent, {
columns: true,
skip_empty_lines: true,
});
artifactThreeRows[0].rowIdentity = '';
await writeCsv(
artifactThreePath,
[
'rowIdentity',
'artifactPath',
'canonicalId',
'issuerOwnerClass',
'evidenceIssuerComponent',
'evidenceMethod',
'issuingComponent',
'issuingComponentBindingBasis',
'issuingComponentBindingEvidence',
'claimScope',
'status',
],
artifactThreeRows,
);
try {
await harness.validateGeneratedArtifacts({ projectDir: tempProjectRoot });
assert(false, 'Wave-1 validation harness rejects missing required row identity values');
} catch (error) {
assert(
error.code === WAVE1_VALIDATION_ERROR_CODES.REQUIRED_ROW_IDENTITY_MISSING,
'Wave-1 validation harness emits deterministic row-identity error code',
);
}
await harness.generateAndValidate({
projectDir: tempProjectRoot,
bmadDir: tempBmadDir,
bmadFolderName: '_bmad',
sidecarPath: path.join(tempSourceTasksDir, 'help.artifact.yaml'),
sourceMarkdownPath: path.join(tempSourceTasksDir, 'help.md'),
});
const artifactFourPath = artifactPathsById.get(4);
const artifactFourRows = csv.parse(await fs.readFile(artifactFourPath, 'utf8'), {
columns: true,
skip_empty_lines: true,
});
artifactFourRows[0].issuedArtifactEvidenceRowIdentity = '';
await writeCsv(
artifactFourPath,
[
'surface',
'sourcePath',
'legacyName',
'canonicalId',
'displayName',
'normalizedCapabilityKey',
'authoritySourceType',
'authoritySourcePath',
'issuerOwnerClass',
'issuingComponent',
'issuedArtifactEvidencePath',
'issuedArtifactEvidenceRowIdentity',
'issuingComponentBindingEvidence',
'status',
],
artifactFourRows,
);
try {
await harness.validateGeneratedArtifacts({ projectDir: tempProjectRoot });
assert(false, 'Wave-1 validation harness rejects PASS rows missing required evidence-link fields');
} catch (error) {
assert(
error.code === WAVE1_VALIDATION_ERROR_CODES.REQUIRED_EVIDENCE_LINK_MISSING,
'Wave-1 validation harness emits deterministic evidence-link error code for missing row identity link',
);
}
await harness.generateAndValidate({
projectDir: tempProjectRoot,
bmadDir: tempBmadDir,
bmadFolderName: '_bmad',
sidecarPath: path.join(tempSourceTasksDir, 'help.artifact.yaml'),
sourceMarkdownPath: path.join(tempSourceTasksDir, 'help.md'),
});
const artifactNineTamperedRows = csv.parse(await fs.readFile(artifactPathsById.get(9), 'utf8'), {
columns: true,
skip_empty_lines: true,
});
artifactNineTamperedRows[0].issuingComponent = 'self-attested-generator-component';
await writeCsv(
artifactPathsById.get(9),
[
'stage',
'artifactPath',
'rowIdentity',
'canonicalId',
'sourcePath',
'rowCountForStageCanonicalId',
'commandValue',
'expectedCommandValue',
'descriptionValue',
'expectedDescriptionValue',
'descriptionAuthoritySourceType',
'descriptionAuthoritySourcePath',
'commandAuthoritySourceType',
'commandAuthoritySourcePath',
'issuerOwnerClass',
'issuingComponent',
'issuedArtifactEvidencePath',
'issuedArtifactEvidenceRowIdentity',
'issuingComponentBindingEvidence',
'stageStatus',
'status',
],
artifactNineTamperedRows,
);
try {
await harness.validateGeneratedArtifacts({ projectDir: tempProjectRoot });
assert(false, 'Wave-1 validation harness rejects self-attested issuer claims that diverge from validator evidence');
} catch (error) {
assert(
error.code === WAVE1_VALIDATION_ERROR_CODES.SELF_ATTESTED_ISSUER_CLAIM,
'Wave-1 validation harness emits deterministic self-attested issuer-claim rejection code',
);
}
await harness.generateAndValidate({
projectDir: tempProjectRoot,
bmadDir: tempBmadDir,
bmadFolderName: '_bmad',
sidecarPath: path.join(tempSourceTasksDir, 'help.artifact.yaml'),
sourceMarkdownPath: path.join(tempSourceTasksDir, 'help.md'),
});
const artifactThreeTamperedRows = csv.parse(await fs.readFile(artifactPathsById.get(3), 'utf8'), {
columns: true,
skip_empty_lines: true,
});
artifactThreeTamperedRows[0].issuingComponentBindingEvidence = '{"broken":true}';
await writeCsv(
artifactPathsById.get(3),
[
'rowIdentity',
'artifactPath',
'canonicalId',
'issuerOwnerClass',
'evidenceIssuerComponent',
'evidenceMethod',
'issuingComponent',
'issuingComponentBindingBasis',
'issuingComponentBindingEvidence',
'claimScope',
'status',
],
artifactThreeTamperedRows,
);
try {
await harness.validateGeneratedArtifacts({ projectDir: tempProjectRoot });
assert(false, 'Wave-1 validation harness rejects malformed replay-evidence payloads');
} catch (error) {
assert(
error.code === WAVE1_VALIDATION_ERROR_CODES.BINDING_EVIDENCE_INVALID,
'Wave-1 validation harness emits deterministic replay-evidence validation error code',
);
}
} catch (error) {
assert(false, 'Deterministic validation artifact suite setup', error.message);
} finally {
await fs.remove(tempValidationHarnessRoot);
}
console.log('');
// ============================================================
// Summary
// ============================================================
console.log(`${colors.cyan}========================================`);
console.log('Test Results:');
console.log(` Passed: ${colors.green}${passed}${colors.reset}`);
console.log(` Failed: ${colors.red}${failed}${colors.reset}`);
console.log(`========================================${colors.reset}\n`);
if (failed === 0) {
console.log(`${colors.green}✨ All installation component tests passed!${colors.reset}\n`);
process.exit(0);
} else {
console.log(`${colors.red}❌ Some installation component tests failed${colors.reset}\n`);
process.exit(1);
}
}
// Run tests
runTests().catch((error) => {
console.error(`${colors.red}Test runner failed:${colors.reset}`, error.message);
console.error(error.stack);
process.exit(1);
});