158 lines
3.5 KiB
JavaScript
158 lines
3.5 KiB
JavaScript
|
|
import fs from 'node:fs/promises';
|
|||
|
|
import path from 'node:path';
|
|||
|
|
|
|||
|
|
export const SCENARIO_GROUP_COUNTS = Object.freeze({
|
|||
|
|
REL: 11,
|
|||
|
|
AUTH: 8,
|
|||
|
|
CHAT: 15,
|
|||
|
|
MEM: 16,
|
|||
|
|
FILE: 8,
|
|||
|
|
IMGPG: 8,
|
|||
|
|
XLS: 12,
|
|||
|
|
PAGE: 13,
|
|||
|
|
DATA: 13,
|
|||
|
|
SEARCH: 8,
|
|||
|
|
AGENT: 11,
|
|||
|
|
SCHED: 5,
|
|||
|
|
WX: 13,
|
|||
|
|
BILL: 7,
|
|||
|
|
MS: 9,
|
|||
|
|
PLAZA: 5,
|
|||
|
|
CFG: 8,
|
|||
|
|
UI: 8,
|
|||
|
|
COMP: 9,
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
export const SCENARIO_MODE_BY_GROUP = Object.freeze({
|
|||
|
|
REL: 'upgrade',
|
|||
|
|
AUTH: 'scenarios',
|
|||
|
|
CHAT: 'scenarios',
|
|||
|
|
MEM: 'scenarios',
|
|||
|
|
FILE: 'scenarios',
|
|||
|
|
IMGPG: 'scenarios',
|
|||
|
|
XLS: 'scenarios',
|
|||
|
|
PAGE: 'scenarios',
|
|||
|
|
DATA: 'scenarios',
|
|||
|
|
SEARCH: 'providers',
|
|||
|
|
AGENT: 'scenarios',
|
|||
|
|
SCHED: 'scenarios',
|
|||
|
|
WX: 'scenarios',
|
|||
|
|
BILL: 'scenarios',
|
|||
|
|
MS: 'scenarios',
|
|||
|
|
PLAZA: 'scenarios',
|
|||
|
|
CFG: 'deterministic',
|
|||
|
|
UI: 'browser',
|
|||
|
|
COMP: 'upgrade',
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const ALWAYS_REQUIRED = new Set([
|
|||
|
|
...range('REL', 1, 11),
|
|||
|
|
'AUTH-01',
|
|||
|
|
'AUTH-04',
|
|||
|
|
'AUTH-05',
|
|||
|
|
'AUTH-06',
|
|||
|
|
'CHAT-01',
|
|||
|
|
'CHAT-02',
|
|||
|
|
'CHAT-06',
|
|||
|
|
'CHAT-07',
|
|||
|
|
'CHAT-08',
|
|||
|
|
'CHAT-09',
|
|||
|
|
'CHAT-11',
|
|||
|
|
'CHAT-12',
|
|||
|
|
'CHAT-13',
|
|||
|
|
'CHAT-15',
|
|||
|
|
'MEM-08',
|
|||
|
|
'MEM-13',
|
|||
|
|
'MEM-14',
|
|||
|
|
'MEM-15',
|
|||
|
|
'MEM-16',
|
|||
|
|
'FILE-07',
|
|||
|
|
'PAGE-08',
|
|||
|
|
'PAGE-09',
|
|||
|
|
'PAGE-10',
|
|||
|
|
'PAGE-12',
|
|||
|
|
'DATA-06',
|
|||
|
|
'DATA-10',
|
|||
|
|
'DATA-12',
|
|||
|
|
'AGENT-10',
|
|||
|
|
'AGENT-11',
|
|||
|
|
...range('WX', 9, 13),
|
|||
|
|
'BILL-02',
|
|||
|
|
'BILL-03',
|
|||
|
|
'BILL-04',
|
|||
|
|
'MS-01',
|
|||
|
|
'MS-03',
|
|||
|
|
'MS-04',
|
|||
|
|
'CFG-08',
|
|||
|
|
...range('COMP', 1, 4),
|
|||
|
|
'COMP-08',
|
|||
|
|
'COMP-09',
|
|||
|
|
]);
|
|||
|
|
|
|||
|
|
function range(prefix, start, end) {
|
|||
|
|
return Array.from(
|
|||
|
|
{ length: end - start + 1 },
|
|||
|
|
(_, index) => `${prefix}-${String(start + index).padStart(2, '0')}`,
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function expectedScenarioIds() {
|
|||
|
|
return Object.entries(SCENARIO_GROUP_COUNTS).flatMap(
|
|||
|
|
([prefix, count]) => range(prefix, 1, count),
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function isScenarioExemptable(id) {
|
|||
|
|
return !ALWAYS_REQUIRED.has(id);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function validateScenarioCatalog(catalog) {
|
|||
|
|
const expected = expectedScenarioIds();
|
|||
|
|
const expectedSet = new Set(expected);
|
|||
|
|
const ids = catalog.map((scenario) => scenario.id);
|
|||
|
|
const unique = new Set(ids);
|
|||
|
|
const missing = expected.filter((id) => !unique.has(id));
|
|||
|
|
const unexpected = ids.filter((id) => !expectedSet.has(id));
|
|||
|
|
const duplicates = ids.filter((id, index) => ids.indexOf(id) !== index);
|
|||
|
|
|
|||
|
|
if (catalog.length !== expected.length || unique.size !== expected.length) {
|
|||
|
|
throw new Error(
|
|||
|
|
`Release gate catalog must contain exactly ${expected.length} unique scenarios `
|
|||
|
|
+ `(found ${catalog.length}/${unique.size})`,
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
if (missing.length || unexpected.length || duplicates.length) {
|
|||
|
|
throw new Error(
|
|||
|
|
`Release gate catalog mismatch: missing=${missing.join(',') || '-'} `
|
|||
|
|
+ `unexpected=${unexpected.join(',') || '-'} duplicates=${[...new Set(duplicates)].join(',') || '-'}`,
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
for (const scenario of catalog) {
|
|||
|
|
if (!scenario.name || !scenario.group || !scenario.mode) {
|
|||
|
|
throw new Error(`Scenario ${scenario.id} is missing name, group, or mode`);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return catalog;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export async function loadScenarioCatalog({
|
|||
|
|
root = path.resolve(new URL('..', import.meta.url).pathname),
|
|||
|
|
guardianPath = path.join(root, 'docs', 'production-release-guardian.md'),
|
|||
|
|
} = {}) {
|
|||
|
|
const source = await fs.readFile(guardianPath, 'utf8');
|
|||
|
|
const catalog = [];
|
|||
|
|
const scenarioPattern = /^- \*\*([A-Z]+-\d{2})\*\*:(.+)$/gm;
|
|||
|
|
for (const match of source.matchAll(scenarioPattern)) {
|
|||
|
|
const id = match[1];
|
|||
|
|
const group = id.split('-')[0];
|
|||
|
|
catalog.push({
|
|||
|
|
id,
|
|||
|
|
name: match[2].trim(),
|
|||
|
|
group,
|
|||
|
|
mode: SCENARIO_MODE_BY_GROUP[group],
|
|||
|
|
exemptable: isScenarioExemptable(id),
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
return validateScenarioCatalog(catalog);
|
|||
|
|
}
|