Skip to content

fix: isJson assertion fails after res.setBody() with object in node-vm#7191

Merged
bijin-bruno merged 3 commits intousebruno:mainfrom
lohit-bruno:isjson_assertion_fix
Feb 18, 2026
Merged

fix: isJson assertion fails after res.setBody() with object in node-vm#7191
bijin-bruno merged 3 commits intousebruno:mainfrom
lohit-bruno:isjson_assertion_fix

Conversation

@lohit-bruno
Copy link
Collaborator

@lohit-bruno lohit-bruno commented Feb 18, 2026

Description

fix: isJson assertion fails after res.setBody() with object in node-vm

Contribution Checklist:

  • I've used AI significantly to create this pull request
  • The pull request only addresses one issue or adds one feature.
  • The pull request does not introduce any breaking changes
  • I have added screenshots or gifs to help explain the change if applicable.
  • I have read the contribution guidelines.
  • Create an issue and link to the pull request.

Note: Keeping the PR small and focused helps make it easier to review and merge. If you have multiple changes you want to make, please consider submitting them as separate pull requests.

Publishing to New Package Managers

Please see here for more information.

Summary by CodeRabbit

  • New Features

    • Added a .to.be.json assertion to validate response bodies as JSON objects, supporting nested structures and various execution contexts
  • Bug Fixes

    • Improved JSON-object detection so the assertion reliably recognizes plain objects across VM or cross-realm scenarios
  • Tests

    • Added comprehensive test coverage validating the JSON assertion across plain objects, nested structures, arrays, strings, null values, and cross-VM scenarios

Objects created inside Node's vm.createContext() have a different Object
constructor than the host realm. When res.setBody() is called with a JS
object from a script, _.cloneDeep preserves the cross-realm prototype,
causing obj.constructor === Object to fail in the isJson assertion.

Replace with Object.prototype.toString.call() which is cross-realm safe.
The bundled chai in QuickJS only exposes { expect, assert } via
requireObject — no Assertion class. Access the prototype through
Object.getPrototypeOf(expect(null)) and use Object.defineProperty
to register the json property directly.
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 18, 2026

No actionable comments were generated in the recent review. 🎉


Walkthrough

Replaces constructor-based plain-object detection with a cross-realm-safe toString check, adds a custom Chai "json" assertion, and expands tests to validate isJson across plain, nested, array, null, and cross-VM response mutations.

Changes

Cohort / File(s) Summary
Runtime JSON detection
packages/bruno-js/src/runtime/assert-runtime.js
Replaces obj.constructor === Object with Object.prototype.toString.call(obj) === '[object Object]' for cross-realm/plain-object detection; adds explanatory comments.
Chai shim (quickjs)
packages/bruno-js/src/sandbox/quickjs/shims/test.js
Adds a custom json assertion to the Chai expect prototype that validates plain objects (non-null, not arrays, toString check) and provides success/failure messages.
Runtime tests
packages/bruno-js/tests/runtime.spec.js
Adds assert-runtime test suite, helpers, and multiple tests covering isJson for plain objects, nested objects, arrays, strings, null, and cross-realm VM-created objects including setBody mutation inside VM.
Integration test script
packages/bruno-tests/collection/scripting/api/res/setBody/isJson after setBody.bru
New BRUNO test that posts JSON, mutates response body in post-response script, asserts response is JSON and verifies fields.

Sequence Diagram(s)

sequenceDiagram
  participant TestRunner as Test Runner
  participant VM as QuickJS/ScriptRuntime (VM)
  participant Response as Response (res)
  participant Assert as AssertRuntime

  TestRunner->>VM: execute script (res.setBody / post-response)
  VM->>Response: mutate body (setBody -> plain object)
  Note right of Response: Body may be cross-realm object
  TestRunner->>Assert: run isJson assertions with Response
  Assert->>Response: inspect body using Object.prototype.toString.call(obj)
  alt plain object
    Assert-->>TestRunner: pass
  else not plain object
    Assert-->>TestRunner: fail with message
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested reviewers

  • helloanoop
  • naman-bruno
  • bijin-bruno

