fix: isJson assertion fails after res.setBody() with object in node-vm#7191
fix: isJson assertion fails after res.setBody() with object in node-vm#7191bijin-bruno merged 3 commits intousebruno:mainfrom
isJson assertion fails after res.setBody() with object in node-vm#7191Conversation
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.
|
No actionable comments were generated in the recent review. 🎉 WalkthroughReplaces 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
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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
packages/bruno-js/tests/runtime.spec.js (1)
278-281: No test covers the QuickJS runtime path forisJson.The new
jsongetter 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.definePropertydescriptor issue) would go undetected here. SincerunAssertionsalready acceptsruntime, 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: Missingreturn this;breaks post-assertion chaining.Chai's
addPropertywrapper automatically returnsthiswhen the callback returnsundefined— rawObject.definePropertyskips that wrapper, so any user who writesexpect(obj).to.be.json.and.have.property(...)will get aTypeErrorin the QuickJS sandbox. Current usages are all terminal so it doesn't bite today, but it creates an inconsistency with the node-vm path wherechai.Assertion.addPropertyis 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.
| // 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]'; |
There was a problem hiding this comment.
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...).
…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...).
Description
fix:
isJsonassertion fails afterres.setBody()with object innode-vmContribution Checklist:
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
.to.be.jsonassertion to validate response bodies as JSON objects, supporting nested structures and various execution contextsBug Fixes
Tests