mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-18 01:51:20 -07:00
refactor(setting): Improve settings migration and tool loading (#7445)
Co-authored-by: psinha40898 <pyushsinha20@gmail.com>
This commit is contained in:
163
packages/cli/src/utils/deepMerge.test.ts
Normal file
163
packages/cli/src/utils/deepMerge.test.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { customDeepMerge } from './deepMerge.js';
|
||||
import { MergeStrategy } from '../config/settingsSchema.js';
|
||||
|
||||
describe('customDeepMerge', () => {
|
||||
it('should merge simple objects', () => {
|
||||
const target = { a: 1, b: 2 };
|
||||
const source = { b: 3, c: 4 };
|
||||
const getMergeStrategy = () => undefined;
|
||||
const result = customDeepMerge(getMergeStrategy, target, source);
|
||||
expect(result).toEqual({ a: 1, b: 3, c: 4 });
|
||||
});
|
||||
|
||||
it('should merge nested objects', () => {
|
||||
const target = { a: { x: 1 }, b: 2 };
|
||||
const source = { a: { y: 2 }, c: 3 };
|
||||
const getMergeStrategy = () => undefined;
|
||||
const result = customDeepMerge(getMergeStrategy, target, source);
|
||||
expect(result).toEqual({ a: { x: 1, y: 2 }, b: 2, c: 3 });
|
||||
});
|
||||
|
||||
it('should replace arrays by default', () => {
|
||||
const target = { a: [1, 2] };
|
||||
const source = { a: [3, 4] };
|
||||
const getMergeStrategy = () => undefined;
|
||||
const result = customDeepMerge(getMergeStrategy, target, source);
|
||||
expect(result).toEqual({ a: [3, 4] });
|
||||
});
|
||||
|
||||
it('should concatenate arrays with CONCAT strategy', () => {
|
||||
const target = { a: [1, 2] };
|
||||
const source = { a: [3, 4] };
|
||||
const getMergeStrategy = (path: string[]) =>
|
||||
path.join('.') === 'a' ? MergeStrategy.CONCAT : undefined;
|
||||
const result = customDeepMerge(getMergeStrategy, target, source);
|
||||
expect(result).toEqual({ a: [1, 2, 3, 4] });
|
||||
});
|
||||
|
||||
it('should union arrays with UNION strategy', () => {
|
||||
const target = { a: [1, 2, 3] };
|
||||
const source = { a: [3, 4, 5] };
|
||||
const getMergeStrategy = (path: string[]) =>
|
||||
path.join('.') === 'a' ? MergeStrategy.UNION : undefined;
|
||||
const result = customDeepMerge(getMergeStrategy, target, source);
|
||||
expect(result).toEqual({ a: [1, 2, 3, 4, 5] });
|
||||
});
|
||||
|
||||
it('should shallow merge objects with SHALLOW_MERGE strategy', () => {
|
||||
const target = { a: { x: 1, y: 1 } };
|
||||
const source = { a: { y: 2, z: 2 } };
|
||||
const getMergeStrategy = (path: string[]) =>
|
||||
path.join('.') === 'a' ? MergeStrategy.SHALLOW_MERGE : undefined;
|
||||
const result = customDeepMerge(getMergeStrategy, target, source);
|
||||
// This is still a deep merge, but the properties of the object are merged.
|
||||
expect(result).toEqual({ a: { x: 1, y: 2, z: 2 } });
|
||||
});
|
||||
|
||||
it('should handle multiple source objects', () => {
|
||||
const target = { a: 1 };
|
||||
const source1 = { b: 2 };
|
||||
const source2 = { c: 3 };
|
||||
const getMergeStrategy = () => undefined;
|
||||
const result = customDeepMerge(getMergeStrategy, target, source1, source2);
|
||||
expect(result).toEqual({ a: 1, b: 2, c: 3 });
|
||||
});
|
||||
|
||||
it('should return an empty object if no sources are provided', () => {
|
||||
const getMergeStrategy = () => undefined;
|
||||
const result = customDeepMerge(getMergeStrategy);
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
it('should return a deep copy of the first source if only one is provided', () => {
|
||||
const target = { a: { b: 1 } };
|
||||
const getMergeStrategy = () => undefined;
|
||||
const result = customDeepMerge(getMergeStrategy, target);
|
||||
expect(result).toEqual(target);
|
||||
expect(result).not.toBe(target);
|
||||
});
|
||||
|
||||
it('should not mutate the original source objects', () => {
|
||||
const target = { a: { x: 1 }, b: [1, 2] };
|
||||
const source = { a: { y: 2 }, b: [3, 4] };
|
||||
const originalTarget = JSON.parse(JSON.stringify(target));
|
||||
const originalSource = JSON.parse(JSON.stringify(source));
|
||||
const getMergeStrategy = () => undefined;
|
||||
|
||||
customDeepMerge(getMergeStrategy, target, source);
|
||||
|
||||
expect(target).toEqual(originalTarget);
|
||||
expect(source).toEqual(originalSource);
|
||||
});
|
||||
|
||||
it('should not mutate sources when merging multiple levels deep', () => {
|
||||
const s1 = { data: { common: { val: 'from s1' }, s1_only: true } };
|
||||
const s2 = { data: { common: { val: 'from s2' }, s2_only: true } };
|
||||
const s1_original = JSON.parse(JSON.stringify(s1));
|
||||
const s2_original = JSON.parse(JSON.stringify(s2));
|
||||
|
||||
const getMergeStrategy = () => undefined;
|
||||
const result = customDeepMerge(getMergeStrategy, s1, s2);
|
||||
|
||||
expect(s1).toEqual(s1_original);
|
||||
expect(s2).toEqual(s2_original);
|
||||
expect(result).toEqual({
|
||||
data: {
|
||||
common: { val: 'from s2' },
|
||||
s1_only: true,
|
||||
s2_only: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle complex nested strategies', () => {
|
||||
const target = {
|
||||
level1: {
|
||||
arr1: [1, 2],
|
||||
arr2: [1, 2],
|
||||
obj1: { a: 1 },
|
||||
},
|
||||
};
|
||||
const source = {
|
||||
level1: {
|
||||
arr1: [3, 4],
|
||||
arr2: [2, 3],
|
||||
obj1: { b: 2 },
|
||||
},
|
||||
};
|
||||
const getMergeStrategy = (path: string[]) => {
|
||||
const p = path.join('.');
|
||||
if (p === 'level1.arr1') return MergeStrategy.CONCAT;
|
||||
if (p === 'level1.arr2') return MergeStrategy.UNION;
|
||||
if (p === 'level1.obj1') return MergeStrategy.SHALLOW_MERGE;
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const result = customDeepMerge(getMergeStrategy, target, source);
|
||||
|
||||
expect(result).toEqual({
|
||||
level1: {
|
||||
arr1: [1, 2, 3, 4],
|
||||
arr2: [1, 2, 3],
|
||||
obj1: { a: 1, b: 2 },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should not pollute the prototype', () => {
|
||||
const maliciousSource = JSON.parse('{"__proto__": {"polluted": "true"}}');
|
||||
const getMergeStrategy = () => undefined;
|
||||
const result = customDeepMerge(getMergeStrategy, {}, maliciousSource);
|
||||
|
||||
expect(result).toEqual({});
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
expect(({} as any).polluted).toBeUndefined();
|
||||
});
|
||||
});
|
||||
90
packages/cli/src/utils/deepMerge.ts
Normal file
90
packages/cli/src/utils/deepMerge.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { MergeStrategy } from '../config/settingsSchema.js';
|
||||
|
||||
type Mergeable =
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null
|
||||
| undefined
|
||||
| object
|
||||
| Mergeable[];
|
||||
|
||||
type MergeableObject = Record<string, Mergeable>;
|
||||
|
||||
function isPlainObject(item: unknown): item is MergeableObject {
|
||||
return !!item && typeof item === 'object' && !Array.isArray(item);
|
||||
}
|
||||
|
||||
function mergeRecursively(
|
||||
target: MergeableObject,
|
||||
source: MergeableObject,
|
||||
getMergeStrategyForPath: (path: string[]) => MergeStrategy | undefined,
|
||||
path: string[] = [],
|
||||
) {
|
||||
for (const key of Object.keys(source)) {
|
||||
if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
|
||||
continue;
|
||||
}
|
||||
const newPath = [...path, key];
|
||||
const srcValue = source[key];
|
||||
const objValue = target[key];
|
||||
const mergeStrategy = getMergeStrategyForPath(newPath);
|
||||
|
||||
if (mergeStrategy === MergeStrategy.SHALLOW_MERGE && objValue && srcValue) {
|
||||
const obj1 =
|
||||
typeof objValue === 'object' && objValue !== null ? objValue : {};
|
||||
const obj2 =
|
||||
typeof srcValue === 'object' && srcValue !== null ? srcValue : {};
|
||||
target[key] = { ...obj1, ...obj2 };
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Array.isArray(objValue)) {
|
||||
const srcArray = Array.isArray(srcValue) ? srcValue : [srcValue];
|
||||
if (mergeStrategy === MergeStrategy.CONCAT) {
|
||||
target[key] = objValue.concat(srcArray);
|
||||
continue;
|
||||
}
|
||||
if (mergeStrategy === MergeStrategy.UNION) {
|
||||
target[key] = [...new Set(objValue.concat(srcArray))];
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (isPlainObject(objValue) && isPlainObject(srcValue)) {
|
||||
mergeRecursively(objValue, srcValue, getMergeStrategyForPath, newPath);
|
||||
} else if (isPlainObject(srcValue)) {
|
||||
target[key] = {};
|
||||
mergeRecursively(
|
||||
target[key] as MergeableObject,
|
||||
srcValue,
|
||||
getMergeStrategyForPath,
|
||||
newPath,
|
||||
);
|
||||
} else {
|
||||
target[key] = srcValue;
|
||||
}
|
||||
}
|
||||
return target;
|
||||
}
|
||||
|
||||
export function customDeepMerge(
|
||||
getMergeStrategyForPath: (path: string[]) => MergeStrategy | undefined,
|
||||
...sources: MergeableObject[]
|
||||
): MergeableObject {
|
||||
const result: MergeableObject = {};
|
||||
|
||||
for (const source of sources) {
|
||||
if (source) {
|
||||
mergeRecursively(result, source, getMergeStrategyForPath);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
Reference in New Issue
Block a user