Files
tickte-system/tests/unit/workersEvents.test.mjs
T
ClaudeandClaude Sonnet 5.5 66fe0f38ef Workers P2: eventToSteps verwirft Schritte ohne Ziel bei fehlender workerId
fx() setzt 'to' jetzt immer, sobald ein drittes Argument übergeben wird; stepIsValid prüft 'to' per Object.hasOwn. Ereignis 'eingang' ohne workerId liefert keinen Umschlag ohne Ziel mehr.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-18 01:22:51 +00:00

268 lines
19 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import test from 'node:test'
import assert from 'node:assert/strict'
import { loadModule } from './load.mjs'
// Ereignis-Logik der Hustler-Seite: Deduplizierung, Zustandsableitung, Animationsschritte
const E = await loadModule('src/lib/workersEvents.js')
const { ROLES } = await loadModule('src/lib/workersRoster.js')
const TS = '2026-10-17T20:00:00.000Z'
const ev = (kind, workerId, extra = {}) => ({ id: `${kind}-${workerId}`, ts: TS, workerId, kind, text: 'Text', ...extra })
test('EVENT_KINDS: 26 Arten aus Übersicht §5', () => {
assert.deepEqual([...E.EVENT_KINDS], ['auftrag', 'leerlauf', 'gelesen', 'fund', 'doppelt', 'uebergabe', 'bewertet', 'top3', 'botlead', 'entwurf', 'pult', 'gesendet', 'eingang', 'sortiert', 'bau_start', 'bau_fertig', 'pruefung_zurueck', 'pruefung_ok', 'vorschau_link', 'angebot', 'deal', 'gesperrt', 'budget_warnung', 'budget_stopp', 'pause', 'fehler'])
assert.deepEqual([...E.STEP_OPS], ['task', 'say', 'pose', 'give', 'fx', 'cheer'])
assert.deepEqual([...E.SPOTS], ['pult', 'tresor', 'aussen', 'archiv'])
})
test('takeNewEvents: dedupliziert nach id, sortiert nach ts, Gedächtnis begrenzt (Review Focus 3)', () => {
const seen = new Set()
const a = { id: 'a', ts: '2026-10-17T20:00:02Z' }
const b = { id: 'b', ts: '2026-10-17T20:00:01Z' }
assert.deepEqual(E.takeNewEvents(seen, [a, b]).map((e) => e.id), ['b', 'a'])
assert.deepEqual(E.takeNewEvents(seen, [a, b, { id: 'c', ts: '2026-10-17T20:00:03Z' }]).map((e) => e.id), ['c'])
const noId = { ts: '2026-10-17T20:00:04Z', workerId: 'kim', kind: 'gelesen', text: 'x' }
assert.equal(E.takeNewEvents(seen, [noId]).length, 1)
assert.equal(E.takeNewEvents(seen, [noId]).length, 0)
assert.deepEqual(E.takeNewEvents(seen, [null, {}, { id: 'z' }]), [])
const big = new Set()
E.takeNewEvents(big, Array.from({ length: 1500 }, (_, i) => ({ id: `i${i}`, ts: new Date(Date.UTC(2026, 9, 17, 0, 0, i)).toISOString() })))
assert.ok(big.size <= 1000)
})
test('takeNewEvents: gleiche Zeitstempel bleiben in Eingangsreihenfolge, Gedächtnis verwirft die ältesten', () => {
const seen = new Set()
const same = ['x1', 'x2', 'x3'].map((id) => ({ id, ts: TS }))
assert.deepEqual(E.takeNewEvents(seen, same).map((e) => e.id), ['x1', 'x2', 'x3'])
// Zwei Aufrufe über der Grenze: die zuerst gemerkten Ids fallen heraus, die neuesten bleiben
const big = new Set()
E.takeNewEvents(big, Array.from({ length: 1200 }, (_, i) => ({ id: `i${i}`, ts: new Date(Date.UTC(2026, 9, 17, 0, 0, i)).toISOString() })))
assert.equal(big.has('i1199'), true)
assert.equal(big.has('i0'), false)
// Ein junges Ereignis bleibt bekannt; ein längst vergessenes gälte wieder als neu (das Gedächtnis ist begrenzt)
assert.equal(E.takeNewEvents(big, [{ id: 'i1199', ts: new Date(Date.UTC(2026, 9, 17, 0, 19, 59)).toISOString() }]).length, 0)
assert.equal(E.takeNewEvents(big, [{ id: 'i0', ts: new Date(Date.UTC(2026, 9, 17, 0, 0, 0)).toISOString() }]).length, 1)
// Dasselbe Ereignis zweimal in einer Runde (Überlappung der Abfragen) zählt einmal
assert.equal(E.takeNewEvents(new Set(), [same[0], { ...same[0] }]).length, 1)
// Kein Array -> leer, Eingabe bleibt unverändert
assert.deepEqual(E.takeNewEvents(new Set(), null), [])
const input = [{ id: 'q', ts: '2026-10-17T20:00:02Z' }, { id: 'p', ts: '2026-10-17T20:00:01Z' }]
E.takeNewEvents(new Set(), input)
assert.deepEqual(input.map((e) => e.id), ['q', 'p'])
})
test('latestTs: größter gültiger Zeitstempel', () => {
assert.equal(E.latestTs([{ ts: '2026-10-17T20:00:01+00:00' }, { ts: '2026-10-17T20:00:05Z' }, { ts: 'kaputt' }], null), '2026-10-17T20:00:05Z')
assert.equal(E.latestTs([], 'alt'), 'alt')
assert.equal(E.latestTs([], null), null)
// Ein älterer Stand wird nicht überschrieben
assert.equal(E.latestTs([{ ts: '2026-10-17T20:00:01Z' }], '2026-10-17T20:00:09Z'), '2026-10-17T20:00:09Z')
assert.equal(E.latestTs(null), null)
})
test('reduceEvents: Status aus der Ereignisart, grau bleibt grau, Altes und Fremdes wird ignoriert (Review Focus 4)', () => {
const base = {
kim: { status: 'wartet', task: '', lastTs: null },
rita: { status: 'grau', task: '', lastTs: null },
bea: { status: 'arbeitet', task: 'alt', lastTs: '2026-10-17T20:00:09Z' },
}
const copy = JSON.parse(JSON.stringify(base))
const out = E.reduceEvents(base, [ev('gelesen', 'kim', { text: 'Kim hat 3 Gesuche gelesen' }), ev('fund', 'rita'), ev('fehler', 'bea'), ev('pause', 'system'), ev('fund', 'zed'), ev('auftrag', 'otto', { to: 'kim' })])
assert.deepEqual(base, copy)
assert.deepEqual(out.kim, { status: 'wartet', task: 'Text', lastTs: TS }) // zuletzt: Auftrag von Otto
assert.equal(out.rita.status, 'grau')
assert.equal(out.bea.status, 'arbeitet')
assert.equal(out.bea.task, 'alt') // Ereignis älter als lastTs
assert.equal(E.reduceEvents(base, [ev('leerlauf', 'kim')]).kim.status, 'leerlauf')
assert.equal(E.reduceEvents(base, [ev('fehler', 'kim')]).kim.status, 'fehler')
assert.equal(E.indexWorkers({ lastEventTs: TS, workers: [{ workerId: 'kim', status: 'arbeitet', task: 't', connected: false, counters: { a: 1 } }] }).kim.status, 'grau')
})
test('reduceEvents: Empfänger, Zusatzfelder, Reihenfolge und Randfälle', () => {
const base = {
kim: { status: 'leerlauf', task: '', lastTs: null, connected: true, counters: { gelesen: 4 } },
rita: { status: 'grau', task: '', lastTs: null, connected: false },
bea: { status: 'arbeitet', task: 'alt', lastTs: '2026-10-17T20:00:09Z' },
}
// Übergabe: Absender arbeitet, Empfänger wartet mit dem Text; Zusatzfelder bleiben
const out = E.reduceEvents(base, [ev('uebergabe', 'kim', { to: 'bea' }), ev('uebergabe', 'kim', { to: 'rita', id: 'u2' })])
assert.equal(out.kim.status, 'arbeitet')
assert.equal(out.kim.connected, true)
assert.deepEqual(out.kim.counters, { gelesen: 4 })
assert.equal(out.bea.task, 'alt') // Empfänger mit neuerem lastTs bleibt unberührt
assert.equal(out.bea.status, 'arbeitet')
assert.equal(out.rita.status, 'grau') // grauer Empfänger bleibt grau
assert.notEqual(out.kim, base.kim) // Einträge sind Kopien
assert.notEqual(out.rita, base.rita)
// Empfänger ohne Eintrag oder unbekannt: kein Wurf, kein neuer Eintrag
const out2 = E.reduceEvents(base, [ev('auftrag', 'otto', { to: 'zed' }), ev('uebergabe', 'kim', { to: 'kim', id: 'self' })])
assert.equal('zed' in out2, false)
assert.equal('otto' in out2, false)
assert.equal(out2.kim.status, 'arbeitet') // Selbstadressierung macht aus dem Absender keinen Wartenden
// Spätere Ereignisse gewinnen, ältere derselben Runde wirken nicht mehr
const later = { id: 'l', ts: '2026-10-17T20:00:05Z', workerId: 'kim', kind: 'leerlauf', text: 'später' }
const earlier = { id: 'e', ts: '2026-10-17T20:00:01Z', workerId: 'kim', kind: 'fund', text: 'früher' }
const out3 = E.reduceEvents(base, [later, earlier])
assert.deepEqual([out3.kim.status, out3.kim.task, out3.kim.lastTs], ['leerlauf', 'später', '2026-10-17T20:00:05Z'])
// Ereignis ohne gültiges ts wirkt nicht; kaputte Eingaben werfen nicht
assert.equal(E.reduceEvents(base, [{ id: 'k', ts: 'kaputt', workerId: 'kim', kind: 'fehler', text: 'x' }]).kim.status, 'leerlauf')
assert.deepEqual(E.reduceEvents({}, [ev('fund', 'kim')]), {})
assert.deepEqual(E.reduceEvents(null, null), {})
assert.doesNotThrow(() => E.reduceEvents(base, [null, 7, {}, { ts: TS }]))
// Eigenschaften der Prototypkette sind keine Worker
assert.deepEqual(E.reduceEvents({}, [ev('fund', 'constructor'), ev('fund', '__proto__')]), {})
// Kein Text: Aufgabe bleibt leer statt „undefined“
assert.equal(E.reduceEvents(base, [{ id: 'n', ts: TS, workerId: 'kim', kind: 'gelesen' }]).kim.task, '')
})
test('indexWorkers: grau bei connected false, lastTs aus lastEventTs, Randfälle', () => {
const idx = E.indexWorkers({
lastEventTs: TS,
workers: [
{ workerId: 'kim', status: 'arbeitet', task: 'liest', connected: true, counters: { gelesen: 2 } },
{ workerId: 'rita', status: 'wartet', task: 'x', connected: false },
{ workerId: 'bea', status: 'leerlauf' },
{ task: 'ohne Kennung' },
null,
],
})
assert.deepEqual(Object.keys(idx), ['kim', 'rita', 'bea'])
assert.deepEqual(idx.kim, { status: 'arbeitet', task: 'liest', lastTs: TS, connected: true, counters: { gelesen: 2 } })
assert.equal(idx.rita.status, 'grau')
assert.equal(idx.rita.connected, false)
assert.deepEqual(idx.bea, { status: 'leerlauf', task: '', lastTs: TS, connected: true, counters: {} })
assert.deepEqual(E.indexWorkers(null), {})
assert.deepEqual(E.indexWorkers({}), {})
assert.equal(E.indexWorkers({ workers: [{ workerId: 'kim', status: 'wartet' }] }).kim.lastTs, null)
})
test('eventToSteps: feste Zeilen', () => {
assert.deepEqual(E.eventToSteps(ev('gelesen', 'kim', { meta: { count: 3 } })), [{ op: 'pose', who: 'kim', pose: 'type' }, { op: 'task', who: 'kim', text: 'Text' }, { op: 'say', who: 'kim', text: '3 gelesen' }])
assert.deepEqual(E.eventToSteps(ev('doppelt', 'arne')), [{ op: 'task', who: 'arne', text: 'Text' }, { op: 'say', who: 'arne', text: 'Schon bekannt' }, { op: 'fx', fx: 'stampNo', at: 'arne' }])
assert.deepEqual(E.eventToSteps(ev('uebergabe', 'rita', { to: 'bea' })), [{ op: 'task', who: 'rita', text: 'Text' }, { op: 'give', from: 'rita', to: 'bea' }])
assert.deepEqual(E.eventToSteps(ev('pruefung_zurueck', 'paul', { to: 'bob', meta: { round: 2 } })), [{ op: 'task', who: 'paul', text: 'Text' }, { op: 'fx', fx: 'stampNo', at: 'paul' }, { op: 'say', who: 'paul', text: 'Runde 2 zurück' }, { op: 'give', from: 'paul', to: 'bob' }])
assert.deepEqual(E.eventToSteps(ev('deal', 'system')), [0, 0.4, 0.8].map((delay) => ({ op: 'fx', fx: 'coin', at: 'aussen', to: 'tresor', delay })).concat([{ op: 'cheer', who: 'otto' }, { op: 'say', who: 'otto', text: 'Deal! Super!' }]))
// system: kein task-Schritt, Arne stempelt
assert.deepEqual(E.eventToSteps(ev('gesperrt', 'system')), [{ op: 'fx', fx: 'stampNo', at: 'arne' }, { op: 'say', who: 'arne', text: 'Gesperrt' }])
})
test('eventToSteps: Tabelle der übrigen Arten', () => {
const steps = (kind, w, extra) => E.eventToSteps(ev(kind, w, extra))
assert.deepEqual(steps('auftrag', 'otto', { to: 'kim' }), [{ op: 'task', who: 'otto', text: 'Text' }, { op: 'say', who: 'otto', text: 'Kim, los!' }, { op: 'say', who: 'kim', text: 'Verstanden' }])
assert.deepEqual(steps('leerlauf', 'bob'), [{ op: 'task', who: 'bob', text: 'Text' }, { op: 'pose', who: 'bob', pose: 'coffee' }])
assert.deepEqual(steps('gelesen', 'kim'), [{ op: 'pose', who: 'kim', pose: 'type' }, { op: 'task', who: 'kim', text: 'Text' }, { op: 'say', who: 'kim', text: 'Gelesen' }])
assert.deepEqual(steps('fund', 'ali'), [{ op: 'pose', who: 'ali', pose: 'type' }, { op: 'task', who: 'ali', text: 'Text' }, { op: 'say', who: 'ali', text: 'Neuer Fund!' }, { op: 'fx', fx: 'paper', at: 'ali' }])
assert.deepEqual(steps('bewertet', 'bea', { meta: { score: 87 } }), [{ op: 'pose', who: 'bea', pose: 'type' }, { op: 'task', who: 'bea', text: 'Text' }, { op: 'say', who: 'bea', text: '87 Punkte' }])
assert.equal(steps('bewertet', 'bea').at(-1).text, 'Bewertet')
assert.deepEqual(steps('top3', 'bea'), [{ op: 'task', who: 'bea', text: 'Text' }, { op: 'say', who: 'bea', text: 'Top 3!' }, { op: 'fx', fx: 'stampOk', at: 'bea' }])
assert.deepEqual(steps('botlead', 'arne'), [{ op: 'task', who: 'arne', text: 'Text' }, { op: 'say', who: 'arne', text: 'Abgelegt' }, { op: 'fx', fx: 'drawer', at: 'arne' }])
assert.deepEqual(steps('entwurf', 'max'), [{ op: 'pose', who: 'max', pose: 'type' }, { op: 'task', who: 'max', text: 'Text' }, { op: 'say', who: 'max', text: 'Entwurf fertig' }])
assert.deepEqual(steps('pult', 'clara'), [{ op: 'task', who: 'clara', text: 'Text' }, { op: 'say', who: 'clara', text: 'Zur Freigabe' }, { op: 'fx', fx: 'envelope', at: 'clara', to: 'pult' }])
assert.deepEqual(steps('gesendet', 'max'), [{ op: 'task', who: 'max', text: 'Text' }, { op: 'say', who: 'max', text: 'Raus damit!' }, { op: 'fx', fx: 'envelope', at: 'max', to: 'aussen' }])
assert.deepEqual(steps('gesendet', 'system'), [{ op: 'fx', fx: 'envelope', at: 'pult', to: 'aussen' }])
assert.deepEqual(steps('eingang', 'nora'), [{ op: 'task', who: 'nora', text: 'Text' }, { op: 'fx', fx: 'envelope', at: 'aussen', to: 'nora' }, { op: 'say', who: 'nora', text: 'Antwort!' }])
assert.deepEqual(steps('bau_start', 'bob'), [{ op: 'pose', who: 'bob', pose: 'type' }, { op: 'task', who: 'bob', text: 'Text' }, { op: 'say', who: 'bob', text: 'Los geht’s' }])
assert.deepEqual(steps('bau_fertig', 'ben'), [{ op: 'task', who: 'ben', text: 'Text' }, { op: 'say', who: 'ben', text: 'Fertig!' }, { op: 'give', from: 'ben', to: 'paul' }])
assert.deepEqual(steps('pruefung_ok', 'paul'), [{ op: 'task', who: 'paul', text: 'Text' }, { op: 'fx', fx: 'stampOk', at: 'paul' }, { op: 'say', who: 'paul', text: 'Passt!' }])
assert.equal(steps('pruefung_zurueck', 'paul', { to: 'bob' }).find((s) => s.op === 'say').text, 'Zurück')
assert.deepEqual(steps('vorschau_link', 'max'), [{ op: 'task', who: 'max', text: 'Text' }, { op: 'say', who: 'max', text: 'Link raus' }, { op: 'fx', fx: 'envelope', at: 'max', to: 'aussen' }])
assert.deepEqual(steps('angebot', 'theo'), [{ op: 'task', who: 'theo', text: 'Text' }, { op: 'say', who: 'theo', text: 'Kauf-Link!' }, { op: 'fx', fx: 'paper', at: 'theo' }])
assert.deepEqual(steps('gesperrt', 'nora'), [{ op: 'task', who: 'nora', text: 'Text' }, { op: 'fx', fx: 'stampNo', at: 'nora' }, { op: 'say', who: 'nora', text: 'Gesperrt' }])
assert.deepEqual(steps('budget_warnung', 'otto'), [{ op: 'task', who: 'otto', text: 'Text' }, { op: 'say', who: 'otto', text: 'Budget 80 %' }, { op: 'fx', fx: 'alert', at: 'otto' }])
assert.deepEqual(steps('budget_stopp', 'otto'), [{ op: 'task', who: 'otto', text: 'Text' }, { op: 'say', who: 'otto', text: 'Budget leer' }, { op: 'fx', fx: 'alert', at: 'otto' }])
assert.deepEqual(steps('pause', 'system'), [])
assert.deepEqual(steps('pause', 'otto'), [])
assert.deepEqual(steps('fehler', 'bea'), [{ op: 'task', who: 'bea', text: 'Text' }, { op: 'fx', fx: 'alert', at: 'bea' }, { op: 'say', who: 'bea', text: 'Fehler' }])
})
test('eventToSteps: sortiert nach meta.classification', () => {
const say = (classification) => E.eventToSteps(ev('sortiert', 'nora', { meta: classification === undefined ? null : { classification } })).find((s) => s.op === 'say').text
const erwartet = { ja: 'Ja!', frage: 'Frage', nein: 'Nein', abmelden: 'Abmeldung', abwesend: 'Abwesend', unzustellbar: 'Unzustellbar', beschwerde: 'Beschwerde!', unklar: 'Unklar', irgendwas: 'Sortiert', constructor: 'Sortiert' }
for (const [k, v] of Object.entries(erwartet)) assert.equal(say(k), v, k)
assert.equal(say(undefined), 'Sortiert')
assert.deepEqual(E.eventToSteps(ev('sortiert', 'nora', { meta: { classification: 'ja' } })).map((s) => s.op), ['task', 'fx', 'say'])
})
test('eventToSteps: meta als Objekt, JSON-Text oder null', () => {
const zeile = (meta) => E.eventToSteps(ev('gelesen', 'kim', { meta })).find((s) => s.op === 'say').text
assert.equal(zeile({ count: 5 }), '5 gelesen')
assert.equal(zeile('{"count":7}'), '7 gelesen')
assert.equal(zeile(null), 'Gelesen')
assert.equal(zeile(undefined), 'Gelesen')
assert.equal(zeile('kein json'), 'Gelesen')
assert.equal(zeile('[1,2]'), 'Gelesen')
assert.equal(zeile({ count: '5' }), 'Gelesen')
assert.equal(zeile({ count: NaN }), 'Gelesen')
})
test('eventToSteps: jede Art liefert gültige Schritte, Blasen ≤ 20 Zeichen, Unbekanntes wird verworfen (Review Focus 4)', () => {
const ids = new Set(ROLES.map((r) => r.workerId))
const known = (v) => ids.has(v) || E.SPOTS.includes(v)
for (const kind of E.EVENT_KINDS) {
for (const w of ['kim', 'system', 'zed']) {
for (const s of E.eventToSteps(ev(kind, w, { to: 'bea', meta: { count: 99999999, score: 123456789, round: 2, classification: 'ja' } }))) {
assert.ok(E.STEP_OPS.includes(s.op), kind)
if (s.op === 'say') assert.ok(s.text.length <= 20, s.text)
if (s.op === 'fx') {
assert.ok(known(s.at), `${kind}/${w}: at=${s.at}`)
if (s.to) assert.ok(known(s.to))
} else {
for (const k of ['who', 'from', 'to']) if (s[k]) assert.ok(ids.has(s[k]), `${kind}/${w}: ${k}=${s[k]}`)
}
}
}
}
assert.deepEqual(E.eventToSteps(ev('fund', 'zed')), [])
assert.deepEqual(E.eventToSteps(ev('fund', 'system')), [])
assert.deepEqual(E.eventToSteps(ev('neueArt', 'kim')), [{ op: 'task', who: 'kim', text: 'Text' }])
assert.deepEqual(E.eventToSteps(ev('neueArt', 'zed')), [])
assert.deepEqual(E.eventToSteps(ev('uebergabe', 'rita', { to: 'zed' })), [{ op: 'task', who: 'rita', text: 'Text' }])
})
test('eventToSteps: fehlende oder fremde workerId erzeugt nirgends einen Schritt ohne Ziel (Review Focus 4)', () => {
const ids = new Set(ROLES.map((r) => r.workerId))
const known = (v) => ids.has(v) || E.SPOTS.includes(v)
// eingang ohne workerId: weder der Umschlag ohne Ziel noch andere Schritte bleiben übrig
assert.deepEqual(E.eventToSteps({ kind: 'eingang', ts: TS, text: 'Text' }), [])
assert.deepEqual(E.eventToSteps({ kind: 'eingang', ts: TS, text: 'Text', workerId: null }), [])
assert.deepEqual(E.eventToSteps(ev('eingang', 'zed')), [])
assert.deepEqual(E.eventToSteps(ev('eingang', 'max')).map((s) => s.op), ['task', 'fx', 'say'])
// jede Art mit fehlender, leerer, falsch getypter und fremder workerId: jeder vorhandene Verweis (auch ein ausdrücklich undefinierter) muss bekannt sein
for (const kind of E.EVENT_KINDS) {
for (const w of [undefined, null, '', 7, {}, 'zed', 'system']) {
const e = { id: `${kind}-x`, ts: TS, kind, text: 'Text', meta: { count: 3, score: 4, round: 2 } }
if (w !== undefined) e.workerId = w
for (const s of E.eventToSteps(e)) {
const label = `${kind}/${String(w)}: ${JSON.stringify(s)}`
if (s.op === 'fx') {
assert.ok(known(s.at), label)
if ('to' in s) assert.ok(known(s.to), label)
} else {
for (const k of ['who', 'from', 'to']) if (k in s) assert.ok(ids.has(s[k]), label)
}
}
}
}
})
test('eventToSteps: kaputte Ereignisse werfen nicht, Text wird gekürzt statt verworfen', () => {
for (const bad of [null, undefined, 7, 'x', [], {}]) assert.deepEqual(E.eventToSteps(bad), [])
assert.deepEqual(E.eventToSteps({ kind: 'gelesen', workerId: 'kim' }), [{ op: 'pose', who: 'kim', pose: 'type' }, { op: 'task', who: 'kim', text: '' }, { op: 'say', who: 'kim', text: 'Gelesen' }])
const lang = 'x'.repeat(300)
assert.equal(E.eventToSteps(ev('doppelt', 'arne', { text: lang }))[0].text, lang) // task-Text bleibt vollständig, nur Blasen werden gekürzt
// Auftrag an eine unbekannte Kennung: nur Ottos Aufgabe, keine Blasen für Fremde
assert.deepEqual(E.eventToSteps(ev('auftrag', 'otto', { to: 'zed' })).map((s) => s.op), ['task'])
})
test('needsStateRefresh und cutBubble', () => {
assert.equal(E.needsStateRefresh([ev('gelesen', 'kim'), ev('pult', 'clara')]), true)
assert.equal(E.needsStateRefresh([ev('gelesen', 'kim'), ev('fund', 'kim')]), false)
for (const k of ['pult', 'gesendet', 'deal', 'botlead', 'gesperrt', 'budget_warnung', 'budget_stopp', 'pause', 'top3', 'angebot']) assert.equal(E.needsStateRefresh([ev(k, 'otto')]), true, k)
assert.equal(E.needsStateRefresh([]), false)
assert.equal(E.needsStateRefresh(null), false)
assert.equal(E.cutBubble('Dies ist ein sehr langer Satz!'), 'Dies ist ein sehr la')
assert.equal(E.cutBubble(null), '')
assert.equal(E.cutBubble('kurz'), 'kurz')
assert.equal(E.cutBubble(12345), '') // nur Text wird gekürzt, alles andere ist leer
})