Files
website/client/test/eventParamMultiple.test.js
wtclaude 743f1bc8b2
All checks were successful
PR Checks / bot-tests (pull_request) Successful in 47s
PR Checks / client-build (pull_request) Successful in 48s
PR Checks / server-tests (pull_request) Successful in 13m55s
feat(events): a param that holds several values (MODULE_API 1.14.0)
Rust PLAN_REDESIGNS §12.3 (D344): an event action param may declare
`multiple: true`, with an optional `max` (1-100, 20 when left out), on a
`string` or `int` param. The permission reward steps take several permissions
in one step through it.

- registries: the flag and `max` are checked at registration and served in the
  catalog; refused on other types, on a duration, on a template's inputs, and
  with a list as the example. A param that does not say it reads back exactly
  as declared, without the keys.
- spec: the value is a JSON array, each item the type's literal, duplicates
  dropped in order, at most `max`; a bare value is a list of one; `required`
  means at least one; an empty item is refused.
- authorize: a multiple param's example is priced as a list of one, since
  cost() is handed the array.
- editor: a multi-select. Chips above a grouped, filterable checklist for a
  source that answered; the typeahead for a searchable source; a tag box
  otherwise. Ticking past max is refused in words. A new step holds the
  example as a list of one.
- run console: params read as `name: value`, a list joined by commas.
- MODULE_API 1.13.0 -> 1.14.0 on both halves; engagement-triggers.json and the
  catalog's OpenAPI note regenerated.

Additive only: Module-uo (`^1.10.0`) declares no multiple param.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
2026-10-12 15:40:30 -05:00

95 lines
4.2 KiB
JavaScript

// A param that holds several values (MODULE_API 1.14.0; Rust PLAN_REDESIGNS
// §12.3): what the multi-select stores, when it gives way to JSON, and how the
// run console reads a list back.
import { test } from 'node:test'
import assert from 'node:assert/strict'
import {
blankStep,
paramsMode,
PARAM_FORM,
PARAM_JSON,
listValue,
setParamList,
addToList,
filterOptions,
missingParams,
describeParams,
} from '../src/lib/eventAuthoring.js'
const LEND = {
id: 'test.perm.lend',
params: [
{ name: 'server', type: 'string', required: true, example: 'main' },
{ name: 'perms', type: 'string', required: true, example: 'nightvision.allowed', multiple: true, max: 3 },
{ name: 'ids', type: 'int', required: false, example: 7, multiple: true, max: 20 },
],
}
const step = (params) => ({ actionId: LEND.id, paramsText: JSON.stringify(params) })
test('a new step holds the example as a list of one', () => {
const fresh = JSON.parse(blankStep(LEND).paramsText)
assert.deepEqual(fresh.perms, ['nightvision.allowed'])
assert.deepEqual(fresh.ids, [7])
assert.equal(fresh.server, 'main')
})
test('a list is a value the form can hold; a structure in it is not', () => {
assert.equal(paramsMode(step({ perms: ['a', 'b'] }), LEND).mode, PARAM_FORM)
assert.equal(paramsMode(step({ perms: 'a' }), LEND).mode, PARAM_FORM)
const nested = paramsMode(step({ perms: [{ a: 1 }] }), LEND)
assert.equal(nested.mode, PARAM_JSON)
assert.match(nested.reason, /"perms" holds a structure/)
// A list against a single-value param still forces JSON, as before 1.14.0.
assert.equal(paramsMode(step({ server: ['main'] }), LEND).mode, PARAM_JSON)
})
test('listValue reads a bare value as a list of one, and nothing as none', () => {
assert.deepEqual(listValue(['a', 'b']), ['a', 'b'])
assert.deepEqual(listValue('a'), ['a'])
for (const empty of [undefined, null, '']) assert.deepEqual(listValue(empty), [])
})
test('setParamList coerces, drops duplicates in order, and removes an empty list', () => {
const s = step({ server: 'main' })
assert.deepEqual(JSON.parse(setParamList(s, 'ids', ['3', '1', '3'], 'int')).ids, [3, 1])
assert.deepEqual(JSON.parse(setParamList(s, 'perms', ['b', 'a', 'b', ''], 'string')).perms, ['b', 'a'])
const cleared = JSON.parse(setParamList(step({ server: 'main', perms: ['a'] }), 'perms', [], 'string'))
assert.equal('perms' in cleared, false)
// A box that does not parse is handed back untouched.
assert.equal(setParamList({ paramsText: '{nope' }, 'perms', ['a'], 'string'), '{nope')
})
test('addToList ignores blanks and repeats, and refuses past max in words', () => {
assert.deepEqual(addToList(['a'], ' b ', 3).list, ['a', 'b'])
assert.deepEqual(addToList(['a'], 'a', 3).list, ['a'])
assert.deepEqual(addToList(['a'], ' ', 3).list, ['a'])
assert.match(addToList(['a', 'b', 'c'], 'd', 3).refused, /at most 3/)
})
test('filterOptions matches value, label or group and keeps the source order', () => {
const options = [
{ value: 'nightvision.allowed', label: 'nightvision.allowed', group: 'NightVision' },
{ value: 'betterchat.color', label: 'betterchat.color', group: 'BetterChat' },
{ value: 'kits.vip', label: 'kits.vip', group: 'Kits' },
]
assert.equal(filterOptions(options, '').length, 3)
assert.deepEqual(filterOptions(options, 'NIGHT').map((o) => o.value), ['nightvision.allowed'])
assert.deepEqual(filterOptions(options, 'chat').map((o) => o.value), ['betterchat.color'])
assert.deepEqual(filterOptions(options, 'i').map((o) => o.value), ['nightvision.allowed', 'kits.vip'])
})
test('an empty required list is missing', () => {
assert.deepEqual(missingParams(step({ server: 'main', perms: [] }), LEND), ['perms'])
assert.deepEqual(missingParams(step({ server: 'main' }), LEND), ['perms'])
assert.deepEqual(missingParams(step({ server: 'main', perms: ['a'] }), LEND), [])
})
test('the run console shows a list as its values joined by commas', () => {
assert.equal(
describeParams({ server: 'main', perms: ['nightvision.allowed', 'betterchat.color'], confirm: true }),
'server: main · perms: nightvision.allowed, betterchat.color · confirm: true',
)
assert.equal(describeParams({ where: { a: 1 } }), 'where: {"a":1}')
assert.equal(describeParams(null), '')
})