Add Prettier and ESLint tooling (Refs #77)

This commit is contained in:
2026-10-09 15:56:13 -04:00
parent 8ed059b8d3
commit 16469e2d43
711 changed files with 99737 additions and 28286 deletions
+282 -56
View File
@@ -1,6 +1,16 @@
export const requirementKinds = ['round', 'matches', 'populated-minutes', 'player-hours', 'population',
'sustained-population', 'healthy-minutes', 'failure-budget', 'recovery', 'manual'] as const;
export type RequirementKind = typeof requirementKinds[number];
export const requirementKinds = [
'round',
'matches',
'populated-minutes',
'player-hours',
'population',
'sustained-population',
'healthy-minutes',
'failure-budget',
'recovery',
'manual'
] as const;
export type RequirementKind = (typeof requirementKinds)[number];
export interface BetaRequirement {
id: string;
kind: RequirementKind;
@@ -21,90 +31,306 @@ export interface BetaPlan {
waveSize: number; // 0 deploys all selected servers; otherwise pass each wave before expanding.
}
export const requirementLabels: Record<RequirementKind, string> = {
round: 'Reach round', matches: 'Complete qualifying matches', 'populated-minutes': 'Populated minutes',
'player-hours': 'Player-hours', population: 'Reach player count', 'sustained-population': 'Sustained population (minutes)',
'healthy-minutes': 'Continuous healthy minutes', 'failure-budget': 'Maximum health outages',
recovery: 'Successful recovery cycles', manual: 'Tester checklist'
round: 'Reach round',
matches: 'Complete qualifying matches',
'populated-minutes': 'Populated minutes',
'player-hours': 'Player-hours',
population: 'Reach player count',
'sustained-population': 'Sustained population (minutes)',
'healthy-minutes': 'Continuous healthy minutes',
'failure-budget': 'Maximum health outages',
recovery: 'Successful recovery cycles',
manual: 'Tester checklist'
};
export function requirement(kind: RequirementKind, id: string): BetaRequirement {
return { id, kind, label: requirementLabels[kind], target: kind === 'round' ? 25 : kind === 'failure-budget' ? 0 : 1,
minRound: 0, minPlayers: 1, minMinutes: 0 };
return {
id,
kind,
label: requirementLabels[kind],
target: kind === 'round' ? 25 : kind === 'failure-budget' ? 0 : 1,
minRound: 0,
minPlayers: 1,
minMinutes: 0
};
}
export function defaultPlan(): BetaPlan {
return { version: 1, requirements: [requirement('healthy-minutes', 'health')], scope: 'all', quorum: 1,
timeoutHours: 24, failureAction: 'fail', rollbackRestart: 'when-empty', waveSize: 0 };
return {
version: 1,
requirements: [requirement('healthy-minutes', 'health')],
scope: 'all',
quorum: 1,
timeoutHours: 24,
failureAction: 'fail',
rollbackRestart: 'when-empty',
waveSize: 0
};
}
export const builtInPlans: Array<{ name: string; plan: BetaPlan }> = [
{ name: 'Quick smoke test', plan: defaultPlan() },
{ name: 'Gameplay validation', plan: { ...defaultPlan(), requirements: [
{ ...requirement('matches', 'matches'), target: 2, minRound: 25 },
{ ...requirement('populated-minutes', 'playtime'), target: 90 }, requirement('failure-budget', 'outages')
] } },
{ name: 'Long soak', plan: { ...defaultPlan(), timeoutHours: 72, requirements: [
{ ...requirement('healthy-minutes', 'health'), target: 360 },
{ ...requirement('player-hours', 'participation'), target: 10 }, requirement('failure-budget', 'outages')
] } }
{
name: 'Gameplay validation',
plan: {
...defaultPlan(),
requirements: [
{ ...requirement('matches', 'matches'), target: 2, minRound: 25 },
{ ...requirement('populated-minutes', 'playtime'), target: 90 },
requirement('failure-budget', 'outages')
]
}
},
{
name: 'Long soak',
plan: {
...defaultPlan(),
timeoutHours: 72,
requirements: [
{ ...requirement('healthy-minutes', 'health'), target: 360 },
{ ...requirement('player-hours', 'participation'), target: 10 },
requirement('failure-budget', 'outages')
]
}
}
];
/** Strict allow-list validation at every boundary (including stored presets). */
export function parseBetaPlan(value: unknown): BetaPlan {
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('Test plan must be an object');
if (!value || typeof value !== 'object' || Array.isArray(value))
throw new Error('Test plan must be an object');
const p = value as BetaPlan;
if (p.version !== 1 || !['all', 'quorum', 'map', 'game', 'map-game'].includes(p.scope)) throw new Error('Invalid test plan version or scope');
if (!['fail', 'pause', 'rollback'].includes(p.failureAction) || !['manual', 'when-empty'].includes(p.rollbackRestart)) throw new Error('Invalid failure policy');
for (const [key, min, max] of [['quorum', 1, 1000], ['timeoutHours', 0.01, 8760], ['waveSize', 0, 1000]] as const) {
if (typeof p[key] !== 'number' || !Number.isFinite(p[key]) || p[key] < min || p[key] > max) throw new Error(`Invalid ${key}`);
if (p.version !== 1 || !['all', 'quorum', 'map', 'game', 'map-game'].includes(p.scope))
throw new Error('Invalid test plan version or scope');
if (
!['fail', 'pause', 'rollback'].includes(p.failureAction) ||
!['manual', 'when-empty'].includes(p.rollbackRestart)
)
throw new Error('Invalid failure policy');
for (const [key, min, max] of [
['quorum', 1, 1000],
['timeoutHours', 0.01, 8760],
['waveSize', 0, 1000]
] as const) {
if (typeof p[key] !== 'number' || !Number.isFinite(p[key]) || p[key] < min || p[key] > max)
throw new Error(`Invalid ${key}`);
}
if (!Number.isInteger(p.quorum) || !Number.isInteger(p.waveSize)) throw new Error('Quorum and wave size must be integers');
if (!Array.isArray(p.requirements) || !p.requirements.length || p.requirements.length > 30) throw new Error('Choose 1–30 requirements');
if (!Number.isInteger(p.quorum) || !Number.isInteger(p.waveSize))
throw new Error('Quorum and wave size must be integers');
if (!Array.isArray(p.requirements) || !p.requirements.length || p.requirements.length > 30)
throw new Error('Choose 1–30 requirements');
const ids = new Set<string>();
const requirements = p.requirements.map(r => {
if (!r || typeof r !== 'object' || !requirementKinds.includes(r.kind)) throw new Error('Unknown requirement');
if (typeof r.id !== 'string' || !/^[a-zA-Z0-9_-]{1,64}$/.test(r.id) || r.id in Object.prototype || ids.has(r.id)) throw new Error('Requirement IDs must be unique, non-reserved identifiers');
const requirements = p.requirements.map((r) => {
if (!r || typeof r !== 'object' || !requirementKinds.includes(r.kind))
throw new Error('Unknown requirement');
if (
typeof r.id !== 'string' ||
!/^[a-zA-Z0-9_-]{1,64}$/.test(r.id) ||
r.id in Object.prototype ||
ids.has(r.id)
)
throw new Error('Requirement IDs must be unique, non-reserved identifiers');
ids.add(r.id);
if (typeof r.label !== 'string' || !r.label.trim() || r.label.length > 200) throw new Error('Requirement needs a label (up to 200 characters)');
if (typeof r.label !== 'string' || !r.label.trim() || r.label.length > 200)
throw new Error('Requirement needs a label (up to 200 characters)');
for (const key of ['target', 'minRound', 'minPlayers', 'minMinutes'] as const) {
if (typeof r[key] !== 'number' || !Number.isFinite(r[key]) || r[key] < 0 || r[key] > 1_000_000) throw new Error(`Invalid requirement ${key}`);
if (
typeof r[key] !== 'number' ||
!Number.isFinite(r[key]) ||
r[key] < 0 ||
r[key] > 1_000_000
)
throw new Error(`Invalid requirement ${key}`);
}
if (r.kind !== 'failure-budget' && r.target <= 0) throw new Error('Requirement target must be positive');
if (['round', 'matches', 'population', 'failure-budget', 'recovery', 'manual'].includes(r.kind) && !Number.isInteger(r.target)) throw new Error('Count targets must be integers');
if (!Number.isInteger(r.minRound) || !Number.isInteger(r.minPlayers)) throw new Error('Round and player qualifiers must be integers');
if (r.kind === 'manual' && r.target !== 1) throw new Error('Add a separate requirement for each checklist item');
return { id: r.id, kind: r.kind, label: r.label.trim(), target: r.target, minRound: r.minRound,
minPlayers: r.minPlayers, minMinutes: r.minMinutes };
if (r.kind !== 'failure-budget' && r.target <= 0)
throw new Error('Requirement target must be positive');
if (
['round', 'matches', 'population', 'failure-budget', 'recovery', 'manual'].includes(
r.kind
) &&
!Number.isInteger(r.target)
)
throw new Error('Count targets must be integers');
if (!Number.isInteger(r.minRound) || !Number.isInteger(r.minPlayers))
throw new Error('Round and player qualifiers must be integers');
if (r.kind === 'manual' && r.target !== 1)
throw new Error('Add a separate requirement for each checklist item');
return {
id: r.id,
kind: r.kind,
label: r.label.trim(),
target: r.target,
minRound: r.minRound,
minPlayers: r.minPlayers,
minMinutes: r.minMinutes
};
});
return { version: 1, requirements, scope: p.scope, quorum: p.quorum, timeoutHours: p.timeoutHours,
failureAction: p.failureAction, rollbackRestart: p.rollbackRestart, waveSize: p.waveSize };
return {
version: 1,
requirements,
scope: p.scope,
quorum: p.quorum,
timeoutHours: p.timeoutHours,
failureAction: p.failureAction,
rollbackRestart: p.rollbackRestart,
waveSize: p.waveSize
};
}
export interface RequirementProgress { id: string; label: string; current: number; target: number; passed: boolean; failed: boolean; detail: string }
export interface RequirementProgress {
id: string;
label: string;
current: number;
target: number;
passed: boolean;
failed: boolean;
detail: string;
}
export interface BetaTestView {
plan: BetaPlan;
outcome: string;
reason: string | null;
servers: Array<{ server: string; server_deleted_at: string | null; phase: number; passed: boolean; progress: RequirementProgress[]; observingAt?: string | null;
observation?: { lastGameplayAt: string | null; lastEvent: string | null; players: number | null; round: number | null; matchStarted: boolean; gameEnded: boolean; healthy: boolean | null; healthCheckedAt: string | null } }>;
evidence: Array<{ id: number; server: string; server_deleted_at: string | null; received_at: string; kind: string; payload: string }>;
servers: Array<{
server: string;
server_deleted_at: string | null;
phase: number;
passed: boolean;
progress: RequirementProgress[];
observingAt?: string | null;
observation?: {
lastGameplayAt: string | null;
lastEvent: string | null;
players: number | null;
round: number | null;
matchStarted: boolean;
gameEnded: boolean;
healthy: boolean | null;
healthCheckedAt: string | null;
};
}>;
evidence: Array<{
id: number;
server: string;
server_deleted_at: string | null;
received_at: string;
kind: string;
payload: string;
}>;
}
export interface BetaPlanIssue { field: string; message: string; requirementId?: string }
export interface BetaPlanIssue {
field: string;
message: string;
requirementId?: string;
}
export function betaPlanIssues(plan: BetaPlan, serverCount?: number): BetaPlanIssue[] {
const issues: BetaPlanIssue[] = [];
const check = (field: string, value: number, min: number, max: number, integer: boolean, message: string, requirementId?: string) => {
if (typeof value !== 'number' || !Number.isFinite(value) || value < min || value > max || (integer && !Number.isInteger(value))) issues.push({ field, message, requirementId });
const check = (
field: string,
value: number,
min: number,
max: number,
integer: boolean,
message: string,
requirementId?: string
) => {
if (
typeof value !== 'number' ||
!Number.isFinite(value) ||
value < min ||
value > max ||
(integer && !Number.isInteger(value))
)
issues.push({ field, message, requirementId });
};
check('timeoutHours', plan.timeoutHours, .01, 8760, false, 'Enter a testing window between 0.01 and 8,760 hours.');
check('waveSize', plan.waveSize, 0, 1000, true, 'Enter a whole number from 0 to 1,000; 0 deploys to all selected servers.');
if (plan.scope === 'quorum') check('quorum', plan.quorum, 1, serverCount ?? 1000, true, `Choose between 1 and ${serverCount ?? 1000} qualifying servers.`);
if (!plan.requirements.length) issues.push({ field: 'requirements', message: 'Add at least one requirement to continue.' });
check(
'timeoutHours',
plan.timeoutHours,
0.01,
8760,
false,
'Enter a testing window between 0.01 and 8,760 hours.'
);
check(
'waveSize',
plan.waveSize,
0,
1000,
true,
'Enter a whole number from 0 to 1,000; 0 deploys to all selected servers.'
);
if (plan.scope === 'quorum')
check(
'quorum',
plan.quorum,
1,
serverCount ?? 1000,
true,
`Choose between 1 and ${serverCount ?? 1000} qualifying servers.`
);
if (!plan.requirements.length)
issues.push({
field: 'requirements',
message: 'Add at least one requirement to continue.'
});
for (const r of plan.requirements) {
if (!r.label?.trim()) issues.push({ field: `${r.id}-label`, message: 'Describe what this requirement checks.', requirementId: r.id });
const integral = ['round', 'matches', 'population', 'failure-budget', 'recovery', 'manual'].includes(r.kind);
check(`${r.id}-target`, r.target, r.kind === 'failure-budget' ? 0 : .000001, 1_000_000, integral, `Enter ${integral ? 'a whole number' : 'a number'} ${r.kind === 'failure-budget' ? 'of zero or more' : 'greater than zero'} for ${requirementLabels[r.kind].toLowerCase()}.`, r.id);
if (['round', 'matches', 'populated-minutes', 'player-hours', 'sustained-population'].includes(r.kind)) check(`${r.id}-minPlayers`, r.minPlayers, 1, 1_000_000, true, 'Enter at least one player, using a whole number.', r.id);
if (!r.label?.trim())
issues.push({
field: `${r.id}-label`,
message: 'Describe what this requirement checks.',
requirementId: r.id
});
const integral = [
'round',
'matches',
'population',
'failure-budget',
'recovery',
'manual'
].includes(r.kind);
check(
`${r.id}-target`,
r.target,
r.kind === 'failure-budget' ? 0 : 0.000001,
1_000_000,
integral,
`Enter ${integral ? 'a whole number' : 'a number'} ${r.kind === 'failure-budget' ? 'of zero or more' : 'greater than zero'} for ${requirementLabels[r.kind].toLowerCase()}.`,
r.id
);
if (
[
'round',
'matches',
'populated-minutes',
'player-hours',
'sustained-population'
].includes(r.kind)
)
check(
`${r.id}-minPlayers`,
r.minPlayers,
1,
1_000_000,
true,
'Enter at least one player, using a whole number.',
r.id
);
if (r.kind === 'matches') {
check(`${r.id}-minRound`, r.minRound, 0, 1_000_000, true, 'Enter a whole round number; 0 allows any round.', r.id);
check(`${r.id}-minMinutes`, r.minMinutes, 0, 1_000_000, false, 'Enter a duration of zero or more minutes.', r.id);
check(
`${r.id}-minRound`,
r.minRound,
0,
1_000_000,
true,
'Enter a whole round number; 0 allows any round.',
r.id
);
check(
`${r.id}-minMinutes`,
r.minMinutes,
0,
1_000_000,
false,
'Enter a duration of zero or more minutes.',
r.id
);
}
}
return issues;