246 lines
8.3 KiB
JavaScript
Executable file
246 lines
8.3 KiB
JavaScript
Executable file
import { promises as fs } from 'node:fs'
|
|
import path from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
|
|
const __filename = fileURLToPath(import.meta.url)
|
|
const __dirname = path.dirname(__filename)
|
|
const uiRoot = path.resolve(__dirname, '..')
|
|
const repoRoot = path.resolve(uiRoot, '..')
|
|
|
|
const configPath = path.join(uiRoot, 'config', 'e2e-critical-workflows.json')
|
|
const docsSpecPath = path.join(
|
|
repoRoot,
|
|
'docs',
|
|
'archive',
|
|
'cross-corpus',
|
|
'CROSS_CORPUS_E2E_CRITICAL_WORKFLOWS.md'
|
|
)
|
|
const evidenceDir = path.join(repoRoot, 'docs', 'evidence', 'cross-corpus')
|
|
const evidenceJsonPath = path.join(evidenceDir, 'corpus-e2e-critical-workflow-coverage.json')
|
|
const evidenceMdPath = path.join(evidenceDir, 'corpus-e2e-critical-workflow-coverage.md')
|
|
|
|
function relativeRepoPath(filePath) {
|
|
return path.relative(repoRoot, filePath).replaceAll('\\', '/')
|
|
}
|
|
|
|
function assertNonEmptyString(value, fieldName, scopeId) {
|
|
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
throw new Error('Invalid ' + fieldName + ' for ' + scopeId + ': expected non-empty string.')
|
|
}
|
|
}
|
|
|
|
function assertNonEmptyArray(value, fieldName, scopeId) {
|
|
if (!Array.isArray(value) || value.length === 0) {
|
|
throw new Error('Invalid ' + fieldName + ' for ' + scopeId + ': expected non-empty array.')
|
|
}
|
|
}
|
|
|
|
function markdownList(items) {
|
|
return items.map((item) => '- ' + item).join('\n')
|
|
}
|
|
|
|
function extractTitles(source) {
|
|
const describeTitles = []
|
|
const testTitles = []
|
|
|
|
const describePattern = /test\.describe\(\s*(['"])(.*?)\1/g
|
|
for (const match of source.matchAll(describePattern)) {
|
|
describeTitles.push(match[2])
|
|
}
|
|
|
|
const testPattern = /(^|[^A-Za-z0-9_])test\(\s*(['"])(.*?)\2/gm
|
|
for (const match of source.matchAll(testPattern)) {
|
|
testTitles.push(match[3])
|
|
}
|
|
|
|
return { describeTitles, testTitles }
|
|
}
|
|
|
|
function formatSpecsMarkdown(config, rows) {
|
|
const sections = rows.map((row) => {
|
|
const checks = [
|
|
'Category: ' + row.category,
|
|
'Test File: ' + row.test_file,
|
|
'Describe Block: ' + row.describe_title,
|
|
'Scenario Title: ' + row.test_title,
|
|
'Focus: ' + row.focus,
|
|
'Validation Status: ' + row.validation.status.toUpperCase()
|
|
]
|
|
|
|
return [
|
|
'## ' + row.title,
|
|
'',
|
|
'- Scenario ID: ' + row.id,
|
|
markdownList(checks),
|
|
''
|
|
].join('\n')
|
|
}).join('\n')
|
|
|
|
return [
|
|
'# Cross-Corpus Critical E2E Workflow Specs',
|
|
'',
|
|
'Updated: ' + config.updated_at,
|
|
'Source of truth: ' + relativeRepoPath(configPath),
|
|
'',
|
|
'This document defines the required critical E2E workflow scenarios for the 8.0 polish-track runboard remediation surface and links each requirement to an explicit Playwright scenario title.',
|
|
'',
|
|
sections
|
|
].join('\n')
|
|
}
|
|
|
|
function formatCoverageMarkdown(report) {
|
|
const rows = report.scenarios.map((scenario) =>
|
|
'| ' + scenario.id + ' | ' + scenario.title + ' | ' + scenario.test_file + ' | ' + scenario.validation.status.toUpperCase() + ' |'
|
|
).join('\n')
|
|
|
|
const categoryRows = Object.entries(report.summary.scenarios_by_category)
|
|
.sort(([a], [b]) => a.localeCompare(b))
|
|
.map(([category, count]) => '- ' + category + ': ' + count)
|
|
.join('\n')
|
|
|
|
const missingRows = report.summary.missing_scenarios.length > 0
|
|
? report.summary.missing_scenarios.map((entry) => '- ' + entry.id + ': ' + entry.reason).join('\n')
|
|
: '- none'
|
|
|
|
return [
|
|
'# Corpus Critical E2E Workflow Coverage',
|
|
'',
|
|
'Generated: ' + report.generated_at,
|
|
'Status: **' + report.summary.status.toUpperCase() + '**',
|
|
'',
|
|
'## Coverage Summary',
|
|
'',
|
|
'- Scenarios: ' + report.summary.total_scenarios,
|
|
'- Validated scenarios: ' + report.summary.validated_scenarios,
|
|
'- Unique test files: ' + report.summary.unique_test_files,
|
|
'',
|
|
'## Scenarios by Category',
|
|
'',
|
|
(categoryRows || '- none'),
|
|
'',
|
|
'## Missing or Invalid Scenarios',
|
|
'',
|
|
missingRows,
|
|
'',
|
|
'## Scenario Validation',
|
|
'',
|
|
'| Scenario ID | Title | Test File | Status |',
|
|
'|---|---|---|---|',
|
|
(rows || '| - | - | - | - |'),
|
|
'',
|
|
'## Evidence Files',
|
|
'',
|
|
'- Spec doc: ' + relativeRepoPath(docsSpecPath),
|
|
'- Coverage JSON: ' + relativeRepoPath(evidenceJsonPath),
|
|
'- Coverage Markdown: ' + relativeRepoPath(evidenceMdPath),
|
|
''
|
|
].join('\n')
|
|
}
|
|
|
|
async function main() {
|
|
const config = JSON.parse(await fs.readFile(configPath, 'utf8'))
|
|
assertNonEmptyArray(config.scenarios, 'scenarios', 'e2e-critical-workflows')
|
|
|
|
await fs.mkdir(evidenceDir, { recursive: true })
|
|
await fs.mkdir(path.dirname(docsSpecPath), { recursive: true })
|
|
|
|
const seenIds = new Set()
|
|
const fileCache = new Map()
|
|
const missingScenarios = []
|
|
const scenariosByCategory = {}
|
|
const uniqueFiles = new Set()
|
|
const validated = []
|
|
|
|
for (const scenario of config.scenarios) {
|
|
assertNonEmptyString(scenario.id, 'scenario.id', 'scenario')
|
|
if (seenIds.has(scenario.id)) throw new Error('Duplicate scenario id: ' + scenario.id)
|
|
seenIds.add(scenario.id)
|
|
|
|
assertNonEmptyString(scenario.title, 'scenario.title', scenario.id)
|
|
assertNonEmptyString(scenario.category, 'scenario.category', scenario.id)
|
|
assertNonEmptyString(scenario.test_file, 'scenario.test_file', scenario.id)
|
|
assertNonEmptyString(scenario.describe_title, 'scenario.describe_title', scenario.id)
|
|
assertNonEmptyString(scenario.test_title, 'scenario.test_title', scenario.id)
|
|
assertNonEmptyString(scenario.focus, 'scenario.focus', scenario.id)
|
|
|
|
scenariosByCategory[scenario.category] = (scenariosByCategory[scenario.category] || 0) + 1
|
|
uniqueFiles.add(scenario.test_file)
|
|
|
|
let fileStatus = fileCache.get(scenario.test_file)
|
|
if (!fileStatus) {
|
|
const absolute = path.join(repoRoot, scenario.test_file)
|
|
try {
|
|
const source = await fs.readFile(absolute, 'utf8')
|
|
fileStatus = {
|
|
exists: true,
|
|
titles: extractTitles(source)
|
|
}
|
|
} catch {
|
|
fileStatus = { exists: false, titles: { describeTitles: [], testTitles: [] } }
|
|
}
|
|
fileCache.set(scenario.test_file, fileStatus)
|
|
}
|
|
|
|
const describeMatched =
|
|
fileStatus.exists && fileStatus.titles.describeTitles.includes(scenario.describe_title)
|
|
const testMatched =
|
|
fileStatus.exists && fileStatus.titles.testTitles.includes(scenario.test_title)
|
|
|
|
const reasons = []
|
|
if (!fileStatus.exists) reasons.push('missing test file')
|
|
if (fileStatus.exists && !describeMatched) reasons.push('missing describe title')
|
|
if (fileStatus.exists && !testMatched) reasons.push('missing test title')
|
|
|
|
const status = reasons.length === 0 ? 'pass' : 'fail'
|
|
if (status !== 'pass') {
|
|
missingScenarios.push({ id: scenario.id, reason: reasons.join(', ') })
|
|
}
|
|
|
|
validated.push({
|
|
...scenario,
|
|
validation: {
|
|
status,
|
|
file_exists: fileStatus.exists,
|
|
describe_title_found: describeMatched,
|
|
test_title_found: testMatched
|
|
}
|
|
})
|
|
}
|
|
|
|
const validatedCount = validated.filter((scenario) => scenario.validation.status === 'pass').length
|
|
const status = validatedCount === validated.length ? 'pass' : 'fail'
|
|
|
|
const report = {
|
|
schema_version: 1,
|
|
generated_at: new Date().toISOString(),
|
|
source: relativeRepoPath(configPath),
|
|
scenarios: validated,
|
|
summary: {
|
|
status,
|
|
total_scenarios: validated.length,
|
|
validated_scenarios: validatedCount,
|
|
unique_test_files: uniqueFiles.size,
|
|
scenarios_by_category: scenariosByCategory,
|
|
missing_scenarios: missingScenarios
|
|
}
|
|
}
|
|
|
|
const specsMarkdown = formatSpecsMarkdown(config, validated)
|
|
const coverageMarkdown = formatCoverageMarkdown(report)
|
|
|
|
await fs.writeFile(docsSpecPath, specsMarkdown + '\n', 'utf8')
|
|
await fs.writeFile(evidenceJsonPath, JSON.stringify(report, null, 2) + '\n', 'utf8')
|
|
await fs.writeFile(evidenceMdPath, coverageMarkdown + '\n', 'utf8')
|
|
|
|
console.log('[e2e-critical-workflows] ' + status.toUpperCase() + ' (' + report.summary.total_scenarios + ' scenarios)')
|
|
console.log('[e2e-critical-workflows] Specs: ' + relativeRepoPath(docsSpecPath))
|
|
console.log('[e2e-critical-workflows] Coverage JSON: ' + relativeRepoPath(evidenceJsonPath))
|
|
console.log('[e2e-critical-workflows] Coverage MD: ' + relativeRepoPath(evidenceMdPath))
|
|
|
|
if (status !== 'pass') process.exitCode = 1
|
|
}
|
|
|
|
main().catch((error) => {
|
|
console.error('[e2e-critical-workflows] ' + error.message)
|
|
process.exitCode = 1
|
|
})
|