Skip to content

Commit 754d3ea

Browse files
DavertMikclaude
andcommitted
test(utils): add unit coverage for trace.js + safeStringify extensions + truncateString
49 new test cases across two files: - `test/unit/utils/trace_test.js` (new, 37 cases) — direct coverage of every export from `lib/utils/trace.js`: - `pickActingHelper` (all 3 paths) - `traceDirFor` (determinism, uniqueness, sanitization, missing args) - `snapshotDirFor` (uniqueness, embedded timestamp) - `artifactLinks` (each artifact, console-count override, storage counts, custom indent, empty input, ordering invariant) - `fileToUrl` / `artifactsToFileUrls` - `writeTraceMarkdown` (golden output, error block, optional commands, default `file`) - `captureSnapshot` (default options, full opt-out matrix, fullPage, formatHtml integration, ConsoleMessage normalization, Playwright grabStorageState path, Puppeteer/WebDriver fallback path, empty storage handled, missing helper methods, error swallowing, default prefix) - `test/unit/circular_reference_test.js` (extended, 12 new cases): - `safeStringify` Function/BigInt/Symbol/Error coercions including nested mixed types (BigInt was a real bug; JSON.stringify(BigInt) used to throw and trigger the legacy fallback path) - `truncateString` under / equal / over maxBytes, non-string coercion, empty string Total unit suite: 121 passing (was 72). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent eaca22d commit 754d3ea

2 files changed

Lines changed: 516 additions & 1 deletion

File tree

test/unit/circular_reference_test.js

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { expect } from 'chai'
2-
import { safeStringify } from '../../lib/utils.js'
2+
import { safeStringify, truncateString } from '../../lib/utils.js'
33
import { createTest } from '../../lib/mocha/test.js'
44
import { createSuite } from '../../lib/mocha/suite.js'
55
import MochaSuite from 'mocha/lib/suite.js'
@@ -160,6 +160,103 @@ describe('Circular Reference Handling', function () {
160160
})
161161
})
162162

163+
describe('safeStringify type coercions', function () {
164+
it('coerces functions to "[Function: name]"', function () {
165+
const obj = { fn: function namedFn() {}, anon: () => {} }
166+
const parsed = JSON.parse(safeStringify(obj))
167+
expect(parsed.fn).to.equal('[Function: namedFn]')
168+
expect(parsed.anon).to.match(/^\[Function: .*\]$/)
169+
})
170+
171+
it('coerces BigInt values (which JSON.stringify cannot natively handle)', function () {
172+
const result = safeStringify({ big: 12345678901234567890n })
173+
expect(result).to.contain('12345678901234567890n')
174+
// Verify the legacy fallback path is NOT triggered
175+
expect(result).to.not.contain('Failed to serialize')
176+
})
177+
178+
it('coerces Symbol to its toString()', function () {
179+
const obj = { s: Symbol('marker') }
180+
const parsed = JSON.parse(safeStringify(obj))
181+
expect(parsed.s).to.equal('Symbol(marker)')
182+
})
183+
184+
it('coerces Error to {name, message, stack}', function () {
185+
const obj = { err: new Error('boom') }
186+
const parsed = JSON.parse(safeStringify(obj))
187+
expect(parsed.err).to.have.property('name', 'Error')
188+
expect(parsed.err).to.have.property('message', 'boom')
189+
expect(parsed.err).to.have.property('stack')
190+
expect(parsed.err.stack).to.be.a('string').and.include('boom')
191+
})
192+
193+
it('coerces an Error at the top level', function () {
194+
const result = safeStringify(new TypeError('bad arg'))
195+
const parsed = JSON.parse(result)
196+
expect(parsed.name).to.equal('TypeError')
197+
expect(parsed.message).to.equal('bad arg')
198+
})
199+
200+
it('handles nested mixed types together', function () {
201+
const obj = {
202+
regular: 1,
203+
fn: function n() {},
204+
big: 1n,
205+
sym: Symbol('s'),
206+
err: new RangeError('range'),
207+
nested: { again: { fn: () => {} } },
208+
}
209+
const parsed = JSON.parse(safeStringify(obj))
210+
expect(parsed.regular).to.equal(1)
211+
expect(parsed.fn).to.equal('[Function: n]')
212+
expect(parsed.big).to.equal('1n')
213+
expect(parsed.sym).to.equal('Symbol(s)')
214+
expect(parsed.err.name).to.equal('RangeError')
215+
expect(parsed.nested.again.fn).to.match(/^\[Function:/)
216+
})
217+
218+
it('preserves indentation when space arg is provided', function () {
219+
const result = safeStringify({ a: 1 }, [], 2)
220+
expect(result).to.contain('\n "a": 1')
221+
})
222+
})
223+
224+
describe('truncateString', function () {
225+
it('returns input as-is when under maxBytes', function () {
226+
const result = truncateString('hello', 100)
227+
expect(result.value).to.equal('hello')
228+
expect(result.truncated).to.be.false
229+
expect(result.fullLength).to.equal(5)
230+
})
231+
232+
it('returns input as-is when exactly equal to maxBytes', function () {
233+
const result = truncateString('xxxxx', 5)
234+
expect(result.truncated).to.be.false
235+
expect(result.value).to.equal('xxxxx')
236+
})
237+
238+
it('truncates and appends marker when over maxBytes', function () {
239+
const result = truncateString('x'.repeat(50), 10)
240+
expect(result.truncated).to.be.true
241+
expect(result.fullLength).to.equal(50)
242+
expect(result.value.startsWith('xxxxxxxxxx')).to.be.true
243+
expect(result.value).to.contain('truncated 40 more chars')
244+
})
245+
246+
it('coerces non-string inputs via String()', function () {
247+
const result = truncateString(12345, 100)
248+
expect(result.value).to.equal('12345')
249+
expect(result.truncated).to.be.false
250+
})
251+
252+
it('handles empty string', function () {
253+
const result = truncateString('', 10)
254+
expect(result.value).to.equal('')
255+
expect(result.truncated).to.be.false
256+
expect(result.fullLength).to.equal(0)
257+
})
258+
})
259+
163260
describe('Integration with existing serialization', function () {
164261
it('should work with serializeTest function', function () {
165262
const test = createTest('Integration Test', () => {})

0 commit comments

Comments
 (0)