Deep Equal Symmetry Design
Problem
deepEqual is documented to require the same property set for objects, except that actual may have extra symbol properties. The current behavior violates that contract:
samsam.deepEqual({ a: 1, b: 2, c: 3 }, { a: 1, b: 2, foo: undefined }); // true
This regression was introduced by 3fbe355, fix #251: compare props on the prototype chain (#267).
Regression Source
The 2025 prototype-chain change made two relevant edits in lib/deep-equal.js:
- It replaced
Object.keys(...)withallEnumerableKeysInProtoChain(...)for string-key discovery. - It removed the explicit
hasOwnPropertyguard when walking expected keys.
That combination preserved key-count checking, but stopped verifying that the enumerable string-key sets actually match. If both objects expose the same number of enumerable string keys and the expectation value for a missing key is undefined, deepEqual can return true for different property sets.
Decision
Keep the prototype-chain behavior from 3fbe355, but restore symmetric comparison for enumerable string keys:
actualandexpectationmust expose the same enumerable string keys across the prototype chain.- Values for those keys must still compare with existing recursive
deepEquallogic. - The documented exception remains:
actualmay have extra symbol properties that are missing fromexpectation.
Options Considered
-
Restore pre-2025 own-property semantics. Rejected because it would regress the intended fix for objects such as browser
URLinstances with meaningful enumerable getters on the prototype chain. -
Keep asymmetry and treat object comparison as a subset check. Rejected because it makes
deepEqualargument-order dependent, contradicts the documentation, and overlaps with matcher semantics better handled elsewhere in Samsam/Sinon. -
Keep prototype-chain support and explicitly verify symmetric enumerable string-key membership. Chosen because it fixes the regression with the smallest semantic change and matches the published contract.
Implementation Shape
Update object comparison in lib/deep-equal.js so that, before recursive value comparison:
- enumerable string-key counts still match
- every enumerable string key found on
actualis also enumerable onexpectation - every enumerable string key found on
expectationis also enumerable onactual
The existing symbol handling should remain asymmetric and expectation-driven.
Testing
Add regression tests in lib/deep-equal.test.js for:
- different enumerable own string-key sets with equal counts
- different enumerable prototype-chain string-key sets with equal counts
- the documented symbol exception still allowing extra symbols on
actual
Run targeted mocha first, then full npm test, then npm run lint.
Risk
The main risk is accidentally tightening symbol handling and breaking the documented exception. Tests must cover that explicitly.