From 1a796b6680cf2c092d3c16d9bf883ba60346f275 Mon Sep 17 00:00:00 2001 From: Ehab Younes Date: Tue, 27 Jan 2026 12:20:16 +0300 Subject: [PATCH 1/2] Normalize "empty" string values for comparison in the config watcher --- src/configWatcher.ts | 16 +++++++++++++- test/unit/configWatcher.test.ts | 39 +++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/src/configWatcher.ts b/src/configWatcher.ts index 562668ec..3a68e257 100644 --- a/src/configWatcher.ts +++ b/src/configWatcher.ts @@ -26,7 +26,7 @@ export function watchConfigurationChanges( const newValue = getValue(); - if (!isDeepStrictEqual(newValue, appliedValues.get(setting))) { + if (!configValuesEqual(newValue, appliedValues.get(setting))) { changedSettings.push(setting); appliedValues.set(setting, newValue); } @@ -37,3 +37,17 @@ export function watchConfigurationChanges( } }); } + +function configValuesEqual(a: unknown, b: unknown): boolean { + return isDeepStrictEqual(normalizeEmptyValue(a), normalizeEmptyValue(b)); +} + +/** + * Normalize "empty" string values to a canonical form for comparison. + */ +function normalizeEmptyValue(value: unknown): unknown { + if (value === undefined || value === null || value === "") { + return undefined; + } + return value; +} diff --git a/test/unit/configWatcher.test.ts b/test/unit/configWatcher.test.ts index 0ce6862c..76f524de 100644 --- a/test/unit/configWatcher.test.ts +++ b/test/unit/configWatcher.test.ts @@ -94,4 +94,43 @@ describe("watchConfigurationChanges", () => { expect(changes).toEqual([["test.setting"]]); dispose(); }); + + it("treats undefined, null, and empty string as equivalent", () => { + const config = new MockConfigurationProvider(); + config.set("test.setting", undefined); + const { changes, dispose } = createWatcher("test.setting"); + + config.set("test.setting", null); + config.set("test.setting", ""); + config.set("test.setting", undefined); + + expect(changes).toEqual([]); + dispose(); + }); + + interface ValueChangeTestCase { + name: string; + from: unknown; + to: unknown; + } + + it.each([ + { name: "undefined to value", from: undefined, to: "value" }, + { name: "value to empty string", from: "value", to: "" }, + { name: "undefined to false", from: undefined, to: false }, + { name: "undefined to zero", from: undefined, to: 0 }, + { name: "undefined to empty array", from: undefined, to: [] }, + { name: "null to value", from: null, to: "value" }, + { name: "empty string to value", from: "", to: "value" }, + { name: "value to different value", from: "old", to: "new" }, + ])("detects change: $name", ({ from, to }) => { + const config = new MockConfigurationProvider(); + config.set("test.setting", from); + const { changes, dispose } = createWatcher("test.setting"); + + config.set("test.setting", to); + + expect(changes).toEqual([["test.setting"]]); + dispose(); + }); }); From 39958cacff3c839ed8918aaf2a17624bbcfbae46 Mon Sep 17 00:00:00 2001 From: Ehab Younes Date: Tue, 27 Jan 2026 17:18:07 +0300 Subject: [PATCH 2/2] Handle empty arrays --- src/configWatcher.ts | 9 +++++++-- test/unit/configWatcher.test.ts | 5 +++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/configWatcher.ts b/src/configWatcher.ts index 3a68e257..ee63f448 100644 --- a/src/configWatcher.ts +++ b/src/configWatcher.ts @@ -43,10 +43,15 @@ function configValuesEqual(a: unknown, b: unknown): boolean { } /** - * Normalize "empty" string values to a canonical form for comparison. + * Normalize empty values (undefined, null, "", []) to a canonical form for comparison. */ function normalizeEmptyValue(value: unknown): unknown { - if (value === undefined || value === null || value === "") { + if ( + value === undefined || + value === null || + value === "" || + (Array.isArray(value) && value.length === 0) + ) { return undefined; } return value; diff --git a/test/unit/configWatcher.test.ts b/test/unit/configWatcher.test.ts index 76f524de..2327b2df 100644 --- a/test/unit/configWatcher.test.ts +++ b/test/unit/configWatcher.test.ts @@ -95,13 +95,14 @@ describe("watchConfigurationChanges", () => { dispose(); }); - it("treats undefined, null, and empty string as equivalent", () => { + it("treats undefined, null, empty string, and empty array as equivalent", () => { const config = new MockConfigurationProvider(); config.set("test.setting", undefined); const { changes, dispose } = createWatcher("test.setting"); config.set("test.setting", null); config.set("test.setting", ""); + config.set("test.setting", []); config.set("test.setting", undefined); expect(changes).toEqual([]); @@ -119,9 +120,9 @@ describe("watchConfigurationChanges", () => { { name: "value to empty string", from: "value", to: "" }, { name: "undefined to false", from: undefined, to: false }, { name: "undefined to zero", from: undefined, to: 0 }, - { name: "undefined to empty array", from: undefined, to: [] }, { name: "null to value", from: null, to: "value" }, { name: "empty string to value", from: "", to: "value" }, + { name: "empty array to non-empty array", from: [], to: ["item"] }, { name: "value to different value", from: "old", to: "new" }, ])("detects change: $name", ({ from, to }) => { const config = new MockConfigurationProvider();