Poem

Across contexts where objects roam,
toString maps each object's home.
Chai learns "json" to prove what's true,
Tests jump VMs to check the view.
A small change keeps assertions bright—🎉

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main fix: addressing an isJson assertion failure that occurs after res.setBody() is called with objects created in Node's vm context, which is the central issue addressed across all changes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
packages/bruno-js/tests/runtime.spec.js (1)

278-281: No test covers the QuickJS runtime path for isJson.

The new json getter in the QuickJS shim (test.js) is the other half of this fix, but every test in this suite uses the default 'nodevm' runtime. A failure in the IIFE (e.g., wrong prototype lookup, Object.defineProperty descriptor issue) would go undetected here. Since runAssertions already accepts runtime, it's a one-liner to add a parallel case.

       it('should pass for a plain object', () => {
         const results = runAssertions(
           [{ name: 'res.body', value: 'isJson', enabled: true }],
           makeResponse({ id: 1, name: 'test' })
         );
         expect(results[0].status).toBe('pass');
       });
+
+      it('should pass for a plain object (quickjs runtime)', () => {
+        const results = runAssertions(
+          [{ name: 'res.body', value: 'isJson', enabled: true }],
+          makeResponse({ id: 1, name: 'test' }),
+          'quickjs'
+        );
+        expect(results[0].status).toBe('pass');
+      });
+
+      it('should fail for an array (quickjs runtime)', () => {
+        const results = runAssertions(
+          [{ name: 'res.body', value: 'isJson', enabled: true }],
+          makeResponse([1, 2, 3]),
+          'quickjs'
+        );
+        expect(results[0].status).toBe('fail');
+      });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/bruno-js/tests/runtime.spec.js` around lines 278 - 281, Add a test
invocation that exercises the QuickJS runtime path by calling the existing
runAssertions helper with runtime set to 'quickjs' (e.g., new AssertRuntime({
runtime: 'quickjs' }) via runAssertions(assertions, response, 'quickjs')) so the
QuickJS shim's json getter and IIFE code path (including prototype lookup and
Object.defineProperty behavior) are executed; modify the relevant test block to
run the same assertions twice — once with the default 'nodevm' and once with
'quickjs' — to ensure isJson behavior is covered.
packages/bruno-js/src/sandbox/quickjs/shims/test.js (1)

70-75: Missing return this; breaks post-assertion chaining.

Chai's addProperty wrapper automatically returns this when the callback returns undefined — raw Object.defineProperty skips that wrapper, so any user who writes expect(obj).to.be.json.and.have.property(...) will get a TypeError in the QuickJS sandbox. Current usages are all terminal so it doesn't bite today, but it creates an inconsistency with the node-vm path where chai.Assertion.addProperty is used (which is chainable by design).

♻️ Proposed fix
         get: function () {
           var obj = this._obj;
           var isJson = typeof obj === 'object' && obj !== null && !Array.isArray(obj) &&
             Object.prototype.toString.call(obj) === '[object Object]';
           this.assert(isJson, 'expected #{this} to be JSON', 'expected #{this} not to be JSON');
+          return this;
         },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/bruno-js/src/sandbox/quickjs/shims/test.js` around lines 70 - 75,
