Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 126 additions & 0 deletions tests/core/config/merge-utils.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/**
* Unit tests for merge-utils
*
* Tests deep merge strategy per ADR-PRO-002: scalars last-wins,
* objects deep merge, arrays replace, +append, null delete, isPlainObject.
*/

const { deepMerge, mergeAll, isPlainObject } = require('../../../.aios-core/core/config/merge-utils');

describe('merge-utils', () => {
describe('isPlainObject', () => {
test('returns true for plain objects', () => {
expect(isPlainObject({})).toBe(true);
expect(isPlainObject({ a: 1 })).toBe(true);
});

test('returns true for Object.create(null)', () => {
expect(isPlainObject(Object.create(null))).toBe(true);
});

test('returns false for arrays', () => {
expect(isPlainObject([])).toBe(false);
});

test('returns false for null', () => {
expect(isPlainObject(null)).toBe(false);
});

test('returns false for primitives', () => {
expect(isPlainObject('string')).toBe(false);
expect(isPlainObject(42)).toBe(false);
expect(isPlainObject(true)).toBe(false);
expect(isPlainObject(undefined)).toBe(false);
});

test('returns false for class instances', () => {
expect(isPlainObject(new Date())).toBe(false);
expect(isPlainObject(new Map())).toBe(false);
});
});

describe('deepMerge', () => {
test('scalars: source overrides target', () => {
const result = deepMerge({ a: 1 }, { a: 2 });
expect(result.a).toBe(2);
});

test('adds new keys from source', () => {
const result = deepMerge({ a: 1 }, { b: 2 });
expect(result).toEqual({ a: 1, b: 2 });
});

test('deep merges nested objects', () => {
const target = { db: { host: 'localhost', port: 5432 } };
const source = { db: { host: 'remote', timeout: 30 } };
const result = deepMerge(target, source);
expect(result.db).toEqual({ host: 'remote', port: 5432, timeout: 30 });
});

test('arrays: source replaces target', () => {
const result = deepMerge({ tags: ['a', 'b'] }, { tags: ['c'] });
expect(result.tags).toEqual(['c']);
});

test('+append: concatenates arrays', () => {
const result = deepMerge({ items: [1, 2] }, { 'items+append': [3, 4] });
expect(result.items).toEqual([1, 2, 3, 4]);
});

test('+append: creates new array when target key missing', () => {
const result = deepMerge({}, { 'items+append': [1, 2] });
expect(result.items).toEqual([1, 2]);
});
Comment on lines +65 to +73
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Missing assertion: the +append suffixed key should not appear in the result.

Both +append tests validate the merged array value under result.items, but neither asserts that the literal 'items+append' key is absent from the result object. If the implementation accidentally leaks the raw key, these tests would not catch it.

🧪 Suggested additional assertions
  test('+append: concatenates arrays', () => {
    const result = deepMerge({ items: [1, 2] }, { 'items+append': [3, 4] });
    expect(result.items).toEqual([1, 2, 3, 4]);
+   expect('items+append' in result).toBe(false);
  });

  test('+append: creates new array when target key missing', () => {
    const result = deepMerge({}, { 'items+append': [1, 2] });
    expect(result.items).toEqual([1, 2]);
+   expect('items+append' in result).toBe(false);
  });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/core/config/merge-utils.test.js` around lines 65 - 73, Add assertions
to the '+append' tests to ensure the suffixed key is removed from the merged
output: after calling deepMerge in both tests (the ones using 'items+append'),
verify that result does not have the literal 'items+append' property (e.g.,
assert result['items+append'] is undefined or that the key is not in
Object.keys(result)); update the two tests referencing deepMerge to include this
check so any leakage of the raw suffixed key is caught.


test('null value deletes key', () => {
const result = deepMerge({ a: 1, b: 2 }, { a: null });
expect(result).toEqual({ b: 2 });
expect('a' in result).toBe(false);
});

test('does not mutate inputs', () => {
const target = { a: { b: 1 } };
const source = { a: { c: 2 } };
const result = deepMerge(target, source);
expect(target.a).toEqual({ b: 1 });
expect(result.a).toEqual({ b: 1, c: 2 });
});

test('returns source when target not plain object', () => {
expect(deepMerge('string', { a: 1 })).toEqual({ a: 1 });
});

test('returns target when source is undefined', () => {
expect(deepMerge({ a: 1 }, undefined)).toEqual({ a: 1 });
});
});
Comment on lines +42 to +96
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

deepMerge block has 10 tests, not the 12 stated in the PR description and issue #270.

Counting each test() call: scalars, new keys, deep merge, array replace, +append concat, +append missing key, null delete, immutability, non-object target, undefined source = 10. The PR table and issue both promise 12.

The two most useful additions to close the gap (and cover realistic branches):

  1. null targetdeepMerge(null, { a: 1 }) is a distinct code path from the 'string' target already tested at line 89.
  2. +append against a non-array target key — what happens when the existing value is a scalar (e.g., deepMerge({ items: 5 }, { 'items+append': [1] }))? The current suite leaves this branch untested.
🧪 Suggested additional test cases
+   test('returns source when target is null', () => {
+     expect(deepMerge(null, { a: 1 })).toEqual({ a: 1 });
+   });
+
+   test('+append: replaces scalar target with appended array', () => {
+     // Validate the defined behavior when target key is not an array
+     const result = deepMerge({ items: 5 }, { 'items+append': [1, 2] });
+     expect(result.items).toEqual([1, 2]); // adjust expectation to match actual implementation
+   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/core/config/merge-utils.test.js` around lines 42 - 96, Add two unit
tests to the deepMerge suite to reach the promised 12: one that verifies
deepMerge(null, { a: 1 }) returns { a: 1 } (exercise the null target branch),
and one that verifies '+append' against a non-array target (e.g., deepMerge({
items: 5 }, { 'items+append': [1] }) results in items being [1] or concatenated
appropriately per impl) to cover the scalar target +append branch; place them
alongside the existing tests in the describe('deepMerge') block and use the same
expect style as other tests.


describe('mergeAll', () => {
test('merges multiple layers in order', () => {
const result = mergeAll(
{ a: 1, b: 2 },
{ b: 3, c: 4 },
{ c: 5 },
);
expect(result).toEqual({ a: 1, b: 3, c: 5 });
});

test('skips null and non-object layers', () => {
const result = mergeAll({ a: 1 }, null, undefined, 'string', { b: 2 });
expect(result).toEqual({ a: 1, b: 2 });
});

test('returns empty object when no layers', () => {
expect(mergeAll()).toEqual({});
});

test('deep merges across layers', () => {
const result = mergeAll(
{ db: { host: 'localhost' } },
{ db: { port: 5432 } },
{ db: { host: 'remote' } },
);
expect(result.db).toEqual({ host: 'remote', port: 5432 });
});
});
Comment on lines +98 to +125
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

mergeAll block has 4 tests, not the 6 promised by the PR and issue #270 — and the explicitly scoped single-layer case is missing.

Issue #270 calls out "correct handling of zero/single layers" as a required scenario. Zero layers is covered at line 113; the single-layer case is absent. The overall count is 4 vs the claimed 6, leaving two gaps:

  1. Single layermergeAll({ a: 1 }) should return { a: 1 } unchanged.
  2. All-invalid layersmergeAll(null, undefined, 'string') should return {}, a distinct path from the mixed-valid test at line 108.
🧪 Suggested additional test cases
+   test('returns single layer unchanged', () => {
+     expect(mergeAll({ a: 1, b: 2 })).toEqual({ a: 1, b: 2 });
+   });
+
+   test('returns empty object when all layers are invalid', () => {
+     expect(mergeAll(null, undefined, 'string')).toEqual({});
+   });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
describe('mergeAll', () => {
test('merges multiple layers in order', () => {
const result = mergeAll(
{ a: 1, b: 2 },
{ b: 3, c: 4 },
{ c: 5 },
);
expect(result).toEqual({ a: 1, b: 3, c: 5 });
});
test('skips null and non-object layers', () => {
const result = mergeAll({ a: 1 }, null, undefined, 'string', { b: 2 });
expect(result).toEqual({ a: 1, b: 2 });
});
test('returns empty object when no layers', () => {
expect(mergeAll()).toEqual({});
});
test('deep merges across layers', () => {
const result = mergeAll(
{ db: { host: 'localhost' } },
{ db: { port: 5432 } },
{ db: { host: 'remote' } },
);
expect(result.db).toEqual({ host: 'remote', port: 5432 });
});
});
describe('mergeAll', () => {
test('merges multiple layers in order', () => {
const result = mergeAll(
{ a: 1, b: 2 },
{ b: 3, c: 4 },
{ c: 5 },
);
expect(result).toEqual({ a: 1, b: 3, c: 5 });
});
test('skips null and non-object layers', () => {
const result = mergeAll({ a: 1 }, null, undefined, 'string', { b: 2 });
expect(result).toEqual({ a: 1, b: 2 });
});
test('returns empty object when no layers', () => {
expect(mergeAll()).toEqual({});
});
test('deep merges across layers', () => {
const result = mergeAll(
{ db: { host: 'localhost' } },
{ db: { port: 5432 } },
{ db: { host: 'remote' } },
);
expect(result.db).toEqual({ host: 'remote', port: 5432 });
});
test('returns single layer unchanged', () => {
expect(mergeAll({ a: 1, b: 2 })).toEqual({ a: 1, b: 2 });
});
test('returns empty object when all layers are invalid', () => {
expect(mergeAll(null, undefined, 'string')).toEqual({});
});
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/core/config/merge-utils.test.js` around lines 98 - 125, The tests for
mergeAll are missing the single-layer and all-invalid-layers cases: add a test
that calls mergeAll({ a: 1 }) and asserts it returns { a: 1 } (unchanged) and
another test that calls mergeAll(null, undefined, 'string') and asserts it
returns {} (empty object); place both tests inside the existing
describe('mergeAll') block next to the other cases so they cover the
zero/single/all-invalid layer scenarios for the mergeAll function.

});