From 84bebe13196beb402babb9926bba72ad175ba007 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 1 Sep 2025 06:20:20 +0000 Subject: [PATCH 1/2] Initial plan From 9139aa29c5879a6f180baab0bc47a2741afe3717 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 1 Sep 2025 06:35:12 +0000 Subject: [PATCH 2/2] Changes before error encountered Co-authored-by: kobenguyent <7845001+kobenguyent@users.noreply.github.com> --- lib/utils.js | 50 ++++++- test/unit/circular_reference_test.js | 186 +++++++++++++++++++++++++++ 2 files changed, 234 insertions(+), 2 deletions(-) create mode 100644 test/unit/circular_reference_test.js diff --git a/lib/utils.js b/lib/utils.js index df4235761..3c9ad024e 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -203,7 +203,7 @@ module.exports.test = { if (fs.existsSync(dataFile)) { break } - + // Use Node.js child_process.spawnSync with platform-specific sleep commands // This avoids busy waiting and allows other processes to run try { @@ -221,7 +221,7 @@ module.exports.test = { // No-op loop - much lighter than previous approaches } } - + // Exponential backoff: gradually increase polling interval to reduce resource usage pollInterval = Math.min(pollInterval * 1.2, maxPollInterval) } @@ -576,6 +576,52 @@ module.exports.humanizeString = function (string) { return _result.join(' ').trim() } +/** + * Creates a circular-safe replacer function for JSON.stringify + * @param {string[]} keysToSkip - Keys to skip during serialization to break circular references + * @returns {Function} Replacer function for JSON.stringify + */ +function createCircularSafeReplacer(keysToSkip = []) { + const seen = new WeakSet() + const defaultSkipKeys = ['parent', 'tests', 'suite', 'root', 'runner', 'ctx'] + const skipKeys = new Set([...defaultSkipKeys, ...keysToSkip]) + + return function (key, value) { + // Skip specific keys that commonly cause circular references + if (key && skipKeys.has(key)) { + return undefined + } + + if (value === null || typeof value !== 'object') { + return value + } + + // Handle circular references + if (seen.has(value)) { + return `[Circular Reference to ${value.constructor?.name || 'Object'}]` + } + + seen.add(value) + return value + } +} + +/** + * Safely stringify an object, handling circular references + * @param {any} obj - Object to stringify + * @param {string[]} keysToSkip - Additional keys to skip during serialization + * @param {number} space - Number of spaces for indentation (default: 0) + * @returns {string} JSON string representation + */ +module.exports.safeStringify = function (obj, keysToSkip = [], space = 0) { + try { + return JSON.stringify(obj, createCircularSafeReplacer(keysToSkip), space) + } catch (error) { + // Fallback for any remaining edge cases + return JSON.stringify({ error: `Failed to serialize: ${error.message}` }, null, space) + } +} + module.exports.serializeError = function (error) { if (error) { const { stack, uncaught, message, actual, expected } = error diff --git a/test/unit/circular_reference_test.js b/test/unit/circular_reference_test.js new file mode 100644 index 000000000..e8d722cfc --- /dev/null +++ b/test/unit/circular_reference_test.js @@ -0,0 +1,186 @@ +const { expect } = require('chai') +const { safeStringify } = require('../../lib/utils') +const { createTest } = require('../../lib/mocha/test') +const { createSuite } = require('../../lib/mocha/suite') +const MochaSuite = require('mocha/lib/suite') + +describe('Circular Reference Handling', function () { + describe('safeStringify utility', function () { + it('should handle objects without circular references normally', function () { + const obj = { + name: 'test', + value: 42, + nested: { prop: 'value' }, + } + + const result = safeStringify(obj) + const parsed = JSON.parse(result) + + expect(parsed.name).to.equal('test') + expect(parsed.value).to.equal(42) + expect(parsed.nested.prop).to.equal('value') + }) + + it('should handle simple circular references', function () { + const obj = { name: 'test' } + obj.self = obj + + const result = safeStringify(obj) + expect(result).to.not.throw + expect(result).to.contain('test') + expect(result).to.contain('Circular Reference') + }) + + it('should skip default problematic keys', function () { + const obj = { + name: 'test', + parent: { title: 'parent' }, + tests: [{ title: 'test1' }], + suite: { title: 'suite' }, + root: { title: 'root' }, + ctx: { title: 'context' }, + } + + const result = safeStringify(obj) + const parsed = JSON.parse(result) + + expect(parsed.name).to.equal('test') + expect(parsed.parent).to.be.undefined + expect(parsed.tests).to.be.undefined + expect(parsed.suite).to.be.undefined + expect(parsed.root).to.be.undefined + expect(parsed.ctx).to.be.undefined + }) + + it('should skip custom keys when specified', function () { + const obj = { + name: 'test', + customKey: 'should be skipped', + keepThis: 'should be kept', + } + + const result = safeStringify(obj, ['customKey']) + const parsed = JSON.parse(result) + + expect(parsed.name).to.equal('test') + expect(parsed.customKey).to.be.undefined + expect(parsed.keepThis).to.equal('should be kept') + }) + + it('should handle complex nested circular references', function () { + const parent = { name: 'parent', children: [] } + const child1 = { name: 'child1', parent: parent } + const child2 = { name: 'child2', parent: parent } + + parent.children.push(child1, child2) + + const result = safeStringify(parent) + expect(result).to.not.throw + expect(result).to.contain('parent') + expect(result).to.contain('children') + }) + }) + + describe('CodeceptJS objects circular reference handling', function () { + let rootSuite, suite, test + + beforeEach(function () { + rootSuite = new MochaSuite('', null, true) + suite = createSuite(rootSuite, 'Test Suite') + test = createTest('Test 1', () => {}) + test.addToSuite(suite) + }) + + it('should handle Test objects with circular references', function () { + // Before fix: JSON.stringify(test) would throw + const result = safeStringify(test) + expect(result).to.not.throw + + const parsed = JSON.parse(result) + expect(parsed.title).to.equal('Test 1') + expect(parsed.tags).to.be.an('array') + expect(parsed.codeceptjs).to.be.true + // parent should be skipped to break circular reference + expect(parsed.parent).to.be.undefined + }) + + it('should handle Suite objects with circular references', function () { + // Before fix: JSON.stringify(suite) would throw + const result = safeStringify(suite) + expect(result).to.not.throw + + const parsed = JSON.parse(result) + expect(parsed.title).to.equal('Test Suite') + expect(parsed.codeceptjs).to.be.true + // tests should be skipped to break circular reference + expect(parsed.tests).to.be.undefined + }) + + it('should preserve essential Test properties while avoiding circular references', function () { + test.opts = { timeout: 5000 } + test.tags = ['@smoke'] + test.meta = { feature: 'login' } + test.notes = [{ type: 'info', text: 'test note' }] + test.artifacts = ['screenshot.png'] + + const result = safeStringify(test) + const parsed = JSON.parse(result) + + expect(parsed.opts).to.deep.equal({ timeout: 5000 }) + expect(parsed.tags).to.deep.equal(['@smoke']) + expect(parsed.meta).to.deep.equal({ feature: 'login' }) + expect(parsed.notes).to.deep.equal([{ type: 'info', text: 'test note' }]) + expect(parsed.artifacts).to.deep.equal(['screenshot.png']) + expect(parsed.parent).to.be.undefined // Circular reference broken + }) + + it('should preserve essential Suite properties while avoiding circular references', function () { + suite.opts = { retries: 3 } + suite.tags = ['@feature'] + + const result = safeStringify(suite) + const parsed = JSON.parse(result) + + expect(parsed.opts).to.deep.equal({ retries: 3 }) + expect(parsed.tags).to.deep.equal(['@feature']) + expect(parsed.tests).to.be.undefined // Circular reference broken + }) + + it('should handle deeply nested objects with multiple circular references', function () { + // Create a more complex structure + const childSuite = createSuite(suite, 'Child Suite') + const childTest = createTest('Child Test', () => {}) + childTest.addToSuite(childSuite) + + const result = safeStringify(suite) + expect(result).to.not.throw + + const parsed = JSON.parse(result) + expect(parsed.title).to.equal('Test Suite') + }) + }) + + describe('Integration with existing serialization', function () { + it('should work with serializeTest function', function () { + const test = createTest('Integration Test', () => {}) + const suite = createSuite(new MochaSuite('', null, true), 'Integration Suite') + test.addToSuite(suite) + + // The existing serializeTest should continue to work + const serialized = test.simplify() + expect(serialized).to.be.an('object') + expect(serialized.title).to.equal('Integration Test') + expect(serialized.parent).to.be.an('object') + expect(serialized.parent.title).to.equal('Integration Suite') + }) + + it('should work with serializeSuite function', function () { + const suite = createSuite(new MochaSuite('', null, true), 'Integration Suite') + + // The existing serializeSuite should continue to work + const serialized = suite.simplify() + expect(serialized).to.be.an('object') + expect(serialized.title).to.equal('Integration Suite') + }) + }) +})