diff --git a/packages/shared/sdk-server/__tests__/overrides/createOverrideSource.test.ts b/packages/shared/sdk-server/__tests__/overrides/createOverrideSource.test.ts index 9c78540129..f33d245f7b 100644 --- a/packages/shared/sdk-server/__tests__/overrides/createOverrideSource.test.ts +++ b/packages/shared/sdk-server/__tests__/overrides/createOverrideSource.test.ts @@ -14,6 +14,15 @@ function makeContext(logger: LDLogger, withFilesystem: boolean = true): ClientCo }); } +function makeContextOn(logger: LDLogger, osName: string): ClientContext { + const platform = createBasicPlatform(); + platform.info.platformData.mockReturnValue({ os: { name: osName } }); + return new ClientContext('sdk-key', new Configuration({ logger }), { + ...platform, + fileSystem: new MockFilesystem(), + }); +} + describe('given a client context with filesystem support', () => { let logger: MockLogger; let context: ClientContext; @@ -161,6 +170,28 @@ describe('given a client context with filesystem support', () => { expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining('more than once')); }); + it('drops a path repeated in a different case on Windows, where file names ignore case', () => { + const windowsLogger = makeMockLogger(); + const source = createOverrideSource( + { type: 'file', paths: ['C:\\overrides\\a.json', 'c:\\Overrides\\A.JSON'] }, + makeContextOn(windowsLogger, 'Windows'), + ) as FileOverrideSource; + + expect(source.config.paths).toEqual(['C:\\overrides\\a.json']); + expect(windowsLogger.warn).toHaveBeenCalledWith(expect.stringContaining('more than once')); + }); + + it('keeps paths that differ only in case on other platforms', () => { + const linuxLogger = makeMockLogger(); + const source = createOverrideSource( + { type: 'file', paths: ['/overrides/a.json', '/Overrides/A.json'] }, + makeContextOn(linuxLogger, 'Linux'), + ) as FileOverrideSource; + + expect(source.config.paths).toEqual(['/overrides/a.json', '/Overrides/A.json']); + expect(linuxLogger.warn).not.toHaveBeenCalled(); + }); + it('fails construction for a YAML file when no parser is configured', () => { // The SDK does not include a YAML parser, so such a file could never load. That is a // configuration error, reported once at construction rather than on every reload. diff --git a/packages/shared/sdk-server/src/overrides/createOverrideSource.ts b/packages/shared/sdk-server/src/overrides/createOverrideSource.ts index 01d682e3e7..f6ce1373bd 100644 --- a/packages/shared/sdk-server/src/overrides/createOverrideSource.ts +++ b/packages/shared/sdk-server/src/overrides/createOverrideSource.ts @@ -45,8 +45,18 @@ function validateFileOverrideSourceOptions( throw new Error('The file-based override source requires at least one file path'); } // A path listed more than once would make every entry of that file a duplicate of itself, so - // the repeats are dropped and the first occurrence keeps its place in the order. - const paths = options.paths.filter((path, index) => options.paths.indexOf(path) === index); + // the repeats are dropped and the first occurrence keeps its place in the order. Windows file + // names ignore case, so there a path repeated in a different case names the same file. + const ignoreCase = clientContext.platform.info.platformData().os?.name === 'Windows'; + const seen = new Set(); + const paths = options.paths.filter((path) => { + const comparable = ignoreCase ? path.toLowerCase() : path; + if (seen.has(comparable)) { + return false; + } + seen.add(comparable); + return true; + }); if (paths.length !== options.paths.length) { logger?.warn( 'dataSystem.overrides.paths lists the same file more than once; the repeats are ignored',