/** * 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(' tag'); assert(compiled.includes(''), 'Compiled agent contains tag'); assert(compiled.includes(''), 'Compiled agent contains 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); });