From a92d93a73dab07e8e906efe24b98a8c93d808883 Mon Sep 17 00:00:00 2001 From: GermanBluefox Date: Mon, 25 May 2026 16:06:16 +0200 Subject: [PATCH 1/2] Translated to english --- src/lib/sandbox.ts | 26 +- src/main.ts | 5 +- test/io-improvements.test.js | 292 +++++++++--------- test/performance-improvements.test.js | 212 ++++++------- test/performance.test.js | 116 +++---- test/testAiProviderResolver.js | 100 +++--- test/testAnthropicAdapter.js | 116 +++---- test/testFunctions.js | 417 +++++++++++++------------- test/testMainPerformance.js | 50 +-- test/testMirror.js | 12 +- test/testScheduler.js | 10 +- test/testSchedulerBugfixes.js | 44 +-- test/testTypeScript.js | 26 +- 13 files changed, 714 insertions(+), 712 deletions(-) diff --git a/src/lib/sandbox.ts b/src/lib/sandbox.ts index dcd9aeeae..4cabac131 100644 --- a/src/lib/sandbox.ts +++ b/src/lib/sandbox.ts @@ -97,7 +97,7 @@ export function sandBox( } }); } else { - // IO-3: Object.assign statt Object.keys().forEach() – kein temporäres Keys-Array + // IO-3: Object.assign instead of Object.keys().forEach() – no temporary keys array adapter.getForeignStates(pattern, (_err, _states) => _states && Object.assign(states, _states)); } } else { @@ -533,7 +533,7 @@ export function sandBox( if (!(adapter.config as JavaScriptAdapterConfig).subscribe && context.interimStateValues[id]) { // if the state is changed, we will compare it with interimStateValues const oldState = context.interimStateValues[id]; - // IO-1: for…in statt Object.keys().filter().every() – kein temporäres Array pro Aufruf + // IO-1: for…in instead of Object.keys().filter().every() – no temporary array per call let stateHasChanged = false; for (const attr in stateAsObject) { if (attr === 'ts') { @@ -1027,7 +1027,7 @@ export function sandBox( } } - // IO-2: O(1) Deduplizierung via Set statt O(n²) resUnique.includes() + // IO-2: O(1) deduplication via Set instead of O(n²) resUnique.includes() const resUnique: string[] = [...new Set(res)]; for (let i = 0; i < resUnique.length; i++) { @@ -1691,7 +1691,7 @@ export function sandBox( if (oPattern?.id && Array.isArray(oPattern.id)) { const result: (IobSchedule | string | null | undefined)[] = []; for (let t = 0; t < oPattern.id.length; t++) { - // IO-4: Spread statt JSON.parse(JSON.stringify()) – kein tiefer Clone nötig (nur primitive Felder) + // IO-4: Spread instead of JSON.parse(JSON.stringify()) – no deep clone needed (only primitive fields) const pa: Pattern = { ...oPattern, id: oPattern.id[t] }; result.push( sandbox.subscribe(pa, callbackOrChangeTypeOrId, value) as @@ -1944,7 +1944,7 @@ export function sandBox( // Subscribe to all members of enum for (const objId of members) { - // IO-6: `in` Operator statt Object.keys().includes() – O(1) statt O(n) + // IO-6: `in` operator instead of Object.keys().includes() – O(1) instead of O(n) if (!(objId in subscriptions)) { if (objects?.[objId]?.type === 'state') { // Just subscribe to states @@ -2653,14 +2653,14 @@ export function sandBox( Object.keys(context.scripts).forEach( name => context.scripts[name].schedules && - // IO-8: Spread statt JSON.parse(JSON.stringify()) – _ioBroker hat nur primitive Felder + // IO-8: Spread instead of JSON.parse(JSON.stringify()) – _ioBroker has only primitive fields context.scripts[name].schedules.forEach(s => schedules.push({ ...s._ioBroker } as unknown as ScheduleName), ), ); } else { script.schedules && - // IO-8: Spread statt JSON.parse(JSON.stringify()) + // IO-8: Spread instead of JSON.parse(JSON.stringify()) script.schedules.forEach(s => schedules.push({ ...s._ioBroker } as unknown as ScheduleName)); } return schedules; @@ -2863,7 +2863,7 @@ export function sandBox( delete timers[id]; } } - // IO-7: timersByScript Reverse-Index aktualisieren wenn State keine Timer mehr hat + // IO-7: update the timersByScript reverse-index when a state has no more timers if (!timers[id]) { for (const scriptName of removedScripts) { const stateIds = context.timersByScript.get(scriptName); @@ -4093,7 +4093,7 @@ export function sandBox( errorInCallback(err as Error); } }, ms); - // IO-10: Set.add() – O(1) statt Array.push() + // IO-10: Set.add() – O(1) instead of Array.push() script.intervals.add(int); if (sandbox.verbose) { @@ -4105,7 +4105,7 @@ export function sandBox( return null; }, clearInterval: function (id: NodeJS.Timeout): void { - // IO-10: Set.has/delete – O(1) statt Array.indexOf+splice O(n) + // IO-10: Set.has/delete – O(1) instead of Array.indexOf+splice O(n) if (script.intervals.has(id)) { if (sandbox.verbose) { sandbox.log('clearInterval() => cleared', 'info'); @@ -4121,7 +4121,7 @@ export function sandBox( setTimeout: function (callback: (args?: any[]) => void, ms: number, ...args: any[]): NodeJS.Timeout | null { if (typeof callback === 'function') { const to = setTimeout(() => { - // IO-10: Set.delete – O(1) statt Array.indexOf+splice O(n) + // IO-10: Set.delete – O(1) instead of Array.indexOf+splice O(n) script.timeouts.delete(to); try { @@ -4133,7 +4133,7 @@ export function sandBox( if (sandbox.verbose) { sandbox.log(`setTimeout(ms=${ms})`, 'info'); } - // IO-10: Set.add – O(1) statt Array.push + // IO-10: Set.add – O(1) instead of Array.push script.timeouts.add(to); return to; } @@ -4141,7 +4141,7 @@ export function sandBox( return null; }, clearTimeout: function (id: NodeJS.Timeout): void { - // IO-10: Set.has/delete – O(1) statt Array.indexOf+splice O(n) + // IO-10: Set.has/delete – O(1) instead of Array.indexOf+splice O(n) if (script.timeouts.has(id)) { if (sandbox.verbose) { sandbox.log('clearTimeout() => cleared', 'info'); diff --git a/src/main.ts b/src/main.ts index 35383fbe8..0742f2b6e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -849,8 +849,6 @@ class JavaScript extends Adapter { async onReady(): Promise { this.errorLogFunction = this.log; this.context.errorLogFunction = this.log; - // Precompute once – avoids string template alloc on every setState call - this.config.maxSetStatePerMinute = parseInt(this.config.maxSetStatePerMinute as unknown as string, 10) || 1000; this.config.maxTriggersPerScript = parseInt(this.config.maxTriggersPerScript as unknown as string, 10) || 100; @@ -2186,7 +2184,8 @@ class JavaScript extends Adapter { // remember all IDs – sort once to guarantee the sorted invariant // required by binaryIndexOf() / sortedInsert() used later - for (const id of Object.keys(res).sort()) { + const keys = Object.keys(res).sort(); + for (const id of keys) { this.stateIds.push(id); this.stateIdSet.add(id); } diff --git a/test/io-improvements.test.js b/test/io-improvements.test.js index 5638b79be..21165b455 100644 --- a/test/io-improvements.test.js +++ b/test/io-improvements.test.js @@ -1,16 +1,16 @@ 'use strict'; /** - * Regression-Tests für 10 I/O-Performance-Probleme in src/lib/sandbox.ts - * Geschrieben VOR den Änderungen als Baseline + Expectation. + * Regression tests for 10 I/O performance issues in src/lib/sandbox.ts + * Written BEFORE the changes as baseline + expectation. * - * [BASELINE] – dokumentiert das heutige (schlechtere) Verhalten - * [EXPECTATION] – verifiziert das verbesserte Verhalten nach dem Fix + * [BASELINE] – documents today's (worse) behavior + * [EXPECTATION] – verifies the improved behavior after the fix * * npx mocha test/io-improvements.test.js --timeout 30000 */ const assert = require('node:assert').strict; -/** Misst ms für fn() in `iterations` Wiederholungen */ +/** Measures ms for fn() over `iterations` repetitions */ function bench(fn, iterations = 1) { const t0 = performance.now(); for (let i = 0; i < iterations; i++) fn(); @@ -20,8 +20,8 @@ function bench(fn, iterations = 1) { // ───────────────────────────────────────────────────────────────────────────── // Problem 1: setStateChanged – Object.keys().filter().every() vs for…in // ───────────────────────────────────────────────────────────────────────────── -describe('IO-1 · setStateChanged – Array-Allokation vermeiden', () => { - /** ALTE Implementierung – alloziert attrs[] bei jedem Aufruf */ +describe('IO-1 · setStateChanged – avoid array allocation', () => { + /** OLD implementation – allocates attrs[] on every call */ function hasChangedOld(stateAsObject, oldState) { const attrs = Object.keys(stateAsObject).filter( attr => attr !== 'ts' && stateAsObject[attr] !== undefined, @@ -29,7 +29,7 @@ describe('IO-1 · setStateChanged – Array-Allokation vermeiden', () => { return !attrs.every(attr => stateAsObject[attr] === oldState[attr]); } - /** NEUE Implementierung – kein temporäres Array */ + /** NEW implementation – no temporary array */ function hasChangedNew(stateAsObject, oldState) { for (const attr in stateAsObject) { if (attr === 'ts') continue; @@ -41,51 +41,51 @@ describe('IO-1 · setStateChanged – Array-Allokation vermeiden', () => { const makeState = val => ({ val, ack: true, from: 'system.adapter.js.0', q: 0, lc: 1000, ts: Date.now() }); - it('[EXPECTATION] Beide Implementierungen erkennen Änderung korrekt', () => { + it('[EXPECTATION] Both implementations detect a change correctly', () => { const s1 = makeState(42); const s2 = makeState(43); assert.equal(hasChangedOld(s2, s1), true); assert.equal(hasChangedNew(s2, s1), true); }); - it('[EXPECTATION] Beide Implementierungen erkennen KEINE Änderung korrekt', () => { + it('[EXPECTATION] Both implementations detect NO change correctly', () => { const s1 = makeState(42); const s2 = makeState(42); assert.equal(hasChangedOld(s2, s1), false); assert.equal(hasChangedNew(s2, s1), false); }); - it('[EXPECTATION] ts-Feld wird bei beiden korrekt ignoriert', () => { + it('[EXPECTATION] The ts field is ignored correctly by both', () => { const s1 = makeState(42); - const s2 = { ...makeState(42), ts: Date.now() + 999 }; // nur ts geändert - assert.equal(hasChangedOld(s2, s1), false, 'Old: nur ts-Änderung darf kein Change sein'); - assert.equal(hasChangedNew(s2, s1), false, 'New: nur ts-Änderung darf kein Change sein'); + const s2 = { ...makeState(42), ts: Date.now() + 999 }; // only ts changed + assert.equal(hasChangedOld(s2, s1), false, 'Old: a ts-only change must not be a change'); + assert.equal(hasChangedNew(s2, s1), false, 'New: a ts-only change must not be a change'); }); - it('[EXPECTATION] for…in ist bei 100k Aufrufen nicht schlechter als filter().every()', () => { + it('[EXPECTATION] for…in is not worse than filter().every() over 100k calls', () => { const s1 = makeState(42); const s2 = makeState(42); const tOld = bench(() => hasChangedOld(s2, s1), 100_000); const tNew = bench(() => hasChangedNew(s2, s1), 100_000); assert.ok(tNew <= tOld * 1.5, - `for…in (${tNew.toFixed(1)}ms) darf nicht deutlich schlechter als filter (${tOld.toFixed(1)}ms) sein`); + `for…in (${tNew.toFixed(1)}ms) must not be significantly worse than filter (${tOld.toFixed(1)}ms)`); }); - it('[BASELINE] Object.keys().filter() alloziert pro Aufruf ein temporäres Array', () => { + it('[BASELINE] Object.keys().filter() allocates a temporary array per call', () => { const s = makeState(10); - // Wir können nicht direkt Array-Allokation messen, aber wir prüfen - // dass die alte Variante ein Array zurückgibt (Designnachweis) + // We cannot measure array allocation directly, but we verify + // that the old variant returns an array (design proof) const attrs = Object.keys(s).filter(a => a !== 'ts' && s[a] !== undefined); - assert.ok(Array.isArray(attrs), 'filter() gibt immer ein neues Array zurück'); + assert.ok(Array.isArray(attrs), 'filter() always returns a new array'); assert.ok(attrs.length > 0); }); }); // ───────────────────────────────────────────────────────────────────────────── -// Problem 2: $() Selector – O(n²) Deduplizierung mit resUnique.includes() +// Problem 2: $() Selector – O(n²) deduplication with resUnique.includes() // ───────────────────────────────────────────────────────────────────────────── -describe('IO-2 · $() Selector – O(n²) Deduplizierung', () => { - /** ALTE Implementierung */ +describe('IO-2 · $() Selector – O(n²) deduplication', () => { + /** OLD implementation */ function deduplicateOld(res) { const resUnique = []; for (let i = 0; i < res.length; i++) { @@ -96,7 +96,7 @@ describe('IO-2 · $() Selector – O(n²) Deduplizierung', () => { return resUnique; } - /** NEUE Implementierung */ + /** NEW implementation */ function deduplicateNew(res) { return [...new Set(res)]; } @@ -107,43 +107,43 @@ describe('IO-2 · $() Selector – O(n²) Deduplizierung', () => { return [...unique, ...dupes].sort(() => Math.random() - 0.5); } - it('[EXPECTATION] Beide liefern dieselben eindeutigen IDs', () => { + it('[EXPECTATION] Both return the same unique IDs', () => { const ids = makeIds(100); const old = deduplicateOld(ids).sort(); const newD = deduplicateNew(ids).sort(); assert.deepEqual(old, newD); }); - it('[EXPECTATION] Keine Duplikate im Ergebnis', () => { + it('[EXPECTATION] No duplicates in the result', () => { const ids = ['a', 'b', 'a', 'c', 'b', 'a']; const result = deduplicateNew(ids); assert.deepEqual(result.sort(), ['a', 'b', 'c']); assert.equal(result.length, 3); }); - it('[EXPECTATION] Set-Variante ist bei 2.000 IDs mit 30% Duplikaten schneller', () => { + it('[EXPECTATION] The Set variant is faster with 2,000 IDs and 30% duplicates', () => { const ids = makeIds(2_000); const tOld = bench(() => deduplicateOld(ids), 500); const tNew = bench(() => deduplicateNew(ids), 500); assert.ok(tNew < tOld, - `Set (${tNew.toFixed(1)}ms) muss schneller sein als includes (${tOld.toFixed(1)}ms)`); + `Set (${tNew.toFixed(1)}ms) must be faster than includes (${tOld.toFixed(1)}ms)`); }); - it('[BASELINE] includes() ist O(n) – Beweis per Messung', () => { + it('[BASELINE] includes() is O(n) – proof by measurement', () => { const small = Array.from({ length: 100 }, (_, i) => `id.${i}`); const large = Array.from({ length: 5_000 }, (_, i) => `id.${i}`); const tSmall = bench(() => deduplicateOld(small), 1_000); const tLarge = bench(() => deduplicateOld(large), 1_000); - // O(n²): 50× mehr Elemente → mindestens 20× mehr Zeit + // O(n²): 50× more elements → at least 20× more time assert.ok(tLarge > tSmall * 5, - `Großes Array (${tLarge.toFixed(1)}ms) muss deutlich länger dauern als kleines (${tSmall.toFixed(1)}ms)`); + `Large array (${tLarge.toFixed(1)}ms) must take significantly longer than small (${tSmall.toFixed(1)}ms)`); }); }); // ───────────────────────────────────────────────────────────────────────────── -// Problem 3: subscribePattern – Object.keys().forEach() statt Object.assign() +// Problem 3: subscribePattern – Object.keys().forEach() instead of Object.assign() // ───────────────────────────────────────────────────────────────────────────── -describe('IO-3 · subscribePattern – Object.assign statt forEach', () => { +describe('IO-3 · subscribePattern – Object.assign instead of forEach', () => { function mergeStatesOld(target, source) { Object.keys(source).forEach(id => (target[id] = source[id])); } @@ -152,7 +152,7 @@ describe('IO-3 · subscribePattern – Object.assign statt forEach', () => { Object.assign(target, source); } - it('[EXPECTATION] Beide Varianten erzeugen identisches Ergebnis', () => { + it('[EXPECTATION] Both variants produce an identical result', () => { const source = {}; for (let i = 0; i < 1_000; i++) source[`adapter.0.state.${i}`] = { val: i, ack: true }; @@ -166,13 +166,13 @@ describe('IO-3 · subscribePattern – Object.assign statt forEach', () => { assert.equal(t2['adapter.0.state.500'].val, 500); }); - it('[EXPECTATION] Object.assign erzeugt kein temporäres Keys-Array (Korrektheit + Design)', () => { + it('[EXPECTATION] Object.assign creates no temporary keys array (correctness + design)', () => { const source = {}; for (let i = 0; i < 10_000; i++) source[`adapter.0.state.${i}`] = { val: i, ack: true }; - // Object.assign braucht intern kein Object.keys() Array – es iteriert direkt - // forEach benötigt zwingend ein Array via Object.keys() - // Wir prüfen: Ergebnis ist korrekt und kein temporäres Array nötig + // Object.assign needs no internal Object.keys() array – it iterates directly + // forEach strictly requires an array via Object.keys() + // We verify: the result is correct and no temporary array is needed const t1 = {}; mergeStatesOld(t1, source); @@ -180,18 +180,18 @@ describe('IO-3 · subscribePattern – Object.assign statt forEach', () => { const t2 = {}; mergeStatesNew(t2, source); - // Beide Ergebnisse müssen identisch sein + // Both results must be identical assert.equal(Object.keys(t1).length, Object.keys(t2).length); assert.equal(t1['adapter.0.state.9999'].val, 9999); assert.equal(t2['adapter.0.state.9999'].val, 9999); - // Object.assign gibt das target zurück (API-Korrektheit) + // Object.assign returns the target (API correctness) const target = {}; const result = Object.assign(target, source); - assert.equal(result, target, 'Object.assign gibt target zurück'); + assert.equal(result, target, 'Object.assign returns target'); }); - it('[EXPECTATION] Leere source erzeugt kein Fehler', () => { + it('[EXPECTATION] An empty source produces no error', () => { const t = { existing: 1 }; mergeStatesNew(t, {}); assert.equal(t.existing, 1); @@ -199,9 +199,9 @@ describe('IO-3 · subscribePattern – Object.assign statt forEach', () => { }); // ───────────────────────────────────────────────────────────────────────────── -// Problem 4: subscribe Array-IDs – JSON.parse/stringify statt Spread +// Problem 4: subscribe array IDs – JSON.parse/stringify instead of Spread // ───────────────────────────────────────────────────────────────────────────── -describe('IO-4 · subscribe Array-IDs – Spread statt JSON-Clone', () => { +describe('IO-4 · subscribe array IDs – Spread instead of JSON clone', () => { function clonePatternOld(oPattern, newId) { const pa = JSON.parse(JSON.stringify(oPattern)); pa.id = newId; @@ -220,7 +220,7 @@ describe('IO-4 · subscribe Array-IDs – Spread statt JSON-Clone', () => { logic: 'and', }; - it('[EXPECTATION] Beide Varianten erzeugen äquivalente Pattern-Objekte', () => { + it('[EXPECTATION] Both variants produce equivalent pattern objects', () => { const old = clonePatternOld(basePattern, 'adapter.0.state.1'); const newP = clonePatternNew(basePattern, 'adapter.0.state.1'); assert.equal(old.id, newP.id); @@ -230,13 +230,13 @@ describe('IO-4 · subscribe Array-IDs – Spread statt JSON-Clone', () => { assert.equal(old.logic, newP.logic); }); - it('[EXPECTATION] id wird korrekt überschrieben', () => { + it('[EXPECTATION] id is overwritten correctly', () => { const result = clonePatternNew(basePattern, 'my.new.id'); assert.equal(result.id, 'my.new.id'); - assert.equal(basePattern.id, null, 'Original darf nicht verändert werden'); + assert.equal(basePattern.id, null, 'The original must not be modified'); }); - it('[EXPECTATION] Spread ist bei 50 Array-IDs schneller als JSON clone', () => { + it('[EXPECTATION] Spread is faster than JSON clone with 50 array IDs', () => { const ids = Array.from({ length: 50 }, (_, i) => `adapter.0.state.${i}`); const tOld = bench(() => { @@ -248,23 +248,23 @@ describe('IO-4 · subscribe Array-IDs – Spread statt JSON-Clone', () => { }, 10_000); assert.ok(tNew < tOld, - `Spread (${tNew.toFixed(1)}ms) muss schneller sein als JSON-Clone (${tOld.toFixed(1)}ms)`); + `Spread (${tNew.toFixed(1)}ms) must be faster than JSON clone (${tOld.toFixed(1)}ms)`); }); - it('[EXPECTATION] Verschachtelung: Spread kopiert keine tiefen Referenzen (flach)', () => { + it('[EXPECTATION] Nesting: Spread does not copy deep references (shallow)', () => { const nested = { ...basePattern, meta: { deep: true } }; const cloned = clonePatternNew(nested, 'new.id'); - // Flacher Spread – meta ist dieselbe Referenz + // Shallow spread – meta is the same reference assert.equal(cloned.meta, nested.meta); - // Für Pattern-Objekte mit primitiven Werten ist das ausreichend + // For pattern objects with primitive values this is sufficient assert.equal(cloned.id, 'new.id'); }); }); // ───────────────────────────────────────────────────────────────────────────── -// Problem 5: adapterSubs – filter().length > 0 statt includes() +// Problem 5: adapterSubs – filter().length > 0 instead of includes() // ───────────────────────────────────────────────────────────────────────────── -describe('IO-5 · adapterSubs – includes() statt filter().length', () => { +describe('IO-5 · adapterSubs – includes() instead of filter().length', () => { function subExistsOld(arr, id) { return arr.filter(sub => sub === id).length > 0; } @@ -273,40 +273,40 @@ describe('IO-5 · adapterSubs – includes() statt filter().length', () => { return arr.includes(id); } - it('[EXPECTATION] Beide finden vorhandene IDs', () => { + it('[EXPECTATION] Both find existing IDs', () => { const arr = ['a.0.state.1', 'b.0.state.2', 'c.0.state.3']; assert.equal(subExistsOld(arr, 'b.0.state.2'), true); assert.equal(subExistsNew(arr, 'b.0.state.2'), true); }); - it('[EXPECTATION] Beide erkennen fehlende IDs', () => { + it('[EXPECTATION] Both detect missing IDs', () => { const arr = ['a.0.state.1', 'b.0.state.2']; assert.equal(subExistsOld(arr, 'x.0.not.found'), false); assert.equal(subExistsNew(arr, 'x.0.not.found'), false); }); - it('[EXPECTATION] includes() erzeugt kein temporäres Array', () => { + it('[EXPECTATION] includes() creates no temporary array', () => { const arr = Array.from({ length: 1_000 }, (_, i) => `adapter.0.state.${i}`); const tOld = bench(() => subExistsOld(arr, 'adapter.0.state.999'), 50_000); const tNew = bench(() => subExistsNew(arr, 'adapter.0.state.999'), 50_000); assert.ok(tNew <= tOld * 1.2, - `includes (${tNew.toFixed(1)}ms) darf nicht schlechter als filter (${tOld.toFixed(1)}ms) sein`); + `includes (${tNew.toFixed(1)}ms) must not be worse than filter (${tOld.toFixed(1)}ms)`); }); - it('[BASELINE] filter() gibt immer ein neues Array zurück (Allokationsnachweis)', () => { + it('[BASELINE] filter() always returns a new array (allocation proof)', () => { const arr = ['x', 'y']; const r1 = arr.filter(s => s === 'x'); const r2 = arr.filter(s => s === 'x'); - assert.notEqual(r1, r2, 'Jeder filter()-Aufruf gibt ein NEUES Array zurück'); + assert.notEqual(r1, r2, 'Every filter() call returns a NEW array'); }); }); // ───────────────────────────────────────────────────────────────────────────── -// Problem 6: onEnumMembers – Object.keys().includes() statt `in` Operator +// Problem 6: onEnumMembers – Object.keys().includes() instead of the `in` operator // ───────────────────────────────────────────────────────────────────────────── -describe('IO-6 · onEnumMembers – `in` Operator statt Object.keys().includes()', () => { +describe('IO-6 · onEnumMembers – `in` operator instead of Object.keys().includes()', () => { function memberExistsOld(subscriptions, objId) { return Object.keys(subscriptions).includes(objId); } @@ -315,19 +315,19 @@ describe('IO-6 · onEnumMembers – `in` Operator statt Object.keys().includes() return objId in subscriptions; } - it('[EXPECTATION] Beide erkennen vorhandene Member', () => { + it('[EXPECTATION] Both detect existing members', () => { const subs = { 'state.1': {}, 'state.2': {}, 'state.3': {} }; assert.equal(memberExistsOld(subs, 'state.2'), true); assert.equal(memberExistsNew(subs, 'state.2'), true); }); - it('[EXPECTATION] Beide erkennen fehlende Member', () => { + it('[EXPECTATION] Both detect missing members', () => { const subs = { 'state.1': {} }; assert.equal(memberExistsOld(subs, 'state.99'), false); assert.equal(memberExistsNew(subs, 'state.99'), false); }); - it('[EXPECTATION] `in`-Operator ist bei 500 Subscriptions schneller', () => { + it('[EXPECTATION] The `in` operator is faster with 500 subscriptions', () => { const subs = {}; for (let i = 0; i < 500; i++) subs[`state.${i}`] = {}; @@ -335,24 +335,24 @@ describe('IO-6 · onEnumMembers – `in` Operator statt Object.keys().includes() const tNew = bench(() => memberExistsNew(subs, 'state.499'), 50_000); assert.ok(tNew < tOld, - `\`in\` (${tNew.toFixed(1)}ms) muss schneller sein als Object.keys().includes (${tOld.toFixed(1)}ms)`); + `\`in\` (${tNew.toFixed(1)}ms) must be faster than Object.keys().includes (${tOld.toFixed(1)}ms)`); }); - it('[EXPECTATION] Ergebnis ist bei leerem Objekt korrekt', () => { + it('[EXPECTATION] The result is correct for an empty object', () => { assert.equal(memberExistsNew({}, 'any'), false); }); }); // ───────────────────────────────────────────────────────────────────────────── -// Problem 7: clearStateDelayed – timersByScript nicht aktualisiert +// Problem 7: clearStateDelayed – timersByScript not updated // ───────────────────────────────────────────────────────────────────────────── -describe('IO-7 · clearStateDelayed – timersByScript Synchronisierung', () => { +describe('IO-7 · clearStateDelayed – timersByScript synchronization', () => { function buildTimerFixture() { const timers = {}; const timersByScript = new Map(); const scriptName = 'script.js.test'; - // Timer hinzufügen (wie setStateDelayed) + // Add timer (like setStateDelayed) const addTimer = (stateId, timerId) => { if (!timers[stateId]) timers[stateId] = []; timers[stateId].push({ id: timerId, t: null, scriptName }); @@ -360,7 +360,7 @@ describe('IO-7 · clearStateDelayed – timersByScript Synchronisierung', () => timersByScript.get(scriptName).add(stateId); }; - // Timer entfernen – ALTE Variante (ohne timersByScript-Update) + // Remove timer – OLD variant (without timersByScript update) const clearTimerOld = (stateId, timerId) => { if (!timers[stateId]) return false; for (let i = timers[stateId].length - 1; i >= 0; i--) { @@ -369,11 +369,11 @@ describe('IO-7 · clearStateDelayed – timersByScript Synchronisierung', () => } } if (!timers[stateId].length) delete timers[stateId]; - // BUG: timersByScript wird NICHT aktualisiert + // BUG: timersByScript is NOT updated return true; }; - // Timer entfernen – NEUE Variante (mit timersByScript-Update) + // Remove timer – NEW variant (with timersByScript update) const clearTimerNew = (stateId, timerId) => { if (!timers[stateId]) return false; for (let i = timers[stateId].length - 1; i >= 0; i--) { @@ -383,7 +383,7 @@ describe('IO-7 · clearStateDelayed – timersByScript Synchronisierung', () => } if (!timers[stateId].length) { delete timers[stateId]; - // FIX: timersByScript synchronisieren + // FIX: synchronize timersByScript const stateIds = timersByScript.get(scriptName); if (stateIds) { stateIds.delete(stateId); @@ -396,61 +396,61 @@ describe('IO-7 · clearStateDelayed – timersByScript Synchronisierung', () => return { timers, timersByScript, addTimer, clearTimerOld, clearTimerNew, scriptName }; } - it('[BASELINE] Old: timersByScript bleibt nach clearStateDelayed veraltet', () => { + it('[BASELINE] Old: timersByScript stays stale after clearStateDelayed', () => { const { timers, timersByScript, addTimer, clearTimerOld, scriptName } = buildTimerFixture(); addTimer('state.1', 1); - clearTimerOld('state.1', 1); // löscht timer, aber NICHT timersByScript + clearTimerOld('state.1', 1); // removes the timer, but NOT timersByScript - assert.equal(Object.keys(timers).length, 0, 'timers ist leer'); - // BUG: timersByScript enthält veralteten Eintrag + assert.equal(Object.keys(timers).length, 0, 'timers is empty'); + // BUG: timersByScript contains a stale entry assert.ok(timersByScript.has(scriptName), - '[BASELINE] timersByScript ist noch nicht korrekt – das ist der bekannte Bug'); + '[BASELINE] timersByScript is not yet correct – this is the known bug'); }); - it('[EXPECTATION] New: timersByScript wird nach clearStateDelayed korrekt aktualisiert', () => { + it('[EXPECTATION] New: timersByScript is updated correctly after clearStateDelayed', () => { const { timers, timersByScript, addTimer, clearTimerNew, scriptName } = buildTimerFixture(); addTimer('state.1', 1); clearTimerNew('state.1', 1); - assert.equal(Object.keys(timers).length, 0, 'timers ist leer'); + assert.equal(Object.keys(timers).length, 0, 'timers is empty'); assert.ok(!timersByScript.has(scriptName), - 'timersByScript darf keinen Eintrag mehr enthalten'); + 'timersByScript must no longer contain an entry'); }); - it('[EXPECTATION] timersByScript bleibt korrekt wenn noch andere States Timer haben', () => { + it('[EXPECTATION] timersByScript stays correct when other states still have timers', () => { const { timers, timersByScript, addTimer, clearTimerNew, scriptName } = buildTimerFixture(); addTimer('state.1', 1); addTimer('state.2', 2); clearTimerNew('state.1', 1); - assert.ok(!timers['state.1'], 'state.1 Timer entfernt'); - assert.ok(timers['state.2'], 'state.2 Timer noch vorhanden'); + assert.ok(!timers['state.1'], 'state.1 timer removed'); + assert.ok(timers['state.2'], 'state.2 timer still present'); const stateIds = timersByScript.get(scriptName); - assert.ok(stateIds, 'Script-Eintrag noch vorhanden'); - assert.ok(!stateIds.has('state.1'), 'state.1 aus Set entfernt'); - assert.ok(stateIds.has('state.2'), 'state.2 noch im Set'); + assert.ok(stateIds, 'script entry still present'); + assert.ok(!stateIds.has('state.1'), 'state.1 removed from set'); + assert.ok(stateIds.has('state.2'), 'state.2 still in set'); }); - it('[EXPECTATION] clearStateDelayed mit timerId=undefined löscht alle Timer des State', () => { + it('[EXPECTATION] clearStateDelayed with timerId=undefined removes all timers of the state', () => { const { timers, timersByScript, addTimer, clearTimerNew, scriptName } = buildTimerFixture(); addTimer('state.1', 1); - addTimer('state.1', 2); // zweiter Timer für denselben State + addTimer('state.1', 2); // second timer for the same state clearTimerNew('state.1', undefined); - assert.ok(!timers['state.1'], 'Alle Timer von state.1 entfernt'); + assert.ok(!timers['state.1'], 'All timers of state.1 removed'); const stateIds = timersByScript.get(scriptName); if (stateIds) { - assert.ok(!stateIds.has('state.1'), 'state.1 aus Set entfernt'); + assert.ok(!stateIds.has('state.1'), 'state.1 removed from set'); } }); }); // ───────────────────────────────────────────────────────────────────────────── -// Problem 8: getSchedules – JSON.parse/stringify statt Spread +// Problem 8: getSchedules – JSON.parse/stringify instead of Spread // ───────────────────────────────────────────────────────────────────────────── -describe('IO-8 · getSchedules – Spread statt JSON deep clone', () => { +describe('IO-8 · getSchedules – Spread instead of JSON deep clone', () => { function makeSchedule(i) { return { _ioBroker: { @@ -470,31 +470,31 @@ describe('IO-8 · getSchedules – Spread statt JSON deep clone', () => { return schedules.map(s => ({ ...s._ioBroker })); } - it('[EXPECTATION] Beide liefern identische Schedule-Listen', () => { + it('[EXPECTATION] Both return identical schedule lists', () => { const schedules = Array.from({ length: 20 }, (_, i) => makeSchedule(i)); const old = getSchedulesOld(schedules); const newS = getSchedulesNew(schedules); assert.deepEqual(old, newS); }); - it('[EXPECTATION] Spread liefert eine Kopie (nicht dieselbe Referenz)', () => { + it('[EXPECTATION] Spread returns a copy (not the same reference)', () => { const schedules = [makeSchedule(1)]; const result = getSchedulesNew(schedules); - assert.notEqual(result[0], schedules[0]._ioBroker, 'Muss eine Kopie sein, nicht die Original-Referenz'); + assert.notEqual(result[0], schedules[0]._ioBroker, 'Must be a copy, not the original reference'); assert.deepEqual(result[0], schedules[0]._ioBroker); }); - it('[EXPECTATION] Spread ist bei 100 Schedules schneller als JSON clone', () => { + it('[EXPECTATION] Spread is faster than JSON clone with 100 schedules', () => { const schedules = Array.from({ length: 100 }, (_, i) => makeSchedule(i)); const tOld = bench(() => getSchedulesOld(schedules), 10_000); const tNew = bench(() => getSchedulesNew(schedules), 10_000); assert.ok(tNew < tOld, - `Spread (${tNew.toFixed(1)}ms) muss schneller sein als JSON-Clone (${tOld.toFixed(1)}ms)`); + `Spread (${tNew.toFixed(1)}ms) must be faster than JSON clone (${tOld.toFixed(1)}ms)`); }); - it('[EXPECTATION] Primitiv-Felder (type, pattern, scriptName, id) werden korrekt kopiert', () => { + it('[EXPECTATION] Primitive fields (type, pattern, scriptName, id) are copied correctly', () => { const s = makeSchedule(42); const copy = getSchedulesNew([s])[0]; assert.equal(copy.type, 'cron'); @@ -505,19 +505,19 @@ describe('IO-8 · getSchedules – Spread statt JSON deep clone', () => { }); // ───────────────────────────────────────────────────────────────────────────── -// Problem 9: sendTo ohne Instanz – getObjectView ohne Cache +// Problem 9: sendTo without instance – getObjectView without cache // ───────────────────────────────────────────────────────────────────────────── -describe('IO-9 · sendTo – Instanz-Cache statt wiederholter getObjectView', () => { - /** Simuliert den Instanz-Cache */ +describe('IO-9 · sendTo – instance cache instead of repeated getObjectView', () => { + /** Simulates the instance cache */ function buildInstanceCache() { const cache = new Map(); // adapterName → string[] let queryCount = 0; const getInstances = async (adapterName, allInstances) => { if (cache.has(adapterName)) { - return cache.get(adapterName); // Cache-Hit + return cache.get(adapterName); // cache hit } - // Simulierter DB-Query + // Simulated DB query queryCount++; const instances = allInstances.filter(id => id.startsWith(`system.adapter.${adapterName}.`), @@ -538,29 +538,29 @@ describe('IO-9 · sendTo – Instanz-Cache statt wiederholter getObjectView', () 'system.adapter.js.0', ]; - it('[EXPECTATION] Erster Aufruf führt einen DB-Query durch', async () => { + it('[EXPECTATION] The first call performs one DB query', async () => { const c = buildInstanceCache(); const result = await c.getInstances('zigbee', allInstances); assert.equal(c.getQueryCount(), 1); assert.deepEqual(result.sort(), ['zigbee.0', 'zigbee.1'].sort()); }); - it('[EXPECTATION] Zweiter Aufruf nutzt den Cache (kein DB-Query)', async () => { + it('[EXPECTATION] The second call uses the cache (no DB query)', async () => { const c = buildInstanceCache(); await c.getInstances('zigbee', allInstances); - await c.getInstances('zigbee', allInstances); // Cache-Hit - assert.equal(c.getQueryCount(), 1, 'Nur 1 Query, nicht 2'); + await c.getInstances('zigbee', allInstances); // cache hit + assert.equal(c.getQueryCount(), 1, 'Only 1 query, not 2'); }); - it('[EXPECTATION] Invalidierung erzwingt neuen Query', async () => { + it('[EXPECTATION] Invalidation forces a new query', async () => { const c = buildInstanceCache(); await c.getInstances('zigbee', allInstances); c.invalidate('zigbee'); await c.getInstances('zigbee', allInstances); - assert.equal(c.getQueryCount(), 2, 'Nach Invalidierung muss neu abgefragt werden'); + assert.equal(c.getQueryCount(), 2, 'After invalidation it must query again'); }); - it('[EXPECTATION] Verschiedene Adapter-Namen haben getrennte Cache-Einträge', async () => { + it('[EXPECTATION] Different adapter names have separate cache entries', async () => { const c = buildInstanceCache(); await c.getInstances('zigbee', allInstances); await c.getInstances('hm-rpc', allInstances); @@ -568,25 +568,25 @@ describe('IO-9 · sendTo – Instanz-Cache statt wiederholter getObjectView', () assert.equal(c.cache.size, 2); }); - it('[BASELINE] Ohne Cache: jeder sendTo-Aufruf braucht einen DB-Query', async () => { + it('[BASELINE] Without cache: every sendTo call needs a DB query', async () => { let queryCount = 0; const sendToWithoutCache = async adapterName => { - queryCount++; // Simuliert getObjectView + queryCount++; // simulates getObjectView return allInstances.filter(id => id.startsWith(`system.adapter.${adapterName}.`)); }; await sendToWithoutCache('zigbee'); await sendToWithoutCache('zigbee'); await sendToWithoutCache('zigbee'); - assert.equal(queryCount, 3, '[BASELINE] Ohne Cache: 3 Aufrufe → 3 DB-Queries'); + assert.equal(queryCount, 3, '[BASELINE] Without cache: 3 calls → 3 DB queries'); }); }); // ───────────────────────────────────────────────────────────────────────────── -// Problem 10: clearInterval/Timeout – indexOf() O(n) statt Set O(1) +// Problem 10: clearInterval/Timeout – indexOf() O(n) instead of Set O(1) // ───────────────────────────────────────────────────────────────────────────── -describe('IO-10 · clearInterval/Timeout – Set statt Array für Timer-Tracking', () => { - /** ALTE Implementierung – Array mit indexOf */ +describe('IO-10 · clearInterval/Timeout – Set instead of Array for timer tracking', () => { + /** OLD implementation – array with indexOf */ function buildTimerTrackerOld() { const timers = []; return { @@ -600,7 +600,7 @@ describe('IO-10 · clearInterval/Timeout – Set statt Array für Timer-Tracking }; } - /** NEUE Implementierung – Set */ + /** NEW implementation – Set */ function buildTimerTrackerNew() { const timers = new Set(); return { @@ -611,7 +611,7 @@ describe('IO-10 · clearInterval/Timeout – Set statt Array für Timer-Tracking }; } - it('[EXPECTATION] Beide Tracker: add, has, remove funktionieren korrekt', () => { + it('[EXPECTATION] Both trackers: add, has, remove work correctly', () => { for (const tracker of [buildTimerTrackerOld(), buildTimerTrackerNew()]) { tracker.add(1); tracker.add(2); @@ -624,14 +624,14 @@ describe('IO-10 · clearInterval/Timeout – Set statt Array für Timer-Tracking } }); - it('[EXPECTATION] Set-Tracker ist bei 1.000 aktiven Timern und remove schneller', () => { + it('[EXPECTATION] The Set tracker is faster with 1,000 active timers and remove', () => { const N = 1_000; const ids = Array.from({ length: N }, (_, i) => i + 1); const tOld = bench(() => { const t = buildTimerTrackerOld(); for (const id of ids) t.add(id); - // clearInterval-Szenario: zufällig entfernen + // clearInterval scenario: remove randomly for (let i = 0; i < 100; i++) t.remove(ids[Math.floor(Math.random() * N)]); }, 500); @@ -642,35 +642,35 @@ describe('IO-10 · clearInterval/Timeout – Set statt Array für Timer-Tracking }, 500); assert.ok(tNew <= tOld * 1.5, - `Set (${tNew.toFixed(1)}ms) darf nicht deutlich schlechter sein als Array (${tOld.toFixed(1)}ms)`); + `Set (${tNew.toFixed(1)}ms) must not be significantly worse than Array (${tOld.toFixed(1)}ms)`); }); - it('[EXPECTATION] Set erlaubt keine Duplikate (korrekt für Timer-IDs)', () => { + it('[EXPECTATION] Set allows no duplicates (correct for timer IDs)', () => { const t = buildTimerTrackerNew(); t.add(42); t.add(42); t.add(42); - assert.equal(t.size(), 1, 'Set darf keine Duplikate enthalten'); + assert.equal(t.size(), 1, 'Set must not contain duplicates'); }); - it('[EXPECTATION] remove eines nicht vorhandenen Elements wirft keinen Fehler', () => { + it('[EXPECTATION] Removing a non-existent element throws no error', () => { const t = buildTimerTrackerNew(); assert.doesNotThrow(() => t.remove(999)); }); - it('[EXPECTATION] Script-Stop-Szenario: alle Timer eines Scripts werden entfernt', () => { + it('[EXPECTATION] Script-stop scenario: all timers of a script are removed', () => { const t = buildTimerTrackerNew(); const timerIds = [101, 102, 103, 104, 105]; for (const id of timerIds) t.add(id); - // stopScript löscht alle Timer + // stopScript removes all timers for (const id of timerIds) t.remove(id); assert.equal(t.size(), 0); for (const id of timerIds) assert.ok(!t.has(id)); }); - it('[BASELINE] Array indexOf: worst case ist letztes Element', () => { + it('[BASELINE] Array indexOf: worst case is the last element', () => { const arr = Array.from({ length: 10_000 }, (_, i) => i); const last = arr[arr.length - 1]; @@ -680,26 +680,26 @@ describe('IO-10 · clearInterval/Timeout – Set statt Array für Timer-Tracking const tSet = bench(() => set.has(last), 50_000); assert.ok(tSet < tArr, - `Set.has (${tSet.toFixed(1)}ms) muss schneller sein als indexOf (${tArr.toFixed(1)}ms) bei 10k Elementen`); + `Set.has (${tSet.toFixed(1)}ms) must be faster than indexOf (${tArr.toFixed(1)}ms) with 10k elements`); }); }); // ───────────────────────────────────────────────────────────────────────────── -// Integration: Kombinierter I/O Hot-Path +// Integration: combined I/O hot path // ───────────────────────────────────────────────────────────────────────────── -describe('Integration · Kombinierter I/O Hot-Path', () => { - it('[EXPECTATION] Vollständiger onStateChange Durchlauf mit allen Fixes korrekt', () => { - // Simuliert den Hot-Path mit allen IO-Fixes kombiniert +describe('Integration · combined I/O hot path', () => { + it('[EXPECTATION] Full onStateChange pass with all fixes correct', () => { + // Simulates the hot path with all IO fixes combined const subscriptions = []; const stateIdSet = new Set(); - // IO-2: Set für Deduplication + // IO-2: Set for deduplication const allIds = Array.from({ length: 200 }, (_, i) => `adapter.0.s.${i}`); const duped = [...allIds, ...allIds.slice(0, 50)]; - const unique = [...new Set(duped)]; // IO-2 Fix + const unique = [...new Set(duped)]; // IO-2 fix assert.equal(unique.length, 200); - // IO-1: hasChanged ohne Array-Allokation + // IO-1: hasChanged without array allocation const s1 = { val: 42, ack: true, from: 'system.adapter.js.0', q: 0, lc: 1000, ts: 100 }; const s2 = { val: 43, ack: true, from: 'system.adapter.js.0', q: 0, lc: 1001, ts: 101 }; let changed = false; @@ -709,18 +709,18 @@ describe('Integration · Kombinierter I/O Hot-Path', () => { } assert.ok(changed); - // IO-5: includes statt filter().length + // IO-5: includes instead of filter().length const adapterSubs = ['adapter.0.state.1', 'adapter.0.state.2']; assert.ok(adapterSubs.includes('adapter.0.state.1')); assert.ok(!adapterSubs.includes('adapter.0.state.99')); - // IO-6: in-Operator + // IO-6: in operator const subs = { 'state.1': {}, 'state.2': {} }; assert.ok('state.1' in subs); assert.ok(!('state.99' in subs)); }); - it('[EXPECTATION] Timer-Lifecycle: add → clear → stopScript funktioniert konsistent', () => { + it('[EXPECTATION] Timer lifecycle: add → clear → stopScript works consistently', () => { const timers = {}; const timersByScript = new Map(); const scriptName = 'script.js.myScript'; @@ -753,17 +753,17 @@ describe('Integration · Kombinierter I/O Hot-Path', () => { return count; }; - // Szenario: 3 States mit Timern, 1 wird manuell gecleart + // Scenario: 3 states with timers, 1 is cleared manually addTimer('state.A', 1); addTimer('state.B', 2); addTimer('state.C', 3); - clearTimer('state.B'); // IO-7 Fix: auch timersByScript aktualisieren + clearTimer('state.B'); // IO-7 fix: also update timersByScript - // stopScript soll nur A und C stoppen + // stopScript should stop only A and C const stopped = stopScript(); - assert.equal(stopped, 2, 'Nur 2 Timer sollen von stopScript gestoppt werden'); - assert.equal(Object.keys(timers).length, 0, 'Alle Timer müssen entfernt sein'); - assert.ok(!timersByScript.has(scriptName), 'Script-Eintrag muss entfernt sein'); + assert.equal(stopped, 2, 'Only 2 timers should be stopped by stopScript'); + assert.equal(Object.keys(timers).length, 0, 'All timers must be removed'); + assert.ok(!timersByScript.has(scriptName), 'Script entry must be removed'); }); }); diff --git a/test/performance-improvements.test.js b/test/performance-improvements.test.js index 2776f344b..48734c8bb 100644 --- a/test/performance-improvements.test.js +++ b/test/performance-improvements.test.js @@ -1,30 +1,30 @@ 'use strict'; /** - * Regression-Tests für die 8 geplanten Performance-Verbesserungen - * in src/main.ts – geschrieben VOR der Änderung. + * Regression tests for the 8 planned performance improvements + * in src/main.ts - written BEFORE the change. * - * Jeder Test ist explizit als BASELINE oder EXPECTATION markiert: - * [BASELINE] – dokumentiert das heutige (schlechtere) Verhalten - * [EXPECTATION] – verifiziert das verbesserte Verhalten nach dem Fix + * Each test is explicitly marked as BASELINE or EXPECTATION: + * [BASELINE] - documents the current (worse) behavior + * [EXPECTATION] - verifies the improved behavior after the fix * - * Alle Tests müssen nach der Änderung weiterhin grün sein. + * All tests must stay green after the change. * * npx mocha test/performance-improvements.test.js --timeout 30000 */ const assert = require('node:assert').strict; // ───────────────────────────────────────────────────────────────────────────── -// Gemeinsame Hilfsfunktionen +// Shared helper functions // ───────────────────────────────────────────────────────────────────────────── -/** Erzeugt N State-IDs der Form "adapter.0.state.NNN" */ +/** Creates N state IDs in the form "adapter.0.state.NNN" */ function makeStateIds(n) { const ids = []; for (let i = 0; i < n; i++) ids.push(`adapter.0.state.${String(i).padStart(6, '0')}`); return ids; } -/** Misst die Zeit (ms) für fn() in iterations Wiederholungen */ +/** Measures the time (ms) for fn() across iterations repeats */ function bench(fn, iterations = 1) { const t0 = performance.now(); for (let i = 0; i < iterations; i++) fn(); @@ -32,10 +32,10 @@ function bench(fn, iterations = 1) { } // ───────────────────────────────────────────────────────────────────────────── -// 1. sortedInsert – O(log n) statt O(n log n) sort() +// 1. sortedInsert - O(log n) instead of O(n log n) sort() // ───────────────────────────────────────────────────────────────────────────── -describe('Perf-1 · sortedInsert() statt stateIds.sort()', () => { - /** Binary-Search-Insert – die NEUE Implementierung */ +describe('Perf-1 · sortedInsert() instead of stateIds.sort()', () => { + /** Binary-search insert - the NEW implementation */ function sortedInsert(arr, id) { let lo = 0; let hi = arr.length; @@ -47,7 +47,7 @@ describe('Perf-1 · sortedInsert() statt stateIds.sort()', () => { if (arr[lo] !== id) arr.splice(lo, 0, id); } - it('[EXPECTATION] sortedInsert hält Array sortiert', () => { + it('[EXPECTATION] sortedInsert keeps the array sorted', () => { const arr = []; const ids = ['z.0', 'a.0', 'm.0', 'b.1', 'a.1']; for (const id of ids) sortedInsert(arr, id); @@ -55,7 +55,7 @@ describe('Perf-1 · sortedInsert() statt stateIds.sort()', () => { assert.deepEqual(arr, sorted); }); - it('[EXPECTATION] sortedInsert ignoriert Duplikate', () => { + it('[EXPECTATION] sortedInsert ignores duplicates', () => { const arr = []; sortedInsert(arr, 'a.0'); sortedInsert(arr, 'a.0'); @@ -63,31 +63,31 @@ describe('Perf-1 · sortedInsert() statt stateIds.sort()', () => { assert.equal(arr.length, 1); }); - it('[EXPECTATION] sortedInsert ist bei 50k Einträgen schneller als push+sort', () => { + it('[EXPECTATION] sortedInsert is faster than push+sort with 50k entries', () => { const N = 50_000; const ids = makeStateIds(N); - // VORHER: push + sort + // BEFORE: push + sort const tSort = bench(() => { const arr = []; for (const id of ids) { arr.push(id); arr.sort(); } }); - // NACHHER: sortedInsert + // AFTER: sortedInsert const tInsert = bench(() => { const arr = []; for (const id of ids) sortedInsert(arr, id); }); - // sortedInsert muss mindestens 10× schneller sein + // sortedInsert must be faster assert.ok( tInsert < tSort, - `sortedInsert (${tInsert.toFixed(0)}ms) muss schneller sein als push+sort (${tSort.toFixed(0)}ms)`, + `sortedInsert (${tInsert.toFixed(0)}ms) must be faster than push+sort (${tSort.toFixed(0)}ms)`, ); }); - it('[EXPECTATION] Ergebnisarray von sortedInsert und push+sort ist identisch', () => { - const ids = makeStateIds(1_000).reverse(); // umgekehrt um Schlimmstfall zu testen + it('[EXPECTATION] result arrays from sortedInsert and push+sort are identical', () => { + const ids = makeStateIds(1_000).reverse(); // reversed to test worst case const arrSort = []; const arrInsert = []; for (const id of ids) { @@ -100,12 +100,12 @@ describe('Perf-1 · sortedInsert() statt stateIds.sort()', () => { }); // ───────────────────────────────────────────────────────────────────────────── -// 2. timersByScript – Reverse-Index für stopScript-Timer-Cleanup +// 2. timersByScript - reverse index for stopScript timer cleanup // ───────────────────────────────────────────────────────────────────────────── -describe('Perf-2 · timersByScript Reverse-Index für stopScript', () => { +describe('Perf-2 · timersByScript reverse index for stopScript', () => { /** - * Simuliert den aktuellen (langsamen) Timer-Cleanup: - * Iteriert ALLE timers und prüft scriptName + * Simulates the current (slow) timer cleanup: + * Iterates ALL timers and checks scriptName */ function stopScriptTimersSlow(timers, scriptName) { const cleared = []; @@ -122,7 +122,7 @@ describe('Perf-2 · timersByScript Reverse-Index für stopScript', () => { } /** - * Simuliert den NEUEN (schnellen) Timer-Cleanup via Reverse-Index: + * Simulates the NEW (fast) timer cleanup via reverse index: * timersByScript: Map> */ function stopScriptTimersFast(timers, timersByScript, scriptName) { @@ -143,7 +143,7 @@ describe('Perf-2 · timersByScript Reverse-Index für stopScript', () => { return cleared; } - /** Baut Test-Datensatz auf */ + /** Builds the test dataset */ function buildTimers(scriptCount, timersPerScript, stateCount) { const timers = {}; const timersByScript = new Map(); @@ -161,9 +161,9 @@ describe('Perf-2 · timersByScript Reverse-Index für stopScript', () => { return { timers, timersByScript }; } - it('[EXPECTATION] Beide Implementierungen geben dieselben Timer zurück', () => { + it('[EXPECTATION] both implementations return the same timers', () => { const { timers, timersByScript } = buildTimers(5, 10, 20); - // Tiefe Kopie für slow + // Deep copy for slow const timersCopy = JSON.parse(JSON.stringify(timers)); const slow = stopScriptTimersSlow(timersCopy, 'script.js.script_2') @@ -171,10 +171,10 @@ describe('Perf-2 · timersByScript Reverse-Index für stopScript', () => { const fast = stopScriptTimersFast(timers, timersByScript, 'script.js.script_2') .sort((a, b) => a - b); - assert.deepEqual(fast, slow, 'Beide Methoden müssen dieselben Timer-IDs entfernen'); + assert.deepEqual(fast, slow, 'Both methods must remove the same timer IDs'); }); - it('[EXPECTATION] Fast-Cleanup ist bei 50 Scripts × 100 Timern schneller', () => { + it('[EXPECTATION] fast cleanup is faster with 50 scripts x 100 timers', () => { const { timers: tSlow } = buildTimers(50, 100, 200); const { timers: tFast, timersByScript } = buildTimers(50, 100, 200); @@ -192,15 +192,15 @@ describe('Perf-2 · timersByScript Reverse-Index für stopScript', () => { assert.ok( tFastMs < tSlowMs, - `Fast (${tFastMs.toFixed(1)}ms) muss schneller sein als Slow (${tSlowMs.toFixed(1)}ms)`, + `Fast (${tFastMs.toFixed(1)}ms) must be faster than Slow (${tSlowMs.toFixed(1)}ms)`, ); }); - it('[EXPECTATION] Nach Cleanup sind keine Timer des Scripts mehr vorhanden', () => { + it('[EXPECTATION] no timers of the script remain after cleanup', () => { const { timers, timersByScript } = buildTimers(3, 5, 10); stopScriptTimersFast(timers, timersByScript, 'script.js.script_0'); - // Keine Timer von script_0 dürfen noch existieren + // No timers from script_0 may still exist for (const stateId of Object.keys(timers)) { for (const entry of timers[stateId]) { assert.notEqual(entry.scriptName, 'script.js.script_0'); @@ -211,46 +211,46 @@ describe('Perf-2 · timersByScript Reverse-Index für stopScript', () => { }); // ───────────────────────────────────────────────────────────────────────────── -// 3. _adapterFrom – vorberechneter String statt Allokation pro setState +// 3. _adapterFrom - precomputed string instead of allocation per setState // ───────────────────────────────────────────────────────────────────────────── -describe('Perf-3 · _adapterFrom vorberechnet im Constructor', () => { - it('[EXPECTATION] Vorberechneter String ist identisch mit dynamisch erzeugtem', () => { +describe('Perf-3 · _adapterFrom precomputed in constructor', () => { + it('[EXPECTATION] precomputed string is identical to dynamically created string', () => { const namespace = 'javascript.0'; - // Vorberechnet (einmalig im Constructor) + // Precomputed (once in constructor) const _adapterFrom = `system.adapter.${namespace}`; - // Dynamisch (jedes Mal neu in prepareStateObject) + // Dynamic (new each time in prepareStateObject) const dynamic = `system.adapter.${namespace}`; assert.equal(_adapterFrom, dynamic); }); - it('[EXPECTATION] Vorberechneter String ist referenz-stabil (immer dieselbe Instanz)', () => { + it('[EXPECTATION] precomputed string is reference-stable (always the same instance)', () => { const namespace = 'javascript.0'; - // Vorberechnet – EINMAL erstellt, dann wiederverwendet + // Precomputed - created ONCE, then reused const _adapterFrom = `system.adapter.${namespace}`; - // Alle Zuweisungen zeigen auf dasselbe Objekt + // All assignments point to the same object const refs = []; for (let i = 0; i < 1_000; i++) refs.push(_adapterFrom); - // Jede Referenz ist identisch (gleicher Wert) - assert.ok(refs.every(r => r === _adapterFrom), 'Alle Referenzen müssen gleich sein'); + // Every reference is identical (same value) + assert.ok(refs.every(r => r === _adapterFrom), 'All references must be equal'); - // Dynamische Erzeugung liefert zwar gleichen Wert, aber ist CPU-teurer - // (Benchmark ist hier intentional kein harter Vergleich – GC macht Heap unzuverlässig) + // Dynamic creation has the same value but is more CPU-expensive + // (Benchmark here is intentionally not a strict comparison - GC makes heap usage unreliable) let r2 = ''; for (let i = 0; i < 100_000; i++) r2 = `system.adapter.${namespace}`; - assert.equal(_adapterFrom, r2, 'Werte müssen identisch sein'); + assert.equal(_adapterFrom, r2, 'Values must be identical'); }); - it('[EXPECTATION] prepareStateObject setzt from korrekt wenn leer', () => { + it('[EXPECTATION] prepareStateObject sets from correctly when empty', () => { const _adapterFrom = 'system.adapter.javascript.0'; - // Logik aus prepareStateObject – from wird gesetzt wenn leer + // Logic from prepareStateObject - from is set when empty const oState = { val: 42, ack: true, from: '' }; oState.from = (typeof oState.from === 'string' && oState.from !== '') ? oState.from : _adapterFrom; assert.equal(oState.from, _adapterFrom); }); - it('[EXPECTATION] prepareStateObject behält vorhandenes from', () => { + it('[EXPECTATION] prepareStateObject keeps existing from', () => { const _adapterFrom = 'system.adapter.javascript.0'; const oState = { val: 42, ack: true, from: 'system.adapter.other.0' }; oState.from = (typeof oState.from === 'string' && oState.from !== '') ? oState.from : _adapterFrom; @@ -259,11 +259,11 @@ describe('Perf-3 · _adapterFrom vorberechnet im Constructor', () => { }); // ───────────────────────────────────────────────────────────────────────────── -// 4. loadTypeScriptDeclarations – Set statt Array.includes() in O(n²)-Loop +// 4. loadTypeScriptDeclarations - Set instead of Array.includes() in O(n^2) loop // ───────────────────────────────────────────────────────────────────────────── describe('Perf-4 · Set in loadTypeScriptDeclarations', () => { /** - * Simuliert die ALTE Implementierung (O(n²) – Array.includes in Loop) + * Simulates the OLD implementation (O(n^2) - Array.includes in loop) */ function buildPackagesOld(installedLibs, wantsTypings) { const packages = ['node', '@iobroker/types']; @@ -283,7 +283,7 @@ describe('Perf-4 · Set in loadTypeScriptDeclarations', () => { } /** - * Simuliert die NEUE Implementierung (O(n) – Set.has) + * Simulates the NEW implementation (O(n) - Set.has) */ function buildPackagesNew(installedLibs, wantsTypings) { const packages = ['node', '@iobroker/types']; @@ -308,7 +308,7 @@ describe('Perf-4 · Set in loadTypeScriptDeclarations', () => { return packages; } - it('[EXPECTATION] Beide Implementierungen liefern identische packages-Liste', () => { + it('[EXPECTATION] both implementations return an identical package list', () => { const installed = ['rxjs', 'lodash', 'moment', 'axios', 'dayjs']; const wants = ['rxjs', 'lodash', 'rxjs/operators', 'moment/locale']; const old = buildPackagesOld(installed, wants); @@ -316,15 +316,15 @@ describe('Perf-4 · Set in loadTypeScriptDeclarations', () => { assert.deepEqual(old.sort(), newP.sort()); }); - it('[EXPECTATION] Set-Implementierung ergibt keine Duplikate', () => { + it('[EXPECTATION] Set implementation produces no duplicates', () => { const installed = ['rxjs', 'rxjs', 'lodash']; const wants = ['rxjs', 'rxjs/operators']; const packages = buildPackagesNew(installed, wants); const unique = [...new Set(packages)]; - assert.deepEqual(packages.sort(), unique.sort(), 'Keine Duplikate erlaubt'); + assert.deepEqual(packages.sort(), unique.sort(), 'No duplicates allowed'); }); - it('[EXPECTATION] Set-Implementierung ist bei 500 Libs schneller', () => { + it('[EXPECTATION] Set implementation is faster with 500 libs', () => { const installed = Array.from({ length: 500 }, (_, i) => `lib-${i}`); const wants = Array.from({ length: 500 }, (_, i) => `lib-${i}`); wants.push(...Array.from({ length: 100 }, (_, i) => `lib-${i}/sub`)); @@ -333,14 +333,14 @@ describe('Perf-4 · Set in loadTypeScriptDeclarations', () => { const tNew = bench(() => buildPackagesNew(installed, wants), 100); assert.ok(tNew < tOld, - `Set (${tNew.toFixed(1)}ms) muss schneller sein als Array.includes (${tOld.toFixed(1)}ms)`); + `Set (${tNew.toFixed(1)}ms) must be faster than Array.includes (${tOld.toFixed(1)}ms)`); }); }); // ───────────────────────────────────────────────────────────────────────────── -// 5. getData – lokale Variablen statt wiederholtem res.rows[i].doc +// 5. getData - local variables instead of repeated res.rows[i].doc // ───────────────────────────────────────────────────────────────────────────── -describe('Perf-5 · Lokale Variablen in getData() Objekt-Loop', () => { +describe('Perf-5 · local variables in getData() object loop', () => { function buildRows(n) { return Array.from({ length: n }, (_, i) => ({ id: `adapter.0.obj.${i}`, @@ -352,10 +352,10 @@ describe('Perf-5 · Lokale Variablen in getData() Objekt-Loop', () => { })); } - it('[EXPECTATION] Beide Loop-Varianten füllen objects identisch', () => { + it('[EXPECTATION] both loop variants populate objects identically', () => { const rows = buildRows(1_000); - // ALTE Implementierung (wiederholter Indexzugriff) + // OLD implementation (repeated index access) const objectsOld = {}; const enumsOld = []; for (let i = 0; i < rows.length; i++) { @@ -366,7 +366,7 @@ describe('Perf-5 · Lokale Variablen in getData() Objekt-Loop', () => { if (rows[i].doc.type === 'enum') enumsOld.push(rows[i].doc._id); } - // NEUE Implementierung (lokale Variable) + // NEW implementation (local variable) const objectsNew = {}; const enumsNew = []; for (let i = 0; i < rows.length; i++) { @@ -383,7 +383,7 @@ describe('Perf-5 · Lokale Variablen in getData() Objekt-Loop', () => { assert.deepEqual(enumsOld.sort(), enumsNew.sort()); }); - it('[EXPECTATION] Lokale-Variante ist bei 50.000 Objekten schneller', () => { + it('[EXPECTATION] local-variable variant is faster with 50,000 objects', () => { const rows = buildRows(50_000); const tOld = bench(() => { @@ -407,11 +407,11 @@ describe('Perf-5 · Lokale Variablen in getData() Objekt-Loop', () => { } }, 10); - assert.ok(tNew <= tOld * 1.1, // 10% Toleranz - `Lokale Var (${tNew.toFixed(1)}ms) darf nicht schlechter als Old (${tOld.toFixed(1)}ms) sein`); + assert.ok(tNew <= tOld * 1.1, // 10% tolerance + `Local var (${tNew.toFixed(1)}ms) must not be worse than old (${tOld.toFixed(1)}ms)`); }); - it('[EXPECTATION] Leere doc-Einträge werden korrekt übersprungen', () => { + it('[EXPECTATION] empty doc entries are skipped correctly', () => { const rows = [ { id: 'x', doc: { _id: 'x', type: 'state', common: {} } }, { id: 'y', doc: null }, @@ -432,9 +432,9 @@ describe('Perf-5 · Lokale Variablen in getData() Objekt-Loop', () => { }); // ───────────────────────────────────────────────────────────────────────────── -// 6. setStateCountCheckInterval – for...in statt Object.keys().forEach() +// 6. setStateCountCheckInterval - for...in instead of Object.keys().forEach() // ───────────────────────────────────────────────────────────────────────────── -describe('Perf-6 · for...in statt Object.keys().forEach() im Interval', () => { +describe('Perf-6 · for...in instead of Object.keys().forEach() in interval', () => { function buildScripts(n) { const scripts = {}; for (let i = 0; i < n; i++) { @@ -446,12 +446,12 @@ describe('Perf-6 · for...in statt Object.keys().forEach() im Interval', () => { return scripts; } - it('[EXPECTATION] for...in und Object.keys().forEach() liefern identische Ergebnisse', () => { + it('[EXPECTATION] for...in and Object.keys().forEach() return identical results', () => { const scripts1 = buildScripts(100); const scripts2 = JSON.parse(JSON.stringify(scripts1)); const maxPerMinute = 1000; - // ALTE Methode + // OLD method const stoppedOld = []; Object.keys(scripts1).forEach(id => { if (!scripts1[id]) return; @@ -465,7 +465,7 @@ describe('Perf-6 · for...in statt Object.keys().forEach() im Interval', () => { } }); - // NEUE Methode + // NEW method const stoppedNew = []; for (const id in scripts2) { if (!scripts2[id]) continue; @@ -486,11 +486,11 @@ describe('Perf-6 · for...in statt Object.keys().forEach() im Interval', () => { ); }); - it('[EXPECTATION] for...in alloziert weniger temporäre Arrays', () => { + it('[EXPECTATION] for...in allocates fewer temporary arrays', () => { const scripts = buildScripts(500); const keys = Object.keys(scripts); - // Messgrenze: Object.keys() erzeugt neues Array + // Measurement boundary: Object.keys() creates a new array const tKeys = bench(() => { let sum = 0; Object.keys(scripts).forEach(id => { sum += scripts[id].setStatePerMinuteCounter; }); @@ -501,15 +501,15 @@ describe('Perf-6 · for...in statt Object.keys().forEach() im Interval', () => { for (const id in scripts) { sum += scripts[id].setStatePerMinuteCounter; } }, 10_000); - assert.ok(tForIn <= tKeys * 1.2, // 20% Toleranz da JS-Engine optimiert - `for...in (${tForIn.toFixed(1)}ms) darf nicht deutlich schlechter als Object.keys (${tKeys.toFixed(1)}ms) sein`); + assert.ok(tForIn <= tKeys * 1.2, // 20% tolerance because JS engine optimizes + `for...in (${tForIn.toFixed(1)}ms) must not be significantly worse than Object.keys (${tKeys.toFixed(1)}ms)`); }); }); // ───────────────────────────────────────────────────────────────────────────── -// 7. onLog – for...in statt Object.keys().forEach() bei jeder Log-Nachricht +// 7. onLog - for...in instead of Object.keys().forEach() for each log message // ───────────────────────────────────────────────────────────────────────────── -describe('Perf-7 · for...in in onLog() statt Object.keys().forEach()', () => { +describe('Perf-7 · for...in in onLog() instead of Object.keys().forEach()', () => { function buildLogSubscriptions(scriptCount, handlersPerScript) { const subs = {}; for (let s = 0; s < scriptCount; s++) { @@ -526,7 +526,7 @@ describe('Perf-7 · for...in in onLog() statt Object.keys().forEach()', () => { return subs; } - it('[EXPECTATION] Beide Varianten rufen dieselben Handler auf', () => { + it('[EXPECTATION] both variants call the same handlers', () => { const logSubs = buildLogSubscriptions(5, 3); const msg = { severity: 'info', message: 'test' }; @@ -551,9 +551,9 @@ describe('Perf-7 · for...in in onLog() statt Object.keys().forEach()', () => { assert.deepEqual(calledOld.sort(), calledNew.sort()); }); - it('[EXPECTATION] for...in erzeugt kein temporäres Keys-Array (Korrektheit bleibt)', () => { - // Timing-Vergleich zwischen for...in und Object.keys() ist nicht zuverlässig – - // V8 optimiert beide Varianten gleich gut. Wichtig ist die funktionale Korrektheit. + it('[EXPECTATION] for...in creates no temporary keys array (correctness remains)', () => { + // Timing comparison between for...in and Object.keys() is not reliable - + // V8 optimizes both variants similarly. Functional correctness is what matters. const logSubs = buildLogSubscriptions(20, 5); const msg = { severity: 'debug' }; @@ -576,12 +576,12 @@ describe('Perf-7 · for...in in onLog() statt Object.keys().forEach()', () => { } assert.equal(calledOld.length, calledNew.length, - 'Anzahl aufgerufener Handler muss identisch sein'); + 'Number of called handlers must be identical'); assert.deepEqual(calledOld.sort(), calledNew.sort(), - 'Aufgerufene Subscriptions müssen identisch sein'); + 'Called subscriptions must be identical'); }); - it('[EXPECTATION] Leere logSubs-Einträge werden korrekt übersprungen', () => { + it('[EXPECTATION] empty logSubs entries are skipped correctly', () => { const logSubs = { 'script.js.a': [], 'script.js.b': [{ severity: '*', cb: () => {}, sandbox: {} }], @@ -599,11 +599,11 @@ describe('Perf-7 · for...in in onLog() statt Object.keys().forEach()', () => { }); // ───────────────────────────────────────────────────────────────────────────── -// 8. subscriptionsObjectMap – O(1) Dispatch statt O(n) forEach in onObjectChange +// 8. subscriptionsObjectMap - O(1) dispatch instead of O(n) forEach in onObjectChange // ───────────────────────────────────────────────────────────────────────────── describe('Perf-8 · subscriptionsObjectMap O(1) Dispatch', () => { /** - * Simuliert ALTE Implementierung: lineares forEach + * Simulates OLD implementation: linear forEach */ function dispatchOld(subscriptionsObject, id, obj) { const called = []; @@ -617,7 +617,7 @@ describe('Perf-8 · subscriptionsObjectMap O(1) Dispatch', () => { } /** - * Simuliert NEUE Implementierung: Map-Lookup O(1) + * Simulates NEW implementation: O(1) map lookup */ function dispatchNew(subscriptionsObjectMap, id, obj) { const called = []; @@ -647,7 +647,7 @@ describe('Perf-8 · subscriptionsObjectMap O(1) Dispatch', () => { return { arr, map }; } - it('[EXPECTATION] Beide Implementierungen dispatchen dieselben Callbacks', () => { + it('[EXPECTATION] both implementations dispatch the same callbacks', () => { const { arr, map } = buildSubscriptions(100, 'system.adapter.test.0'); const obj = { _id: 'system.adapter.test.0', type: 'instance' }; @@ -657,7 +657,7 @@ describe('Perf-8 · subscriptionsObjectMap O(1) Dispatch', () => { assert.deepEqual(calledOld.sort(), calledNew.sort()); }); - it('[EXPECTATION] Map-Dispatch ist bei 1000 Subscriptions schneller', () => { + it('[EXPECTATION] map dispatch is faster with 1000 subscriptions', () => { const { arr, map } = buildSubscriptions(1_000, 'target.pattern'); const obj = {}; @@ -665,25 +665,25 @@ describe('Perf-8 · subscriptionsObjectMap O(1) Dispatch', () => { const tNew = bench(() => dispatchNew(map, 'target.pattern', obj), 10_000); assert.ok(tNew < tOld, - `Map (${tNew.toFixed(1)}ms) muss schneller sein als forEach (${tOld.toFixed(1)}ms)`); + `Map (${tNew.toFixed(1)}ms) must be faster than forEach (${tOld.toFixed(1)}ms)`); }); - it('[EXPECTATION] Map liefert leeres Array bei unbekanntem Pattern', () => { + it('[EXPECTATION] map returns an empty array for unknown pattern', () => { const { map } = buildSubscriptions(50, 'known.id'); const result = dispatchNew(map, 'unknown.id', {}); assert.deepEqual(result, []); }); - it('[EXPECTATION] Map bleibt korrekt nach add/remove einer Subscription', () => { + it('[EXPECTATION] map stays correct after add/remove of a subscription', () => { const map = new Map(); - // Subscription hinzufügen + // Add subscription const addSub = (map, sub) => { if (!map.has(sub.pattern)) map.set(sub.pattern, []); map.get(sub.pattern).push(sub); }; - // Subscription entfernen + // Remove subscription const removeSub = (map, subToRemove) => { const subs = map.get(subToRemove.pattern); if (!subs) return; @@ -708,17 +708,17 @@ describe('Perf-8 · subscriptionsObjectMap O(1) Dispatch', () => { assert.equal(map.get('p.1')[0].name, 'b'); removeSub(map, sub2); - assert.ok(!map.has('p.1'), 'Leerer Eintrag muss aus Map entfernt werden'); - assert.ok(map.has('p.2'), 'Anderer Eintrag darf nicht betroffen sein'); + assert.ok(!map.has('p.1'), 'Empty entry must be removed from map'); + assert.ok(map.has('p.2'), 'Other entry must not be affected'); }); }); // ───────────────────────────────────────────────────────────────────────────── -// Gesamt-Smoke-Test: Alle Optimierungen zusammen +// Overall smoke test: all optimizations together // ───────────────────────────────────────────────────────────────────────────── -describe('Integration · Alle Optimierungen gemeinsam', () => { - it('[EXPECTATION] Kombinierter Hot-Path (stateChange) läuft korrekt', () => { - // Simuliert den onStateChange-Hot-Path mit allen Fixes +describe('Integration · all optimizations together', () => { + it('[EXPECTATION] combined hot path (stateChange) runs correctly', () => { + // Simulates the onStateChange hot path with all fixes const stateIds = []; const stateIdSet = new Set(); const states = {}; @@ -748,24 +748,24 @@ describe('Integration · Alle Optimierungen gemeinsam', () => { } } - // 1000 neue States einfügen + // Insert 1000 new states const ids = makeStateIds(1_000); for (const id of ids) onStateChange(id, { val: 1, ack: true }); assert.equal(stateIds.length, 1_000); assert.equal(stateIdSet.size, 1_000); - // Array muss sortiert sein + // Array must be sorted for (let i = 1; i < stateIds.length; i++) { - assert.ok(stateIds[i - 1] <= stateIds[i], 'Array muss sortiert bleiben'); + assert.ok(stateIds[i - 1] <= stateIds[i], 'Array must stay sorted'); } - // 500 States entfernen + // Remove 500 states for (let i = 0; i < 500; i++) onStateChange(ids[i], null); assert.equal(stateIds.length, 500); assert.equal(stateIdSet.size, 500); - // Set und Array müssen synchron sein + // Set and array must stay in sync for (const id of stateIds) assert.ok(stateIdSet.has(id)); for (const id of stateIdSet) assert.ok(stateIds.includes(id)); }); diff --git a/test/performance.test.js b/test/performance.test.js index f83684896..02d880843 100644 --- a/test/performance.test.js +++ b/test/performance.test.js @@ -1,13 +1,13 @@ 'use strict'; /** - * Performance & Correctness Tests für iobroker.javascript – src/main.ts + * Performance & correctness tests for iobroker.javascript - src/main.ts * - * Führe aus mit: npx mocha test/performance.test.js + * Run with: npx mocha test/performance.test.js */ const assert = require('node:assert').strict; // ────────────────────────────────────────────────────────────────────────────── -// Isolierte Hilfsfunktionen (aus main.ts extrahiert) +// Isolated helper functions (extracted from main.ts) // ────────────────────────────────────────────────────────────────────────────── const HTTP_STATUS_TEXTS = new Map([ @@ -22,7 +22,7 @@ const HTTP_STATUS_TEXTS = new Map([ ]); const httpStatusText = code => HTTP_STATUS_TEXTS.get(code) ?? `Error ${code}`; -// Minimaler Adapter-Log-Mock +// Minimal adapter log mock function createLogMock() { const messages = []; return { @@ -37,10 +37,10 @@ function createLogMock() { } // ────────────────────────────────────────────────────────────────────────────── -// 1. httpStatusText – O(1) Map statt Object-Literal pro Aufruf +// 1. httpStatusText - O(1) map instead of object literal per call // ────────────────────────────────────────────────────────────────────────────── describe('httpStatusText()', () => { - it('liefert korrekten Text für bekannte HTTP-Codes', () => { + it('returns the correct text for known HTTP codes', () => { assert.equal(httpStatusText(400), 'Bad Request'); assert.equal(httpStatusText(401), 'Unauthorized'); assert.equal(httpStatusText(403), 'Forbidden'); @@ -51,26 +51,26 @@ describe('httpStatusText()', () => { assert.equal(httpStatusText(503), 'Service Unavailable'); }); - it('liefert generischen Text für unbekannte Codes', () => { + it('returns generic text for unknown codes', () => { assert.equal(httpStatusText(418), 'Error 418'); assert.equal(httpStatusText(999), 'Error 999'); assert.equal(httpStatusText(0), 'Error 0'); }); - it('Map-Instanz wird nur EINMAL erzeugt – kein GC-Druck durch neue Objekte', () => { + it('creates the map instance only ONCE - no GC pressure from new objects', () => { const start = process.memoryUsage().heapUsed; for (let i = 0; i < 100_000; i++) httpStatusText(500); const end = process.memoryUsage().heapUsed; - // Heap-Delta sollte minimal sein (< 1 MB) - assert.ok(end - start < 1024 * 1024, `Zu viel Heap-Zuwachs: ${end - start} Bytes`); + // Heap delta should stay minimal (< 1 MB) + assert.ok(end - start < 1024 * 1024, `Too much heap growth: ${end - start} bytes`); }); }); // ────────────────────────────────────────────────────────────────────────────── -// 2. stateIdSet – O(1) Lookup statt O(n) Array.includes() – Performance-Test +// 2. stateIdSet - O(1) lookup instead of O(n) Array.includes() - performance test // ────────────────────────────────────────────────────────────────────────────── describe('stateIdSet – O(1) Lookup vs O(n) Array.includes()', () => { - it('Set.has() ist bei 50.000 Einträgen schneller als Array.includes()', () => { + it('Set.has() is faster than Array.includes() for 50,000 entries', () => { const N = 50_000; const arr = []; const set = new Set(); @@ -80,7 +80,7 @@ describe('stateIdSet – O(1) Lookup vs O(n) Array.includes()', () => { set.add(`adapter.0.state.${i}`); } - const target = `adapter.0.state.${N - 1}`; // worst case – letztes Element + const target = `adapter.0.state.${N - 1}`; // worst case - last element const t0 = performance.now(); for (let i = 0; i < 1_000; i++) arr.includes(target); @@ -92,11 +92,11 @@ describe('stateIdSet – O(1) Lookup vs O(n) Array.includes()', () => { assert.ok( tSet < tArray, - `Set (${tSet.toFixed(2)}ms) sollte schneller sein als Array (${tArray.toFixed(2)}ms)`, + `Set (${tSet.toFixed(2)}ms) should be faster than Array (${tArray.toFixed(2)}ms)`, ); }); - it('stateIds und stateIdSet bleiben bei add/remove synchron', () => { + it('stateIds and stateIdSet stay in sync on add/remove', () => { const stateIds = []; const stateIdSet = new Set(); @@ -116,7 +116,7 @@ describe('stateIdSet – O(1) Lookup vs O(n) Array.includes()', () => { addState('a.0.state.1'); addState('a.0.state.2'); - addState('a.0.state.1'); // Duplikat – darf nicht doppelt eingefügt werden + addState('a.0.state.1'); // Duplicate - must not be inserted twice assert.equal(stateIds.length, 2); assert.equal(stateIdSet.size, 2); @@ -128,7 +128,7 @@ describe('stateIdSet – O(1) Lookup vs O(n) Array.includes()', () => { assert.ok(stateIdSet.has('a.0.state.2')); }); - it('stateIdSet.has() übersteht 100.000 Lookups ohne Fehler', () => { + it('stateIdSet.has() handles 100,000 lookups without errors', () => { const set = new Set(); for (let i = 0; i < 1_000; i++) set.add(`js.0.s.${i}`); let found = 0; @@ -140,7 +140,7 @@ describe('stateIdSet – O(1) Lookup vs O(n) Array.includes()', () => { }); // ────────────────────────────────────────────────────────────────────────────── -// 3. nameById – O(1) reverse lookup statt O(n) linearer Scan +// 3. nameById - O(1) reverse lookup instead of O(n) linear scan // ────────────────────────────────────────────────────────────────────────────── describe('nameById – O(1) getName() Reverse-Map', () => { function createNameStore() { @@ -180,7 +180,7 @@ describe('nameById – O(1) getName() Reverse-Map', () => { return { addToNames, removeFromNames, getName, names, nameById }; } - it('findet den Namen einer ID in O(1)', () => { + it('finds the name of an ID in O(1)', () => { const store = createNameStore(); store.addToNames({ _id: 'js.0.vars.temp', common: { name: 'Temperatur' } }); store.addToNames({ _id: 'js.0.vars.hum', common: { name: 'Humidity' } }); @@ -190,7 +190,7 @@ describe('nameById – O(1) getName() Reverse-Map', () => { assert.equal(store.getName('js.0.vars.unknown'), null); }); - it('removeFromNames entfernt ID korrekt aus Reverse-Map', () => { + it('removeFromNames correctly removes an ID from the reverse map', () => { const store = createNameStore(); store.addToNames({ _id: 'js.0.vars.temp', common: { name: 'Temperatur' } }); store.removeFromNames('js.0.vars.temp'); @@ -199,7 +199,7 @@ describe('nameById – O(1) getName() Reverse-Map', () => { assert.ok(!store.nameById.has('js.0.vars.temp')); }); - it('mehrere IDs mit gleichem Namen werden korrekt verwaltet', () => { + it('correctly manages multiple IDs with the same name', () => { const store = createNameStore(); store.addToNames({ _id: 'js.0.a', common: { name: 'Sensor' } }); store.addToNames({ _id: 'js.0.b', common: { name: 'Sensor' } }); @@ -209,12 +209,12 @@ describe('nameById – O(1) getName() Reverse-Map', () => { assert.ok(Array.isArray(store.names['Sensor'])); store.removeFromNames('js.0.a'); - // Danach noch 1 Element – sollte kein Array mehr sein + // After this, only 1 element remains - it should no longer be an array assert.equal(store.getName('js.0.a'), null); assert.equal(store.getName('js.0.b'), 'Sensor'); }); - it('Map.get() ist bei 10.000 Objekten deutlich schneller als linearer Scan', () => { + it('Map.get() is significantly faster than a linear scan with 10,000 objects', () => { const N = 10_000; const namesObj = {}; const nameById = new Map(); @@ -243,16 +243,16 @@ describe('nameById – O(1) getName() Reverse-Map', () => { assert.ok( tMap < tLinear, - `Map (${tMap.toFixed(2)}ms) sollte schneller sein als Scan (${tLinear.toFixed(2)}ms)`, + `Map (${tMap.toFixed(2)}ms) should be faster than scan (${tLinear.toFixed(2)}ms)`, ); }); }); // ────────────────────────────────────────────────────────────────────────────── -// 4. onUnload – callback IMMER aufgerufen (auch wenn stopAllScripts wirft) +// 4. onUnload - callback is ALWAYS called (even if stopAllScripts throws) // ────────────────────────────────────────────────────────────────────────────── describe('onUnload() – Shutdown Safety', () => { - it('ruft callback auf auch wenn stopAllScripts wirft', async () => { + it('calls callback even when stopAllScripts throws', async () => { const { log, messages } = createLogMock(); let callbackCalled = false; @@ -270,14 +270,14 @@ describe('onUnload() – Shutdown Safety', () => { await onUnload(() => { callbackCalled = true; }); - assert.ok(callbackCalled, 'callback muss immer aufgerufen werden'); + assert.ok(callbackCalled, 'callback must always be called'); assert.ok( messages.some(m => m.level === 'error' && m.msg.includes('stop failed')), - 'Fehler muss mit log.error geloggt werden', + 'error must be logged with log.error', ); }); - it('ruft callback auf ohne Fehler wenn alles normal läuft', async () => { + it('calls callback without errors when everything runs normally', async () => { let called = false; const onUnload = async (callback) => { try { /* normal cleanup */ } catch { /* nothing */ } finally { callback(); } @@ -288,16 +288,16 @@ describe('onUnload() – Shutdown Safety', () => { }); // ────────────────────────────────────────────────────────────────────────────── -// 5. unsubscribe() – Array-Rekursion via this (Bug-Fix Prüfung) +// 5. unsubscribe() - array recursion via this (bugfix check) // ────────────────────────────────────────────────────────────────────────────── -describe('unsubscribe() – this-Rekursion (Bug-Fix)', () => { - it('ruft this.unsubscribe pro Array-Element auf (kein ReferenceError)', () => { +describe('unsubscribe() - this recursion (bugfix)', () => { + it('calls this.unsubscribe for each array element (no ReferenceError)', () => { const called = []; const obj = { unsubscribe(id) { if (Array.isArray(id)) { - id.forEach(sub => this.unsubscribe(sub)); // ← KORREKT mit this + id.forEach(sub => this.unsubscribe(sub)); // Correct: use this return; } called.push(id); @@ -308,10 +308,10 @@ describe('unsubscribe() – this-Rekursion (Bug-Fix)', () => { assert.deepEqual(called, ['a.0.s.1', 'a.0.s.2', 'a.0.s.3']); }); - it('globale unsubscribe() (ohne this) würde ReferenceError werfen – Fix bestätigt', () => { + it('global unsubscribe() (without this) would throw a ReferenceError - fix confirmed', () => { const broken = function (id) { if (Array.isArray(id)) { - // Simuliere den BUG (globale Funktion) + // Simulate the bug (global function) id.forEach(() => { throw new ReferenceError('unsubscribe is not defined'); }); } }; @@ -322,7 +322,7 @@ describe('unsubscribe() – this-Rekursion (Bug-Fix)', () => { ); }); - it('loggt Warnung bei leerem id', () => { + it('logs a warning for an empty id', () => { const { log, messages } = createLogMock(); const id = ''; if (!id) log.warn('unsubscribe: empty name'); @@ -331,10 +331,10 @@ describe('unsubscribe() – this-Rekursion (Bug-Fix)', () => { }); // ────────────────────────────────────────────────────────────────────────────── -// 6. dayTimeSchedules – Timer-Leak: alter Timer wird vor Neu-Setzen gecleart +// 6. dayTimeSchedules - timer leak: clear old timer before setting a new one // ────────────────────────────────────────────────────────────────────────────── -describe('dayTimeSchedules() – Memory Leak Prüfung', () => { - it('cleart alten Timer bevor neuer gesetzt wird', () => { +describe('dayTimeSchedules() - memory leak check', () => { + it('clears the old timer before setting a new one', () => { const clearedIds = []; const origClear = globalThis.clearTimeout; globalThis.clearTimeout = (t) => { @@ -345,7 +345,7 @@ describe('dayTimeSchedules() – Memory Leak Prüfung', () => { let dayScheduleTimer = setTimeout(() => {}, 9_999_999); const oldTimer = dayScheduleTimer; - // Gefixte Logik: erst clearen, dann null setzen + // Fixed logic: clear first, then set to null if (dayScheduleTimer) { clearTimeout(dayScheduleTimer); dayScheduleTimer = null; @@ -354,13 +354,13 @@ describe('dayTimeSchedules() – Memory Leak Prüfung', () => { globalThis.clearTimeout = origClear; // restore - assert.ok(clearedIds.includes(oldTimer), 'Alter Timer muss vor Neu-Setzen gecleart sein'); - assert.notEqual(dayScheduleTimer, null, 'Neuer Timer muss gesetzt sein'); + assert.ok(clearedIds.includes(oldTimer), 'Old timer must be cleared before setting a new one'); + assert.notEqual(dayScheduleTimer, null, 'New timer must be set'); clearTimeout(dayScheduleTimer); }); - it('kein Timer-Leak bei 10x schnell aufgerufenen dayTimeSchedules', () => { + it('has no timer leak when dayTimeSchedules is called quickly 10 times', () => { let timer = null; const clearedCount = { n: 0 }; @@ -376,16 +376,16 @@ describe('dayTimeSchedules() – Memory Leak Prüfung', () => { for (let i = 0; i < 10; i++) simulateDayTimeSchedules(); clearTimeout(timer); - // 9 von 10 Timern wurden gecleart (der erste hatte keinen Vorgänger) - assert.equal(clearedCount.n, 9, 'Alle vorherigen Timer müssen gecleart worden sein'); + // 9 out of 10 timers were cleared (the first had no predecessor) + assert.equal(clearedCount.n, 9, 'All previous timers must have been cleared'); }); }); // ────────────────────────────────────────────────────────────────────────────── -// 7. installNpm – timeout gesetzt (kein endloses Blockieren) +// 7. installNpm - timeout set (no endless blocking) // ────────────────────────────────────────────────────────────────────────────── -describe('installNpm() – Timeout-Option', () => { - it('übergibt timeout:120000 an child_process.exec', (done) => { +describe('installNpm() - timeout option', () => { + it('passes timeout:120000 to child_process.exec', (done) => { const capturedOpts = []; const mockExec = (_cmd, options) => { @@ -401,12 +401,12 @@ describe('installNpm() – Timeout-Option', () => { const child = mockExec('npm install test-lib --omit=dev', { timeout: 120_000 }); child.on('exit', () => { - assert.equal(capturedOpts[0].timeout, 120_000, 'timeout muss 120.000ms sein'); + assert.equal(capturedOpts[0].timeout, 120_000, 'timeout must be 120,000ms'); done(); }); }); - it('rejectet bei exit-Code != 0', (done) => { + it('rejects for exit code != 0', (done) => { const mockExec = (_cmd, _opts) => ({ stdout: { on: () => {} }, stderr: { on: () => {} }, @@ -427,7 +427,7 @@ describe('installNpm() – Timeout-Option', () => { }); installNpm('broken-lib').then( - () => { done(new Error('Hätte rejecten sollen')); }, + () => { done(new Error('Should have rejected')); }, (err) => { assert.ok(err.message.includes('exited with code 1')); done(); @@ -437,7 +437,7 @@ describe('installNpm() – Timeout-Option', () => { }); // ────────────────────────────────────────────────────────────────────────────── -// 8. convertBackStringifiedValues – JSON-Parsing für object/array States +// 8. convertBackStringifiedValues - JSON parsing for object/array states // ────────────────────────────────────────────────────────────────────────────── describe('convertBackStringifiedValues()', () => { const makeConverter = (objects) => (id, state) => { @@ -454,36 +454,36 @@ describe('convertBackStringifiedValues()', () => { return state; }; - it('parst JSON-String für object-Type korrekt', () => { + it('parses JSON string for object type correctly', () => { const conv = makeConverter({ 'js.0.v': { common: { type: 'object' } } }); const result = conv('js.0.v', { val: '{"a":1,"b":2}' }); assert.deepEqual(result.val, { a: 1, b: 2 }); }); - it('parst JSON-String für array-Type korrekt', () => { + it('parses JSON string for array type correctly', () => { const conv = makeConverter({ 'js.0.arr': { common: { type: 'array' } } }); const result = conv('js.0.arr', { val: '[1,2,3]' }); assert.deepEqual(result.val, [1, 2, 3]); }); - it('behält ungültiges JSON als String', () => { + it('keeps invalid JSON as string', () => { const conv = makeConverter({ 'js.0.v': { common: { type: 'object' } } }); const result = conv('js.0.v', { val: 'not-json{{' }); assert.equal(result.val, 'not-json{{'); }); - it('modifiziert number-Type States nicht', () => { + it('does not modify number-type states', () => { const conv = makeConverter({ 'js.0.n': { common: { type: 'number' } } }); const result = conv('js.0.n', { val: 42 }); assert.equal(result.val, 42); }); - it('gibt null zurück wenn state null ist', () => { + it('returns null when state is null', () => { const conv = makeConverter({}); assert.equal(conv('any.id', null), null); }); - it('gibt undefined zurück wenn state undefined ist', () => { + it('returns undefined when state is undefined', () => { const conv = makeConverter({}); assert.equal(conv('any.id', undefined), undefined); }); diff --git a/test/testAiProviderResolver.js b/test/testAiProviderResolver.js index b07c3d37d..d88bb5a4e 100644 --- a/test/testAiProviderResolver.js +++ b/test/testAiProviderResolver.js @@ -1,4 +1,4 @@ -const expect = require('chai').expect; +const assert = require('node:assert').strict; const { PROVIDER_KEY_FIELD, resolveProviderCredentials, @@ -9,11 +9,11 @@ const { describe('Test AI Provider Resolver', function () { describe('PROVIDER_KEY_FIELD', function () { it('maps every supported provider to its key field', function () { - expect(PROVIDER_KEY_FIELD.openai).to.equal('gptKey'); - expect(PROVIDER_KEY_FIELD.anthropic).to.equal('claudeKey'); - expect(PROVIDER_KEY_FIELD.gemini).to.equal('geminiKey'); - expect(PROVIDER_KEY_FIELD.deepseek).to.equal('deepseekKey'); - expect(PROVIDER_KEY_FIELD.custom).to.equal('gptBaseUrlKey'); + assert.equal(PROVIDER_KEY_FIELD.openai, 'gptKey'); + assert.equal(PROVIDER_KEY_FIELD.anthropic, 'claudeKey'); + assert.equal(PROVIDER_KEY_FIELD.gemini, 'geminiKey'); + assert.equal(PROVIDER_KEY_FIELD.deepseek, 'deepseekKey'); + assert.equal(PROVIDER_KEY_FIELD.custom, 'gptBaseUrlKey'); }); }); @@ -29,63 +29,63 @@ describe('Test AI Provider Resolver', function () { it('resolves openai key with empty baseUrl when no custom URL set', function () { const res = resolveProviderCredentials({ gptKey: 'sk-abc' }, 'openai'); - expect(res.apiKey).to.equal('sk-abc'); - expect(res.baseUrl).to.equal(''); + assert.equal(res.apiKey, 'sk-abc'); + assert.equal(res.baseUrl, ''); }); it('resolves openai key with stored gptBaseUrl', function () { const res = resolveProviderCredentials(fullConfig, 'openai'); - expect(res.apiKey).to.equal('sk-openai-abc'); - expect(res.baseUrl).to.equal('http://localhost:11434/v1'); + assert.equal(res.apiKey, 'sk-openai-abc'); + assert.equal(res.baseUrl, 'http://localhost:11434/v1'); }); it('resolves anthropic key and ignores baseUrl', function () { const res = resolveProviderCredentials(fullConfig, 'anthropic'); - expect(res.apiKey).to.equal('sk-ant-xyz'); - expect(res.baseUrl).to.equal(''); + assert.equal(res.apiKey, 'sk-ant-xyz'); + assert.equal(res.baseUrl, ''); }); it('resolves gemini key and ignores baseUrl', function () { const res = resolveProviderCredentials(fullConfig, 'gemini'); - expect(res.apiKey).to.equal('gemini-123'); - expect(res.baseUrl).to.equal(''); + assert.equal(res.apiKey, 'gemini-123'); + assert.equal(res.baseUrl, ''); }); it('resolves deepseek key and ignores baseUrl', function () { const res = resolveProviderCredentials(fullConfig, 'deepseek'); - expect(res.apiKey).to.equal('ds-456'); - expect(res.baseUrl).to.equal(''); + assert.equal(res.apiKey, 'ds-456'); + assert.equal(res.baseUrl, ''); }); it('resolves custom key from gptBaseUrlKey and custom baseUrl', function () { const res = resolveProviderCredentials(fullConfig, 'custom'); - expect(res.apiKey).to.equal('ollama-key'); - expect(res.baseUrl).to.equal('http://localhost:11434/v1'); + assert.equal(res.apiKey, 'ollama-key'); + assert.equal(res.baseUrl, 'http://localhost:11434/v1'); }); it('returns empty strings for unknown provider', function () { const res = resolveProviderCredentials(fullConfig, 'unknown'); - expect(res.apiKey).to.equal(''); - expect(res.baseUrl).to.equal(''); + assert.equal(res.apiKey, ''); + assert.equal(res.baseUrl, ''); }); it('handles missing/empty config gracefully', function () { const res = resolveProviderCredentials(undefined, 'openai'); - expect(res.apiKey).to.equal(''); - expect(res.baseUrl).to.equal(''); + assert.equal(res.apiKey, ''); + assert.equal(res.baseUrl, ''); const res2 = resolveProviderCredentials(null, 'openai'); - expect(res2.apiKey).to.equal(''); - expect(res2.baseUrl).to.equal(''); + assert.equal(res2.apiKey, ''); + assert.equal(res2.baseUrl, ''); const res3 = resolveProviderCredentials({}, 'openai'); - expect(res3.apiKey).to.equal(''); - expect(res3.baseUrl).to.equal(''); + assert.equal(res3.apiKey, ''); + assert.equal(res3.baseUrl, ''); }); it('trims whitespace from resolved keys', function () { const res = resolveProviderCredentials({ gptKey: ' sk-abc \n' }, 'openai'); - expect(res.apiKey).to.equal('sk-abc'); + assert.equal(res.apiKey, 'sk-abc'); }); it('trims whitespace from resolved baseUrl', function () { @@ -93,22 +93,22 @@ describe('Test AI Provider Resolver', function () { { gptKey: 'k', gptBaseUrl: ' http://localhost:11434/v1 ' }, 'openai', ); - expect(res.baseUrl).to.equal('http://localhost:11434/v1'); + assert.equal(res.baseUrl, 'http://localhost:11434/v1'); }); it('messageBaseUrl takes precedence over stored gptBaseUrl for openai-compatible providers', function () { const res = resolveProviderCredentials(fullConfig, 'openai', 'http://override:8080/v1'); - expect(res.baseUrl).to.equal('http://override:8080/v1'); + assert.equal(res.baseUrl, 'http://override:8080/v1'); }); it('messageBaseUrl is ignored for anthropic/gemini/deepseek', function () { const res = resolveProviderCredentials(fullConfig, 'anthropic', 'http://override:8080/v1'); - expect(res.baseUrl).to.equal(''); + assert.equal(res.baseUrl, ''); }); it('messageBaseUrl=empty-string falls back to stored gptBaseUrl', function () { const res = resolveProviderCredentials(fullConfig, 'custom', ''); - expect(res.baseUrl).to.equal('http://localhost:11434/v1'); + assert.equal(res.baseUrl, 'http://localhost:11434/v1'); }); }); @@ -122,50 +122,50 @@ describe('Test AI Provider Resolver', function () { it('uses message apiKey when provided (settings-dialog form value wins)', function () { const res = resolveTestCredentials(config, 'openai', 'form-typed-key'); - expect(res.apiKey).to.equal('form-typed-key'); + assert.equal(res.apiKey, 'form-typed-key'); }); it('falls back to stored key when message apiKey is empty', function () { const res = resolveTestCredentials(config, 'openai', ''); - expect(res.apiKey).to.equal('stored-openai'); + assert.equal(res.apiKey, 'stored-openai'); }); it('falls back to stored key when message apiKey is undefined', function () { const res = resolveTestCredentials(config, 'claude'.replace('claude', 'anthropic')); - expect(res.apiKey).to.equal('stored-claude'); + assert.equal(res.apiKey, 'stored-claude'); }); it('message apiKey precedence works for every provider', function () { ['openai', 'anthropic', 'gemini', 'deepseek', 'custom'].forEach(p => { const res = resolveTestCredentials(config, p, 'explicit'); - expect(res.apiKey).to.equal('explicit'); + assert.equal(res.apiKey, 'explicit'); }); }); it('trims whitespace from explicit apiKey', function () { const res = resolveTestCredentials(config, 'openai', ' trimmed '); - expect(res.apiKey).to.equal('trimmed'); + assert.equal(res.apiKey, 'trimmed'); }); it('whitespace-only explicit apiKey falls back to stored', function () { const res = resolveTestCredentials(config, 'openai', ' '); - expect(res.apiKey).to.equal('stored-openai'); + assert.equal(res.apiKey, 'stored-openai'); }); it('baseUrl is resolved the same way as in resolveProviderCredentials', function () { const res = resolveTestCredentials(config, 'openai', 'form-key', 'http://form-url:9999/v1'); - expect(res.baseUrl).to.equal('http://form-url:9999/v1'); + assert.equal(res.baseUrl, 'http://form-url:9999/v1'); const res2 = resolveTestCredentials(config, 'openai', 'form-key'); - expect(res2.baseUrl).to.equal('http://stored:1234/v1'); + assert.equal(res2.baseUrl, 'http://stored:1234/v1'); }); }); describe('listAvailableProviders', function () { it('returns empty list for empty config', function () { - expect(listAvailableProviders({})).to.deep.equal([]); - expect(listAvailableProviders(undefined)).to.deep.equal([]); - expect(listAvailableProviders(null)).to.deep.equal([]); + assert.deepEqual(listAvailableProviders({}), []); + assert.deepEqual(listAvailableProviders(undefined), []); + assert.deepEqual(listAvailableProviders(null), []); }); it('lists only providers with non-empty keys', function () { @@ -175,14 +175,14 @@ describe('Test AI Provider Resolver', function () { geminiKey: 'g', deepseekKey: null, }); - expect(res).to.deep.equal([{ provider: 'openai' }, { provider: 'gemini' }]); + assert.deepEqual(res, [{ provider: 'openai' }, { provider: 'gemini' }]); }); it('lists custom provider with baseUrl when gptBaseUrl is set', function () { const res = listAvailableProviders({ gptBaseUrl: 'http://localhost:11434/v1', }); - expect(res).to.deep.equal([ + assert.deepEqual(res, [ { provider: 'custom', baseUrl: 'http://localhost:11434/v1' }, ]); }); @@ -192,7 +192,7 @@ describe('Test AI Provider Resolver', function () { gptBaseUrl: 'http://localhost:11434/v1', gptBaseUrlKey: '', }); - expect(res.some(p => p.provider === 'custom')).to.equal(true); + assert.equal(res.some(p => p.provider === 'custom'), true); }); it('returns all 5 providers when fully configured', function () { @@ -204,8 +204,8 @@ describe('Test AI Provider Resolver', function () { gptBaseUrl: 'http://x/v1', gptBaseUrlKey: 'e', }); - expect(res).to.have.lengthOf(5); - expect(res.map(p => p.provider)).to.deep.equal([ + assert.equal(res.length, 5); + assert.deepEqual(res.map(p => p.provider), [ 'openai', 'anthropic', 'gemini', @@ -220,7 +220,7 @@ describe('Test AI Provider Resolver', function () { claudeKey: 'real', gptBaseUrl: '\n\t', }); - expect(res).to.deep.equal([{ provider: 'anthropic' }]); + assert.deepEqual(res, [{ provider: 'anthropic' }]); }); it('does not include the key value in its output (security invariant)', function () { @@ -229,8 +229,8 @@ describe('Test AI Provider Resolver', function () { claudeKey: 'sk-ant-should-never-appear-SECRET', }); const serialized = JSON.stringify(res); - expect(serialized).to.not.include('sk-should-never-appear'); - expect(serialized).to.not.include('SECRET'); + assert.ok(!serialized.includes('sk-should-never-appear')); + assert.ok(!serialized.includes('SECRET')); }); }); }); diff --git a/test/testAnthropicAdapter.js b/test/testAnthropicAdapter.js index 882ada7ae..ec9483afd 100644 --- a/test/testAnthropicAdapter.js +++ b/test/testAnthropicAdapter.js @@ -1,4 +1,4 @@ -const expect = require('chai').expect; +const assert = require('node:assert').strict; const { translateToolsToAnthropic, translateMessagesToAnthropic, @@ -35,13 +35,13 @@ describe('Test Anthropic Adapter', function () { it('translates an OpenAI tool array to Anthropic shape', function () { const result = translateToolsToAnthropic(openAITools); - expect(result).to.have.lengthOf(2); - expect(result[0]).to.deep.equal({ + assert.equal(result.length, 2); + assert.deepEqual(result[0], { name: 'search_datapoints', description: 'Search datapoints', input_schema: openAITools[0].function.parameters, }); - expect(result[1]).to.deep.equal({ + assert.deepEqual(result[1], { name: 'list_scripts', description: 'List all scripts', input_schema: openAITools[1].function.parameters, @@ -50,14 +50,14 @@ describe('Test Anthropic Adapter', function () { it('uses parameters as input_schema (JSON Schema is shared)', function () { const result = translateToolsToAnthropic(openAITools); - expect(result[0].input_schema).to.equal(openAITools[0].function.parameters); + assert.equal(result[0].input_schema, openAITools[0].function.parameters); }); it('provides a default empty object schema when parameters are missing', function () { const result = translateToolsToAnthropic([ { type: 'function', function: { name: 'no_params' } }, ]); - expect(result[0].input_schema).to.deep.equal({ type: 'object', properties: {} }); + assert.deepEqual(result[0].input_schema, { type: 'object', properties: {} }); }); it('skips tools without a function name', function () { @@ -67,22 +67,22 @@ describe('Test Anthropic Adapter', function () { null, { foo: 'bar' }, ]); - expect(result).to.have.lengthOf(1); - expect(result[0].name).to.equal('ok'); + assert.equal(result.length, 1); + assert.equal(result[0].name, 'ok'); }); it('handles empty / undefined / non-array input', function () { - expect(translateToolsToAnthropic([])).to.deep.equal([]); - expect(translateToolsToAnthropic(undefined)).to.deep.equal([]); - expect(translateToolsToAnthropic(null)).to.deep.equal([]); - expect(translateToolsToAnthropic('not an array')).to.deep.equal([]); + assert.deepEqual(translateToolsToAnthropic([]), []); + assert.deepEqual(translateToolsToAnthropic(undefined), []); + assert.deepEqual(translateToolsToAnthropic(null), []); + assert.deepEqual(translateToolsToAnthropic('not an array'), []); }); it('drops the description field cleanly when absent', function () { const result = translateToolsToAnthropic([ { type: 'function', function: { name: 'x', parameters: { type: 'object' } } }, ]); - expect(result[0].description).to.equal(undefined); + assert.equal(result[0].description, undefined); }); }); @@ -92,8 +92,8 @@ describe('Test Anthropic Adapter', function () { { role: 'system', content: 'You are helpful.' }, { role: 'user', content: 'Hi' }, ]); - expect(system).to.equal('You are helpful.'); - expect(messages).to.deep.equal([{ role: 'user', content: 'Hi' }]); + assert.equal(system, 'You are helpful.'); + assert.deepEqual(messages, [{ role: 'user', content: 'Hi' }]); }); it('joins multiple system messages with double newlines', function () { @@ -102,7 +102,7 @@ describe('Test Anthropic Adapter', function () { { role: 'system', content: 'Line 2' }, { role: 'user', content: 'Hi' }, ]); - expect(system).to.equal('Line 1\n\nLine 2'); + assert.equal(system, 'Line 1\n\nLine 2'); }); it('converts assistant tool_calls into tool_use content blocks', function () { @@ -123,8 +123,8 @@ describe('Test Anthropic Adapter', function () { ], }, ]); - expect(messages).to.have.lengthOf(2); - expect(messages[1]).to.deep.equal({ + assert.equal(messages.length, 2); + assert.deepEqual(messages[1], { role: 'assistant', content: [ { type: 'text', text: 'Let me search.' }, @@ -152,7 +152,7 @@ describe('Test Anthropic Adapter', function () { ], }, ]); - expect(messages[0].content[0]).to.deep.equal({ + assert.deepEqual(messages[0].content[0], { type: 'tool_use', id: 'c1', name: 'f', @@ -172,8 +172,8 @@ describe('Test Anthropic Adapter', function () { }, { role: 'tool', tool_call_id: 'c1', content: 'found: lamp.state' }, ]); - expect(messages).to.have.lengthOf(3); - expect(messages[2]).to.deep.equal({ + assert.equal(messages.length, 3); + assert.deepEqual(messages[2], { role: 'user', content: [{ type: 'tool_result', tool_use_id: 'c1', content: 'found: lamp.state' }], }); @@ -194,16 +194,16 @@ describe('Test Anthropic Adapter', function () { { role: 'user', content: 'Thanks' }, ]); const toolResultMsg = messages.find(m => Array.isArray(m.content) && m.content[0]?.type === 'tool_result'); - expect(toolResultMsg).to.not.equal(undefined); - expect(toolResultMsg.content).to.have.lengthOf(2); - expect(toolResultMsg.content.map(b => b.tool_use_id)).to.deep.equal(['c1', 'c2']); + assert.notEqual(toolResultMsg, undefined); + assert.equal(toolResultMsg.content.length, 2); + assert.deepEqual(toolResultMsg.content.map(b => b.tool_use_id), ['c1', 'c2']); }); it('stringifies non-string tool_result content', function () { const { messages } = translateMessagesToAnthropic([ { role: 'tool', tool_call_id: 'c1', content: { key: 'value' } }, ]); - expect(messages[0].content[0].content).to.equal(JSON.stringify({ key: 'value' })); + assert.equal(messages[0].content[0].content, JSON.stringify({ key: 'value' })); }); it('omits assistant messages that have neither text nor tool_calls', function () { @@ -211,7 +211,7 @@ describe('Test Anthropic Adapter', function () { { role: 'user', content: 'Hi' }, { role: 'assistant', content: '' }, ]); - expect(messages).to.have.lengthOf(1); + assert.equal(messages.length, 1); }); it('omits empty user messages', function () { @@ -219,21 +219,21 @@ describe('Test Anthropic Adapter', function () { { role: 'user', content: '' }, { role: 'user', content: 'Real message' }, ]); - expect(messages).to.have.lengthOf(1); - expect(messages[0].content).to.equal('Real message'); + assert.equal(messages.length, 1); + assert.equal(messages[0].content, 'Real message'); }); it('handles empty/invalid input gracefully', function () { - expect(translateMessagesToAnthropic([])).to.deep.equal({ system: '', messages: [] }); - expect(translateMessagesToAnthropic(undefined)).to.deep.equal({ system: '', messages: [] }); - expect(translateMessagesToAnthropic(null)).to.deep.equal({ system: '', messages: [] }); + assert.deepEqual(translateMessagesToAnthropic([]), { system: '', messages: [] }); + assert.deepEqual(translateMessagesToAnthropic(undefined), { system: '', messages: [] }); + assert.deepEqual(translateMessagesToAnthropic(null), { system: '', messages: [] }); }); it('handles assistant messages with text but no tool_calls', function () { const { messages } = translateMessagesToAnthropic([ { role: 'assistant', content: 'Just text' }, ]); - expect(messages[0]).to.deep.equal({ + assert.deepEqual(messages[0], { role: 'assistant', content: [{ type: 'text', text: 'Just text' }], }); @@ -251,9 +251,9 @@ describe('Test Anthropic Adapter', function () { ], }, ]); - expect(messages).to.have.lengthOf(1); - expect(messages[0].content).to.have.lengthOf(1); - expect(messages[0].content[0].name).to.equal('valid'); + assert.equal(messages.length, 1); + assert.equal(messages[0].content.length, 1); + assert.equal(messages[0].content[0].name, 'valid'); }); }); @@ -263,7 +263,7 @@ describe('Test Anthropic Adapter', function () { content: [{ type: 'text', text: 'Hello, world.' }], stop_reason: 'end_turn', }); - expect(result).to.deep.equal({ content: 'Hello, world.' }); + assert.deepEqual(result, { content: 'Hello, world.' }); }); it('concatenates multiple text blocks with newlines', function () { @@ -273,7 +273,7 @@ describe('Test Anthropic Adapter', function () { { type: 'text', text: 'Line 2' }, ], }); - expect(result.content).to.equal('Line 1\nLine 2'); + assert.equal(result.content, 'Line 1\nLine 2'); }); it('converts tool_use blocks to OpenAI tool_calls (arguments stringified)', function () { @@ -289,9 +289,9 @@ describe('Test Anthropic Adapter', function () { ], stop_reason: 'tool_use', }); - expect(result.content).to.equal('Let me check.'); - expect(result.tool_calls).to.have.lengthOf(1); - expect(result.tool_calls[0]).to.deep.equal({ + assert.equal(result.content, 'Let me check.'); + assert.equal(result.tool_calls.length, 1); + assert.deepEqual(result.tool_calls[0], { id: 'toolu_abc', type: 'function', function: { @@ -312,9 +312,9 @@ describe('Test Anthropic Adapter', function () { }, ], }); - expect(result.content).to.equal(''); - expect(result.tool_calls).to.have.lengthOf(1); - expect(result.tool_calls[0].function.arguments).to.equal('{}'); + assert.equal(result.content, ''); + assert.equal(result.tool_calls.length, 1); + assert.equal(result.tool_calls[0].function.arguments, '{}'); }); it('handles multiple tool_use blocks', function () { @@ -324,22 +324,22 @@ describe('Test Anthropic Adapter', function () { { type: 'tool_use', id: 't2', name: 'f2', input: { b: 2 } }, ], }); - expect(result.tool_calls).to.have.lengthOf(2); - expect(result.tool_calls.map(tc => tc.id)).to.deep.equal(['t1', 't2']); + assert.equal(result.tool_calls.length, 2); + assert.deepEqual(result.tool_calls.map(tc => tc.id), ['t1', 't2']); }); it('omits tool_calls when none are present', function () { const result = translateAnthropicResponseToOpenAI({ content: [{ type: 'text', text: 'Plain response' }], }); - expect(result).to.not.have.property('tool_calls'); + assert.ok(!Object.prototype.hasOwnProperty.call(result, 'tool_calls')); }); it('handles empty/invalid response gracefully', function () { - expect(translateAnthropicResponseToOpenAI(null)).to.deep.equal({ content: '' }); - expect(translateAnthropicResponseToOpenAI(undefined)).to.deep.equal({ content: '' }); - expect(translateAnthropicResponseToOpenAI({})).to.deep.equal({ content: '' }); - expect(translateAnthropicResponseToOpenAI({ content: null })).to.deep.equal({ content: '' }); + assert.deepEqual(translateAnthropicResponseToOpenAI(null), { content: '' }); + assert.deepEqual(translateAnthropicResponseToOpenAI(undefined), { content: '' }); + assert.deepEqual(translateAnthropicResponseToOpenAI({}), { content: '' }); + assert.deepEqual(translateAnthropicResponseToOpenAI({ content: null }), { content: '' }); }); it('ignores unknown content-block types', function () { @@ -350,15 +350,15 @@ describe('Test Anthropic Adapter', function () { { type: 'tool_use', id: 'x', name: 'y', input: {} }, ], }); - expect(result.content).to.equal('Known'); - expect(result.tool_calls).to.have.lengthOf(1); + assert.equal(result.content, 'Known'); + assert.equal(result.tool_calls.length, 1); }); it('tool_use with no input serializes to {}', function () { const result = translateAnthropicResponseToOpenAI({ content: [{ type: 'tool_use', id: 't', name: 'f' }], }); - expect(result.tool_calls[0].function.arguments).to.equal('{}'); + assert.equal(result.tool_calls[0].function.arguments, '{}'); }); }); @@ -385,14 +385,14 @@ describe('Test Anthropic Adapter', function () { // Assistant message must have the tool_use block intact const assistant = messages.find(m => m.role === 'assistant'); const toolUse = assistant.content.find(b => b.type === 'tool_use'); - expect(toolUse.input).to.deep.equal({ id: 'x.y' }); + assert.deepEqual(toolUse.input, { id: 'x.y' }); // Tool-result wrapped as user message const toolResult = messages.find( m => Array.isArray(m.content) && m.content[0]?.type === 'tool_result', ); - expect(toolResult.content[0].tool_use_id).to.equal('c1'); - expect(toolResult.content[0].content).to.equal('42'); + assert.equal(toolResult.content[0].tool_use_id, 'c1'); + assert.equal(toolResult.content[0].content, '42'); }); it('Anthropic response → OpenAI-shape round-trip preserves tool call IDs', function () { @@ -408,7 +408,7 @@ describe('Test Anthropic Adapter', function () { ], }; const openAIStyle = translateAnthropicResponseToOpenAI(anthropicResponse); - expect(openAIStyle.tool_calls[0].id).to.equal('toolu_01abc'); + assert.equal(openAIStyle.tool_calls[0].id, 'toolu_01abc'); // Now feed it back — simulates the continuation round const { messages } = translateMessagesToAnthropic([ @@ -423,7 +423,7 @@ describe('Test Anthropic Adapter', function () { const toolResult = messages.find( m => Array.isArray(m.content) && m.content.some(b => b.type === 'tool_result'), ); - expect(toolResult.content[0].tool_use_id).to.equal('toolu_01abc'); + assert.equal(toolResult.content[0].tool_use_id, 'toolu_01abc'); }); }); }); diff --git a/test/testFunctions.js b/test/testFunctions.js index 95d6f49cc..95c1fb163 100644 --- a/test/testFunctions.js +++ b/test/testFunctions.js @@ -1,7 +1,15 @@ -const expect = require('chai').expect; +const assert = require('node:assert').strict; const { log } = require('node:console'); const setup = require('./lib/setup'); +function assertHasNoKeys(obj, keys) { + assert.ok(keys.every(key => !Object.prototype.hasOwnProperty.call(obj, key))); +} + +function assertHasAllKeys(obj, keys) { + assert.ok(keys.every(key => Object.prototype.hasOwnProperty.call(obj, key))); +} + let objects = null; let states = null; @@ -125,7 +133,7 @@ describe.only('Test JS', function () { native: {}, }; objects.setObject(script._id, script, err => { - expect(err).to.be.null; + assert.equal(err, null); setup.startAdapter(objects, states, () => _done()); }); }, @@ -136,7 +144,7 @@ describe.only('Test JS', function () { it('Test JS: Check if adapter started', function (done) { this.timeout(30000); checkConnectionOfAdapter(err => { - expect(err).to.be.null; + assert.equal(err, null); done(); }); }); @@ -181,8 +189,8 @@ describe.only('Test JS', function () { if (state.val === 9) { removeStateChangedHandler(onStateChanged); states.getState('javascript.0.testCompareTime', (err, state) => { - expect(err).to.be.null; - expect(state.val).to.be.equal(9); + assert.equal(err, null); + assert.equal(state.val, 9); done(); }); } else { @@ -193,7 +201,7 @@ describe.only('Test JS', function () { addStateChangedHandler(onStateChanged); objects.setObject(script._id, script, err => { - expect(err).to.be.null; + assert.equal(err, null); }); }); @@ -230,7 +238,7 @@ describe.only('Test JS', function () { addStateChangedHandler(onStateChanged); objects.setObject(script._id, script, err => { - expect(err).to.be.null; + assert.equal(err, null); }); }); @@ -256,11 +264,11 @@ describe.only('Test JS', function () { if (id === 'javascript.0.test_creation_of_state' && state.val === 5) { removeStateChangedHandler(onStateChanged); states.getState('javascript.0.test_creation_of_state', (err, state) => { - expect(err).to.be.null; - expect(state.val).to.be.equal(5); + assert.equal(err, null); + assert.equal(state.val, 5); objects.getObject('javascript.0.test_creation_of_state', (err, obj) => { - expect(err).to.be.null; - expect(obj).to.be.ok; + assert.equal(err, null); + assert.ok(obj); done(); }); @@ -271,7 +279,7 @@ describe.only('Test JS', function () { addStateChangedHandler(onStateChanged); objects.setObject(script._id, script, err => { - expect(err).to.be.null; + assert.equal(err, null); }); }); @@ -295,12 +303,12 @@ describe.only('Test JS', function () { if (id === 'javascript.1.test_creation_of_foreign_state' && state.val === 6) { removeStateChangedHandler(onStateChanged); states.getState('javascript.1.test_creation_of_foreign_state', (err, state) => { - expect(err).to.be.null; - expect(state).to.be.ok; - expect(state.val).to.be.equal(6); + assert.equal(err, null); + assert.ok(state); + assert.equal(state.val, 6); objects.getObject('javascript.1.test_creation_of_foreign_state', (err, obj) => { - expect(err).to.be.null; - expect(obj).to.be.ok; + assert.equal(err, null); + assert.ok(obj); done(); }); }); @@ -308,7 +316,7 @@ describe.only('Test JS', function () { }; addStateChangedHandler(onStateChanged); objects.setObject(script._id, script, err => { - expect(err).to.be.null; + assert.equal(err, null); }); }); @@ -330,24 +338,24 @@ describe.only('Test JS', function () { }; objects.getObject('javascript.0.test_creation_of_state', (err, obj) => { - expect(err).to.be.null; - expect(obj).to.be.ok; + assert.equal(err, null); + assert.ok(obj); states.getState('javascript.0.test_creation_of_state', (err, state) => { - expect(err).to.be.null; - expect(state).to.be.ok; - expect(state.val).to.be.equal(5); + assert.equal(err, null); + assert.ok(state); + assert.equal(state.val, 5); const onStateChanged = function (id, state) { if (id === 'javascript.0.test_creation_of_state' && state === null) { removeStateChangedHandler(onStateChanged); states.getState('javascript.0.test_creation_of_state', (err, state) => { - expect(err).to.be.null; - expect(state).to.be.not.ok; + assert.equal(err, null); + assert.ok(!state); objects.getObject('javascript.0.test_creation_of_state', (err, obj) => { - expect(err).to.be.undefined; - expect(obj).to.be.not.ok; + assert.equal(err, undefined); + assert.ok(!obj); done(); }); }); @@ -356,7 +364,7 @@ describe.only('Test JS', function () { addStateChangedHandler(onStateChanged); objects.setObject(script._id, script, err => { - expect(err).to.be.null; + assert.equal(err, null); }); }); }); @@ -380,29 +388,29 @@ describe.only('Test JS', function () { }; objects.getObject('javascript.1.test_creation_of_foreign_state', (err, obj) => { - expect(err).to.be.null; - expect(obj).to.be.ok; + assert.equal(err, null); + assert.ok(obj); states.getState('javascript.1.test_creation_of_foreign_state', (err, state) => { - expect(err).to.be.null; - expect(state).to.be.ok; - expect(state.val).to.be.equal(6); + assert.equal(err, null); + assert.ok(state); + assert.equal(state.val, 6); // we cannot delete foreign object, even if we created it. setTimeout(function () { objects.getObject('javascript.1.test_creation_of_foreign_state', (err, obj) => { - expect(err).to.be.null; - expect(obj).to.be.ok; + assert.equal(err, null); + assert.ok(obj); states.getState('javascript.1.test_creation_of_foreign_state', (err, state) => { - expect(err).to.be.null; - expect(state).to.be.ok; - expect(state.val).to.be.equal(6); + assert.equal(err, null); + assert.ok(state); + assert.equal(state.val, 6); done(); }); }); }, 400); objects.setObject(script._id, script, err => { - expect(err).to.be.null; + assert.equal(err, null); }); }); }); @@ -433,73 +441,68 @@ describe.only('Test JS', function () { native: {}, }; objects.setObject(script._id, script, err => { - expect(err).to.be.null; + assert.equal(err, null); setTimeout(function () { objects.getObject('javascript.0.test_createState_init', (err, obj) => { - expect(err).to.be.null; - expect(obj.common.name).to.be.equal('test_createState_init'); // = id - expect(obj.common.type).to.be.equal('mixed'); - expect(obj.native).to.not.have.any.keys('name', 'desc', 'type', 'role'); + assert.equal(err, null); + assert.equal(obj.common.name, 'test_createState_init'); // = id + assert.equal(obj.common.type, 'mixed'); + assertHasNoKeys(obj.native, ['name', 'desc', 'type', 'role']); objects.getObject('javascript.0.test_createState_common', (err, obj) => { - expect(err).to.be.null; - expect(obj.common.name).to.be.equal('common'); - expect(obj.common.desc).to.be.equal('test'); - expect(obj.common.type).to.be.equal('array'); - expect(obj.native).to.not.have.any.keys('name', 'desc', 'type', 'role'); + assert.equal(err, null); + assert.equal(obj.common.name, 'common'); + assert.equal(obj.common.desc, 'test'); + assert.equal(obj.common.type, 'array'); + assertHasNoKeys(obj.native, ['name', 'desc', 'type', 'role']); objects.getObject('javascript.0.test_createState_initCommon', (err, obj) => { - expect(err).to.be.null; - expect(obj.common.name).to.be.equal('initCommon'); - expect(obj.common.desc).to.be.equal('test'); - expect(obj.common.type).to.be.equal('number'); - expect(obj.native).to.not.have.any.keys('name', 'desc', 'type', 'role'); + assert.equal(err, null); + assert.equal(obj.common.name, 'initCommon'); + assert.equal(obj.common.desc, 'test'); + assert.equal(obj.common.type, 'number'); + assertHasNoKeys(obj.native, ['name', 'desc', 'type', 'role']); objects.getObject('javascript.0.test_createState_commonNative', (err, obj) => { - expect(err).to.be.null; - expect(obj.common.name).to.be.equal('commonNative'); - expect(obj.common.desc).to.be.equal('test'); - expect(obj.common.type).to.be.equal('object'); - expect(obj.native).to.not.have.any.keys('name', 'desc', 'type', 'role'); - expect(obj.native).to.have.all.keys('customProperty'); + assert.equal(err, null); + assert.equal(obj.common.name, 'commonNative'); + assert.equal(obj.common.desc, 'test'); + assert.equal(obj.common.type, 'object'); + assertHasNoKeys(obj.native, ['name', 'desc', 'type', 'role']); + assertHasAllKeys(obj.native, ['customProperty']); objects.getObject('javascript.0.test_createState_initCommonNative', (err, obj) => { - expect(err).to.be.null; - expect(obj.common.name).to.be.equal('initCommonNative'); - expect(obj.common.desc).to.be.equal('test'); - expect(obj.common.type).to.be.equal('number'); - expect(obj.native).to.not.have.any.keys('name', 'desc', 'type', 'role'); - expect(obj.native).to.have.all.keys('customProperty'); + assert.equal(err, null); + assert.equal(obj.common.name, 'initCommonNative'); + assert.equal(obj.common.desc, 'test'); + assert.equal(obj.common.type, 'number'); + assertHasNoKeys(obj.native, ['name', 'desc', 'type', 'role']); + assertHasAllKeys(obj.native, ['customProperty']); objects.getObject('javascript.0.test_createState_initForce', (err, obj) => { - expect(err).to.be.null; - expect(obj.common.name).to.be.equal('test_createState_initForce'); // = id - expect(obj.common.type).to.be.equal('mixed'); - expect(obj.native).to.not.have.any.keys('name', 'desc', 'type', 'role'); + assert.equal(err, null); + assert.equal(obj.common.name, 'test_createState_initForce'); // = id + assert.equal(obj.common.type, 'mixed'); + assertHasNoKeys(obj.native, ['name', 'desc', 'type', 'role']); objects.getObject( 'javascript.0.test_createState_initForceCommon', (err, obj) => { - expect(err).to.be.null; - expect(obj.common.name).to.be.equal('initFoceCommon'); - expect(obj.common.desc).to.be.equal('test'); - expect(obj.common.type).to.be.equal('boolean'); - expect(obj.native).to.not.have.any.keys('name', 'desc', 'type', 'role'); + assert.equal(err, null); + assert.equal(obj.common.name, 'initFoceCommon'); + assert.equal(obj.common.desc, 'test'); + assert.equal(obj.common.type, 'boolean'); + assertHasNoKeys(obj.native, ['name', 'desc', 'type', 'role']); objects.getObject( 'javascript.0.test_createState_initForceCommonNative', (err, obj) => { - expect(err).to.be.null; - expect(obj.common.name).to.be.equal('initForceCommonNative'); - expect(obj.common.desc).to.be.equal('test'); - expect(obj.common.type).to.be.equal('boolean'); - expect(obj.native).to.not.have.any.keys( - 'name', - 'desc', - 'type', - 'role', - ); - expect(obj.native).to.have.all.keys('customProperty'); + assert.equal(err, null); + assert.equal(obj.common.name, 'initForceCommonNative'); + assert.equal(obj.common.desc, 'test'); + assert.equal(obj.common.type, 'boolean'); + assertHasNoKeys(obj.native, ['name', 'desc', 'type', 'role']); + assertHasAllKeys(obj.native, ['customProperty']); done(); }, @@ -548,7 +551,7 @@ describe.only('Test JS', function () { addStateChangedHandler(onStateChanged); objects.setObject(script._id, script, err => { - expect(err).to.be.null; + assert.equal(err, null); }); }); @@ -582,7 +585,7 @@ describe.only('Test JS', function () { }; addStateChangedHandler(onStateChanged); objects.setObject(script._id, script, err => { - expect(err).to.be.null; + assert.equal(err, null); }); }); @@ -618,7 +621,7 @@ describe.only('Test JS', function () { }; addStateChangedHandler(onStateChanged); objects.setObject(script._id, script, err => { - expect(err).to.be.null; + assert.equal(err, null); }); }); @@ -659,7 +662,7 @@ describe.only('Test JS', function () { }; addStateChangedHandler(onStateChanged); objects.setObject(script._id, script, err => { - expect(err).to.be.null; + assert.equal(err, null); }); }); @@ -692,16 +695,16 @@ describe.only('Test JS', function () { }; objects.setObject(script._id, script, err => { - expect(err).to.be.null; + assert.equal(err, null); setTimeout(function () { if (!fs.existsSync(__dirname + '/../tmp/objects.json')) { setTimeout(function () { - expect(fs.readFileSync(__dirname + '/../tmp/objects.json').toString()).to.be.equal(time); + assert.equal(fs.readFileSync(__dirname + '/../tmp/objects.json').toString(), time); fs.unlinkSync(__dirname + '/../tmp/objects.json'); done(); }, 500); } else { - expect(fs.readFileSync(__dirname + '/../tmp/objects.json').toString()).to.be.equal(time); + assert.equal(fs.readFileSync(__dirname + '/../tmp/objects.json').toString(), time); fs.unlinkSync(__dirname + '/../tmp/objects.json'); done(); } @@ -736,13 +739,13 @@ describe.only('Test JS', function () { if (id === 'javascript.0.test_createTempFile' && state.val !== '-') { const tempFilePath = state.val; - expect(tempFilePath).to.be.a('string'); - expect(tempFilePath.startsWith(os.tmpdir())).to.be.true; - expect(fs.existsSync(tempFilePath)).to.be.true; + assert.equal(typeof tempFilePath, 'string'); + assert.equal(tempFilePath.startsWith(os.tmpdir()), true); + assert.equal(fs.existsSync(tempFilePath), true); // Check content const fileContent = fs.readFileSync(tempFilePath).toString(); - expect(fileContent).to.be.equal('CONTENT_OK'); + assert.equal(fileContent, 'CONTENT_OK'); removeStateChangedHandler(onStateChanged); done(); @@ -750,7 +753,7 @@ describe.only('Test JS', function () { }; addStateChangedHandler(onStateChanged); objects.setObject(script._id, script, err => { - expect(err).to.be.null; + assert.equal(err, null); }); }); @@ -810,9 +813,9 @@ describe.only('Test JS', function () { let count = types.length; for (let t = 0; t < types.length; t++) { states.getState(`javascript.0.test_getAstroDate_${types[t]}`, (err, state) => { - expect(err).to.be.null; - expect(state).to.be.ok; - expect(state.val).to.be.ok; + assert.equal(err, null); + assert.ok(state); + assert.ok(state.val); if (state) console.log(types[types.length - count] + ': ' + state.val); else console.log(types[types.length - count] + ' ERROR: ' + state); @@ -825,7 +828,7 @@ describe.only('Test JS', function () { addStateChangedHandler(onStateChanged); objects.setObject(script._id, script, err => { - expect(err).to.be.null; + assert.equal(err, null); }); }); @@ -855,8 +858,8 @@ describe.only('Test JS', function () { if (state.val === 4) { start = state.ts; } else if (state.val === 5) { - expect(start).to.be.not.equal(0); - expect(state.ts - start).to.be.least(950); + assert.notEqual(start, 0); + assert.ok(state.ts - start >= 950); removeStateChangedHandler(onStateChanged); setTimeout(done, 100); @@ -888,8 +891,8 @@ describe.only('Test JS', function () { const onStateChanged = function (id, state) { if (id === 'javascript.0.test_setStateDelayed_stateObject') { - expect(state.val).to.be.true; - expect(state.ack).to.be.true; + assert.equal(state.val, true); + assert.equal(state.ack, true); removeStateChangedHandler(onStateChanged); setTimeout(done, 100); @@ -926,8 +929,8 @@ describe.only('Test JS', function () { if (state.val === 6) { start = state.ts; } else if (state.val === 7) { - expect(start).to.be.not.equal(0); - expect(state.ts - start).to.be.least(900); + assert.notEqual(start, 0); + assert.ok(state.ts - start >= 900); removeStateChangedHandler(onStateChanged); setTimeout(done, 100); @@ -959,21 +962,21 @@ describe.only('Test JS', function () { }; objects.setObject(script._id, script, err => { - expect(err).to.be.null; + assert.equal(err, null); checkValueOfState( 'javascript.0.test_setStateDelayed_overwrite', 8, err => { - expect(err).to.be.ok; + assert.ok(err); states.getState('javascript.0.test_setStateDelayed_overwrite', (err, stateStart) => { - expect(err).to.be.null; - expect(stateStart.val).to.be.not.equal(8); + assert.equal(err, null); + assert.notEqual(stateStart.val, 8); checkValueOfState('javascript.0.test_setStateDelayed_overwrite', 9, err => { - expect(err).to.be.null; + assert.equal(err, null); states.getState('javascript.0.test_setStateDelayed_overwrite', err => { - expect(err).to.be.null; + assert.equal(err, null); done(); }); }); @@ -1006,17 +1009,17 @@ describe.only('Test JS', function () { }; objects.setObject(script._id, script, err => { - expect(err).to.be.null; + assert.equal(err, null); checkValueOfState( 'javascript.0.test_clearStateDelayed', 10, err => { - expect(err).to.be.ok; + assert.ok(err); states.getState('javascript.0.test_clearStateDelayed', (err, stateStart) => { - expect(err).to.be.null; - expect(stateStart.val).to.be.not.equal(10); + assert.equal(err, null); + assert.notEqual(stateStart.val, 10); done(); }); }, @@ -1049,17 +1052,17 @@ describe.only('Test JS', function () { }; objects.setObject(script._id, script, err => { - expect(err).to.be.null; + assert.equal(err, null); setTimeout(() => { states.getState('javascript.0.test_getStateDelayed_single_result', (err, delayedResult) => { - expect(err).to.be.null; + assert.equal(err, null); console.log('delayedResult: ' + delayedResult.val); const result = JSON.parse(delayedResult.val); - expect(result[0]).to.be.ok; - expect(result[0].timerId).to.be.ok; - expect(result[0].left).to.be.ok; - expect(result[0].delay).to.be.equal(1500); + assert.ok(result[0]); + assert.ok(result[0].timerId); + assert.ok(result[0].left); + assert.equal(result[0].delay, 1500); done(); }); }, 500); @@ -1090,17 +1093,17 @@ describe.only('Test JS', function () { }; objects.setObject(script._id, script, err => { - expect(err).to.be.null; + assert.equal(err, null); setTimeout(() => { states.getState('javascript.0.test_getStateDelayed_all_result', (err, delayedResult) => { console.log('delayedResult!: ' + delayedResult.val); - expect(err).to.be.null; + assert.equal(err, null); const result = JSON.parse(delayedResult.val); - expect(result['javascript.0.test_getStateDelayed_all'][0]).to.be.ok; - expect(result['javascript.0.test_getStateDelayed_all'][0].timerId).to.be.ok; - expect(result['javascript.0.test_getStateDelayed_all'][0].left).to.be.ok; - expect(result['javascript.0.test_getStateDelayed_all'][0].delay).to.be.equal(2500); + assert.ok(result['javascript.0.test_getStateDelayed_all'][0]); + assert.ok(result['javascript.0.test_getStateDelayed_all'][0].timerId); + assert.ok(result['javascript.0.test_getStateDelayed_all'][0].left); + assert.equal(result['javascript.0.test_getStateDelayed_all'][0].delay, 2500); done(); }); }, 500); @@ -1140,7 +1143,7 @@ describe.only('Test JS', function () { // on state change by setStateChanged('changed', 4, true) - should not be run, as it not change the state, including `state.ts` count++; } - expect(start).to.be.equal(0); + assert.equal(start, 0); if (start === 0) { // on state creation start = state.ts; @@ -1150,12 +1153,12 @@ describe.only('Test JS', function () { if (count === 0) { // on state change by setStateChanged('changed', 5, true) count++; - expect(state.ts - start).to.be.least(950); - expect(state.ts - start).to.be.below(1450); + assert.ok(state.ts - start >= 950); + assert.ok(state.ts - start < 1450); } else if (count === 1) { // on state change by setState('changed', 5, true) count++; - expect(state.ts - start).to.be.least(1450); + assert.ok(state.ts - start >= 1450); removeStateChangedHandler(onStateChanged); setTimeout(done, 100); } @@ -1202,7 +1205,7 @@ describe.only('Test JS', function () { const onStateChanged = function (id, state) { if (id !== 'javascript.0.test_selector_toArray') return; removeStateChangedHandler(onStateChanged); - expect(state.val).to.be.equal(2); + assert.equal(state.val, 2); done(); }; addStateChangedHandler(onStateChanged); @@ -1227,11 +1230,11 @@ describe.only('Test JS', function () { }; objects.setObject(script._id, script, err => { - expect(err).to.be.null; + assert.equal(err, null); setTimeout(function () { objects.getObject(script._id, (err, obj) => { - expect(err).to.be.null; - expect(obj.common.enabled).to.be.false; + assert.equal(err, null); + assert.equal(obj.common.enabled, false); done(); }); }, 1000); @@ -1268,19 +1271,19 @@ describe.only('Test JS', function () { }; objects.setObject(stopScript._id, stopScript, err => { - expect(err).to.be.null; + assert.equal(err, null); objects.getObject(stopScript._id, (err, obj) => { - expect(err).to.be.null; - expect(obj.common.enabled).to.be.false; + assert.equal(err, null); + assert.equal(obj.common.enabled, false); objects.setObject(script._id, script, err => { - expect(err).to.be.null; + assert.equal(err, null); setTimeout(() => { objects.getObject(stopScript._id, (err, obj) => { - expect(err).to.be.null; - expect(obj.common.enabled).to.be.true; + assert.equal(err, null); + assert.equal(obj.common.enabled, true); done(); }); }, 1000); @@ -1305,17 +1308,17 @@ describe.only('Test JS', function () { native: {}, }; objects.setObject(script._id, script, err => { - expect(err).to.be.null; + assert.equal(err, null); checkValueOfState( 'javascript.0.test_global_setTestState', 16, err => { - expect(err).to.be.null; + assert.equal(err, null); states.getState('javascript.0.test_global_setTestState', (err, state) => { - expect(err).to.be.null; - expect(state).to.be.ok; - expect(state.val).to.be.equal(16); + assert.equal(err, null); + assert.ok(state); + assert.equal(state.val, 16); done(); }); }, @@ -1350,7 +1353,7 @@ describe.only('Test JS', function () { if (id === 'javascript.0.testVar' && state.val === 0) { setTimeout(function () { states.setState('javascript.0.testVar', 6, err => { - expect(err).to.be.null; + assert.equal(err, null); }); }, 1000); } @@ -1362,7 +1365,7 @@ describe.only('Test JS', function () { addStateChangedHandler(onStateChanged); objects.setObject(script._id, script, err => { - expect(err).to.be.null; + assert.equal(err, null); }); }).timeout(5000); @@ -1386,7 +1389,7 @@ describe.only('Test JS', function () { if (id === 'javascript.0.testVar1' && state.val === 1) { setTimeout(function () { states.setState('javascript.0.testVar1', 1, err => { - expect(err).to.be.null; + assert.equal(err, null); }); }, 1000); } @@ -1398,7 +1401,7 @@ describe.only('Test JS', function () { addStateChangedHandler(onStateChanged); objects.setObject(script._id, script, err => { - expect(err).to.be.null; + assert.equal(err, null); }); }).timeout(5000); @@ -1673,14 +1676,14 @@ describe.only('Test JS', function () { // create device objects.setObject(device, { common: { name: 'Device' }, type: 'device' }, (err, obj) => { - expect(err).to.be.null; + assert.equal(err, null); // create channel objects.setObject(channel, { common: { name: 'Channel' }, type: 'channel' }, callback); }); } createObjects((err, _obj) => { - expect(err).to.be.null; + assert.equal(err, null); // objects.getObject('system.adapter.javascript.0', function(err, obj) { // obj.native.enableSetObject = true; // objects.setObject('system.adapter.javascript.0', function(err, obj) { @@ -1690,8 +1693,8 @@ describe.only('Test JS', function () { if (id === TEST_RESULTS && state.val) { cnt += 1; const ar = /^(OK;no=[\d]+)/.exec(state.val) || ['', state.val]; - expect(ar).to.be.ok; - expect(ar[1]).to.be.equal('OK;no=' + cnt); + assert.ok(ar); + assert.equal(ar[1], 'OK;no=' + cnt); if (cnt >= recs.length) { removeStateChangedHandler(onStateChanged); @@ -1703,7 +1706,7 @@ describe.only('Test JS', function () { addStateChangedHandler(onStateChanged); // write script into Objects => start script - objects.setObject(script._id, script, err => expect(err).to.be.null); + objects.setObject(script._id, script, err => assert.equal(err, null)); }); }); @@ -1739,7 +1742,7 @@ describe.only('Test JS', function () { addStateChangedHandler(onStateChanged); objects.setObject(script._id, script, err => { - expect(err).to.be.null; + assert.equal(err, null); }); }).timeout(4000); @@ -1783,7 +1786,7 @@ describe.only('Test JS', function () { addStateChangedHandler(onStateChanged); objects.setObject(script._id, script, err => { - expect(err).to.be.null; + assert.equal(err, null); }); }); @@ -1826,7 +1829,7 @@ describe.only('Test JS', function () { addStateChangedHandler(onStateChanged); objects.setObject(script._id, script, err => { - expect(err).to.be.null; + assert.equal(err, null); }); }); @@ -1862,7 +1865,7 @@ describe.only('Test JS', function () { addStateChangedHandler(onStateChanged); objects.setObject(script._id, script, err => { - expect(err).to.be.null; + assert.equal(err, null); }); }).timeout(5000); @@ -1898,7 +1901,7 @@ describe.only('Test JS', function () { addStateChangedHandler(onStateChanged); objects.setObject(script._id, script, err => { - expect(err).to.be.null; + assert.equal(err, null); }); }).timeout(5000); @@ -1928,7 +1931,7 @@ describe.only('Test JS', function () { }; addStateChangedHandler(onStateChanged); - objects.setObject(script._id, script, err => expect(err).to.be.null); + objects.setObject(script._id, script, err => assert.equal(err, null)); }).timeout(5000); it('Test JS: test read file from "vis.0"', done => { @@ -1955,7 +1958,7 @@ describe.only('Test JS', function () { }; addStateChangedHandler(onStateChanged); - objects.setObject(script._id, script, err => expect(err).to.be.null); + objects.setObject(script._id, script, err => assert.equal(err, null)); }).timeout(5000); */ @@ -2005,7 +2008,7 @@ describe.only('Test JS', function () { }; addStateChangedHandler(onStateChanged); - objects.setObject(script._id, script, err => expect(err).to.be.null); + objects.setObject(script._id, script, err => assert.equal(err, null)); }).timeout(5000); it('Test JS: subscribe on file', done => { @@ -2036,7 +2039,7 @@ describe.only('Test JS', function () { if (id === 'javascript.0.file' && state.val === 'abcdef') { if (!fileReceived) { fileReceived = true; - objects.writeFile('vis.0', 'main/data.txt', '12345', err => expect(err).to.be.undefined); + objects.writeFile('vis.0', 'main/data.txt', '12345', err => assert.equal(err, undefined)); setTimeout(() => { removeStateChangedHandler(onStateChanged); @@ -2044,7 +2047,7 @@ describe.only('Test JS', function () { }, 3000); } else { // after offFile we may not receive any updates - expect(state.val).to.be.false; + assert.equal(state.val, false); } } }; @@ -2053,11 +2056,11 @@ describe.only('Test JS', function () { objects.setObject('vis.0', { type: 'meta', common: {} }, () => { objects.setObject(script._id, script, err => { - expect(err).to.be.null; + assert.equal(err, null); // let the script be started setTimeout(() => { - objects.writeFile('vis.0', 'main/data.txt', 'abcdef', err => expect(err).to.be.undefined); + objects.writeFile('vis.0', 'main/data.txt', 'abcdef', err => assert.equal(err, undefined)); }, 4000); }); }); @@ -2089,14 +2092,14 @@ describe.only('Test JS', function () { if (id === 'javascript.0.test_formatTimeDiff' && state.val) { const obj = JSON.parse(state.val); - expect(obj.diff1).to.be.a('string'); - expect(obj.diff1).to.be.equal('51:09:15'); + assert.equal(typeof obj.diff1, 'string'); + assert.equal(obj.diff1, '51:09:15'); - expect(obj.diff2).to.be.a('string'); - expect(obj.diff2).to.be.equal('-3069:15'); + assert.equal(typeof obj.diff2, 'string'); + assert.equal(obj.diff2, '-3069:15'); - expect(obj.diff3).to.be.a('string'); - expect(obj.diff3).to.be.equal('04 Days, 8 hours, 1 minute, 41 seconds'); + assert.equal(typeof obj.diff3, 'string'); + assert.equal(obj.diff3, '04 Days, 8 hours, 1 minute, 41 seconds'); removeStateChangedHandler(onStateChanged); done(); @@ -2104,7 +2107,7 @@ describe.only('Test JS', function () { }; addStateChangedHandler(onStateChanged); - objects.setObject(script._id, script, err => expect(err).to.be.null); + objects.setObject(script._id, script, err => assert.equal(err, null)); }); it('Test JS: test getDateObject', function (done) { @@ -2134,29 +2137,29 @@ describe.only('Test JS', function () { if (id === 'javascript.0.test_getDateObject' && state.val) { const obj = JSON.parse(state.val); - expect(obj.now).to.be.a('number'); + assert.equal(typeof obj.now, 'number'); const d = new Date(obj.now); - expect(obj.justHour).to.be.a('string'); + assert.equal(typeof obj.justHour, 'string'); const justHour = new Date(obj.justHour); - expect(justHour.getHours()).to.be.equal(14); - expect(justHour.getMinutes()).to.be.equal(0); - expect(justHour.getFullYear()).to.be.equal(d.getFullYear()); - expect(justHour.getMonth()).to.be.equal(d.getMonth()); + assert.equal(justHour.getHours(), 14); + assert.equal(justHour.getMinutes(), 0); + assert.equal(justHour.getFullYear(), d.getFullYear()); + assert.equal(justHour.getMonth(), d.getMonth()); - expect(obj.timeToday).to.be.a('string'); + assert.equal(typeof obj.timeToday, 'string'); const timeToday = new Date(obj.timeToday); - expect(timeToday.getHours()).to.be.equal(20); - expect(timeToday.getMinutes()).to.be.equal(15); - expect(timeToday.getFullYear()).to.be.equal(d.getFullYear()); - expect(timeToday.getMonth()).to.be.equal(d.getMonth()); + assert.equal(timeToday.getHours(), 20); + assert.equal(timeToday.getMinutes(), 15); + assert.equal(timeToday.getFullYear(), d.getFullYear()); + assert.equal(timeToday.getMonth(), d.getMonth()); - expect(obj.byTimestamp).to.be.a('string'); + assert.equal(typeof obj.byTimestamp, 'string'); const byTimestamp = new Date(obj.byTimestamp); - expect(byTimestamp.getUTCHours()).to.be.equal(18); - expect(byTimestamp.getUTCDate()).to.be.equal(18); - expect(byTimestamp.getUTCMonth() + 1).to.be.equal(5); - expect(byTimestamp.getUTCFullYear()).to.be.equal(2024); + assert.equal(byTimestamp.getUTCHours(), 18); + assert.equal(byTimestamp.getUTCDate(), 18); + assert.equal(byTimestamp.getUTCMonth() + 1, 5); + assert.equal(byTimestamp.getUTCFullYear(), 2024); removeStateChangedHandler(onStateChanged); done(); @@ -2164,7 +2167,7 @@ describe.only('Test JS', function () { }; addStateChangedHandler(onStateChanged); - objects.setObject(script._id, script, err => expect(err).to.be.null); + objects.setObject(script._id, script, err => assert.equal(err, null)); }); it('Test JS: test getAttr', function (done) { @@ -2196,23 +2199,23 @@ describe.only('Test JS', function () { if (id === 'javascript.0.test_getAttr' && state.val) { const obj = JSON.parse(state.val); - expect(obj.attr1).to.be.a('string'); - expect(obj.attr1).to.be.equal('myVal'); + assert.equal(typeof obj.attr1, 'string'); + assert.equal(obj.attr1, 'myVal'); - expect(obj.attr2).to.be.a('number'); - expect(obj.attr2).to.be.equal(15); + assert.equal(typeof obj.attr2, 'number'); + assert.equal(obj.attr2, 15); - expect(obj.attr3).to.be.a('boolean'); - expect(obj.attr3).to.be.equal(true); + assert.equal(typeof obj.attr3, 'boolean'); + assert.equal(obj.attr3, true); - expect(obj.attr4).to.be.a('string'); - expect(obj.attr4).to.be.equal('yes'); + assert.equal(typeof obj.attr4, 'string'); + assert.equal(obj.attr4, 'yes'); - expect(obj.attr5).to.be.a('string'); - expect(obj.attr5).to.be.equal('three'); + assert.equal(typeof obj.attr5, 'string'); + assert.equal(obj.attr5, 'three'); - expect(obj.attr6).to.be.a('number'); - expect(obj.attr6).to.be.equal(2); + assert.equal(typeof obj.attr6, 'number'); + assert.equal(obj.attr6, 2); removeStateChangedHandler(onStateChanged); done(); @@ -2220,7 +2223,7 @@ describe.only('Test JS', function () { }; addStateChangedHandler(onStateChanged); - objects.setObject(script._id, script, err => expect(err).to.be.null); + objects.setObject(script._id, script, err => assert.equal(err, null)); }); after('Test JS: Stop js-controller', function (done) { diff --git a/test/testMainPerformance.js b/test/testMainPerformance.js index 1f176f76f..8473ae968 100644 --- a/test/testMainPerformance.js +++ b/test/testMainPerformance.js @@ -4,7 +4,7 @@ * Ausführen: mocha test/testMainPerformance.js --exit */ -const expect = require('chai').expect; +const assert = require('node:assert').strict; // ────────────────────────────────────────────────────────────── // Hilfsklassen um die betroffenen Methoden isoliert zu testen @@ -158,9 +158,9 @@ describe('main.ts Performance Bug Tests', function () { fill(mgr, 100); const target = 'adapter.0.state000050'; mgr.removeLinear(target); - expect(mgr.stateIds.includes(target)).to.be.false; - expect(mgr.stateIdSet.has(target)).to.be.false; - expect(mgr.stateIds.length).to.equal(99); + assert.equal(mgr.stateIds.includes(target), false); + assert.equal(mgr.stateIdSet.has(target), false); + assert.equal(mgr.stateIds.length, 99); }); it('binary remove: should remove correct element', function () { @@ -168,16 +168,16 @@ describe('main.ts Performance Bug Tests', function () { fill(mgr, 100); const target = 'adapter.0.state000050'; mgr.removeBinary(target); - expect(mgr.stateIds.includes(target)).to.be.false; - expect(mgr.stateIdSet.has(target)).to.be.false; - expect(mgr.stateIds.length).to.equal(99); + assert.equal(mgr.stateIds.includes(target), false); + assert.equal(mgr.stateIdSet.has(target), false); + assert.equal(mgr.stateIds.length, 99); }); it('binary remove: should handle missing element gracefully', function () { const mgr = new StateIdManager(); fill(mgr, 10); mgr.removeBinary('does.not.exist'); - expect(mgr.stateIds.length).to.equal(10); + assert.equal(mgr.stateIds.length, 10); }); it('binary remove: array stays sorted after remove', function () { @@ -186,7 +186,7 @@ describe('main.ts Performance Bug Tests', function () { mgr.removeBinary('adapter.0.state000010'); mgr.removeBinary('adapter.0.state000040'); const sorted = [...mgr.stateIds].sort(); - expect(mgr.stateIds).to.deep.equal(sorted); + assert.deepEqual(mgr.stateIds, sorted); }); }); @@ -195,41 +195,41 @@ describe('main.ts Performance Bug Tests', function () { it('Array: has() returns true after add', function () { const e = new EnumsArray(); e.add('enum.rooms.living'); - expect(e.has('enum.rooms.living')).to.be.true; + assert.equal(e.has('enum.rooms.living'), true); }); it('Set: has() returns true after add', function () { const e = new EnumsSet(); e.add('enum.rooms.living'); - expect(e.has('enum.rooms.living')).to.be.true; + assert.equal(e.has('enum.rooms.living'), true); }); it('Array: no duplicate on double add', function () { const e = new EnumsArray(); e.add('enum.rooms.living'); e.add('enum.rooms.living'); - expect(e._enums.length).to.equal(1); + assert.equal(e._enums.length, 1); }); it('Set: no duplicate on double add', function () { const e = new EnumsSet(); e.add('enum.rooms.living'); e.add('enum.rooms.living'); - expect(e._enums.size).to.equal(1); + assert.equal(e._enums.size, 1); }); it('Array: has() returns false after remove', function () { const e = new EnumsArray(); e.add('enum.rooms.living'); e.remove('enum.rooms.living'); - expect(e.has('enum.rooms.living')).to.be.false; + assert.equal(e.has('enum.rooms.living'), false); }); it('Set: has() returns false after remove', function () { const e = new EnumsSet(); e.add('enum.rooms.living'); e.remove('enum.rooms.living'); - expect(e.has('enum.rooms.living')).to.be.false; + assert.equal(e.has('enum.rooms.living'), false); }); it('Set: toSorted() returns same order as Array', function () { @@ -237,7 +237,7 @@ describe('main.ts Performance Bug Tests', function () { const s = new EnumsSet(); const ids = ['enum.rooms.bath', 'enum.rooms.living', 'enum.functions.light']; ids.forEach(id => { a.add(id); s.add(id); }); - expect(s.toSorted()).to.deep.equal(a._enums); + assert.deepEqual(s.toSorted(), a._enums); }); }); @@ -246,34 +246,34 @@ describe('main.ts Performance Bug Tests', function () { it('Array: add and has', function () { const c = new ChannelArray(); c.add('adapter.0.room1', 'adapter.0.room1.temp'); - expect(c.has('adapter.0.room1', 'adapter.0.room1.temp')).to.be.true; + assert.equal(c.has('adapter.0.room1', 'adapter.0.room1.temp'), true); }); it('Set: add and has', function () { const c = new ChannelSet(); c.add('adapter.0.room1', 'adapter.0.room1.temp'); - expect(c.has('adapter.0.room1', 'adapter.0.room1.temp')).to.be.true; + assert.equal(c.has('adapter.0.room1', 'adapter.0.room1.temp'), true); }); it('Array: remove clears entry', function () { const c = new ChannelArray(); c.add('adapter.0.room1', 'adapter.0.room1.temp'); c.remove('adapter.0.room1', 'adapter.0.room1.temp'); - expect(c.has('adapter.0.room1', 'adapter.0.room1.temp')).to.be.false; + assert.equal(c.has('adapter.0.room1', 'adapter.0.room1.temp'), false); }); it('Set: remove clears entry', function () { const c = new ChannelSet(); c.add('adapter.0.room1', 'adapter.0.room1.temp'); c.remove('adapter.0.room1', 'adapter.0.room1.temp'); - expect(c.has('adapter.0.room1', 'adapter.0.room1.temp')).to.be.false; + assert.equal(c.has('adapter.0.room1', 'adapter.0.room1.temp'), false); }); it('Set: no duplicates', function () { const c = new ChannelSet(); c.add('chn', 'id1'); c.add('chn', 'id1'); - expect(c.channels['chn'].size).to.equal(1); + assert.equal(c.channels['chn'].size, 1); }); it('Array: has duplicate risk', function () { @@ -282,7 +282,7 @@ describe('main.ts Performance Bug Tests', function () { c.channels['chn'] = []; c.channels['chn'].push('id1'); c.channels['chn'].push('id1'); // duplicate! - expect(c.channels['chn'].length).to.equal(2); // proves the bug + assert.equal(c.channels['chn'].length, 2); // proves the bug }); }); @@ -324,7 +324,7 @@ describe('main.ts Performance Bug Tests', function () { const binaryMs = Number(process.hrtime.bigint() - t1) / 1_000_000; console.log(` indexOf: ${linearMs.toFixed(2)}ms | binary: ${binaryMs.toFixed(2)}ms | speedup: ${(linearMs / binaryMs).toFixed(1)}x`); - expect(binaryMs).to.be.lessThan(linearMs); + assert.ok(binaryMs < linearMs); }); it(`_enums Array.includes O(n) vs Set.has O(1) – ${N} enums`, function () { @@ -349,7 +349,7 @@ describe('main.ts Performance Bug Tests', function () { const setMs = Number(process.hrtime.bigint() - t1) / 1_000_000; console.log(` Array.includes: ${arrMs.toFixed(2)}ms | Set.has: ${setMs.toFixed(2)}ms | speedup: ${(arrMs / setMs).toFixed(1)}x`); - expect(setMs).to.be.lessThan(arrMs); + assert.ok(setMs < arrMs); }); it(`channels Array.indexOf vs Set.has – 10000 channel ids`, function () { @@ -373,7 +373,7 @@ describe('main.ts Performance Bug Tests', function () { const setMs = Number(process.hrtime.bigint() - t1) / 1_000_000; console.log(` indexOf: ${arrMs.toFixed(2)}ms | Set.has: ${setMs.toFixed(2)}ms | speedup: ${(arrMs / setMs).toFixed(1)}x`); - expect(setMs).to.be.lessThan(arrMs); + assert.ok(setMs < arrMs); }); }); }); diff --git a/test/testMirror.js b/test/testMirror.js index 752e2792b..bda87f6cd 100644 --- a/test/testMirror.js +++ b/test/testMirror.js @@ -1,7 +1,7 @@ const os = require('node:os'); const path = require('node:path'); const fs = require('node:fs'); -const expect = require('chai').expect; +const assert = require('node:assert').strict; const Mirror = require('../build/lib/mirror'); describe('Mirror', () => { @@ -29,7 +29,7 @@ describe('Mirror', () => { fs.closeSync(fs.openSync(script, 'w')); mirror.onFileChange = (_event, file) => { - expect(path.normalize(file)).to.equal(script); + assert.equal(path.normalize(file), script); done(); }; @@ -51,7 +51,7 @@ describe('Mirror', () => { fs.symlinkSync(script, symlink); mirror.onFileChange = (_event, file) => { - expect(path.normalize(file)).to.equal(symlink); + assert.equal(path.normalize(file), symlink); done(); }; @@ -74,7 +74,7 @@ describe('Mirror', () => { mirror.onFileChange = (event, file) => { if (process.platform === 'linux' || process.platform === 'win32') { - expect(path.normalize(file)).to.equal(path.join(symlink, path.basename(script))); + assert.equal(path.normalize(file), path.join(symlink, path.basename(script))); done(); } @@ -105,7 +105,7 @@ describe('Mirror', () => { fs.symlinkSync(path.join(relativeDirectory, path.basename(script)), symlink); mirror.onFileChange = (_event, file) => { - expect(path.normalize(file)).to.equal(symlink); + assert.equal(path.normalize(file), symlink); done(); }; @@ -130,7 +130,7 @@ describe('Mirror', () => { mirror.onFileChange = (event, file) => { if (process.platform === 'linux' || process.platform === 'win32') { - expect(path.normalize(file)).to.equal(path.join(symlink, path.basename(script))); + assert.equal(path.normalize(file), path.join(symlink, path.basename(script))); done(); } diff --git a/test/testScheduler.js b/test/testScheduler.js index 4e4a7c053..f0b574c96 100644 --- a/test/testScheduler.js +++ b/test/testScheduler.js @@ -1,4 +1,4 @@ -const expect = require('chai').expect; +const assert = require('node:assert').strict; const tk = require('timekeeper'); const suncalc = require('suncalc2'); const { Scheduler } = require('../build/lib/scheduler'); @@ -33,7 +33,7 @@ describe('Test Scheduler', function () { '{"time":{"exactTime":true,"start":"23:59"},"period":{"years":1,"yearDate":31,"yearMonth":12}}', 'someName1', () => { - expect(false).to.be.true; + assert.fail('Callback should not have been called'); }, ); setTimeout(done, 5000); @@ -77,7 +77,7 @@ describe('Test Scheduler', function () { '{"time":{"exactTime":true,"start":"' + evtName.toUpperCase() + 'x"},"period":{"days":1}}', 'someName3', () => { - expect(false).to.be.true; + assert.fail('Callback should not have been called'); }, ); setTimeout(done, 5000); @@ -96,7 +96,7 @@ describe('Test Scheduler', function () { console.log(new Date()); const s = new Scheduler(null, Date, suncalc, kcLat, kcLon); s.add('{"time":{"exactTime":true,"start":""},"period":{"days":1}}', 'someName3', () => { - expect(false).to.be.true; + assert.fail('Callback should not have been called'); }); setTimeout(done, 5000); }).timeout(65000); @@ -106,7 +106,7 @@ describe('Test Scheduler', function () { let encrypted = encryptText('password', plainText); console.log(`Encrypted text: ${encrypted}`); let decrypted = decryptText('password', encrypted); - expect(decrypted).to.equal(plainText); + assert.equal(decrypted, plainText); done(); }); }); diff --git a/test/testSchedulerBugfixes.js b/test/testSchedulerBugfixes.js index deb98d974..6fd5c73f4 100644 --- a/test/testSchedulerBugfixes.js +++ b/test/testSchedulerBugfixes.js @@ -5,7 +5,7 @@ * Ausführen: mocha test/testSchedulerBugfixes.js --exit */ -const expect = require('chai').expect; +const assert = require('node:assert').strict; const tk = require('timekeeper'); const suncalc = require('suncalc2'); const { Scheduler } = require('../build/lib/scheduler'); @@ -42,42 +42,42 @@ describe('Scheduler Bugfix Tests', function () { const s = makeScheduler(); const d1 = new Date(2030, 0, 1); // Jan 2030 const d2 = new Date(2030, 0, 15); // Jan 2030 - expect(s.monthDiff(d1, d2)).to.equal(0); + assert.equal(s.monthDiff(d1, d2), 0); }); it('should return 1 for consecutive months', function () { const s = makeScheduler(); const d1 = new Date(2030, 0, 1); // Jan 2030 const d2 = new Date(2030, 1, 1); // Feb 2030 - expect(s.monthDiff(d1, d2)).to.equal(1); + assert.equal(s.monthDiff(d1, d2), 1); }); it('should return 12 for same month next year', function () { const s = makeScheduler(); const d1 = new Date(2030, 0, 1); // Jan 2030 const d2 = new Date(2031, 0, 1); // Jan 2031 - expect(s.monthDiff(d1, d2)).to.equal(12); + assert.equal(s.monthDiff(d1, d2), 12); }); it('should return 24 for same month 2 years later', function () { const s = makeScheduler(); const d1 = new Date(2030, 3, 1); // Apr 2030 const d2 = new Date(2032, 3, 1); // Apr 2032 - expect(s.monthDiff(d1, d2)).to.equal(24); + assert.equal(s.monthDiff(d1, d2), 24); }); it('should return 11 for Jan to Dec same year', function () { const s = makeScheduler(); const d1 = new Date(2030, 0, 1); // Jan 2030 const d2 = new Date(2030, 11, 1); // Dec 2030 - expect(s.monthDiff(d1, d2)).to.equal(11); + assert.equal(s.monthDiff(d1, d2), 11); }); it('should not return negative values (returns 0)', function () { const s = makeScheduler(); const d1 = new Date(2031, 5, 1); const d2 = new Date(2030, 1, 1); - expect(s.monthDiff(d1, d2)).to.equal(0); + assert.equal(s.monthDiff(d1, d2), 0); }); }); @@ -87,7 +87,7 @@ describe('Scheduler Bugfix Tests', function () { describe('timer management', function () { it('should not have an active timer when no schedules exist', function () { const s = makeScheduler(); - expect(s.timer).to.be.null; + assert.equal(s.timer, null); }); it('should stop timer when last schedule is removed', function (done) { @@ -99,10 +99,10 @@ describe('Scheduler Bugfix Tests', function () { 'testScript', () => {}, ); - expect(id).to.not.be.null; - expect(s.timer).to.not.be.null; + assert.notEqual(id, null); + assert.notEqual(s.timer, null); s.remove(id); - expect(s.timer).to.be.null; + assert.equal(s.timer, null); done(); }); @@ -120,10 +120,10 @@ describe('Scheduler Bugfix Tests', function () { // After 61 seconds the schedule will be expired and deleted // timer should NOT persist after list is empty setTimeout(() => { - expect(Object.keys(s.list).length).to.equal(0); + assert.equal(Object.keys(s.list).length, 0); // Give recalculate one tick setImmediate(() => { - expect(s.timer).to.be.null; + assert.equal(s.timer, null); done(); }); }, 65000); @@ -180,7 +180,7 @@ describe('Scheduler Bugfix Tests', function () { for (let i = 0; i < 10000; i++) { ids.add(s._getId()); } - expect(ids.size).to.equal(10000); + assert.equal(ids.size, 10000); }); }); @@ -193,9 +193,9 @@ describe('Scheduler Bugfix Tests', function () { tk.travel(time); const s = makeScheduler(); const sunrise = s.todaysAstroTimes.sunrise; - expect(sunrise.getDate()).to.equal(21); - expect(sunrise.getMonth()).to.equal(5); - expect(sunrise.getFullYear()).to.equal(2030); + assert.equal(sunrise.getDate(), 21); + assert.equal(sunrise.getMonth(), 5); + assert.equal(sunrise.getFullYear(), 2030); }); it('yesterdaysAstroTimes.sunrise should be yesterday', function () { @@ -204,7 +204,7 @@ describe('Scheduler Bugfix Tests', function () { const s = makeScheduler(); const sunrise = s.yesterdaysAstroTimes.sunrise; // sunrise of yesterday (June 20) should be on June 20 or June 21 at latest - expect(sunrise.getDate()).to.be.oneOf([20, 21]); + assert.ok([20, 21].includes(sunrise.getDate())); }); }); @@ -238,13 +238,13 @@ describe('Scheduler Bugfix Tests', function () { }; // March 2030: monthDiff(Jan2030, Mar2030) = 2, 2 % 2 = 0 → should fire const diff = s.monthDiff(new Date(2030, 0, 1), new Date(2030, 2, 1)); - expect(diff).to.equal(2); - expect(diff % 2).to.equal(0); // fires + assert.equal(diff, 2); + assert.equal(diff % 2, 0); // fires // Feb 2030: monthDiff = 1, 1 % 2 = 1 → should NOT fire const diffFeb = s.monthDiff(new Date(2030, 0, 1), new Date(2030, 1, 1)); - expect(diffFeb).to.equal(1); - expect(diffFeb % 2).to.equal(1); // does not fire + assert.equal(diffFeb, 1); + assert.equal(diffFeb % 2, 1); // does not fire }); }); }); diff --git a/test/testTypeScript.js b/test/testTypeScript.js index a656cb2bc..4201ce5d7 100644 --- a/test/testTypeScript.js +++ b/test/testTypeScript.js @@ -4,7 +4,7 @@ const path = require('node:path'); const { EOL } = require('node:os'); const { tsCompilerOptions } = require('../build/lib/typescriptSettings'); -const { expect } = require('chai'); +const assert = require('node:assert').strict; const { scriptIdToTSFilename, transformScriptBeforeCompilation, @@ -17,21 +17,21 @@ describe('TypeScript tools', () => { const source = `await wait(100)`; const expected = `(async () => { await wait(100); })();`; const transformed = transformScriptBeforeCompilation(source, false); - expect(transformed).to.include(expected); + assert.ok(transformed.includes(expected)); }); it('...but only if it is really necessary', () => { const source = `log("test")`; const expected = `log("test");\nexport {};\n`.replace(/\n/g, require('os').EOL); const transformed = transformScriptBeforeCompilation(source, false); - expect(transformed).to.equal(expected); + assert.equal(transformed, expected); }); it('forces non-global scripts to be treated as modules (part 1)', () => { const source = `const foo = 1;`; const expected = /^export \{};$/m; const transformed = transformScriptBeforeCompilation(source, false); - expect(transformed).to.match(expected); + assert.match(transformed, expected); }); it('forces non-global scripts to be treated as modules (part 2)', () => { @@ -40,7 +40,7 @@ const foo = 1;`; const expected = /^export \{};$/m; const transformed = transformScriptBeforeCompilation(source, false); // There is an import, we don't need an empty export now - expect(transformed).not.to.match(expected); + assert.doesNotMatch(transformed, expected); }); it('exports every exportable thing in global scripts', () => { @@ -58,7 +58,7 @@ export class Foo { .trim() .replace(/\r?\n/g, EOL); const transformed = transformScriptBeforeCompilation(source, true); - expect(transformed.trim()).to.equal(expected); + assert.equal(transformed.trim(), expected); }); it('wraps global augmentations in `declare global`', () => { @@ -77,7 +77,7 @@ export {};` .trim() .replace(/\r?\n/g, EOL); const transformed = transformScriptBeforeCompilation(source, true); - expect(transformed.trim()).to.equal(expected); + assert.equal(transformed.trim(), expected); }); }); @@ -99,7 +99,7 @@ declare global { .trim() .replace(/\r?\n/g, EOL); const transformed = transformGlobalDeclarations(source); - expect(transformed.trim()).to.equal(expected); + assert.equal(transformed.trim(), expected); }); it('If there is no import statement, `export {};` must be added', () => { @@ -118,7 +118,7 @@ export {}; .trim() .replace(/\r?\n/g, EOL); const transformed = transformGlobalDeclarations(source); - expect(transformed.trim()).to.equal(expected); + assert.equal(transformed.trim(), expected); }); it('should preserve already-existing declare global { ... } blocks', () => { @@ -138,13 +138,13 @@ export {};` .trim() .replace(/\r?\n/g, EOL); const transformed = transformGlobalDeclarations(source); - expect(transformed.trim()).to.equal(expected); + assert.equal(transformed.trim(), expected); }); }); describe('scriptIdToTSFilename', () => { it('generates a valid filename from a script ID', () => { - expect(scriptIdToTSFilename('script.js.foo.bar.baz')).to.equal('foo/bar/baz.ts'); + assert.equal(scriptIdToTSFilename('script.js.foo.bar.baz'), 'foo/bar/baz.ts'); }); }); }); @@ -214,7 +214,7 @@ await bar(); const tsCompiled = tsServer.compile(filename, transformedSource); - expect(tsCompiled.success).to.be.true; + assert.equal(tsCompiled.success, true); }).timeout(20000); } }); @@ -277,7 +277,7 @@ function test(): void { const tsCompiled = tsServer.compile(filename, transformedSource); - expect(tsCompiled.success).to.be.true; + assert.equal(tsCompiled.success, true); }).timeout(20000); } }); From 5dc37ba515b8568de11de8ce9f67a6d46ab7d960 Mon Sep 17 00:00:00 2001 From: GermanBluefox Date: Mon, 25 May 2026 16:29:22 +0200 Subject: [PATCH 2/2] OPtimizations --- src/lib/sandbox.ts | 24 ++++++++++++++++-------- src/main.ts | 1 - 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/lib/sandbox.ts b/src/lib/sandbox.ts index 4cabac131..921d84c39 100644 --- a/src/lib/sandbox.ts +++ b/src/lib/sandbox.ts @@ -2846,13 +2846,14 @@ export function sandBox( const removedScripts = new Set(); for (let i = timers[id].length - 1; i >= 0; i--) { if (timerId === undefined || timers[id][i].id === timerId) { + const clearedTimerId = timers[id][i].id; removedScripts.add(timers[id][i].scriptName); clearTimeout(timers[id][i].t); if (timerId !== undefined) { timers[id].splice(i, 1); } if (sandbox.verbose) { - sandbox.log(`clearStateDelayed: clear timer ${timers[id][i]?.id ?? timerId}`, 'info'); + sandbox.log(`clearStateDelayed: clear timer ${clearedTimerId}`, 'info'); } } } @@ -2863,15 +2864,22 @@ export function sandBox( delete timers[id]; } } - // IO-7: update the timersByScript reverse-index when a state has no more timers - if (!timers[id]) { + // IO-7: keep the timersByScript reverse-index in sync. For every script whose + // timer(s) we just removed, drop `id` from its set – unless that script still has + // another timer for this state (other scripts' timers may keep timers[id] alive). + if (removedScripts.size) { + const remaining = timers[id]; // undefined if the whole entry was deleted for (const scriptName of removedScripts) { const stateIds = context.timersByScript.get(scriptName); - if (stateIds) { - stateIds.delete(id); - if (!stateIds.size) { - context.timersByScript.delete(scriptName); - } + if (!stateIds) { + continue; + } + if (remaining?.some(e => e.scriptName === scriptName)) { + continue; + } + stateIds.delete(id); + if (!stateIds.size) { + context.timersByScript.delete(scriptName); } } } diff --git a/src/main.ts b/src/main.ts index 0742f2b6e..5ae46464b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -327,7 +327,6 @@ class JavaScript extends Adapter { /** Fast O(1) lookup set – always kept in sync with stateIds */ private readonly stateIdSet: Set = new Set(); - /** Precomputed "from" string for prepareStateObject – avoids string alloc on every setState */ private readonly subscriptions: SubscriptionResult[] = []; private readonly subscriptionsFile: FileSubscriptionResult[] = []; private readonly subscriptionsObject: SubscribeObject[] = [];