Skip to content

assert: surface prototype mismatch in deepStrictEqual diff - #66501

Open
vaputa wants to merge 1 commit into
nodejs:mainfrom
vaputa:fix/deepstricteq-prototype-mismatch
Open

vaputa wants to merge 1 commit into
nodejs:mainfrom
vaputa:fix/deepstricteq-prototype-mismatch

Conversation

@vaputa

@vaputa vaputa commented Oct 4, 2026 •

Copy link
Copy Markdown
Checklist
  • commit message follows the commit message guidelines (assert: …, includes Assisted-by trailer per the AI guidelines)
  • tests included (test/parallel/test-assert-prototype-mismatch.js + 4 message snapshot updates)
  • documentation updated (doc/api/assert.md)
  • make -j4 test full run — targeted assert family (14 parallel tests + 4 message snapshots + new regression = 18/18) passes locally; relying on CI for the full suite
Description of change

assert.deepStrictEqual() hides prototype mismatches whenever both sides inspect identically. The reported scenario:

assert.deepStrictEqual(new ExtendedArray('hello'), ['hello'])

only hints at the prototype via the class-name prefix, and the worse hidden case loses the failure reason entirely:

assert.deepStrictEqual(new (class {})(), {})
// AssertionError [ERR_ASSERTION]: Values have same structure but are not reference-equal

This adds an explicit diagnostic line to the deep-diff message when the top-level prototypes differ:

  Values have same structure but are not reference-equal
+ Object prototypes differ: (anonymous) !== Object

Design notes, following the reporting/diagnostics direction discussed in #50397 (previously explored in #61716 and #62944) rather than reconstructing prototypes via util.inspect:

  • diagnostic only — prototype reads and hint construction are exception-guarded, so a throwing Proxy getPrototypeOf trap still produces ERR_ASSERTION with stackTraceLimit untouched
  • ctor.name is read once, inside the guard
  • respects skipPrototype: true (no hint when prototype comparison is opted out)
  • scope is top-level prototype differences; nested mismatches are unchanged and the commit message says so

Fixes #50397

AI use disclosure

This change was developed with the help of an AI coding agent (ZCode, powered by GLM — anonymized in the commit trailer per the AI guidelines' naming recommendation). Personal verification performed by the contributor: the diff was reviewed line by line, the targeted assert test family (18/18 including the new regression tests) was run locally on a from-source build, an independent second AI review pass (Codex) was run to audit the patch before submission, and its findings (exception-guard for Proxy traps, single name read, skipPrototype handling) were applied and re-verified. The contributor takes full responsibility for this change and will engage with review feedback personally.

@nodejs-github-bot nodejs-github-bot added assert Issues and PRs related to the assert subsystem. needs-ci PRs that need a full CI run. labels Oct 4, 2026
@github-actions

github-actions Bot commented Oct 4, 2026

Copy link
Copy Markdown
Contributor

Welcome to Node.js, and thank you for your first contribution!

Before review, please take a moment to read:

Please make sure every commit is signed off. For a first pull request, GitHub Actions require collaborator approval and Jenkins CI must be started by a collaborator or triager, so an initial wait is normal.

@vaputa
vaputa force-pushed the fix/deepstricteq-prototype-mismatch branch from 19d8829 to dd7dfd4 Compare October 4, 2026 06:16
@MikeMcC399

Copy link
Copy Markdown
Contributor

This PR is failing linting tests. See the comments in the tests for more detail.

See also Pull requests > Step 6: Test with further details in the linked document section BUILDING > Running tests.

To run the linter, use make lint / vcbuild lint. It will lint JavaScript, C++, and Markdown files.

@codecov

codecov Bot commented Oct 4, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.17647% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.42%. Comparing base (2152942) to head (8178893).
⚠️ Report is 7 commits behind head on main.

Files with missing lines Patch % Lines
lib/internal/assert/assertion_error.js 90.32% 5 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #66501      +/-   ##
==========================================
- Coverage   90.43%   90.42%   -0.02%     
==========================================
  Files         790      790              
  Lines      275435   275491      +56     
  Branches    52811    52836      +25     
==========================================
+ Hits       249100   249119      +19     
- Misses      16730    16779      +49     
+ Partials     9605     9593      -12     
Files with missing lines Coverage Δ
lib/assert.js 98.23% <100.00%> (+<0.01%) ⬆️
lib/internal/assert/utils.js 97.31% <100.00%> (+0.04%) ⬆️
lib/internal/assert/assertion_error.js 95.20% <90.32%> (-0.77%) ⬇️

... and 31 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@BridgeAR BridgeAR left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This can only surface the top level in case a simple object is compared. As soon as the object deviates at a nested level, it would not be reported.

So while this works for simple cases, I do not think this is a good way of handling this. Instead, we might want to surface more information using util.inspect(). That way it would work as expected.

@vaputa
vaputa force-pushed the fix/deepstricteq-prototype-mismatch branch from dd7dfd4 to 41fb434 Compare October 4, 2026 16:18
deepStrictEqual requires both values to share the same prototype, but
the generated diff may not make that failure cause obvious: when both
values are inspected identically (e.g. an instance of an anonymous
class compared to a plain object) the structural diff shows no
difference at all, and when a subclass is involved the mismatch is
only visible implicitly through the inspected class-name prefix.

Append an explicit "Object prototypes differ: X !== Y" line to the
generated message when the operator is deepStrictEqual, both values
are objects, their top-level prototypes differ, and at least one of
the two prototypes is not a default prototype (Object.prototype,
Array.prototype or null), because those cases are already clearly
visible in the inspect output. The diagnostic covers the top-level
values only; prototype differences of nested objects are not
reported.

The diagnostic is derived defensively: the prototype reads and the
prototype names (derived from a single read of the constructor's
name) are wrapped in a try/catch so that exotic objects (e.g. a Proxy
with a throwing `getPrototypeOf` trap or a stateful `name` getter)
cannot replace the assertion error with a different exception nor
leave the global `Error.stackTraceLimit` modified; the hint is simply
omitted in that case. When the comparison is made with
`skipPrototype: true` (via `new assert.Assert({
  skipPrototype: true })`),
the hint is not added because the explicitly ignored difference is
not the cause of the failure.

Refs: nodejs#50397
Assisted-by: a closed-source coding agent
Signed-off-by: vaputa <2475834+vaputa@users.noreply.github.com>
@vaputa
vaputa force-pushed the fix/deepstricteq-prototype-mismatch branch from 41fb434 to 8178893 Compare October 4, 2026 16:29
@vaputa

vaputa commented Oct 4, 2026

Copy link
Copy Markdown
Author

This can only surface the top level in case a simple object is compared. As soon as the object deviates at a nested level, it would not be reported.

So while this works for simple cases, I do not think this is a good way of handling this. Instead, we might want to surface more information using util.inspect(). That way it would work as expected.

Thanks for the feedback. I agree that the top-level-only diagnostic leaves nested prototype mismatches unresolved.
Before reworking this, could you clarify what additional information you’d like util.inspect() to expose? Would you prefer improving its existing representation of objects with custom prototypes, or adding an opt-in mode used by assertion diffs?
I’ll include nested objects and array elements in the regression tests.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

assert Issues and PRs related to the assert subsystem. needs-ci PRs that need a full CI run.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

deepStrictEqual diff is unhelpful when prototype mismatches

4 participants