openpetswithchatandmcp/website/scripts/check-e2e-critical-workflows.mjs
OpenPets Dev 65ac83849e Add 'website/' from commit '966aea480ffe1c340553aa878def30131d2af827'
git-subtree-dir: website
git-subtree-mainline: 8d3849caea
git-subtree-split: 966aea480f
2026-06-17 20:48:39 +00:00

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
})