The probe returned `error: undefined` on a dimension match. `undefined` is not a JSON value -- JSON.stringify silently drops it from an object -- so the shape looked harmless and was not: the harness enforces the declared output schema against the canonical value before materialising it, and an explicit undefined property fails that check. A perfectly healthy status call could not be returned at all. Fixed at the boundary rather than at each site. Every return now funnels through `toJsonValue`, which drops undefined properties exactly as JSON.stringify would, maps non-finite numbers to null (same reasoning: JSON.stringify(NaN) is "null"), turns an undefined array slot into null rather than shifting indices, and refuses a circular value instead of recursing forever. `update` was carrying `errorCode: undefined` and `ensureSlopo` a `detail: undefined` on the same path, so a per-site fix would have left two more of these waiting. The probe also now omits `error` rather than assigning undefined, since it is a plausible thing for another consumer to read. Four regression tests, and they were checked against a negative control: with the sanitiser reverted and the undefined restored, exactly the "probe SUCCEEDS" test fails with "does not survive a JSON round trip" and the others still pass, which is what makes it a test of this bug rather than of the JSON round trip in general.
60 lines
2.0 KiB
JavaScript
60 lines
2.0 KiB
JavaScript
import assert from 'node:assert/strict'
|
|
import { test } from 'node:test'
|
|
|
|
import { toJsonValue } from '../lib/json.js'
|
|
|
|
test('drops undefined properties, which is the shape that broke status', () => {
|
|
const value = toJsonValue({ ok: true, error: undefined, url: 'http://x' })
|
|
assert.deepEqual(value, { ok: true, url: 'http://x' })
|
|
assert.equal(Object.hasOwn(value, 'error'), false)
|
|
})
|
|
|
|
test('the result survives a JSON round trip unchanged', () => {
|
|
const value = toJsonValue({
|
|
nested: { keep: 1, drop: undefined, deeper: { alsoDrop: undefined } },
|
|
list: [1, 'two', { three: true, four: undefined }],
|
|
})
|
|
assert.deepEqual(value, JSON.parse(JSON.stringify(value)))
|
|
})
|
|
|
|
test('an undefined array slot becomes null rather than shifting the indices', () => {
|
|
assert.deepEqual(toJsonValue([1, undefined, 3]), [1, null, 3])
|
|
})
|
|
|
|
test('non-finite numbers become null, exactly as JSON.stringify would', () => {
|
|
assert.deepEqual(toJsonValue({ a: Number.NaN, b: Infinity, c: -Infinity, d: 1.5 }), {
|
|
a: null,
|
|
b: null,
|
|
c: null,
|
|
d: 1.5,
|
|
})
|
|
})
|
|
|
|
test('functions, symbols and undefined vanish; bigints become strings', () => {
|
|
const value = toJsonValue({ fn: () => {}, sym: Symbol('x'), gone: undefined, big: 10n, keep: 'yes' })
|
|
assert.deepEqual(value, { big: '10', keep: 'yes' })
|
|
})
|
|
|
|
test('null and primitives pass through', () => {
|
|
assert.equal(toJsonValue(null), null)
|
|
assert.equal(toJsonValue('a'), 'a')
|
|
assert.equal(toJsonValue(0), 0)
|
|
assert.equal(toJsonValue(false), false)
|
|
})
|
|
|
|
test('a circular value is refused rather than recursed into', () => {
|
|
const circular = { name: 'loop' }
|
|
circular.self = circular
|
|
assert.throws(() => toJsonValue(circular), /circular/)
|
|
})
|
|
|
|
test('a repeated but non-circular reference is fine', () => {
|
|
const shared = { id: 1 }
|
|
assert.deepEqual(toJsonValue({ a: shared, b: shared }), { a: { id: 1 }, b: { id: 1 } })
|
|
})
|
|
|
|
test('an already-clean value is returned unchanged in value', () => {
|
|
const clean = { kind: 'foreground', ok: true, exitCode: 0, report: 'cluster 1:' }
|
|
assert.deepEqual(toJsonValue(clean), clean)
|
|
})
|