fix: Replace object-assign-deep with local utility (#32684)

This commit is contained in:
Alexander Chepurnoy
2026-08-03 20:54:54 +02:00
committed by GitHub
parent 5fd4d6b37a
commit 828038717f
11 changed files with 296 additions and 125 deletions
+1 -1
View File
@@ -2,7 +2,6 @@ import fs from "node:fs";
import path from "node:path";
import bind from "bind-decorator";
import {zip} from "fflate";
import objectAssignDeep from "object-assign-deep";
import type winston from "winston";
import Transport from "winston-transport";
import {Zcl} from "zigbee-herdsman";
@@ -13,6 +12,7 @@ import type Group from "../model/group";
import type {Zigbee2MQTTAPI, Zigbee2MQTTDevice, Zigbee2MQTTResponse, Zigbee2MQTTResponseEndpoints} from "../types/api";
import data from "../util/data";
import logger from "../util/logger";
import {objectAssignDeep} from "../util/objectAssignDeep";
import * as settings from "../util/settings";
import {stringify} from "../util/stringify";
import utils, {assertString, DEFAULT_BIND_GROUP_ID} from "../util/utils";
+1 -2
View File
@@ -1,9 +1,8 @@
import {existsSync, readFileSync, writeFileSync} from "node:fs";
import objectAssignDeep from "object-assign-deep";
import data from "./util/data";
import logger from "./util/logger";
import {objectAssignDeep} from "./util/objectAssignDeep";
import * as settings from "./util/settings";
import utils from "./util/utils";
+6 -6
View File
@@ -59,7 +59,7 @@ export type OnboardData = OnboardInitData | OnboardDoneData | OnboardFailureData
export type OnboardSubmitResponse = {success: true; frontendUrl: string | null} | {success: false; error: string};
export interface Zigbee2MQTTDeviceOptions {
export type Zigbee2MQTTDeviceOptions = {
disabled?: boolean;
retention?: number;
availability?:
@@ -83,9 +83,9 @@ export interface Zigbee2MQTTDeviceOptions {
description?: string;
qos?: 0 | 1 | 2;
disable_automatic_update_check?: boolean;
}
};
export interface Zigbee2MQTTGroupOptions {
export type Zigbee2MQTTGroupOptions = {
ID: number;
optimistic?: boolean;
off_state?: "all_members_off" | "last_member_state";
@@ -96,9 +96,9 @@ export interface Zigbee2MQTTGroupOptions {
friendly_name: string;
description?: string;
qos?: 0 | 1 | 2;
}
};
export interface Zigbee2MQTTSettings {
export type Zigbee2MQTTSettings = {
version?: number;
/** only used internally during startup, removed on successful Z2M start */
onboarding?: true;
@@ -224,7 +224,7 @@ export interface Zigbee2MQTTSettings {
interval: number;
reset_on_check: boolean;
};
}
};
export interface Zigbee2MQTTScene {
id: number;
+3
View File
@@ -26,6 +26,9 @@ declare global {
type PublishEntityState = (entity: Device | Group, payload: KeyValue, stateChangeReason?: StateChangeReason) => Promise<void>;
type RecursivePartial<T> = {[P in keyof T]?: RecursivePartial<T[P]>};
type MakePartialExcept<T, K extends keyof T> = Partial<Omit<T, K>> & Pick<T, K>;
/** Convert `A | B | C` into `A & B & C` */
// biome-ignore lint/suspicious/noExplicitAny: distributive conditional requires `any`
type UnionToIntersection<U> = (U extends any ? (x: U) => void : never) extends (x: infer I) => void ? I : never;
interface KeyValue {
// biome-ignore lint/suspicious/noExplicitAny: API
[s: string]: any;
+67
View File
@@ -0,0 +1,67 @@
/** Anything mergeable: a plain-ish object, explicitly not an array or another iterable. */
export type UnknownRecord = Record<string | number, unknown> & {[Symbol.iterator]?: never};
function isUnknownRecord(value: unknown): value is UnknownRecord {
return value != null && typeof value === "object" && !Array.isArray(value);
}
function cloneArray(input: readonly unknown[]): unknown[] {
const len = input.length;
const output: unknown[] = new Array(len);
for (let i = 0; i < len; i++) {
const val = input[i];
output[i] = isUnknownRecord(val) ? cloneObject(val) : Array.isArray(val) ? cloneArray(val) : val;
}
return output;
}
function cloneObject(input: UnknownRecord): UnknownRecord {
const output: UnknownRecord = {};
for (const key of Object.keys(input)) {
if (key !== "__proto__" && key !== "constructor" && key !== "prototype") {
const val = input[key];
output[key] = isUnknownRecord(val) ? cloneObject(val) : Array.isArray(val) ? cloneArray(val) : val;
}
}
return output;
}
/**
* Merge all sources into `target` recursively.
*
* Key behavior:
* - ignore properties `__proto__`, `constructor` & `prototype`
* - assumes no infinite circular possible (unhandled for perf)
*
* Pass empty object `{}` as `target` to return a new object without modifying any existing objects.
*/
export function objectAssignDeep<T extends UnknownRecord, S extends readonly UnknownRecord[]>(
target: T,
...sources: S
): T & UnionToIntersection<S[number]> {
for (const source of sources) {
for (const key of Object.keys(source)) {
if (key === "__proto__" || key === "constructor" || key === "prototype") {
continue;
}
const value = source[key];
if (isUnknownRecord(value)) {
const existing = target[key];
(target as UnknownRecord)[key] = isUnknownRecord(existing) ? objectAssignDeep({}, existing, value) : cloneObject(value);
} else if (Array.isArray(value)) {
(target as UnknownRecord)[key] = cloneArray(value);
} else {
(target as UnknownRecord)[key] = value;
}
}
}
return target as T & UnionToIntersection<S[number]>;
}
+7 -7
View File
@@ -1,8 +1,8 @@
import path from "node:path";
import type {ValidateFunction} from "ajv";
import Ajv from "ajv";
import objectAssignDeep from "object-assign-deep";
import data from "./data";
import {objectAssignDeep} from "./objectAssignDeep";
import schemaJson from "./settings.schema.json";
import utils from "./utils";
import yaml from "./yaml";
@@ -231,7 +231,10 @@ export function write(): void {
const writeDevicesOrGroups = (type: "devices" | "groups"): void => {
if (typeof actual[type] === "string" || (Array.isArray(actual[type]) && actual[type].length > 0)) {
const fileToWrite = Array.isArray(actual[type]) ? actual[type][0] : actual[type];
const content = objectAssignDeep({}, settings[type]);
// `readDevicesOrGroups()` already set this to an object whenever the config points at separate files, but the
// persisted settings are `Partial`, so the fallback is only here to satisfy the type
/* v8 ignore next */
const content = objectAssignDeep({}, settings[type] ?? {});
// If an array, only write to first file and only devices which are not in the other files.
if (Array.isArray(actual[type])) {
@@ -368,8 +371,7 @@ function read(): Partial<Settings> {
s[type] = {};
for (const file of files) {
const content = yaml.readIfExists(data.joinPath(file));
// @ts-expect-error noMutate not typed properly
s[type] = objectAssignDeep.noMutate(s[type], content);
s[type] = objectAssignDeep({}, s[type], content);
}
}
};
@@ -479,9 +481,7 @@ export function set(path: string[], value: string | number | boolean | KeyValue)
}
export function apply(settings: Record<string, unknown>, throwOnError = true): boolean {
getPersistedSettings(); // Ensure _settings is initialized.
// @ts-expect-error noMutate not typed properly
const newSettings = objectAssignDeep.noMutate(_settings, settings);
const newSettings = objectAssignDeep({}, getPersistedSettings(), settings);
utils.removeNullPropertiesFromObject(newSettings, NULLABLE_SETTINGS);
-2
View File
@@ -52,7 +52,6 @@
"humanize-duration": "^3.34.0",
"js-yaml": "^5.2.2",
"mqtt": "^5.15.2",
"object-assign-deep": "^0.4.0",
"semver": "^7.8.5",
"throttleit": "^3.0.0",
"winston": "^3.19.0",
@@ -69,7 +68,6 @@
"@types/finalhandler": "^1.2.3",
"@types/humanize-duration": "^3.27.4",
"@types/node": "^26.1.2",
"@types/object-assign-deep": "^0.4.3",
"@types/readable-stream": "4.0.24",
"@types/serve-static": "^2.2.0",
"@types/ws": "8.18.1",
-17
View File
@@ -41,9 +41,6 @@ importers:
mqtt:
specifier: ^5.15.2
version: 5.15.2
object-assign-deep:
specifier: ^0.4.0
version: 0.4.0
semver:
specifier: ^7.8.5
version: 7.8.5
@@ -87,9 +84,6 @@ importers:
'@types/node':
specifier: ^26.1.2
version: 26.1.2
'@types/object-assign-deep':
specifier: ^0.4.3
version: 0.4.3
'@types/readable-stream':
specifier: 4.0.24
version: 4.0.24
@@ -537,9 +531,6 @@ packages:
'@types/node@26.1.2':
resolution: {integrity: sha512-Vu4a5UFA9rIIFJ7rB/Vaafh9lrCQszopTCx6KjFboXTGQbPNasehVR5TEiithSDGyd1DEiUByggTZsg8jukeIg==}
'@types/object-assign-deep@0.4.3':
resolution: {integrity: sha512-d9Gxaj5j1hzrxJ61EFEg13B4g4FgrT/DYtcDWFXPehR8DF2SUZbVMFtZIs8exkVRiqrqBpdTc/lUUZjncsPpMw==}
'@types/readable-stream@4.0.24':
resolution: {integrity: sha512-NRvUNC/JFGPJvqdAfEve8oginbM6V08u5NzLWpG8MwA2kTPOLnqk+wpwuPT+mp3aUsxyuT6m2gnrPuHYCruzEg==}
@@ -1114,10 +1105,6 @@ packages:
number-allocator@1.0.14:
resolution: {integrity: sha512-OrL44UTVAvkKdOdRQZIJpLkAdjXGTRda052sN4sO77bKEzYYqWKMBjQvrJFzqygI99gL6Z4u2xctPW1tB8ErvA==}
object-assign-deep@0.4.0:
resolution: {integrity: sha512-54Uvn3s+4A/cMWx9tlRez1qtc7pN7pbQ+Yi7mjLjcBpWLlP+XbSHiHbQW6CElDiV4OvuzqnMrBdkgxI1mT8V/Q==}
engines: {node: '>=6'}
on-finished@2.4.1:
resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==}
engines: {node: '>= 0.8'}
@@ -1796,8 +1783,6 @@ snapshots:
dependencies:
undici-types: 8.3.0
'@types/object-assign-deep@0.4.3': {}
'@types/readable-stream@4.0.24':
dependencies:
'@types/node': 26.1.2
@@ -2358,8 +2343,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
object-assign-deep@0.4.0: {}
on-finished@2.4.1:
dependencies:
ee-first: 1.1.1
+161
View File
@@ -0,0 +1,161 @@
import {describe, expect, it} from "vitest";
import {objectAssignDeep} from "../lib/util/objectAssignDeep";
/** Creates an object with a real own `__proto__`/`constructor`/`prototype` property, like a parsed YAML/JSON payload can. */
const parse = (json: string): Record<string, unknown> => JSON.parse(json);
describe("objectAssignDeep", () => {
it("Mutates and returns the target", () => {
const target = {a: 1};
const result = objectAssignDeep(target, {b: 2});
expect(result).toBe(target);
expect(result).toStrictEqual({a: 1, b: 2});
});
it("Applies sources in order, later ones win", () => {
expect(objectAssignDeep({}, {a: 1, b: 1}, {b: 2, c: 2})).toStrictEqual({a: 1, b: 2, c: 2});
});
it("Copies keys missing from the target", () => {
expect(objectAssignDeep({}, {nested: {deep: {value: 1}}})).toStrictEqual({nested: {deep: {value: 1}}});
});
it("Deep merges nested objects present in both", () => {
const target = {mqtt: {base_topic: "zigbee2mqtt", server: "old"}, advanced: {channel: 11}};
const result = objectAssignDeep(target, {mqtt: {server: "new"}});
expect(result).toStrictEqual({mqtt: {base_topic: "zigbee2mqtt", server: "new"}, advanced: {channel: 11}});
});
it("Replaces nested objects of the target instead of mutating them", () => {
const nested = {a: 1};
const target = {nested};
objectAssignDeep(target, {nested: {b: 2}});
expect(nested).toStrictEqual({a: 1});
expect(target.nested).not.toBe(nested);
expect(target.nested).toStrictEqual({a: 1, b: 2});
});
it("Replaces an existing non-object value with a clone of the source object", () => {
expect(objectAssignDeep({a: 5}, {a: {b: 1}})).toStrictEqual({a: {b: 1}});
expect(objectAssignDeep({a: "str"}, {a: {b: 1}})).toStrictEqual({a: {b: 1}});
expect(objectAssignDeep({a: [1, 2]}, {a: {b: 1}})).toStrictEqual({a: {b: 1}});
// `null` is not `undefined`, so it takes the "existing value" path but is not merged into
expect(objectAssignDeep({a: null}, {a: {b: 1}})).toStrictEqual({a: {b: 1}});
});
it("Overwrites with null and undefined", () => {
expect(objectAssignDeep({a: {b: 1}, c: 1}, {a: null, c: null})).toStrictEqual({a: null, c: null});
expect(objectAssignDeep({a: {b: 1}, c: 1}, {a: undefined, c: undefined})).toStrictEqual({a: undefined, c: undefined});
});
it("Replaces arrays instead of concatenating them", () => {
expect(objectAssignDeep({a: [1, 2, 3]}, {a: [4]})).toStrictEqual({a: [4]});
expect(objectAssignDeep({a: [1, 2, 3]}, {a: []})).toStrictEqual({a: []});
// no existing array either
expect(objectAssignDeep({a: 1}, {a: [4]})).toStrictEqual({a: [4]});
expect(objectAssignDeep({}, {a: [4]})).toStrictEqual({a: [4]});
});
it("Clones arrays and the objects nested inside them", () => {
const source = {a: [{b: 1}, [{c: 2}]]};
const result = objectAssignDeep({}, source) as typeof source;
expect(result).toStrictEqual(source);
expect(result.a).not.toBe(source.a);
expect(result.a[0]).not.toBe(source.a[0]);
expect((result.a[1] as {c: number}[])[0]).not.toBe((source.a[1] as {c: number}[])[0]);
});
it("Breaks all references to the sources", () => {
const source = {a: {b: {c: 1}}};
const result = objectAssignDeep({}, source) as typeof source;
source.a.b.c = 99;
expect(result.a.b.c).toStrictEqual(1);
});
it("Does not mutate the sources", () => {
const source = {a: {b: 1}};
objectAssignDeep({a: {c: 2}}, source);
expect(source).toStrictEqual({a: {b: 1}});
});
it("Copies functions and primitives by value/reference", () => {
const fn = (): number => 1;
const symbol = Symbol("s");
const result = objectAssignDeep({}, {fn, symbol, big: 1n, nan: Number.NaN});
expect(result.fn).toBe(fn);
expect(result.symbol).toBe(symbol);
expect(result.big).toStrictEqual(1n);
expect(result.nan).toBeNaN();
});
it("Reduces non-plain objects to their own enumerable properties", () => {
// documented (inherited) behaviour: only own enumerable properties survive, the prototype is lost
expect(objectAssignDeep({}, {date: new Date(0)})).toStrictEqual({date: {}});
expect(objectAssignDeep({}, {regexp: /abc/g})).toStrictEqual({regexp: {}});
expect(objectAssignDeep({}, {map: new Map([["k", 1]])})).toStrictEqual({map: {}});
expect(objectAssignDeep({}, {set: new Set([1])})).toStrictEqual({set: {}});
class Device {
id = 1;
get computed(): number {
return 2;
}
}
const result = objectAssignDeep({}, {device: new Device()});
expect(result.device).toStrictEqual({id: 1});
expect(result.device).not.toBeInstanceOf(Device);
});
it("Merges deeply nested objects coming from multiple sources", () => {
const result = objectAssignDeep({}, {a: {b: {c: 1}}}, {a: {b: {d: 2}, e: 3}});
expect(result).toStrictEqual({a: {b: {c: 1, d: 2}, e: 3}});
});
it("Never copies keys that could tamper with the prototype chain", () => {
const result = objectAssignDeep({}, parse('{"__proto__": {"polluted": "yes"}, "constructor": {"x": 1}, "prototype": {"y": 2}, "safe": 1}'));
expect(result).toStrictEqual({safe: 1});
expect(Object.getPrototypeOf(result)).toBe(Object.prototype);
expect(({} as {polluted?: string}).polluted).toBeUndefined();
});
it("Never copies unsafe keys nested inside cloned objects", () => {
const result = objectAssignDeep({}, {nested: parse('{"__proto__": {"polluted": "yes"}, "constructor": 1, "prototype": 2, "safe": 1}')});
expect(result).toStrictEqual({nested: {safe: 1}});
expect(Object.getPrototypeOf(result.nested)).toBe(Object.prototype);
});
it("Never copies unsafe keys when merging into an existing object", () => {
const result = objectAssignDeep({nested: {safe: 1}}, {nested: parse('{"__proto__": {"polluted": "yes"}, "other": 2}')});
expect(result).toStrictEqual({nested: {safe: 1, other: 2}});
expect(Object.getPrototypeOf(result.nested)).toBe(Object.prototype);
});
it("Leaves every source untouched when given an empty target", () => {
const first = {a: {b: 1}};
const second = {a: {c: 2}};
const result = objectAssignDeep({}, first, second);
expect(result).toStrictEqual({a: {b: 1, c: 2}});
expect(result).not.toBe(first);
expect(result).not.toBe(second);
expect(result.a).not.toBe(first.a);
expect(first).toStrictEqual({a: {b: 1}});
expect(second).toStrictEqual({a: {c: 2}});
});
});
+10 -11
View File
@@ -4,11 +4,15 @@ import "./mocks/data";
import fs from "node:fs";
import {dump, load} from "js-yaml";
import objectAssignDeep from "object-assign-deep";
import mockedData from "../lib/util/data";
import {objectAssignDeep} from "../lib/util/objectAssignDeep";
import * as settings from "../lib/util/settings";
// mirrors the global `KeyValue`, which is not visible from the test project, previously implied by the untyped `object-assign-deep`
// biome-ignore lint/suspicious/noExplicitAny: freely mutated to build the expected settings
type ExpectedSettings = Record<string, any>;
const configurationFile = mockedData.joinPath("configuration.yaml");
const devicesFile = mockedData.joinPath("devices.yaml");
const devicesFile2 = mockedData.joinPath("devices2.yaml");
@@ -86,8 +90,7 @@ describe("Settings", () => {
it("Should return default settings", () => {
write(configurationFile, {});
const s = settings.get();
// @ts-expect-error workaround
const expected = objectAssignDeep.noMutate({}, settings.testing.defaults);
const expected: ExpectedSettings = objectAssignDeep({}, settings.testing.defaults);
expected.devices = {};
expected.groups = {};
expect(s).toStrictEqual(expected);
@@ -96,8 +99,7 @@ describe("Settings", () => {
it("Should return settings", () => {
write(configurationFile, {serial: {disable_led: true}});
const s = settings.get();
// @ts-expect-error workaround
const expected = objectAssignDeep.noMutate({}, settings.testing.defaults);
const expected: ExpectedSettings = objectAssignDeep({}, settings.testing.defaults);
expected.devices = {};
expected.groups = {};
expected.serial = {disable_led: true};
@@ -124,8 +126,7 @@ describe("Settings", () => {
},
};
// @ts-expect-error workaround
const expected = objectAssignDeep.noMutate({}, settings.testing.defaults);
const expected: ExpectedSettings = objectAssignDeep({}, settings.testing.defaults);
expected.devices = {
"0x00158d00018255df": {
friendly_name: "0x00158d00018255df",
@@ -178,8 +179,7 @@ describe("Settings", () => {
write(configurationFile, {});
// @ts-expect-error workaround
const expected = objectAssignDeep.noMutate({}, settings.testing.defaults);
const expected: ExpectedSettings = objectAssignDeep({}, settings.testing.defaults);
expected.frontend.enabled = true;
expected.frontend.port = 8099;
expected.homeassistant.enabled = true;
@@ -212,8 +212,7 @@ describe("Settings", () => {
expect(settings.validate()).toStrictEqual([]);
const s = settings.get();
// @ts-expect-error workaround
const expected = objectAssignDeep.noMutate({groups: {}, devices: {}}, settings.testing.defaults);
const expected: ExpectedSettings = objectAssignDeep({}, {groups: {}, devices: {}}, settings.testing.defaults);
expected.mqtt.password = "password-in-env-var";
expected.mqtt.server = "server";
expect(s).toStrictEqual(expected);
+40 -79
View File
@@ -3,8 +3,8 @@ import {afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi} fr
import * as data from "./mocks/data";
import {existsSync, readFileSync, rmSync, writeFileSync} from "node:fs";
import objectAssignDeep from "object-assign-deep";
import mockedData from "../lib/util/data";
import {objectAssignDeep} from "../lib/util/objectAssignDeep";
import * as settings from "../lib/util/settings";
import * as settingsMigration from "../lib/util/settingsMigration";
import path from "node:path";
@@ -278,8 +278,7 @@ describe("Settings Migration", () => {
});
it("no change needed - only add version", () => {
// @ts-expect-error workaround
const afterSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
const afterSettings = objectAssignDeep({}, settings.getPersistedSettings());
afterSettings.version = 2;
settingsMigration.migrateIfNecessary();
@@ -290,10 +289,8 @@ describe("Settings Migration", () => {
});
it("remove all", () => {
// @ts-expect-error workaround
const beforeSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
// @ts-expect-error workaround
const afterSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
const beforeSettings = objectAssignDeep({}, settings.getPersistedSettings());
const afterSettings = objectAssignDeep({}, settings.getPersistedSettings());
afterSettings.version = 2;
settings.set(["homeassistant", "legacy_triggers"], true);
@@ -320,8 +317,7 @@ describe("Settings Migration", () => {
settings.set(["external_converters"], ["zyx.js"]);
expect(settings.getPersistedSettings()).toStrictEqual(
// @ts-expect-error workaround
objectAssignDeep.noMutate(beforeSettings, {
objectAssignDeep({}, beforeSettings, {
permit_join: true,
homeassistant: {
legacy_triggers: true,
@@ -395,10 +391,8 @@ describe("Settings Migration", () => {
});
it("remove partial", () => {
// @ts-expect-error workaround
const beforeSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
// @ts-expect-error workaround
const afterSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
const beforeSettings = objectAssignDeep({}, settings.getPersistedSettings());
const afterSettings = objectAssignDeep({}, settings.getPersistedSettings());
afterSettings.version = 2;
settings.set(["advanced", "homeassistant_legacy_triggers"], true);
@@ -417,8 +411,7 @@ describe("Settings Migration", () => {
// console.log(JSON.stringify(settings.getWrittenSettings(), undefined, 2));
expect(settings.getPersistedSettings()).toStrictEqual(
// @ts-expect-error workaround
objectAssignDeep.noMutate(beforeSettings, {
objectAssignDeep({}, beforeSettings, {
permit_join: true,
advanced: {
homeassistant_legacy_triggers: true,
@@ -472,10 +465,8 @@ describe("Settings Migration", () => {
});
it("changes log_level", () => {
// @ts-expect-error workaround
const beforeSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
// @ts-expect-error workaround
const afterSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
const beforeSettings = objectAssignDeep({}, settings.getPersistedSettings());
const afterSettings = objectAssignDeep({}, settings.getPersistedSettings());
afterSettings.version = 2;
afterSettings.advanced = {log_level: "warning"};
@@ -484,8 +475,7 @@ describe("Settings Migration", () => {
// console.log(JSON.stringify(settings.getWrittenSettings(), undefined, 2));
expect(settings.getPersistedSettings()).toStrictEqual(
// @ts-expect-error workaround
objectAssignDeep.noMutate(beforeSettings, {
objectAssignDeep({}, beforeSettings, {
advanced: {
log_level: "warn",
},
@@ -505,10 +495,8 @@ describe("Settings Migration", () => {
});
it("does not changes already migrated log_level", () => {
// @ts-expect-error workaround
const beforeSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
// @ts-expect-error workaround
const afterSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
const beforeSettings = objectAssignDeep({}, settings.getPersistedSettings());
const afterSettings = objectAssignDeep({}, settings.getPersistedSettings());
afterSettings.version = 2;
afterSettings.advanced = {log_level: "warning"};
@@ -517,8 +505,7 @@ describe("Settings Migration", () => {
// console.log(JSON.stringify(settings.getWrittenSettings(), undefined, 2));
expect(settings.getPersistedSettings()).toStrictEqual(
// @ts-expect-error workaround
objectAssignDeep.noMutate(beforeSettings, {
objectAssignDeep({}, beforeSettings, {
advanced: {
log_level: "warning",
},
@@ -538,10 +525,8 @@ describe("Settings Migration", () => {
});
it("does not changes other log_level", () => {
// @ts-expect-error workaround
const beforeSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
// @ts-expect-error workaround
const afterSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
const beforeSettings = objectAssignDeep({}, settings.getPersistedSettings());
const afterSettings = objectAssignDeep({}, settings.getPersistedSettings());
afterSettings.version = 2;
afterSettings.advanced = {log_level: "info"};
@@ -550,8 +535,7 @@ describe("Settings Migration", () => {
// console.log(JSON.stringify(settings.getWrittenSettings(), undefined, 2));
expect(settings.getPersistedSettings()).toStrictEqual(
// @ts-expect-error workaround
objectAssignDeep.noMutate(beforeSettings, {
objectAssignDeep({}, beforeSettings, {
advanced: {
log_level: "info",
},
@@ -571,10 +555,8 @@ describe("Settings Migration", () => {
});
it("transfer all", () => {
// @ts-expect-error workaround
const beforeSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
// @ts-expect-error workaround
const afterSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
const beforeSettings = objectAssignDeep({}, settings.getPersistedSettings());
const afterSettings = objectAssignDeep({}, settings.getPersistedSettings());
afterSettings.version = 2;
afterSettings.advanced = {
transmit_power: 12,
@@ -604,8 +586,7 @@ describe("Settings Migration", () => {
// console.log(JSON.stringify(settings.getWrittenSettings(), undefined, 2));
expect(settings.getPersistedSettings()).toStrictEqual(
// @ts-expect-error workaround
objectAssignDeep.noMutate(beforeSettings, {
objectAssignDeep({}, beforeSettings, {
advanced: {
homeassistant_discovery_topic: "ha_disc",
homeassistant_status_topic: "ha_stat",
@@ -649,10 +630,8 @@ describe("Settings Migration", () => {
});
it("transfer partial", () => {
// @ts-expect-error workaround
const beforeSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
// @ts-expect-error workaround
const afterSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
const beforeSettings = objectAssignDeep({}, settings.getPersistedSettings());
const afterSettings = objectAssignDeep({}, settings.getPersistedSettings());
afterSettings.version = 2;
afterSettings.advanced = {}; // caused by pushing to key and removing all
afterSettings.serial.baudrate = 115200;
@@ -673,8 +652,7 @@ describe("Settings Migration", () => {
// console.log(JSON.stringify(settings.getWrittenSettings(), undefined, 2));
expect(settings.getPersistedSettings()).toStrictEqual(
// @ts-expect-error workaround
objectAssignDeep.noMutate(beforeSettings, {
objectAssignDeep({}, beforeSettings, {
homeassistant: {discovery_topic: "ha_disc_newer"},
advanced: {
homeassistant_discovery_topic: "ha_disc",
@@ -718,10 +696,8 @@ describe("Settings Migration", () => {
});
it("Update", () => {
// @ts-expect-error workaround
const beforeSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
// @ts-expect-error workaround
const afterSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
const beforeSettings = objectAssignDeep({}, settings.getPersistedSettings());
const afterSettings = objectAssignDeep({}, settings.getPersistedSettings());
afterSettings.version = 3;
afterSettings.homeassistant = {enabled: false};
afterSettings.frontend = {enabled: true};
@@ -739,8 +715,7 @@ describe("Settings Migration", () => {
settings.set(["experimental", "transmit_power"], 12);
expect(settings.getPersistedSettings()).toStrictEqual(
// @ts-expect-error workaround
objectAssignDeep.noMutate(beforeSettings, {
objectAssignDeep({}, beforeSettings, {
homeassistant: false,
frontend: true,
availability: {active: {timeout: 15}},
@@ -773,10 +748,8 @@ describe("Settings Migration", () => {
});
it("Update", () => {
// @ts-expect-error workaround
const beforeSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
// @ts-expect-error workaround
const afterSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
const beforeSettings = objectAssignDeep({}, settings.getPersistedSettings());
const afterSettings = objectAssignDeep({}, settings.getPersistedSettings());
afterSettings.version = 3;
afterSettings.homeassistant = {enabled: false};
afterSettings.frontend = {enabled: true};
@@ -787,8 +760,7 @@ describe("Settings Migration", () => {
settings.set(["availability"], {active: {timeout: 15}});
expect(settings.getPersistedSettings()).toStrictEqual(
// @ts-expect-error workaround
objectAssignDeep.noMutate(beforeSettings, {
objectAssignDeep({}, beforeSettings, {
homeassistant: false,
frontend: true,
availability: {active: {timeout: 15}},
@@ -809,18 +781,15 @@ describe("Settings Migration", () => {
});
it("Update when not set, tests that frontend/availability is not added when not set", () => {
// @ts-expect-error workaround
const beforeSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
// @ts-expect-error workaround
const afterSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
const beforeSettings = objectAssignDeep({}, settings.getPersistedSettings());
const afterSettings = objectAssignDeep({}, settings.getPersistedSettings());
afterSettings.version = 3;
afterSettings.homeassistant = {enabled: false};
settings.set(["homeassistant"], false);
expect(settings.getPersistedSettings()).toStrictEqual(
// @ts-expect-error workaround
objectAssignDeep.noMutate(beforeSettings, {
objectAssignDeep({}, beforeSettings, {
homeassistant: false,
}),
);
@@ -854,10 +823,8 @@ describe("Settings Migration", () => {
});
it("Update", () => {
// @ts-expect-error workaround
const beforeSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
// @ts-expect-error workaround
const afterSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
const beforeSettings = objectAssignDeep({}, settings.getPersistedSettings());
const afterSettings = objectAssignDeep({}, settings.getPersistedSettings());
afterSettings.version = 4;
afterSettings.devices = {
"0x123127fffe8d96bc": {
@@ -888,8 +855,7 @@ describe("Settings Migration", () => {
});
expect(settings.getPersistedSettings()).toStrictEqual(
// @ts-expect-error workaround
objectAssignDeep.noMutate(beforeSettings, {
objectAssignDeep({}, beforeSettings, {
devices: {
"0x123127fffe8d96bc": {
friendly_name: "0x847127fffe8d96bc",
@@ -950,10 +916,8 @@ describe("Settings Migration", () => {
});
it("Update", () => {
// @ts-expect-error workaround
const beforeSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
// @ts-expect-error workaround
const afterSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
const beforeSettings = objectAssignDeep({}, settings.getPersistedSettings());
const afterSettings = objectAssignDeep({}, settings.getPersistedSettings());
afterSettings.version = 5;
expect(settings.getPersistedSettings()).toStrictEqual(beforeSettings);
@@ -964,8 +928,7 @@ describe("Settings Migration", () => {
const migratedSettings = settings.getPersistedSettings();
expect(migratedSettings).toStrictEqual(afterSettings);
// @ts-expect-error workaround
const migratedState = objectAssignDeep.noMutate({}, DEFAULT_STATE);
const migratedState = objectAssignDeep({}, DEFAULT_STATE);
delete (migratedState["0x0017880104e45517"] as Record<string, unknown>).update;
delete (migratedState[1] as Record<string, unknown>).update;
@@ -976,10 +939,8 @@ describe("Settings Migration", () => {
const consoleErrorSpy = vi.spyOn(console, "error");
writeFileSync(path.join(data.mockDir, "state.json"), "notjson", "utf8");
// @ts-expect-error workaround
const beforeSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
// @ts-expect-error workaround
const afterSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
const beforeSettings = objectAssignDeep({}, settings.getPersistedSettings());
const afterSettings = objectAssignDeep({}, settings.getPersistedSettings());
afterSettings.version = 5;
expect(settings.getPersistedSettings()).toStrictEqual(beforeSettings);