check(JSON.stringify(out.updatedConfig)===JSON.stringify(want),'v1 config upgrades to the eight v2 fields (the rest dropped; learnLog alone does not turn debugLog on)')
check(oldDefault.updatedConfig.relayTimeout===3600&&out.updatedConfig.relayTimeout===90,'the old default idle timeout (180 s) becomes 3600 s; a custom value stays')
check(A('a11').actionId==='send_on'&&same(A('a11').options,{channel:v(8),state:v('toggle'),buss:v(24)}),'v1 mon_on (monMode auto) -> aux send_on on monSend')
check(A('a12').actionId==='send_level_set'&&A('a12').options.buss.value===24&&A('a13').actionId==='send_level_adjust'&&A('a13').options.delta.value===-2,'v1 mon_level / mon_nudge -> send_level_set / send_level_adjust on monSend')
check(A('a14').actionId==='raw_osc'&&A('a15').actionId==='snapshot_next'&&A('a17').actionId==='reload','ids that did not change stay; resend -> reload')
check(A('a16').actionId==='mute','an action whose option is an expression cannot be wrapped and is left as it is')
check(aux.updatedActions.every((a,i)=>a.id===actions[i].id&&a.controlId==='ctl')&&actions[0].actionId==='send_on'&&actions[0].options.send!==undefined,'ids and control ids are kept, and the input is not mutated')
check(F('f1').feedbackId==='send_on'&&same(F('f1').options,{channel:v(3),buss:v(24)})&&F('f1').style.bgcolor===1&&F('f1').isInverted.value===true,'v1 feedback send_on {send} -> {buss}, style and invert kept')
check(G('a11').actionId==='group_assign'&&same(G('a11').options,{channel:v(8),state:v('toggle'),buss:v(12)}),'v1 mon_on with monMode group -> group_assign on monSend')
check(G('a12').actionId==='mon_level'&&G('a13').actionId==='mon_nudge','v1 mon_level / mon_nudge have no group equivalent and stay as they are')
check(grp.updatedFeedbacks.find((f)=>f.id==='f5').feedbackId==='group_assigned'&&grp.updatedFeedbacks.find((f)=>f.id==='f6').feedbackId==='mon_level_above','v1 mon_on feedback with monMode group -> group_assigned; mon_level_above stays')
check(adjustedLevel(-10,1)===-9&&adjustedLevel(5,30)===LEVEL.max&&adjustedLevel(-150,1)===-60&&adjustedLevel(-150,-1)===LEVEL.min&&adjustedLevel(-89.5,1)===-88.5&&adjustedLevel(-88,-5)===LEVEL.min,'adjustedLevel clamps, restarts at -60 from -inf, drops to -inf below the floor')
check(fmtDb(-150)==='-inf'&&fmtDb(-3.04)==='-3'&&fmtDb(0)==='0'&&fmtDb(-0.04)==='0'&&fmtDb(2.25)==='2.3','fmtDb: -inf at the floor, one decimal, no -0')
check(fmtCue(100)==='1'&&fmtCue(250)==='2.5','fmtCue (raw cue number x 100)')
check(parse('/Input_Channels/7/Aux_Send/3/send_pre-post',[1])===null,'send_pre-post is not parsed (its meaning was never verified)')
check(parse('/Control_Groups/3/name',['KEYS']).n===3&&parse('/Snapshots/name',[3,400,0,'X']).cue===400&&parse('/Meters/values',[5001,10,5002,20]).pairs.length===2,'parse: control group name, snapshot name, meters')
check(near(faderPosition(-7.5),60)&&near(faderPosition(2.5),81.25)&&near(faderPosition(-15),47.5)&&near(faderPosition(-75),3.5),'faderPosition interpolates linearly between the anchors')
check(faderPosition(-90)===0&&faderPosition(-120)===0&&faderPosition(-150)===0&&near(faderPosition(-89.99),1,0.01),'at or below the -90 floor the position is 0, just above it about 1')
check(faderPosition(-150,-150)===0&&near(faderPosition(-149.99,-150),1,0.01)&&near(faderPosition(-60,-150),6)&&faderPosition(-100,-150)>1,'with a -150 floor the taper runs down to -150')
check(raw.length===72*t*BYTES_PER_PIXEL&&h.imageBufferPosition.height===t&&h.imageBufferPosition.y===58-t&&h.imageBufferEncoding.pixelFormat==='RGBA',`horizontal bar buffer is width x strip x 4 bytes (${raw.length} bytes)`)
check(Buffer.from(v.imageBuffer,'base64').length===thicknessFor(72)*72*BYTES_PER_PIXEL&&v.imageBufferPosition.x===72-thicknessFor(72),'vertical meter strip sits at the right edge with the right length')
// names arrive padded and wrapped: trimmed, whitespace runs collapsed, once, in the state
consts=newState()
check(cleanName(' LEAD V RESI \t')==='LEAD V RESI'&&cleanName('')===''&&cleanName(' ')===''&&cleanName(undefined)==='','cleanName trims and collapses whitespace runs')
s.applyMessage('/Matrix_Inputs/8/Channel_Input/name',['Dante In 63 '])
s.applyMessage('/Control_Groups/2/name',[' MC & SPEAKER'])
s.applyMessage('/Snapshots/name',[0,100,0,' Pre Service '])
s.applyMessage('/Console/Name',['S31 AB '])
s.loaded=true
check(s.rawName('input',3)==='MD RESI'&&e1.value==='MD RESI'&&e1.changed===true&&s.rawName('minput',8)==='Dante In 63'&&s.rawName('cg',2)==='MC & SPEAKER'&&s.snapshotName(0)==='Pre Service'&&s.console.name==='S31 AB','input, matrix input, control group, snapshot and console names are cleaned when they arrive')
check(s.applyMessage('/Input_Channels/3/Channel_Input/name',['MD RESI']).changed===false&&s.applyMessage('/Input_Channels/3/Channel_Input/name',['MD RESI']).structural===false,'a name that only differs in padding is no change (no rebuild)')
check(s.choices.inputs()[0].label==='3 · MD RESI'&&s.choices.matrixInputs()[0].label==='8 · Dante In 63'&&s.displayName('minput',8)==='Dante In 63'&&s.displayName('minput',3)==='Matrix In 3','dropdown labels use the cleaned names; an unnamed matrix input reads "Matrix In N"')
check(s.has('minput',4)&&s.rawName('minput',4)===''&&s.displayName('minput',4)==='Matrix In 4','a blank matrix input name still exists and falls back to "Matrix In N"')
check(S.numbers('input').length===60&&S.numbers('buss').length===25&&S.numbers('matrix').length===8&&S.numbers('cg').length===10&&S.numbers('geq').length===16,'every entity that answered a name is present')
check(S.snapshots.list.size===8&&S.snapshots.current===3&&S.snapshots.count===8,`snapshots loaded (${S.snapshots.list.size} listed, current ${S.snapshots.current})`)
// Names -> dropdown choices
constinputChoices=S.choices.inputs()
check(inputChoices.length===60&&inputChoices[0].id===1&&inputChoices[0].label==='1 · KICK In',`input choices carry live names (${JSON.stringify(inputChoices[0])})`)
constbussChoices=S.choices.busses()
check(bussChoices.find((c)=>c.id===4)?.label==='4 · CLK-GD-MD (aux)'&&bussChoices.find((c)=>c.id===24)?.label==='24 · Studio (group)','buss choices carry names and the aux/group mode')
check(S.choices.auxChoices().every((c)=>S.bussMode(c.id)==='aux')&&S.choices.groupChoices().map((c)=>c.id).join()==='9,10,11,12,13,18,23,24,25',`group choices are exactly the group busses (${S.choices.groupChoices().map((c)=>c.id).join()})`)
// Buss mode from the types array, stereo from modes, control groups 1-based
check(S.busses.get(9).mode==='group'&&S.busses.get(4).mode==='aux'&&S.busses.get(22).mode==='aux','buss mode comes from /Console/Aux_Outputs/types (9 group, 4 and 22 aux)')
check(S.busses.get(1).stereo===true&&S.busses.get(4).stereo===false,'buss stereo/mono comes from the modes array (1 stereo, 4 mono)')
check(S.cgs.get(1)?.name==='DRUMS'&&S.cgs.get(10)?.name===''&&S.cgs.get(0)===undefined&&S.choices.cgs()[0].id===1&&S.choices.cgs()[0].label==='1 · DRUMS','control groups are 1-based (1 = DRUMS, 10 = last, no 0)')
check(!sent.some((m)=>/Control_Groups\/0\//.test(m.address)), 'the module never addresses control group 0')
check(!sent.slice(0,loadSent).some((m)=>/^\/Console\/(Input_Channels|Aux_Outputs|Group_Outputs)\/\?$/.test(m.address)),'the unanswered Input_Channels / Aux_Outputs / Group_Outputs count queries are never sent')
check(mIn.length===10&&mIn[0].label==='1 · TB'&&mIn[1].label==='2 · Studio L'&&mIn[6].label==='7 · Studio TB'&&mIn[7].label==='8 · Dante In 63'&&mIn[9].label==='10 · Matrix In 10',`matrix inputs have names: dropdown labels "N · name", padding and runs of spaces cleaned, "Matrix In N" when empty (${mIn.map((c)=>c.label).join(' | ')})`)
check(bussChoices.find((c)=>c.id===17)?.label==='17 · Studio TB (aux)'&&S.rawName('buss',17)==='Studio TB','a padded buss name ("Studio TB") shows with single spaces in the dropdown')
check(sent.slice(0,loadSent).some((m)=>m.address==='/Matrix_Inputs/10/Channel_Input/name/?')&&!sent.some((m)=>/^\/Matrix_Inputs\/\d+\/(Buss_Trim\/name|name)\/\?$/.test(m.address)),'matrix input names are queried at /Matrix_Inputs/N/Channel_Input/name')
check(!sent.slice(0,loadSent).some((m)=>/\/(Filters|Channel_Delay|Insert|Dynamics|EQ)\/|phantom|input_pad|analog_gain|\/trim|\/phase|\/Aux_Send\//.test(m.address)), 'channel parameters and sends are not bulk-loaded at connect')
check(S.matrix.get(1)?.name==='MAINS L'&&S.has('minput',1)&&S.minputs.get(10).name===''&&S.numbers('minput').length===10,'matrix outputs and matrix inputs are named (input 10 has an empty name and still exists)')
check(offenders.size===0,`every address sent during the load is one the console answers (${offenders.size?[...offenders].join(', '):`${newSet(sent.slice(0,loadSent).map((m)=>foldPattern(m.address))).size} distinct patterns`})`)
check(V.matrix_in_1_name==='TB'&&V.matrix_in_7_name==='Studio TB'&&V.matrix_in_10_name==='Matrix In 10'&&'matrix_in_10_name'inctx.variableDefs,'matrix input name variables matrix_in_N_name: cleaned, with the Matrix In N fallback')
check(V.input_1_name==='KICK In'&&V.buss_24_name==='Studio'&&V.buss_24_mode==='Group'&&V.buss_1_stereo==='Stereo'&&V.cg_1_name==='DRUMS'&&V.matrix_1_name==='MAINS L','variables carry live names and modes')
check(typeofV.input_1_fader==='string'&&V.input_1_fader!==''&&['Muted','Unmuted'].includes(V.input_1_mute)&&V.input_1_pan==='C','input fader / mute / pan variables are filled')
check('input_60_pan'inctx.variableDefs&&'cg_10_mute'inctx.variableDefs&&'last_change'inctx.variableDefs&&!('input_61_name'inctx.variableDefs),'variable definitions exist for entities that exist')
// Definitions: every entity option is a searchable dropdown with numeric ids; levels show -inf
check(entityOptions.length>40&&entityOptions.every((o)=>(o.type==='dropdown'||o.type==='multidropdown')&&o.minChoicesForSearch===0&&o.choices.every((c)=>typeofc.id==='number')),`all ${entityOptions.length} entity options are searchable dropdowns with numeric ids`)
check(ctx.actions.matrix_send_level_set.options[2].min===-90&&ctx.actions.matrix_send_level_set.options[2].showMinAsNegativeInfinity===true&&ctx.actions.send_level_set.options[2].min===-150&&ctx.actions.input_fader_set.options[1].min===-150&&ctx.actions.cg_fader_set.options[1].min===-150,'per-kind floor: a matrix send level goes down to -90, aux sends and faders to -150')
check(adjusts.length===8&&adjusts.every(([,a])=>a.description.includes('From \u2212\u221e the first step up jumps to \u221260 dB')),`all ${adjusts.length} level adjust actions say that the first step up from -inf jumps to -60 dB`)
check(ctx.actions.input_mute.options[0].choices[0].label==='1 · KICK In'&&ctx.actions.send_on.options[1].choices.every((c)=>S.bussMode(c.id)==='aux'),'action dropdowns show the live labels; send actions list aux busses only')
check(noGauge.length===0&&Object.values(GAUGE).every((f)=>ctx.feedbacks[f]?.type==='value'),`every level and pan action has a numeric value feedback for gauges (${Object.values(GAUGE).join(', ')})`)
check(noFeedback.length===0&&missing.length===0&&lacksValue.length===0&&lacksBar.length===0,`every action has its feedback (${Object.keys(ctx.actions).length} actions; no feedback listed: ${noFeedback.join(', ')||'none'}; unknown feedback: ${missing.join(', ')||'none'}; level without value: ${lacksValue.join(', ')||'none'}; fader/send level without bar: ${lacksBar.join(', ')||'none'})`)
check(refs.every((id)=>P[id])&&newSet(refs).size===refs.length,`every preset id referenced by the structure exists, none twice (${refs.length} references)`)
check(ids.every((id)=>refs.includes(id)),'every preset is listed in some group')
if(p.type==='simple'&&def.type==='boolean'&&!f.style)problems.push(`${id}: boolean feedback ${f.feedbackId} without style`)
if(p.type==='simple'&&def.type==='advanced'&&f.style)problems.push(`${id}: advanced feedback ${f.feedbackId} with style`)
if(p.type==='layered'&&(def.type!=='boolean'||f.style||!Array.isArray(f.styleOverrides)||!f.styleOverrides.length))problems.push(`${id}: layered feedback ${f.feedbackId} must be boolean with styleOverrides`)
check(problems.length===0,`preset actions, feedbacks, options and local variables are valid (${problems.length?problems.slice(0,5).join('; '):'no problems'})`)
constknobs=ids.filter((id)=>/_knob_/.test(id))
check(knobs.length>100&&knobs.every((id)=>P[id].steps[0].rotate_left?.length&&P[id].steps[0].rotate_right?.length),`all ${knobs.length} knob presets have rotate_left and rotate_right`)
check(msendSections.length===8&&msendSections[7].name===`Matrix sends to ${S.displayName('matrix',8)}`&&msendOn.length===80&&msendKnob.length===80,`a "Matrix sends to <output>" section per matrix output (${msendSections.length}: "${msendSections[7]?.name}") with ${msendOn.length} on/off and ${msendKnob.length} knob presets`)
check(mOn.style.text==='$(S31:matrix_in_1_name)\\n$(local:level) dB'&&mOn.name==='Matrix send on/off: TB → 8 · Studio'&&mOn.localVariables[0].feedbackId==='matrix_send_level_value'&&JSON.stringify(mOn.localVariables[0].options)===JSON.stringify({matrixIn:1,matrixOut:8})&&mOn.steps[0].down[0].actionId==='matrix_send_on'&&mOn.steps[0].down[0].options.state==='toggle'&&mOn.style.bgcolor===0x000000&&mOn.feedbacks.length===1&&mOn.feedbacks[0].feedbackId==='matrix_send_off'&&mOn.feedbacks[0].style.bgcolor===0xb01010&&mOn.feedbacks[0].style.color===0xffffff&&JSON.stringify(mOn.feedbacks[0].options)===JSON.stringify({matrixIn:1,matrixOut:8}),'matrix send on/off preset: toggle matrix_send_on, black when the send is on, red with white text when it is off (like a mute), text from the level value feedback')
check(!JSON.stringify(mOn.feedbacks).includes('matrix_send_on')&&!JSON.stringify(P.msend_knob_1_8.feedbacks).includes('matrix_send_on')&&!ids.filter((id)=>id.startsWith('msend_')).some((id)=>P[id].feedbacks.some((f)=>f.style?.bgcolor===COLORS_GREEN||f.styleOverrides?.some((o)=>o.override===0x00a050))),'no matrix send preset carries a green (send on) feedback any more')
check(mKnob.steps[0].rotate_left[0].actionId==='matrix_send_level_adjust'&&mKnob.steps[0].rotate_left[0].options.delta===-1&&mKnob.steps[0].rotate_right[0].options.delta===1&&mKnob.steps[0].down[0].actionId==='matrix_send_on'&&mKnob.type==='layered'&&mKnob.feedbacks.length===1&&mKnob.feedbacks[0].feedbackId==='matrix_send_off'&&mKnob.feedbacks[0].styleOverrides[0].elementId==='bg'&&mKnob.feedbacks[0].styleOverrides[0].elementProperty==='color'&&mKnob.feedbacks[0].styleOverrides[0].override===0xb01010&&mKnob.localVariables.map((v)=>`${v.variableName}=${v.feedbackId}`).join()==='level=matrix_send_level_value,pos=matrix_send_level_pos,meter=matrix_meter_pos','matrix send knob preset (layered): rotate = matrix_send_level_adjust -1/+1, press toggles on/off, off tints the background red, level, gauge position and meter local variables')
check(meter&&meter.type==='gauge'&&meter.orientation==='vertical'&&meter.x+meter.width<=100&&meter.x>=title.width&&meter.height===100&&meter.multiColour===true&&meter.stops.length===3&&meter.value.value==='$(local:meter)'&&JSON.stringify(mv.options)===JSON.stringify({matrix:8}),`matrix send knob has a vertical meter column on the right fed by the destination output's meter (x ${meter?.x}, width ${meter?.width}, title width ${title?.width})`)
check(iKnob&&!iKnob.elements.some((e)=>e.id==='meter')&&iKnob.elements.find((e)=>e.id==='title').width===100,'other knob presets keep the full-width layout without a meter column')
check(!!P.geq_in_1&&P.geq_in_1.steps[0].down[0].actionId==='geq_on'&&P.geq_in_1.feedbacks[0].feedbackId==='geq_on'&&ids.filter((id)=>id.startsWith('geq_in_')).length===16,'a Graphic EQs section with an in/out toggle and a geq_on feedback per graphic EQ')
if(!posVar||!/_(pos)$/.test(posVar.feedbackId)||ctx.feedbacks[posVar.feedbackId]?.type!=='value')lp.push(`${id}: pos local variable`)
}
check(lp.length===0,`layered presets are valid: element types and fields, unique element ids, style overrides name existing elements, every $(local:x) is declared and backed by a value feedback (${lp.length?lp.slice(0,4).join('; '):`${layered.length} presets, no problems`})`)
constiK=P.input_knob_3
check(iK.elements.map((e)=>`${e.type}:${e.id}`).join()==='box:bg,text:title,text:level,gauge:gauge'&&iK.elements[1].text===`$(S31:input_3_name)`&&iK.elements[2].text==='$(local:level) dB'&&iK.elements[3].orientation==='horizontal'&&iK.elements[3].y===84&&iK.elements[3].height===16&&iK.elements[3].stops.length===1&&iK.elements[3].stops[0].color===0xffffff&&iK.elements[1].fontsize===100&&iK.elements[1].fontsizeAllowShrink===true&&iK.elements[3].trackStyle==='dimmed'&&iK.feedbacks[0].feedbackId==='input_muted'&&iK.feedbacks[0].styleOverrides[0].override===0xb01010&&iK.localVariables.map((v)=>`${v.variableName}=${v.feedbackId}`).join()==='level=input_fader_value,pos=input_fader_pos','input knob: black box, name text, "$(local:level) dB", a white horizontal bottom gauge fed by $(local:pos), text size 100 with shrink, muted tints the background red')
check(sK.feedbacks[0].styleOverrides[0].override===0x00a050&&sK.localVariables[1].feedbackId==='send_level_pos','aux send knob: send on tints the background green, gauge from send_level_pos')
constmt=P.input_meter_7
constg=mt.elements.find((e)=>e.id==='gauge')
check(g.orientation==='vertical'&&g.x===86&&g.width===14&&g.multiColour===true&&g.stops.map((st)=>`${st.value}:${st.color.toString(16)}`).join()==='0:b050,70:f0a000,90:e00000'&&mt.localVariables[0].feedbackId==='input_meter_pos'&&mt.feedbacks.map((f)=>f.feedbackId).join()==='input_signal,input_peak','meter preset: vertical gauge on the right edge, green below 70, amber 70-90, red above 90, signal and peak tint the background')
check(Object.entries(ctx.feedbacks).filter(([id])=>/_bar$/.test(id)).every(([,f])=>/\(legacyimage\)$/.test(f.name))&&Object.keys(ctx.feedbacks).filter((id)=>/_bar$/.test(id)).length===8,'the 8 advanced image bar feedbacks keep their ids and are named "(legacy image)"')
check(tier1.stats.dropped.length===1&&/reduction meter/.test(tier1.stats.dropped[0])&&tier1.stats.count===ids.length-120&&!Object.keys(tier1.presets).some((id)=>/_gr_/.test(id))&&Object.keys(tier1.presets).some((id)=>id.startsWith('send_knob_')),`over the limit the 120 gate / compressor reduction meter presets are dropped first (${tier1.stats.count} left)`)
check(tier2.stats.dropped.length===2&&!Object.keys(tier2.presets).some((id)=>id.startsWith('send_knob_'))&&tier2.stats.count===ids.length-120-900,`still over: the 900 per-input send level knobs go next (${tier2.stats.count} left)`)
check(!/studio mon/i.test(JSON.stringify([P,ctx.presetStructure,ctx.variableDefs]))&&!JSON.stringify(Object.values(ctx.actions).map((a)=>a.name)).match(/studio mon/i),'no site-specific wording in the generated definitions')
check(q===0&&w>q&&(replied===0||replied===1)&&r[w].tags===',f',`toggle on unknown state queries first (query #${q}), then writes (${JSON.stringify(r[w]?.args)})`)
check(pq===0&&pw>pq&&r[pw].tags===',f'&&r[pw].args[0]===1,`input_phantom toggle on an unloaded switch queries first, then writes the opposite of the replied 0 (${JSON.stringify(r[pw]?.args)})`)
check(r[0]?.address==='/Aux_Outputs/3/EQ/eq_in/?'&&writes(r).some((m)=>m.address==='/Aux_Outputs/3/EQ/eq_in'&&m.tags===',f'),'buss_eq_in toggle on an unloaded aux buss queries first')
// A write is confirmed by one query once its guard window ends (not immediately)
{
constmark=sent.length
awaitrun('cg_fader_set',{cg:3,level:-4})
awaitsleep(100)
check(!sent.slice(mark).some((m)=>m.address==='/Control_Groups/3/fader/?'),'no immediate confirm query after a write')
awaitsleep(inst.timing.writeGuardMs+200)
constseq=sent.slice(mark).map((m)=>m.address)
check(seq.filter((a)=>a==='/Control_Groups/3/fader/?').length===1&&seq.indexOf('/Control_Groups/3/fader/?')>seq.indexOf('/Control_Groups/3/fader'),'one confirm query of the same address follows when the write guard ends')
check(seen.length===0&&S.getMute('input',30)===before,'int and T/F writes to a switch are ignored by the mock: no echo and no state change')
inst.queryNow(address)
check(awaitwaitFor(()=>seen.length===1,2000)&&seen[0].tags===',f'&&seen[0].args[0]===(before?1:0),'reading it back still gives the old value, as a float')
awaitsendRaw([want])
check(awaitwaitFor(()=>seen.length===2,2000)&&seen[1].tags===',f'&&seen[1].args[0]===want&&S.getMute('input',30)===!before,'a float write is accepted and echoed as a float')
awaitsendRaw([before?1:0])// restore
awaitsleep(300)
inst.onConsoleMessage=original
raw.close()
// and the module's own switch writes are floats, so the console keeps them (the confirm query after the guard agrees)
check(!!w&&decodeArgs(w).tags===',f'&&S.getMute('input',31)===!was31,'a module switch write goes out as a float and the console accepted it (the confirm query returns the written value)')
check(ctx.rebuilds===before+1,`three quick renames caused exactly one definitions rebuild (${ctx.rebuilds-before})`)
check(ctx.actions.input_mute.options[0].choices.find((c)=>c.id===4)?.label==='4 · Renamed four'&&ctx.variables.input_6_name==='Renamed six','renamed inputs show up in the dropdown choices and variables')
check(ctx.presets.input_mute_5.name.includes('Renamed five'),'presets are rebuilt with the new names')
check(cb('input_muted',{channel:10})===true&&cb('input_muted',{channel:1})===(S.getMute('input',1)===true),'input_muted follows the state (10 was muted by an action)')
check(cb('level_compare',{kind:'buss',buss:24,comparison:'above',threshold:-10})===true&&cb('level_compare',{kind:'cg',cg:2,comparison:'below',threshold:-7})===true&&cb('level_compare',{kind:'input',channel:7,comparison:'above',threshold:20})===false,'level_compare for buss / control group / input')
check(cb('input_phantom_state',{channel:14})===true&&cb('input_hpf_in_state',{channel:14})===true&&cb('input_pad_state',{channel:14})===false&&cb('buss_insert_a_state',{buss:9})===true&&cb('buss_eq_in_state',{buss:3})!==undefined,'switch feedbacks follow the loaded / written state')
check(cb('input_comp_in_state',{channel:33})===false&&(awaitwaitFor(()=>sent.some((m)=>m.address==='/Input_Channels/33/Dynamics/comp_in/?'),2000)),'an unloaded switch feedback reads false and queries the console once')
awaitsleep(120)
check(S.getParam('input',33,'comp_in')===true&&cb('input_comp_in_state',{channel:33})===true,'the replied switch value now drives the feedback (mock default: comp in)')
check(cb('input_fader_value',{channel:5})==='-12.5'&&cb('input_pan_value',{channel:13})==='R100'&&cb('buss_fader_value',{buss:24})==='-3'&&cb('matrix_fader_value',{matrix:2})==='-16'&&cb('cg_fader_value',{cg:2})==='-8','value feedbacks return dB / pan text')
check(cb('snapshot_name_value',{})==='Offering'||cb('snapshot_name_value',{})===S.snapshotName(S.snapshots.current),'snapshot_name_value follows the current snapshot')
// matrix sends, solo, graphic EQs, channel-strip values and send pan
check(cb('matrix_send_on',{matrixIn:2,matrixOut:3})===true&&cb('matrix_send_level_value',{matrixIn:2,matrixOut:3})==='-30'&&cb('matrix_send_level_value',{matrixIn:3,matrixOut:2})==='-60','matrix_send_on and matrix_send_level_value follow the written matrix sends')
check(cb('matrix_send_level_value',{matrixIn:5,matrixOut:5})==='--'&&cb('matrix_send_on',{matrixIn:5,matrixOut:5})===false,'an unloaded matrix send reads "--" / false and is queued for loading')
check(awaitwaitFor(()=>sent.some((m)=>m.address==='/Matrix_Inputs/5/Matrix_Send/5/send_level/?')&&S.getMatrixSend(5,5).level!==undefined,2500),'the unloaded matrix send is queried once and its state arrives')
check(cb('matrix_send_level_value',{matrixIn:5,matrixOut:5})==='-inf'&&cb('matrix_send_on',{matrixIn:5,matrixOut:5})===false,'a matrix send at its -90 floor reads "-inf"')
// matrix_send_off: true only when the console says the send is off (like a mute); unknown is not red, and starts the lazy load
check(cb('matrix_send_on',{matrixIn:2,matrixOut:3})===true&&cb('matrix_send_off',{matrixIn:2,matrixOut:3})===false,'matrix_send_off is false for a send that is on')
check(cb('matrix_send_off',{matrixIn:6,matrixOut:6})===false&&cb('matrix_send_on',{matrixIn:6,matrixOut:6})===false,'matrix_send_off is false for an unknown send (not red until the console says off) and the send is queued for loading')
check(awaitwaitFor(()=>sent.some((m)=>m.address==='/Matrix_Inputs/6/Matrix_Send/6/send_on/?')&&S.getMatrixSend(6,6).on!==undefined,2500),'the unknown send is queried lazily and its on/off arrives')
check(S.getMatrixSend(6,6).on===false&&cb('matrix_send_off',{matrixIn:6,matrixOut:6})===true&&cb('matrix_send_on',{matrixIn:6,matrixOut:6})===false,'matrix_send_off is true for a send the console reports off')
sendOn(1.0)
check(cb('matrix_send_off',{matrixIn:6,matrixOut:6})===false&&cb('matrix_send_on',{matrixIn:6,matrixOut:6})===true,'and false again when it is switched on')
check(awaitwaitFor(()=>S.getGeq(3).on!==undefined&&S.getGeq(3).gains?.get(7)!==undefined,2500)&&cb('geq_on',{geq:3})===true&&cb('geq_band_value',{geq:3,band:7})==='0','the graphic EQ state loads lazily (mock default: in, flat)')
// numeric position feedbacks for native gauges: known state gives the taper position, unknown gives 0 (and starts the lazy load)
check(near(cb('input_fader_pos',{channel:5}),51.25)&&near(cb('buss_fader_pos',{buss:24}),69)&&near(cb('matrix_fader_pos',{matrix:2}),46)&&near(cb('cg_fader_pos',{cg:2}),59),'input / buss / matrix / control group fader position follow the fader taper (-12.5 dB -> 51.25, -3 -> 69, -16 -> 46, -8 -> 59)')
check(near(cb('send_level_pos',{channel:2,buss:3}),61)&&near(cb('matrix_send_level_pos',{matrixIn:2,matrixOut:3}),28)&&near(cb('matrix_send_level_pos',{matrixIn:3,matrixOut:2}),6),'aux send and matrix send position (-7 dB -> 61, -30 -> 28, -60 -> 6)')
check(cb('input_pan_num',{channel:13})===1&&near(cb('send_pan_num',{channel:2,buss:3}),0.1)&&cb('geq_band_db',{geq:1,band:5})===4,'pan is the raw 0..1 number and the graphic EQ band is the dB number')
check(cb('input_fader_pos',{channel:99})===0&&cb('buss_fader_pos',{buss:99})===0&&cb('matrix_fader_pos',{matrix:99})===0&&cb('cg_fader_pos',{cg:99})===0&&cb('input_pan_num',{channel:99})===0.5,'unknown fader state reads 0 (pan reads the 0.5 centre)')
check(lazy.every(([,,,want],i)=>firsts[i]===want),`unloaded send / matrix send / send pan / GEQ band read ${firsts.join(', ')} on first sight`)
check(awaitwaitFor(()=>lazy.every(([,,address])=>sent.some((m)=>m.address===address)),2500),'and each starts its lazy load')
awaitsleep(150)
check(typeofcb('send_level_pos',{channel:45,buss:9})==='number'&&S.getSend(45,9).level!==undefined&&S.getMatrixSend(7,6).level===-90&&cb('matrix_send_level_pos',{matrixIn:7,matrixOut:6})===0&&typeofcb('send_pan_num',{channel:45,buss:9})==='number'&&cb('geq_band_db',{geq:4,band:9})===0,'once the state arrives the position feedbacks return the real values (a matrix send at -90 stays 0)')
check(cb('input_trim_value',{channel:14})==='-3'&&cb('input_gain_value',{channel:14})==='34.5'&&cb('input_hpf_freq_value',{channel:14})==='88'&&cb('buss_trim_value',{buss:1})==='-2'&&cb('matrix_trim_value',{matrix:2})==='1.5','channel-strip value feedbacks: trim, gain, filter frequency (whole Hz), buss and matrix trim')
check(cb('minput_trim_value',{matrixIn:3})==='-1.5'&&cb('minput_phase_state',{matrixIn:3})===true&&cb('minput_phase_state',{matrixIn:4})===false,'matrix input trim text and phase feedback follow the writes')
check(cb('input_lpf_freq_value',{channel:60})==='--'&&(awaitwaitFor(()=>S.getParam('input',60,'lpf_freq')!==undefined,2500))&&cb('input_lpf_freq_value',{channel:60})==='12000','an unloaded channel-strip value reads "--" and loads lazily')
check(cb('send_level_value',{channel:40,buss:8})==='--'&&cb('send_on',{channel:40,buss:8})===false,'an unloaded send pair reads as unknown ("--" / false) and is queued for loading')
check(awaitwaitFor(()=>sent.some((m)=>m.address==='/Input_Channels/40/Aux_Send/8/send_level/?'),2000),'the unloaded pair is queried once its feedback runs')
awaitsleep(120)
check(typeofS.getSend(40,8).level==='number'&&cb('send_level_value',{channel:40,buss:8})!=='--','the pair state arrives and the value feedback now returns a number')
check(id7>=5000&&meterIdForInput(60)===5060&&outputMeterId('buss',3)===5403&&outputMeterId('matrix',2)===5452,`meter ids start at 5000 (input 7 -> ${id7}); output meters use 5400 + buss and 5450 + matrix output`)
// the scale helpers
check(meterLevelDb(42)===-42&&meterLevelDb(0)===0&&meterLevelDb(127)===-Infinity&&meterLevelDb(undefined)===-Infinity&&meterGrDb(3)===3&&meterGrDb(127)===0&&meterGrDb(undefined)===0,'meterLevelDb: 127 = -inf, v = -v dBFS; meterGrDb: 127 = no reduction, v = v dB')
check(fmtMeterLevel(42)==='-42'&&fmtMeterLevel(127)==='-inf'&&fmtMeterLevel(0)==='0'&&meterPosition(127)===0&&meterPosition(60)===0&&near(meterPosition(42),30)&&meterPosition(0)===100&&near(meterPosition(6,'gr'),30)&&meterPosition(127,'gr')===0&&meterPosition(30,'gr')===100,'meter text and positions: -42 -> "-42" and 30 %, 0 -> 100 %, 127 -> "-inf" and 0 %, GR 6 dB -> 30 %')
check(!!subWire&&subWire.tags===',ii'&&subWire.args[0]===id7&&subWire.args[1]===1,`the first meter feedback subscribes lazily: ${JSON.stringify(subWire)}`)
check(S.getMeter(id7).value===127&&cb('input_meter_value',{channel:7})==='-inf'&&cb('input_meter_pos',{channel:7})===0&&cb('input_signal',{channel:7})===false&&cb('input_peak',{channel:7})===false,'a silent channel (127) reads -inf, position 0, no signal, no peak')
// a live channel streams 20-60 (the mock has inputs 1-3 live)
cb('input_meter_value',{channel:2})
check(awaitwaitFor(()=>S.getMeter(meterIdForInput(2)),3000)&&S.getMeter(meterIdForInput(2)).value>=20&&S.getMeter(meterIdForInput(2)).value<=60&&/^-(2\d|[3-5]\d|60)$/.test(cb('input_meter_value',{channel:2}))&&cb('input_signal',{channel:2,thresholdDb:-70})===true,'a live channel streams 20-60: the text reads -20 .. -60 dBFS and signal is present')
check(gate?.args[0].i===meterId(7,'gate')&&comp?.args[0].i===meterId(7,'comp')&&meterId(7,'gate')>=5000&&meterId(7,'comp')!==meterId(7,'gate'),`gate and compressor gain-reduction meters subscribe with kinds 1 and 4 (ids ${meterId(7,'gate')} / ${meterId(7,'comp')})`)
check(cb('input_meter_value',{channel:7})==='-inf'&&cb('input_meter_pos',{channel:7})===0&&cb('input_signal',{channel:7})===false&&cb('input_peak',{channel:7})===false,'silence (127): -inf, position 0 %, no signal, no peak')
inject(0)
check(cb('input_meter_value',{channel:7})==='0'&&cb('input_meter_pos',{channel:7})===100&&cb('input_signal',{channel:7})===true&&cb('input_peak',{channel:7,hold:2})===true,'full scale (0): 0 dBFS, position 100 %, signal and peak')
inject(42)
check(cb('input_meter_value',{channel:7})==='-42'&&near(cb('input_meter_pos',{channel:7}),30),'a value of 42 reads "-42" and sits at 30 % of the -60..0 scale')
check(cb('input_signal',{channel:7,thresholdDb:-50})===true&&cb('input_signal',{channel:7,thresholdDb:-40})===false,'signal is a dBFS threshold: -42 is above -50 and below -40')
check(cb('input_peak',{channel:7,hold:2})===true,'the peak from a moment ago is still held after the level fell back')
// the peak threshold is an option: 4 steps below full scale is a peak at -5 dBFS but not at -1
constid8=meterIdForInput(8)
cb('input_meter_value',{channel:8})
awaitsleep(60)
inject(4,id8)
check(cb('input_peak',{channel:8,peakDb:-5,hold:2})===true&&cb('input_peak',{channel:8,peakDb:-1,hold:2})===false&&cb('input_peak',{channel:8,hold:2})===false,'peak at or above a dBFS option: a reading of 4 peaks at -5 dBFS, not at the default -1')
// gain reduction meters
constgateId=meterId(7,'gate')
inject(6,gateId)
check(cb('input_meter_value',{channel:7,meter:'gate'})==='6'&&near(cb('input_meter_pos',{channel:7,meter:'gate'}),30)&&cb('input_signal',{channel:7,meter:'gate'})===true&&cb('input_peak',{channel:7,meter:'gate'})===false,'gain reduction 6 dB reads "6", sits at 30 % of 0..20 dB and counts as signal; there is no peak')
inject(127,gateId)
check(cb('input_meter_value',{channel:7,meter:'gate'})==='0'&&cb('input_meter_pos',{channel:7,meter:'gate'})===0&&cb('input_signal',{channel:7,meter:'gate'})===false,'gain reduction 127 (inactive) reads "0", 0 % and no signal')
check(Buffer.from(bar.imageBuffer,'base64').length===thicknessFor(72)*72*4&&bar.imageBufferPosition.x===72-thicknessFor(72),'input_meter_bar (legacy) returns a right-edge strip of the right size')
// output meters: buss (the address follows the aux / group side) and matrix output
check(cb('buss_meter_value',{buss:3})==='-30'&&near(cb('buss_meter_pos',{buss:3}),50)&&cb('buss_signal',{buss:3})===true&&cb('buss_peak',{buss:3})===false,'buss meter: 30 -> "-30", 50 %, signal, no peak')
inject(1,5403)
check(cb('buss_peak',{buss:3})===true&&cb('buss_signal',{buss:3,thresholdDb:-0.5})===false,'buss meter: a reading of 1 (-1 dBFS) peaks; signal needs to exceed its threshold')
inject(127,5403)
check(cb('buss_meter_value',{buss:3})==='-inf'&&cb('buss_meter_pos',{buss:3})===0&&cb('buss_signal',{buss:3})===false,'buss meter: 127 is -inf, 0 %, no signal')
inject(50,5452)
check(cb('matrix_meter_value',{matrix:2})==='-50'&&near(cb('matrix_meter_pos',{matrix:2}),(10/60)*100)&&cb('matrix_signal',{matrix:2,thresholdDb:-60})===true&&cb('matrix_signal',{matrix:2})===false&&cb('matrix_peak',{matrix:2})===false,'matrix output meter: 50 -> "-50", position over -60..0, signal against the dBFS option')
inject(0,5452)
check(cb('matrix_peak',{matrix:2})===true&&cb('matrix_meter_pos',{matrix:2})===100,'matrix output meter: 0 is full scale, peak and 100 %')
check(!!watching7&&watching7.size>=4&&!watching7.has(gateInstance)&&inst.watchers.get('meter:'+meterId(7,'gate'))?.has(gateInstance),`meter feedback instances are tracked per meter id (${watching7?.size} watch meter ${id7}; the gate-reduction one watches its own id)`)
// stop the mock's own meter stream (a raw /Meters/clear straight to the console) so only injected values move
check(meterChecks.length>0&&meterChecks.length<=Math.ceil((spanMs/1000)*10)+1,`meter feedback checks throttled to <= 10 Hz (${meterChecks.length} checks in ${spanMs} ms for 100 messages)`)
check(meterChecks.every(([,ids])=>ids.every((x)=>meterInstances.has(x)))&&!meterChecks.some(([,ids])=>ids.includes(gateInstance)),'a meter update re-checks only meter instances: those watching the changed id (and meters with a peak hold running), never the gain reduction one that did not move')
check(!ctx.checks.some(([,types])=>types.some((t)=>/meter|signal|peak/.test(t))),`meter updates never fall back to per-type checks (per-type checks in that window: ${JSON.stringify(ctx.checks.map(([,t])=>t))}, from the keepalive tick)`)
check(ipadRx.some((m)=>m.address==='/Console/Name'),`iPad got /Console/Name via the relay (${ipadRx.length} msgs)`)
check(inst.relay.clientCount===1,'relay tracks one iPad client')
awaitsleep(60)
check(ctx.variables.relay_clients===1&&ctx.variables.relay_client_ips==='127.0.0.1'&&cb('relay_clients',{})===true,'relay_clients variable and feedback')
check(!sent.slice(mark).some((m)=>m.address==='/Input_Channels/7/Channel_Input/post_meter'),'no resubscribe before the delay has passed')
check(awaitwaitFor(()=>sent.slice(mark).some((m)=>m.address==='/Input_Channels/7/Channel_Input/post_meter'),2000),'after the iPad sent /Meters/clear the module re-sent its meter subscription')
check(awaitwaitFor(()=>S.getMeter(meterIdForInput(7))?.value===127,3000),'the silent meter reports its 127 again after the resubscribe: the console really has the subscription back')
check(awaitwaitFor(()=>(S.getMeter(meterIdForInput(2))?.at??0)>t0,3000),'a live meter streams again after the resubscribe (it had stopped at the first /Meters/clear)')
// back-to-back clears: every clear pushes the resubscribe back, and the newest one always gets a resubscribe after it
ipad.send(encode('/Meters/clear',[]),P.relay,'127.0.0.1')// clear 2, while the resubscribe for clear 1 is still waiting
awaitsleep(150)
check(subsSince(m2)===0,'a second clear while the resubscribe is pending moves it back: nothing was re-sent yet, so nothing can land before the newest clear')
check(awaitwaitFor(()=>subsSince(m2)===1,2000),'the resubscribe follows the newest clear')
check(awaitwaitFor(()=>S.getMeter(id7).value===127,3000),'and the console reports again after it (clear 2 did not leave the meters unsubscribed)')
// a clear right after a resubscribe went out: it needs its own resubscribe
check(awaitwaitFor(()=>subsSince(m3)===1,2000),'a clear after the previous resubscribe schedules another one')
check(awaitwaitFor(()=>S.getMeter(id7).value===127,3000)&&(awaitwaitFor(()=>(S.getMeter(meterIdForInput(2))?.at??0)>t3,3000)),'the third clear is followed by working subscriptions too (silent and live meters report again)')
// clears in quick succession all collapse into one resubscribe after the last of them
check(awaitwaitFor(()=>S.getMeter(id7).value===127,3000)&&subsSince(m4)===1,`four clears 60 ms apart give exactly one resubscribe, after the last (${subsSince(m4)})`)
check(inst.link===link0&&inst.relay===relay0&&relay0.clientCount===1&&inst.config.debugLog===true,'a debugLog change only flips the flag: same link, same relay, iPad still registered')
check(awaitpushFader(-8),'a console push reaches the iPad right after the debug toggle')
check(logs.some((l)=>l.level==='debug'&&/console -> /.test(l.message)),'debug logging takes effect without reconnecting')
relay0.clients.get('127.0.0.1').lastSeen=1// silent for ages
check(relay0.timeoutMs===0&&relay0.clientCount===1&&(awaitpushFader(-7)),'relayTimeout 0 = never forget: an iPad silent for ages is still listed and still gets console traffic')
check(inst.relay===relay0&&relay0.clientPort===0&&(awaitpushFader(-6)),'a relayClientPort change is applied in place (0 = reply to the iPad source port)')
check(inst.relay!==relay0&&inst.relay.port===P.spare&&inst.relay.clients===clients0&&inst.relay.clientCount===1,'a relay port change reopens the relay but carries the iPad client table across')
check(inst.link===link0,'the console link is untouched by relay changes')
check(awaitpushFader(-5),'the iPad receives console traffic through the reopened relay without registering again')
check(inst.relay===null&&inst.relayClients===clients0&&clients0.size===1,'disabling the relay closes it and keeps the client table')
awaitinst.configUpdated({...config})
check(!!inst.relay&&inst.relay.port===P.relay&&inst.relay.clientCount===1&&(awaitpushFader(-4)),'enabling it again brings the relay back with the same iPad')
check(inst.link===link0&&logCount(/Loading from the console/)===loads0&&JSON.stringify(ctx.pushes)===pushes0,'none of that reloaded the console or rebuilt / re-pushed any definition')
// a console-link field reopens the link and reloads; the iPad stays registered; the identical result pushes nothing
check(inst.link!==link0&&inst.link.localPort===P.link+5&&logCount(/Loading from the console/)===loads0+1,'a local port change reopens the console link and starts a load')
awaitinst.configUpdated({...config})
check(awaitwaitFor(()=>logCount(/load complete/)===loadsDone+1,20000),'changing it back reloads from the console')
check(inst.relayClients===clients0&&inst.relay.clientCount===1&&(awaitpushFader(-3)),'the iPad stays registered through a console-link reopen')
check(JSON.stringify(ctx.pushes)===pushes0,'the reload after reopening the link pushed no identical definitions again')
ipad.close()
}
// Overlapping lifecycle calls run one after the other (no EADDRINUSE from two connects racing)
check(!threw&&!!i.link&&i.link.port===P.spare+9&&i.relay?.port===P.spare+2&&logCount(/EADDRINUSE|in use/)===before,`init and three overlapping configUpdated calls settled in order (link -> ${i.link?.port}, relay ${i.relay?.port}, no bind errors: ${threw?.message||'ok'})`)
r2.clients.get(`10.0.0.${i}`).lastSeen=i// ten.0.0.1 is the least recently seen
}
r2.clients.get('10.0.0.1').lastSeen=0
r2.touch({address:'10.0.0.99',port:2000})
check(r2.clients.size===16&&!r2.clients.has('10.0.0.1')&&r2.clients.has('10.0.0.99')&&gone2.join()==='10.0.0.1','the client table is capped at 16: the least recently seen client is evicted for a new one')
r2.touch({address:'10.0.0.2',port:1002})
check(r2.clients.size===16&&r2.clients.has('10.0.0.2'),'a known client does not evict anyone')
check(allFine,`${bad.length} invalid configs (empty host, ports 0 / 70000 / text / fractions, bad relay values) give bad_config with a clear message and never throw`)
check(n.cfg?.consolePort===8000&&n.cfg.localPort===9000&&n.cfg.relayPort===8001&&n.cfg.relayClientPort===0&&n.cfg.relayTimeout===0&&n.cfg.host==='127.0.0.1','numeric strings are coerced; relayClientPort 0 and relayTimeout 0 are valid')
check(mainMod.normalizeConfig({host:'127.0.0.1',relayEnable:false,relayPort:'junk'}).error===null,'relay fields are not validated while the relay is off')
check(!threw&&result===false&&errors.length===1&&errors[0].dest==='127.0.0.1:70000',`a synchronous socket.send failure (bad port) is caught: send() returns false, one error event with its destination (${errors[0]?.code})`)
letthrew2=false
try{
link.send('/x',[{nope:1}])
}catch{
threw2=true
}
check(!threw2&&errors.length===2,'an argument that cannot be encoded does not throw either')
check(indexProblem('/Input_Channels/60/fader')===null&&indexProblem('/Aux_Outputs/25/mute')===null&&indexProblem('/Console/Name')===null,'indexProblem accepts in-range entity addresses and non-entity addresses')
check(['/Input_Channels/0/fader','/Input_Channels/-1/fader','/Input_Channels/1.5/fader','/Input_Channels/61/fader','/Input_Channels/NaN/fader','/Input_Channels/undefined/fader','/Aux_Outputs/26/fader','/Input_Channels/3/Aux_Send/26/send_on','/Matrix_Outputs/9/mute','/Matrix_Inputs/11/Matrix_Send/1/send_on','/Control_Groups/11/fader','/Graphic_EQ/17/geq_in','/Graphic_EQ/1/geq_gain_33','/Graphic_EQ/1/geq_gain_1.5'].every((a)=>indexProblem(a)),'indexProblem rejects 0, negatives, fractions, NaN, too large numbers and bad send / band numbers')
check(packets===0,`${bad.length} actions with an out-of-range or non-integer index sent no packet`)
check(wrongWarnings===0,'each rejected action logged exactly one warning')
check(slow===0,'a rejected toggle or adjust does not sit waiting for a reply')
constmark=sent.length
check(inst.write('/Input_Channels/0/fader',[1],1)===false&&inst.queryNow('/Input_Channels/1.5/fader')===false&&(inst.ensure('t:bad',['/Input_Channels/70/EQ/eq_in']),true),'write, queryNow and ensure refuse bad addresses on their own')
check(sent.length===mark,'and send nothing')
check(inst.fireSnapshot(0)===true&&inst.fireSnapshot(9999)===true,'snapshot indexes 0 and 9999 are accepted')
check(S.busses.get(1).stereo===!wasStereo&&ctx.variables.buss_1_stereo===(wasStereo?'Mono':'Stereo')&&JSON.stringify(ctx.pushes)===p0,'a changed stereo flag updates the variable but pushes no definitions')
check(wasAux&&label3==='3 · Group 3 (group)'&&ctx.pushes.actions===JSON.parse(p0).actions+1&&ctx.pushes.presets===JSON.parse(p0).presets+1,`a changed buss type rebuilds and pushes once (${label3})`)
check(optIds('group_fader_set','buss').includes(3)&&!optIds('aux_fader_set','buss').includes(3)&&optIds('group_mute','busses').includes(3)&&!optIds('aux_mute','busses').includes(3)&&ctx.actions.group_fader_set.options[0].choices.find((c)=>c.id===3).label==='3 · Group 3'&&ctx.feedbacks.group_muted.options[0].choices.some((c)=>c.id===3)&&!ctx.feedbacks.aux_muted.options[0].choices.some((c)=>c.id===3),'the buss moved from the aux dropdowns to the group dropdowns (actions and feedbacks)')
check(!ctx.presets.aux_mute_3&&!ctx.presets.aux_knob_3&&!ctx.presets.aux_meter_3&&ctx.presetStructure.find((x)=>x.id==='aux_masters').definitions[0].presets.length===14,'and its aux master presets are gone from "Aux masters" (it has no group-side name, so no group presets either)')
check(ctx.actions.buss_mute.options[0].choices.find((c)=>c.id===3)?.label==='3 · TRACKS (aux)'&&optIds('aux_fader_set','buss').includes(3)&&!optIds('group_fader_set','buss').includes(3)&&!!ctx.presets.aux_mute_3&&ctx.presetStructure.find((x)=>x.id==='aux_masters').definitions[0].presets.includes('aux_mute_3'),'and back again (dropdowns and the "Aux masters" presets)')
check(ctx.pushes.actions===p1+1&&ctx.actions.snapshot_fire.options[0].choices.some((c)=>c.id===8&&/Extra/.test(c.label)),'a new snapshot in the list rebuilds the definitions')
inst.onConsoleMessage(decode(encode('/Matrix_Inputs/2/Channel_Input/name',[' New Name '])),{address:'127.0.0.1',port:P.mock})
awaitsleep(inst.timing.rebuildDebounceMs+400)
constknob2=ctx.presets.msend_knob_2_8
check(S.minputs.get(2).name==='New Name'&&ctx.pushes.actions===pm.actions+1&&ctx.pushes.presets===pm.presets+1,'a matrix input name change rebuilds the definitions once')
check(ctx.actions.matrix_send_on.options[0].choices.find((c)=>c.id===2)?.label==='2 · New Name'&&ctx.actions.minput_trim_set.options[0].choices.find((c)=>c.id===2)?.label==='2 · New Name'&&ctx.feedbacks.matrix_send_on.options[0].choices.find((c)=>c.id===2)?.label==='2 · New Name','the matrix send and matrix input param dropdowns show the new name, cleaned')
check(ctx.variables.matrix_in_2_name==='New Name'&&knob2.name===`Matrix send level (knob): New Name → 8 · ${S.displayName('matrix',8)}`&&ctx.presets.msend_on_2_8.name.includes('New Name')&&knob2.elements[1].text==='$(S31:matrix_in_2_name)'&&ctx.presets.msend_on_2_8.style.text.startsWith('$(S31:matrix_in_2_name)'),'the matrix send preset names and titles carry the new name (titles read the matrix_in_N_name variable)')
check(inst.watchers.get('input:5')?.has('inst-A')&&inst.watchers.get('input:6')?.has('inst-B')&&!inst.watchers.get('input:5')?.has('inst-B'),'each feedback instance is registered under the entity it watches')
check(ids.includes('inst-A')&&!ids.includes('inst-B')&&!ctx.checks.some(([,t])=>t.includes('input_muted')),'a change to input 5 re-checks only the instance watching input 5, with no per-type check')
ctx.byId.length=0
flipMute(51)
awaitsleep(80)
check(ctx.checks.some(([,t])=>t.includes('input_muted'))&&!ctx.byId.flatMap(([,x])=>x).includes('inst-A'),'with nobody watching input 51 the per-type check is the fallback')
// options change: the instance moves to its new key
run1({...a,options:{channel:6}})
check(!inst.watchers.get('input:5')?.has('inst-A')&&inst.watchers.get('input:6')?.has('inst-A'),'a feedback whose options changed is dropped from its old key and registered under the new one')
ctx.feedbacks.input_muted.unsubscribe(a,{})
ctx.feedbacks.input_muted.unsubscribe(b,{})
check(!inst.watching.has('inst-A')&&!inst.watching.has('inst-B')&&!inst.watchers.get('input:6')?.has('inst-A')&&!inst.watchers.get('input:6')?.has('inst-B'),'unsubscribe drops the instance')
check(Object.values(ctx.feedbacks).every((f)=>typeoff.unsubscribe==='function'),'every feedback definition has an unsubscribe hook')
check(ids2.includes('inst-S1')&&!ids2.includes('inst-S2')&&ids2.includes('inst-N'),'a send change re-checks only that send pair; a snapshot change re-checks the snapshot feedbacks')
check(inst.watchers.get('msend:2:3')?.has('inst-M1')&&inst.watchers.get('msend:3:2')?.has('inst-M2')&&inst.watchers.get('geq:1:gain:5')?.has('inst-G1')&&inst.watchers.get('geq:1:gain:6')?.has('inst-G2')&&inst.watchers.get('geq:1:on')?.has('inst-G3'),'matrix send pairs and graphic EQ bands register under msend:in:out and geq:n:gain:band keys')
check(ids3.includes('inst-M1')&&!ids3.includes('inst-M2')&&ids3.includes('inst-G1')&&!ids3.includes('inst-G2')&&!ids3.includes('inst-G3'),'a matrix send change re-checks only that send pair, a band change only that band')
check(ids3b.includes('inst-M1')&&ids3b.includes('inst-M3')&&!ids3b.includes('inst-M2')&&cb('matrix_send_off',{matrixIn:2,matrixOut:3})===true,'a matrix send on/off change re-checks matrix_send_on and matrix_send_off of that send pair only, and the off feedback turns true')
check(['inst-BM','inst-GM','inst-GF'].every((id)=>inst.watchers.get('buss:24')?.has(id))&&inst.watchers.get('buss:25')?.has('inst-GX')&&inst.watchers.get('buss:3')?.has('inst-AX')&&inst.watchers.get(`meter:${outputMeterId('buss',24)}`)?.has('inst-GP'),'group_* and aux_* feedbacks register under the buss key (meters under the buss meter id)')
check(idsG.includes('inst-BM')&&idsG.includes('inst-GM')&&idsG.includes('inst-GF')&&!idsG.includes('inst-GX')&&!idsG.includes('inst-AX')&&!ctx.checks.some(([,t])=>/^(group|aux|buss)_/.test(String(t))),'a group fader change re-checks only the instances watching that buss, by id and with no per-type check')
check(Object.values(ctx.feedbacks).some((f)=>f.options.some((o)=>o.isVisibleExpression))&&bad.length===0,`options referenced by isVisibleExpression have disableAutoExpression (${bad.join(', ')||'level_compare.kind'})`)
check(!sent.slice(mark).some((m)=>/Channel_Input\/name$/.test(m.address))&&S.rawName('input',9)===nameBefore&&logs.filter((l)=>l.level==='warn').length===warns+2,'an empty (or blank) input name is ignored with a warning and nothing is written')
check(!('send_prepost'inctx.actions)&&!('send_pre'inctx.feedbacks),'the unverified send pre/post action and feedback are gone')
check(!Object.keys(ctx.actions).some((id)=>/delay_(set|adjust)$/.test(id))&&'input_delay_on'inctx.actions&&'buss_delay_on'inctx.actions,'the delay value actions are gone; the delay in/out switches stay')
check(ctx.actions.input_gain_set.options[1].min===0&&ctx.actions.input_gain_set.options[1].max===60&&ctx.actions.input_trim_set.options[1].min===-40&&ctx.actions.input_hpf_freq_set.options[1].min===20&&ctx.actions.input_hpf_freq_set.options[1].max===20000,'analog gain is limited to 0..60, trim to -40..40, filter frequencies to 20..20000')
check(/Verify once on your console/.test(ctx.actions.snapshot_fire.description)&&/0 = first/.test(ctx.actions.snapshot_fire.description),'snapshot_fire says it fires by list position and should be verified first')
check(ANCHORS.every(([db,pos])=>near(positionToDb(pos),db,1e-9)&&near(faderPosition(db),pos,1e-9)),'positionToDb returns every anchor of the fader taper exactly')
check(positionToDb(0)===-90&&positionToDb(1)===-90&&positionToDb(0,-150)===-150&&positionToDb(100)===10&&positionToDb(250)===10&&positionToDb(-5)===-90&&positionToDb(NaN)===-90&&positionToDb(undefined,-150)===-150,'positionToDb: position 0 is the floor (-90, or -150 when asked), 100 and more is +10, junk is the floor')
check(worst<0.01&&worst150<0.01,`positionToDb(faderPosition(dB)) returns dB within 0.01 dB over the whole range, for floor -90 and -150 (worst ${worst.toExponential(1)} / ${worst150.toExponential(1)})`)
check(FADE_CURVES.join()==='fader,ease,db'&&fadeLevelAt('fader',-90,0,1)===0&&fadeLevelAt('db',-20,5,1.5)===5&&fadeLevelAt('ease',-50,-10,0)===-50&&fadeLevelAt('db',-150,0,0.5,-90)===-45&&fadeLevelAt('db',-30,-200,0.5,-90)===-60&&fadeLevelAt('fader',-30,-150,1,-90)===-90&&fadeLevelAt('fader',0,40,1)===10,'fadeLevelAt: t >= 1 is exactly the clamped target, t = 0 the start, the lower limit holds')
check(fadeLevelAt('db',-150,0,0)===-90&&fadeLevelAt('db',-150,0,0.5)===-45&&fadeLevelAt('db',0,-150,0.5)===-45&&fadeLevelAt('db',0,-150,0.999)>-90.1&&fadeLevelAt('db',0,-150,1)===-150&&fadeLevelAt('db',-60,0,0.5)===-30,'a dB-linear fade from or to -inf runs between the -90 floor and the target (no silent stretch below -90); the last step is the exact target')
check(['fader','ease','db'].every((c)=>mono(c,-150,-100)&&mono(c,-100,-150)&&mono(c,-150,0)&&mono(c,-95,-20)&&mono(c,-20,-95)&&mono(c,5,-150)),'no fade curve ever steps outside its start..target range or reverses (including fades that stay below -90 dB)')
check(fadeLevelAt('fader',-150,-100,0.5)===-125,'a fade that stays below the -90 floor runs linearly in dB (-150 -> -100 is -125 halfway)')
check(good,'all six set-level actions end with a "Fade time" option (0..60000 ms, step 100, integer, no expression) and a curve dropdown shown only above 0 ms')
check(Object.keys(ctx.actions).filter((id)=>/_adjust$/.test(id)).every((id)=>!ctx.actions[id].options.some((o)=>o.id==='duration'))&&!ctx.actions.input_mute.options.some((o)=>o.id==='duration'),'adjust and switch actions have no fade option')
check(ctx.actions.fade_stop?.name==='Stop all fades'&&ctx.actions.fade_stop.options.length===0&&ctx.feedbacks.fade_running?.type==='boolean'&&ctx.feedbacks.fade_running.name==='A fade is running'&&'fades_running'inctx.variableDefs,'fade_stop action, fade_running feedback and fades_running variable are defined')
check(setIds.every((id)=>/fadetimeabove0/.test(ctx.actions[id].description)),'every set-level action describes the fade')
}
{
// the 'fader' curve: -90 -> 0 in 500 ms is ten evenly spaced steps in fader position, ending exactly on 0
check(FADE_STEP===45&&w.length===Math.ceil(500/FADE_STEP),`500 ms at one step per ${FADE_STEP} ms is ${Math.ceil(500/FADE_STEP)} writes (${w.length})`)
check(dbs.every((v,k)=>k===0||v>dbs[k-1])&&dbs[0]>-90&&dbs[0]<0&&dbs.at(-1)===0,`monotonic, the first write is above -90 and below the target, the last is exactly 0 (${dbs.map((v)=>Math.round(v*10)/10).join(', ')})`)
check(pos.every((p,k)=>near(p-(k?pos[k-1]:0),75/Math.ceil(500/FADE_STEP),0.01)),`the steps are even in fader position (${75/Math.ceil(500/FADE_STEP)} each: ${pos.map((p)=>Math.round(p*10)/10).join(', ')})`)
check(w.at(-1).at-t0>=495&&w.at(-1).at-t0<=700&&gaps[Math.floor(gaps.length/2)]>=35&&gaps[Math.floor(gaps.length/2)]<=75,`it ends between 500 and 700 ms after the action, one step about every ${FADE_STEP} ms (ended after ${w.at(-1).at-t0} ms, median step ${gaps[Math.floor(gaps.length/2)]} ms)`)
check(awaitwaitFor(()=>sent.slice(mark).some((m)=>m.address===`${faderAddr(11)}/?`),2000),'a confirm query follows the last step like after any write')
check(dbs.length===Math.ceil(500/FADE_STEP)&&dbs.every((v,k)=>near(v,-60+(60/Math.ceil(500/FADE_STEP))*(k+1),0.01))&&dbs.at(-1)===0,`'Linear in dB' moves the same dB every step (${dbs.map((v)=>Math.round(v*100)/100).join(', ')})`)
check(dbs.length>=15&&dbs.every((v,k)=>k===0||v>dbs[k-1])&&dbs.at(-1)===0&&d[0]<top*0.5&&d.at(-1)<top*0.25&&at>d.length*0.3&&at<d.length*0.7,`'ease' is slow at the start and the end and fastest in the middle (steps in fader position: first ${d[0].toFixed(1)}, fastest ${top.toFixed(1)} at step ${at+1}, last ${d.at(-1).toFixed(1)}; ${dbs.length} writes)`)
}
{
// another set, a fade, an adjust, fade_stop: each ends a running fade where it is
check(before>=3&&w.length===before+1&&w.at(-1).db===-20&&inst.fades.size===0&&near(S.getFader('input',14),-20),`a plain set ends the running fade: nothing is written after it but the set itself (${before} fade writes, then -20)`)
check(w.length===Math.ceil(300/FADE_STEP)&&w.at(-1).db===-70&&w.every((x,k)=>k===0||x.db<w[k-1].db)&&w[0].db<lastOfA&&w[0].db>-70,`the second fade takes over from where the first one was (${Math.round(lastOfA)} dB, rising) and runs down to -70 without a write from the first (${w.map((x)=>Math.round(x.db)).join(', ')})`)
check(inst.fades.size===2&&ctx.variables.fades_running===2,'two fades on different levels run at the same time (fades_running 2)')
awaitgo('fade_stop',{})
constmarkD=sent.length
constatStop=S.getFader('input',15)
awaitsleep(400)
check(inst.fades.size===0&&!sent.slice(markD).some((m)=>m.address===faderAddr(15)||m.address==='/Input_Channels/15/Aux_Send/1/send_level')&&S.getFader('input',15)===atStop&&atStop>-90&&atStop<0&&ctx.variables.fades_running===0&&cb('fade_running',{})===false,`fade_stop ends every fade, nothing more is written, the levels stay where they were (${Math.round(atStop)} dB) and fades_running reads 0`)
awaitgo('fade_stop',{})
check(inst.fades.size===0,'fade_stop with nothing running does nothing')
}
{
// aux send and the other fader kinds go through the same engine
check(m.length===Math.ceil(500/FADE_STEP)&&m[0]>-90&&m.at(-1)===0&&m.every((v)=>v>=-90&&v<=10),`from the -90 floor on the fader taper (${m.map((v)=>Math.round(v)).join(', ')})`)
check(logCount(/set without a fade/)===warns+1&&w.length===1&&w[0].db===-5&&inst.fades.size===0,'an unknown starting level writes the target at once and logs one warning')
// a fade waiting for its starting level is replaced by a later set
check(S.choices.groupChoices().map((c)=>c.id).join()===GROUPS.join()&&AUXES.length===16,`the mock has ${GROUPS.length} group busses and ${AUXES.length} aux busses`)
check(ids(ctx.actions.buss_fader_set.options,'buss').length===25&&labels(ctx.actions.buss_fader_set.options,'buss').includes('24 · Studio (group)')&&ids(ctx.actions.buss_mute.options,'busses').length===25,'the buss_* dropdowns still list all 25 busses with the suffix')
check(ctx.actions.group_fader_set.options.map((o)=>o.id).join()==='buss,level,duration,curve'&&ctx.actions.aux_fader_set.options.map((o)=>o.id).join()==='buss,level,duration,curve'&&ctx.actions.group_fader_adjust.options.map((o)=>o.id).join()==='buss,delta'&&ctx.actions.aux_mute.options.map((o)=>o.id).join()==='busses,state','option ids are the same as the buss_* ones (fade options on the set actions)')
check(ctx.actions.group_mute.name==='Group: mute'&&ctx.actions.group_solo.name==='Group: solo'&&ctx.actions.group_fader_set.name==='Group: set fader'&&ctx.actions.group_fader_adjust.name==='Group: adjust fader (relative)'&&ctx.actions.aux_mute.name==='Aux master: mute'&&ctx.actions.aux_solo.name==='Aux master: solo'&&ctx.actions.aux_fader_set.name==='Aux master: set fader'&&ctx.actions.aux_fader_adjust.name==='Aux master: adjust fader (relative)'&&ctx.actions.group_assign.name==='Group: assign input on/off','action names: "Group: ..." and "Aux master: ..." (group_assign keeps its name)')
check(/fade time above 0/.test(ctx.actions.group_fader_set.description)&&/fade time above 0/.test(ctx.actions.aux_fader_set.description),'the two set actions describe the fade')
check(oldActions.every((id)=>ctx.actions[id]?.name.startsWith('Any buss (aux or group): '))&&oldFeedbacks.every((id)=>ctx.feedbacks[id]?.name.startsWith('Any buss (aux or group)')),`the old buss_* ids still exist and their names start with "Any buss (aux or group)" (${oldActions.length} actions, ${oldFeedbacks.length} feedbacks)`)
check(ctx.feedbacks.group_muted.name==='Group: muted'&&ctx.feedbacks.aux_fader_pos.name==='Aux master: fader position (0-100, for gauges)'&&ctx.feedbacks.group_peak.name.startsWith('Group meter: ')&&ctx.feedbacks.aux_signal.name.startsWith('Aux master meter: ')&&!('group_fader_bar'inctx.feedbacks)&&!('aux_fader_bar'inctx.feedbacks),'feedback names, and no legacy image-bar variants for group / aux masters')
// writes: the group side for a group buss, the aux side for an aux
// feedbacks read the same state as the buss_* ones
check(cb('group_muted',{buss:24})===true&&cb('buss_muted',{buss:24})===true&&cb('aux_muted',{buss:3})===true&&cb('aux_muted',{buss:1})===false&&cb('group_solo',{buss:24})===true&&cb('aux_solo',{buss:3})===false&&cb('group_fader_value',{buss:24})==='-7'&&cb('aux_fader_value',{buss:1})==='-9'&&near(cb('group_fader_pos',{buss:24}),cb('buss_fader_pos',{buss:24}))&&near(cb('aux_fader_pos',{buss:2}),faderPosition(-22.5)),'group_* / aux_* feedbacks follow the buss state (muted, solo, fader text and position)')
check(namedGroups.join()===GROUPS.join()&&namedAux.length===15&&sec('groups')?.name==='Groups'&&sec('aux_masters')?.name==='Aux masters'&&!sec('busses')&&!sec('buss_meters'),`sections "Groups" (${namedGroups.length} named group busses) and "Aux masters" (${namedAux.length} named aux busses) replace "Busses"`)
check(idsOf(sec('groups')).length===2*namedGroups.length&&idsOf(sec('aux_masters')).length===2*namedAux.length&&sec('groups').definitions.map((d)=>d.name).join()==='Mute,Fader (knob)'&&sec('aux_masters').definitions.map((d)=>d.name).join()==='Mute,Fader (knob)','each has a Mute and a Fader (knob) preset per named buss of that type')
check(aMute.name==='Aux master mute: 4 · CLK-GD-MD'&&aMute.steps[0].down[0].actionId==='aux_mute'&&aMute.feedbacks[0].feedbackId==='aux_muted'&&aKnob.steps[0].rotate_left[0].actionId==='aux_fader_adjust'&&aKnob.localVariables.map((v)=>v.feedbackId).join()==='aux_fader_value,aux_fader_pos'&&aKnob.feedbacks[0].feedbackId==='aux_muted','aux master mute and knob presets use aux_*')
check(!Object.keys(P2).some((id)=>/^buss_(mute|knob|meter)_/.test(id))&&!JSON.stringify(P2).includes('"buss_'),'no preset refers to a buss_* action or feedback any more')
constgMeters=sec('meters_groups')
constaMeters=sec('meters_aux')
check(gMeters?.name==='Group meters'&&aMeters?.name==='Aux meters'&&idsOf(gMeters).length===9&&idsOf(aMeters).length===15&&P2.group_meter_24.localVariables.some((v)=>v.feedbackId==='group_meter_pos')&&P2.group_meter_24.feedbacks.map((f)=>f.feedbackId).join()==='group_signal,group_peak'&&P2.aux_meter_4.feedbacks.map((f)=>f.feedbackId).join()==='aux_signal,aux_peak'&&!sec('buss_meters'),'group meter and aux meter sections (9 and 15 meters) use group_* / aux_* meter feedbacks')
check(idsOf(gMeters).concat(idsOf(aMeters)).every((id)=>P2[id].type==='layered'),'the group and aux meter presets are layered gauges')
// The console-link port is taken: connection_failure, retried, and a config change ends the retrying
constblocker=awaitoccupy(P.linkB)
constbctx=makeContext()
constb=newInstance(bctx)
Object.assign(b.timing,fastTiming)
awaitb.init(cfgB)
constst=()=>bctx.statuses.at(-1)
check(st()?.[0]==='connection_failure'&&/Cannot bind local port \d+: EADDRINUSE/.test(st()[1])&&b.link===null,`an occupied local port is shown as connection_failure (${st()?.[1]})`)
awaitsleep(800)
check(b.link===null&&st()[0]==='connection_failure','it keeps retrying while the port stays occupied')
b.timing.writeGuardMs=0// control: without the guard the same clicks are lost to stale echoes
constcontrol=awaitclicks(40)
b.timing.writeGuardMs=400
check(control.settled<-0.5,`control: with no write guard, 10 clicks at 40 ms against a slow console lose clicks (ends at ${control.settled} dB, not 0)`)
for(constintervalof[40,100]){
constr=awaitclicks(interval)
check(near(r.atEnd,0)&&near(r.settled,0)&&r.written.length===10&&r.written.every((v,k)=>near(v,-9+k)),`10 clicks of +1 dB at ${interval} ms from -10 end at 0 dB and stay there (written ${r.written.map((v)=>Math.round(v)).join(',')}; end ${r.atEnd}, settled ${r.settled})`)
check(sent.slice(mark).filter((m)=>m.address==='/Input_Channels/2/fader/?').length===1&&near(SB.getFader('input',2),-20),'after the guard one confirm query asks for the value and the state agrees')
// the stale-value guard lets an equal value through and a later change from the surface is accepted
check(sent2.some((m)=>m.address==='/Input_Channels/40/Channel_Input/phantom/?')&&!sent2.some((m)=>m.address==='/Input_Channels/40/Channel_Input/phantom')&&logCount(/toggle not applied/)===warns+1&&took>=350&&took<1500,`a toggle on a state the console never answers queries, waits (${took} ms), warns and writes nothing (no default to on)`)
check(asks()===a0+1,'a lazy load is requested once, not on every evaluation')
awaitsleep(b.timing.replyWaitMs+150)
b.ensure('t:eq',['/Input_Channels/40/EQ/eq_in'])
awaitsleep(60)
check(asks()===a0+2,'an unanswered lazy load may be requested again after the wait')
b.ensure('t:eq',['/Input_Channels/40/EQ/eq_in'])
awaitsleep(60)
check(asks()===a0+2,'but not more than once per wait')
awaitb.destroy()
check(b.link===null&&b.relay===null,'destroy closes the link and the relay')
}
{
// The relay port is taken while the console link is fine: warning, retry, back to ok
constblocker=awaitoccupy(P.relayB)
constrctx=makeContext()
constr=newInstance(rctx)
Object.assign(r.timing,fastTiming)
awaitr.init(cfgB)
constst=()=>rctx.statuses.at(-1)
check(awaitwaitFor(()=>st()?.[0]==='unknown_warning',8000)&&st()[1]===`iPad relay: port ${P.relayB} in use`&&!!r.link&&r.relay===null,`an occupied relay port shows a warning while the console link is fine ("${st()?.[1]}")`)
check(r.connectedFlag===true&&r.relayRetryTimer!==null,'the console side keeps working and the relay bind is retried')
awaitsleep(800)
check(st()[0]==='unknown_warning','the warning stays while the port is occupied')
blocker.close()
check(awaitwaitFor(()=>!!r.relay&&st()?.[0]==='ok',5000),'when the port is free the retry opens the relay and the status goes back to ok')
check(off.length===0,`every address sent during the whole run is answered by the real console, a documented trigger node or a meter subscription${off.length?`: ${off.join(', ')}`:''}`)