The getter defined in the Object.defineProperty block (the get: function() that
reads this._obj and calls this.assert) must return this to preserve Chai-style
chaining; update the get function for the JSON assertion to append a `return
this;` after the `this.assert(...)` call so `expect(obj).to.be.json.and...`
continues to work like the chai.Assertion.addProperty path.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/bruno-js/src/runtime/assert-runtime.js`:
- Around line 19-21: Complete the truncated comment above the isJson check to
explain the tradeoff: state that using Object.prototype.toString.call(obj) ===
'[object Object]' is more permissive than checking obj.constructor === Object
because it will treat plain objects and also instances of custom classes
(without a custom Symbol.toStringTag) as JSON-like objects (e.g., new class Foo
{} will return true), and note that this diverges from the previous behavior
which excluded such class instances; reference the isJson variable and the
Object.prototype.toString.call check so future readers understand the rationale
and implications.

---

Nitpick comments:
In `@packages/bruno-js/src/sandbox/quickjs/shims/test.js`:
- Around line 70-75: The getter defined in the Object.defineProperty block (the
get: function() that reads this._obj and calls this.assert) must return this to
preserve Chai-style chaining; update the get function for the JSON assertion to
append a `return this;` after the `this.assert(...)` call so
`expect(obj).to.be.json.and...` continues to work like the
chai.Assertion.addProperty path.

In `@packages/bruno-js/tests/runtime.spec.js`:
- Around line 278-281: Add a test invocation that exercises the QuickJS runtime
path by calling the existing runAssertions helper with runtime set to 'quickjs'
(e.g., new AssertRuntime({ runtime: 'quickjs' }) via runAssertions(assertions,
response, 'quickjs')) so the QuickJS shim's json getter and IIFE code path
(including prototype lookup and Object.defineProperty behavior) are executed;
modify the relevant test block to run the same assertions twice — once with the
default 'nodevm' and once with 'quickjs' — to ensure isJson behavior is covered.

Comment on lines +19 to +21
// Note: toString check is more permissive than constructor check — custom class instances
const isJson = typeof obj === 'object' && obj !== null && !Array.isArray(obj)
&& Object.prototype.toString.call(obj) === '[object Object]';
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Comment on Line 19 is truncated — complete the thought.

The comment ends mid-sentence (— custom class instances) without saying what the implication is. The actual behavioral change is that class instances without a custom [Symbol.toStringTag] will now also pass isJson (e.g., new class Foo {} now returns true where the old constructor === Object check would have returned false). Worth capturing so future readers know the tradeoff.

📝 Suggested completion
-    // Note: toString check is more permissive than constructor check — custom class instances
+    // Note: toString check is more permissive than constructor check — custom class instances
+    // without a [Symbol.toStringTag] also return '[object Object]' and will pass. This is
+    // acceptable since response bodies are never expected to be class instances.
     const isJson = typeof obj === 'object' && obj !== null && !Array.isArray(obj)
       && Object.prototype.toString.call(obj) === '[object Object]';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/bruno-js/src/runtime/assert-runtime.js` around lines 19 - 21,
Complete the truncated comment above the isJson check to explain the tradeoff:
state that using Object.prototype.toString.call(obj) === '[object Object]' is
more permissive than checking obj.constructor === Object because it will treat
plain objects and also instances of custom classes (without a custom
Symbol.toStringTag) as JSON-like objects (e.g., new class Foo {} will return
true), and note that this diverges from the previous behavior which excluded
such class instances; reference the isJson variable and the
Object.prototype.toString.call check so future readers understand the rationale
and implications.

The QuickJS isJson property getter was missing `return this`, preventing
chai assertion chaining (e.g. expect(body).to.be.json.and...).
@bijin-bruno bijin-bruno merged commit 479fc16 into usebruno:main Feb 18, 2026
8 checks passed
bijin-bruno pushed a commit that referenced this pull request Feb 18, 2026
…ode-vm` (#7191)

* fix: isJson assertion fails after res.setBody() with object in node-vm

Objects created inside Node's vm.createContext() have a different Object
constructor than the host realm. When res.setBody() is called with a JS
object from a script, _.cloneDeep preserves the cross-realm prototype,
causing obj.constructor === Object to fail in the isJson assertion.

Replace with Object.prototype.toString.call() which is cross-realm safe.

* fix: register isJson chai assertion in QuickJS test runtime

The bundled chai in QuickJS only exposes { expect, assert } via
requireObject — no Assertion class. Access the prototype through
Object.getPrototypeOf(expect(null)) and use Object.defineProperty
to register the json property directly.

* fix: enable assertion chaining on isJson in QuickJS runtime

The QuickJS isJson property getter was missing `return this`, preventing
chai assertion chaining (e.g. expect(body).to.be.json.and...).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants