json.spec.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. import { runInNewContext } from 'node:vm'
  2. import { describe, expect, it } from 'vitest'
  3. import { isJsonValue, snapshotJsonValue, type JsonValue } from '@deepseek-ai/dsh-util-values'
  4. function objectWithForgedIntrinsicPrototype(revoked = false): Record<string, unknown> {
  5. const prototype = Object.create(null) as Record<string, unknown>
  6. const ForgedObject = function ForgedObject(): void {}
  7. Object.defineProperty(ForgedObject, 'name', { value: 'Object' })
  8. ForgedObject.prototype = prototype
  9. const constructor = revoked ? Proxy.revocable(ForgedObject, {}) : undefined
  10. if (constructor !== undefined) constructor.revoke()
  11. Object.defineProperty(prototype, 'constructor', { value: constructor?.proxy ?? ForgedObject })
  12. return Object.assign(Object.create(prototype) as Record<string, unknown>, { value: 1 })
  13. }
  14. describe('snapshotJsonValue', () => {
  15. it('copies the complete JSON scalar vocabulary and rejects unsupported scalars', () => {
  16. const unsupportedFunction = (): void => {}
  17. expect(snapshotJsonValue(null)).toBeNull()
  18. expect(snapshotJsonValue(true)).toBe(true)
  19. expect(snapshotJsonValue('text')).toBe('text')
  20. expect(snapshotJsonValue(1.25)).toBe(1.25)
  21. expect(snapshotJsonValue(-0)).toBeUndefined()
  22. expect(isJsonValue(-0)).toBe(false)
  23. expect(snapshotJsonValue(Number.NaN)).toBeUndefined()
  24. expect(snapshotJsonValue(Number.POSITIVE_INFINITY)).toBeUndefined()
  25. expect(snapshotJsonValue(1n)).toBeUndefined()
  26. expect(snapshotJsonValue(unsupportedFunction)).toBeUndefined()
  27. expect(snapshotJsonValue(Symbol('value'))).toBeUndefined()
  28. const unsupportedUndefined: unknown = undefined
  29. expect(snapshotJsonValue(unsupportedUndefined)).toBeUndefined()
  30. })
  31. it('recursively detaches dense arrays and plain or null-prototype objects', () => {
  32. const shared = { value: 1 }
  33. const nullPrototype = Object.assign(Object.create(null) as Record<string, unknown>, { shared })
  34. const source = { list: [nullPrototype, shared], alias: shared }
  35. const snapshot = snapshotJsonValue(source)!
  36. shared.value = 2
  37. expect(snapshot).toEqual({ list: [{ shared: { value: 1 } }, { value: 1 }], alias: { value: 1 } })
  38. expect(snapshot).not.toBe(source)
  39. expect(snapshot.list).not.toBe(source.list)
  40. expect(snapshot.alias).not.toBe(shared)
  41. expect(snapshot.list[0]).not.toBe(nullPrototype)
  42. expect(Object.getPrototypeOf(snapshot.list[0])).toBe(Object.prototype)
  43. })
  44. it('accepts intrinsic plain containers from another JavaScript realm', () => {
  45. const foreign = runInNewContext('({ object: { nested: [1] }, array: [2, { ok: true }] })') as {
  46. object: { nested: number[] }
  47. array: JsonValue[]
  48. }
  49. expect(isJsonValue(foreign.object)).toBe(true)
  50. expect(isJsonValue(foreign.array)).toBe(true)
  51. const objectSnapshot = snapshotJsonValue(foreign.object)!
  52. const arraySnapshot = snapshotJsonValue(foreign.array)!
  53. expect(objectSnapshot).toEqual({ nested: [1] })
  54. expect(arraySnapshot).toEqual([2, { ok: true }])
  55. expect(Object.getPrototypeOf(objectSnapshot)).toBe(Object.prototype)
  56. expect(Object.getPrototypeOf(arraySnapshot)).toBe(Array.prototype)
  57. })
  58. it('reads each object value and array slot once while materializing', () => {
  59. class Exotic {
  60. readonly accepted = false
  61. }
  62. let objectReads = 0
  63. let arrayReads = 0
  64. const nested = Object.defineProperty({}, 'value', {
  65. enumerable: true,
  66. get: () => {
  67. objectReads += 1
  68. return objectReads === 1 ? { accepted: true } : new Exotic()
  69. },
  70. })
  71. const array = new Array<unknown>(1)
  72. Object.defineProperty(array, 0, {
  73. enumerable: true,
  74. get: () => {
  75. arrayReads += 1
  76. return arrayReads === 1 ? nested : new Exotic()
  77. },
  78. })
  79. expect(snapshotJsonValue(array)).toEqual([{ value: { accepted: true } }])
  80. expect(objectReads).toBe(1)
  81. expect(arrayReads).toBe(1)
  82. })
  83. it('accepts deeply nested valid JSON without using the JavaScript call stack', () => {
  84. let value: JsonValue = 'leaf'
  85. for (let depth = 0; depth < 5_000; depth++) value = [value]
  86. expect(isJsonValue(value)).toBe(true)
  87. let cursor: JsonValue | undefined = snapshotJsonValue(value)
  88. for (let depth = 0; depth < 5_000; depth++) {
  89. expect(Array.isArray(cursor)).toBe(true)
  90. cursor = Array.isArray(cursor) ? cursor[0] : undefined
  91. }
  92. expect(cursor).toBe('leaf')
  93. })
  94. it('rejects exotic containers, sparse or decorated arrays, cycles, and invalid children', () => {
  95. class ExoticObject {
  96. readonly value = 1
  97. }
  98. class ExoticArray extends Array<number> {}
  99. const sparse = new Array<number>(1)
  100. const compensatedSparse = new Array<number>(1)
  101. Object.defineProperty(compensatedSparse, 'extra', { value: true })
  102. const decorated = [1]
  103. Object.defineProperty(decorated, 'extra', { value: true })
  104. const symbolDecorated = [1]
  105. Object.defineProperty(symbolDecorated, Symbol('extra'), { value: true })
  106. const hiddenObject = Object.defineProperty({}, 'hidden', { value: true })
  107. const symbolObject = { [Symbol('extra')]: true }
  108. const customPrototype = Object.create(null) as Record<string, unknown>
  109. const customPrototypeObject = Object.assign(Object.create(customPrototype) as Record<string, unknown>, { value: 1 })
  110. const forgedIntrinsicObject = objectWithForgedIntrinsicPrototype()
  111. const revokedIntrinsicObject = objectWithForgedIntrinsicPrototype(true)
  112. const forgedPrototype: unknown[] = []
  113. Object.setPrototypeOf(forgedPrototype, null)
  114. const forgedArray = [1]
  115. Object.setPrototypeOf(forgedArray, forgedPrototype)
  116. const cyclic: Record<string, unknown> = {}
  117. cyclic.self = cyclic
  118. const foreignExotics = runInNewContext(`(() => {
  119. class Box { constructor() { this.value = 1 } }
  120. class List extends Array {}
  121. return [new Box(), new List(1)]
  122. })()`) as [object, unknown[]]
  123. expect(snapshotJsonValue(new ExoticObject())).toBeUndefined()
  124. expect(snapshotJsonValue(new Map([['value', 1]]))).toBeUndefined()
  125. expect(snapshotJsonValue(new ExoticArray(1))).toBeUndefined()
  126. expect(snapshotJsonValue(foreignExotics[0])).toBeUndefined()
  127. expect(snapshotJsonValue(foreignExotics[1])).toBeUndefined()
  128. expect(snapshotJsonValue(sparse)).toBeUndefined()
  129. expect(snapshotJsonValue(compensatedSparse)).toBeUndefined()
  130. expect(snapshotJsonValue(decorated)).toBeUndefined()
  131. expect(snapshotJsonValue(symbolDecorated)).toBeUndefined()
  132. expect(snapshotJsonValue(hiddenObject)).toBeUndefined()
  133. expect(snapshotJsonValue(symbolObject)).toBeUndefined()
  134. expect(snapshotJsonValue(customPrototypeObject)).toBeUndefined()
  135. expect(snapshotJsonValue(forgedIntrinsicObject)).toBeUndefined()
  136. expect(snapshotJsonValue(revokedIntrinsicObject)).toBeUndefined()
  137. expect(snapshotJsonValue(forgedArray)).toBeUndefined()
  138. expect(snapshotJsonValue(cyclic)).toBeUndefined()
  139. expect(snapshotJsonValue([undefined])).toBeUndefined()
  140. expect(snapshotJsonValue({ value: undefined })).toBeUndefined()
  141. })
  142. it('preserves a literal __proto__ JSON key without changing the snapshot prototype', () => {
  143. const source = Object.create(null) as Record<string, unknown>
  144. source.__proto__ = { safe: true }
  145. const snapshot = snapshotJsonValue(source)!
  146. expect(Object.getPrototypeOf(snapshot)).toBe(Object.prototype)
  147. expect(Object.prototype.hasOwnProperty.call(snapshot, '__proto__')).toBe(true)
  148. expect(snapshot.__proto__).toEqual({ safe: true })
  149. })
  150. it('propagates a throwing getter after reading it once', () => {
  151. const failure = new Error('getter failed')
  152. let reads = 0
  153. const source = Object.defineProperty({}, 'value', {
  154. enumerable: true,
  155. get: () => {
  156. reads += 1
  157. throw failure
  158. },
  159. })
  160. expect(() => snapshotJsonValue(source)).toThrow(failure)
  161. expect(reads).toBe(1)
  162. })
  163. })
  164. describe('isJsonValue', () => {
  165. it('recognizes supported scalars and rejects every lossy scalar case', () => {
  166. const unsupportedFunction = (): void => {}
  167. const unsupportedUndefined: unknown = undefined
  168. expect(isJsonValue(null)).toBe(true)
  169. expect(isJsonValue(false)).toBe(true)
  170. expect(isJsonValue('text')).toBe(true)
  171. expect(isJsonValue(1.25)).toBe(true)
  172. expect(isJsonValue(-0)).toBe(false)
  173. expect(isJsonValue(Number.NaN)).toBe(false)
  174. expect(isJsonValue(1n)).toBe(false)
  175. expect(isJsonValue(unsupportedFunction)).toBe(false)
  176. expect(isJsonValue(Symbol('value'))).toBe(false)
  177. expect(isJsonValue(unsupportedUndefined)).toBe(false)
  178. })
  179. it('accepts dense arrays and plain objects, including null-prototype records', () => {
  180. const nullPrototype = Object.assign(Object.create(null) as Record<string, unknown>, { value: true })
  181. expect(isJsonValue([1, { nested: null }, nullPrototype])).toBe(true)
  182. expect(isJsonValue({ value: [1, 2] })).toBe(true)
  183. expect(isJsonValue(nullPrototype)).toBe(true)
  184. })
  185. it('rejects sparse or decorated arrays, invalid children, exotic objects, and cycles', () => {
  186. class Exotic {
  187. readonly value = 1
  188. }
  189. class ExoticArray extends Array<number> {}
  190. const sparse = new Array<number>(1)
  191. const compensatedSparse = new Array<number>(1)
  192. Object.defineProperty(compensatedSparse, 'extra', { value: true })
  193. const decorated = Object.assign([1], { extra: true })
  194. const symbolDecorated = [1]
  195. Object.defineProperty(symbolDecorated, Symbol('extra'), { value: true })
  196. const hiddenObject = Object.defineProperty({}, 'hidden', { value: true })
  197. const symbolObject = { [Symbol('extra')]: true }
  198. const customPrototype = Object.create(null) as Record<string, unknown>
  199. const customPrototypeObject = Object.assign(Object.create(customPrototype) as Record<string, unknown>, { value: 1 })
  200. const forgedIntrinsicObject = objectWithForgedIntrinsicPrototype()
  201. const revokedIntrinsicObject = objectWithForgedIntrinsicPrototype(true)
  202. const forgedPrototype: unknown[] = []
  203. Object.setPrototypeOf(forgedPrototype, null)
  204. const forgedArray = [1]
  205. Object.setPrototypeOf(forgedArray, forgedPrototype)
  206. const cyclic: Record<string, unknown> = {}
  207. cyclic.self = cyclic
  208. expect(isJsonValue(sparse)).toBe(false)
  209. expect(isJsonValue(compensatedSparse)).toBe(false)
  210. expect(isJsonValue(decorated)).toBe(false)
  211. expect(isJsonValue(symbolDecorated)).toBe(false)
  212. expect(isJsonValue(hiddenObject)).toBe(false)
  213. expect(isJsonValue(symbolObject)).toBe(false)
  214. expect(isJsonValue(customPrototypeObject)).toBe(false)
  215. expect(isJsonValue(forgedIntrinsicObject)).toBe(false)
  216. expect(isJsonValue(revokedIntrinsicObject)).toBe(false)
  217. expect(isJsonValue(forgedArray)).toBe(false)
  218. expect(isJsonValue(new ExoticArray(1))).toBe(false)
  219. expect(isJsonValue([undefined])).toBe(false)
  220. expect(isJsonValue({ value: undefined })).toBe(false)
  221. expect(isJsonValue(new Exotic())).toBe(false)
  222. expect(isJsonValue(cyclic)).toBe(false)
  223. })
  224. })