fix(rust): lowercase the reward option-source ids, and test the grammar
All checks were successful
PR Checks / client-build (pull_request) Successful in 20s
PR Checks / frozen-manifest (pull_request) Successful in 50s
PR Checks / server-tests (pull_request) Successful in 7m45s

The first boot against real core refused the whole module at register:
`rust.options.runZones` fails core's EVENT_ID grammar, which is lowercase
dotted segments only. The fake api validates none of it, so 310 green
tests said nothing. The four fixed-choice sources and the chat-server
source are renamed, and entry.test now holds every action, budget,
lease and option-source id against a copy of the grammar.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
2026-09-24 07:47:01 -05:00
parent cc185db26b
commit 42029734ad
2 changed files with 28 additions and 10 deletions

View File

@@ -293,11 +293,11 @@ const participationOpen = {
params: [
{ name: 'server', type: 'string', required: true, example: 'main', source: 'rust.options.servers',
description: 'Which server counts.' },
{ name: 'zone', type: 'string', required: false, example: 'Airfield brawl', source: 'rust.options.runZones',
{ name: 'zone', type: 'string', required: false, example: 'Airfield brawl', source: 'rust.options.runzones',
description: 'The name an earlier "Open a zone" step of this run gave its zone. Left blank, the whole server counts (D100).' },
{ name: 'score', type: 'string', required: true, example: 'both', source: 'rust.options.scoreModes',
{ name: 'score', type: 'string', required: true, example: 'both', source: 'rust.options.scoremodes',
description: 'What earns a place: seconds present, kills, or both.' },
{ name: 'killsOf', type: 'string', required: false, example: 'npcs', source: 'rust.options.killsOf',
{ name: 'killsOf', type: 'string', required: false, example: 'npcs', source: 'rust.options.killsof',
description: 'Whose deaths count as a kill: players, NPCs (animals included), or both. The last hit gets it. Needed unless the score is seconds.' },
{ name: 'killWeight', type: 'float', required: false, example: DEFAULT_KILL_WEIGHT,
description: `For a score of both: how many minutes one kill is worth. Left blank, ${DEFAULT_KILL_WEIGHT}.` },
@@ -477,7 +477,7 @@ const kitEntitle = {
params: [
{ name: 'kit', type: 'string', required: true, example: 'main/vip-starter', source: 'rust.options.kits',
description: 'The kit, as server/kit. The reward reaches only that server (D102).' },
{ name: 'recipients', type: 'string', required: true, example: 'top', source: 'rust.options.recipientModes',
{ name: 'recipients', type: 'string', required: true, example: 'top', source: 'rust.options.recipientmodes',
description: 'Who gets it: everyone who scored, the top N, a score of at least X, N drawn at random, or the top X per cent.' },
{ name: 'count', type: 'float', required: false, example: 3,
description: 'N for top and random, X for a minimum score, the percentage for top per cent. Not used for everyone.' },
@@ -641,7 +641,7 @@ const announce = {
version: 1,
budgetMs: BUDGET_MS,
params: [
{ name: 'server', type: 'string', required: true, example: 'main', source: 'rust.options.chatServers',
{ name: 'server', type: 'string', required: true, example: 'main', source: 'rust.options.chatservers',
description: 'Which server, or * for every server (D105).' },
{ name: 'message', type: 'string', required: true, example: 'The airfield brawl starts in five minutes!',
description: `The line, up to ${MAX_CHAT} characters.` },
@@ -805,18 +805,18 @@ const OPTION_SOURCES = [
// Free text: the zones a run will open do not exist when it is authored, and
// the name is checked when the step runs (D100). Declared so the field is
// documented rather than a bare box, and answers nothing.
fixed('rust.options.runZones', 'Zones this run opens', 'The name an earlier "Open a zone" step of the same run gave its zone. Type it; it is checked when the step runs.', []),
fixed('rust.options.scoreModes', 'Score', 'What earns a place in a tally.', [
fixed('rust.options.runzones', 'Zones this run opens', 'The name an earlier "Open a zone" step of the same run gave its zone. Type it; it is checked when the step runs.', []),
fixed('rust.options.scoremodes', 'Score', 'What earns a place in a tally.', [
{ value: 'seconds', label: 'Seconds present' },
{ value: 'kills', label: 'Kills' },
{ value: 'both', label: 'Both — minutes plus a weight per kill' },
]),
fixed('rust.options.killsOf', 'Kills of', 'Whose deaths count as a kill.', [
fixed('rust.options.killsof', 'Kills of', 'Whose deaths count as a kill.', [
{ value: 'players', label: 'Players' },
{ value: 'npcs', label: 'NPCs, animals included' },
{ value: 'both', label: 'Players and NPCs' },
]),
fixed('rust.options.recipientModes', 'Recipients', 'Who a reward goes to (D101).', [
fixed('rust.options.recipientmodes', 'Recipients', 'Who a reward goes to (D101).', [
{ value: 'everyone', label: 'Everyone who scored' },
{ value: 'top', label: 'The top N (ties in)' },
{ value: 'minScore', label: 'A score of at least X' },
@@ -824,7 +824,7 @@ const OPTION_SOURCES = [
{ value: 'topPercent', label: 'The top X per cent (ties in)' },
]),
{
id: 'rust.options.chatServers',
id: 'rust.options.chatservers',
label: 'Chat servers',
description: 'Every enabled server, or * for all of them.',
async resolve() {

View File

@@ -203,6 +203,24 @@ test('the event verbs are registered, and every budget has a verb that spends it
}
})
test('every event id is one the core grammar accepts (phase 13b)', () => {
const { api } = register()
// Core's EVENT_ID (modules/registries.js), copied rather than imported: this
// module cannot reach core's tree. The fake api validates none of it, and
// phase 13b's first boot against real core refused the WHOLE module over one
// camelCase source id (`rust.options.runZones`). Lowercase dotted segments only.
const EVENT_ID = /^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$/
const ids = [
...api.record.eventActions.map((a) => a.id),
...api.record.eventBudgets.map((b) => b.id),
...api.record.eventOptionSources.map((s) => s.id),
...api.record.eventLeases.map((l) => l.id),
]
for (const a of api.record.eventActions) for (const p of a.params) if (p.source) ids.push(p.source)
for (const id of ids) assert.match(id, EVENT_ID, id)
})
test('the leases and their option sources are registered, every source a lease reads (phase 12)', () => {
const { api } = register()