json.spec.ts 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  1. import { describe, expect, it } from 'vitest'
  2. import { isJsonValue, snapshotJsonValue } from '@deepseek-ai/dsh-session'
  3. describe('snapshotJsonValue', () => {
  4. it('copies the complete JSON scalar vocabulary and rejects unsupported scalars', () => {
  5. const unsupportedFunction = (): void => {}
  6. expect(snapshotJsonValue(null)).toBeNull()
  7. expect(snapshotJsonValue(true)).toBe(true)
  8. expect(snapshotJsonValue('text')).toBe('text')
  9. expect(snapshotJsonValue(1.25)).toBe(1.25)
  10. expect(snapshotJsonValue(-0)).toBeUndefined()
  11. expect(isJsonValue(-0)).toBe(false)
  12. expect(snapshotJsonValue(Number.NaN)).toBeUndefined()
  13. expect(snapshotJsonValue(Number.POSITIVE_INFINITY)).toBeUndefined()
  14. expect(snapshotJsonValue(1n)).toBeUndefined()
  15. expect(snapshotJsonValue(unsupportedFunction)).toBeUndefined()
  16. expect(snapshotJsonValue(Symbol('value'))).toBeUndefined()
  17. const unsupportedUndefined: unknown = undefined
  18. expect(snapshotJsonValue(unsupportedUndefined)).toBeUndefined()
  19. })
  20. it('recursively detaches dense arrays and plain or null-prototype objects', () => {
  21. const shared = { value: 1 }
  22. const nullPrototype = Object.assign(Object.create(null) as Record<string, unknown>, { shared })
  23. const source = { list: [nullPrototype, shared], alias: shared }
  24. const snapshot = snapshotJsonValue(source)!
  25. shared.value = 2
  26. expect(snapshot).toEqual({ list: [{ shared: { value: 1 } }, { value: 1 }], alias: { value: 1 } })
  27. expect(snapshot).not.toBe(source)
  28. expect(snapshot.list).not.toBe(source.list)
  29. expect(snapshot.alias).not.toBe(shared)
  30. expect(snapshot.list[0]).not.toBe(nullPrototype)
  31. expect(Object.getPrototypeOf(snapshot.list[0])).toBe(Object.prototype)
  32. })
  33. it('reads each object value and array slot once while materializing', () => {
  34. class Exotic {
  35. readonly accepted = false
  36. }
  37. let objectReads = 0
  38. let arrayReads = 0
  39. const nested = Object.defineProperty({}, 'value', {
  40. enumerable: true,
  41. get: () => {
  42. objectReads += 1
  43. return objectReads === 1 ? { accepted: true } : new Exotic()
  44. },
  45. })
  46. const array = new Array<unknown>(1)
  47. Object.defineProperty(array, 0, {
  48. enumerable: true,
  49. get: () => {
  50. arrayReads += 1
  51. return arrayReads === 1 ? nested : new Exotic()
  52. },
  53. })
  54. expect(snapshotJsonValue(array)).toEqual([{ value: { accepted: true } }])
  55. expect(objectReads).toBe(1)
  56. expect(arrayReads).toBe(1)
  57. })
  58. it('rejects exotic containers, sparse arrays, cycles, and invalid children', () => {
  59. class ExoticObject {
  60. readonly value = 1
  61. }
  62. class ExoticArray extends Array<number> {}
  63. const sparse = new Array<number>(1)
  64. const cyclic: Record<string, unknown> = {}
  65. cyclic.self = cyclic
  66. expect(snapshotJsonValue(new ExoticObject())).toBeUndefined()
  67. expect(snapshotJsonValue(new Map([['value', 1]]))).toBeUndefined()
  68. expect(snapshotJsonValue(new ExoticArray(1))).toBeUndefined()
  69. expect(snapshotJsonValue(sparse)).toBeUndefined()
  70. expect(snapshotJsonValue(cyclic)).toBeUndefined()
  71. expect(snapshotJsonValue([undefined])).toBeUndefined()
  72. expect(snapshotJsonValue({ value: undefined })).toBeUndefined()
  73. })
  74. it('preserves a literal __proto__ JSON key without changing the snapshot prototype', () => {
  75. const source = Object.create(null) as Record<string, unknown>
  76. source.__proto__ = { safe: true }
  77. const snapshot = snapshotJsonValue(source)!
  78. expect(Object.getPrototypeOf(snapshot)).toBe(Object.prototype)
  79. expect(Object.prototype.hasOwnProperty.call(snapshot, '__proto__')).toBe(true)
  80. expect(snapshot.__proto__).toEqual({ safe: true })
  81. })
  82. it('propagates a throwing getter after reading it once', () => {
  83. const failure = new Error('getter failed')
  84. let reads = 0
  85. const source = Object.defineProperty({}, 'value', {
  86. enumerable: true,
  87. get: () => {
  88. reads += 1
  89. throw failure
  90. },
  91. })
  92. expect(() => snapshotJsonValue(source)).toThrow(failure)
  93. expect(reads).toBe(1)
  94. })
  95. })
  96. describe('isJsonValue', () => {
  97. it('recognizes supported scalars and rejects every lossy scalar case', () => {
  98. const unsupportedFunction = (): void => {}
  99. const unsupportedUndefined: unknown = undefined
  100. expect(isJsonValue(null)).toBe(true)
  101. expect(isJsonValue(false)).toBe(true)
  102. expect(isJsonValue('text')).toBe(true)
  103. expect(isJsonValue(1.25)).toBe(true)
  104. expect(isJsonValue(-0)).toBe(false)
  105. expect(isJsonValue(Number.NaN)).toBe(false)
  106. expect(isJsonValue(1n)).toBe(false)
  107. expect(isJsonValue(unsupportedFunction)).toBe(false)
  108. expect(isJsonValue(Symbol('value'))).toBe(false)
  109. expect(isJsonValue(unsupportedUndefined)).toBe(false)
  110. })
  111. it('accepts dense arrays and plain objects, including null-prototype records', () => {
  112. const nullPrototype = Object.assign(Object.create(null) as Record<string, unknown>, { value: true })
  113. expect(isJsonValue([1, { nested: null }, nullPrototype])).toBe(true)
  114. expect(isJsonValue({ value: [1, 2] })).toBe(true)
  115. expect(isJsonValue(nullPrototype)).toBe(true)
  116. })
  117. it('rejects sparse arrays, invalid children, exotic objects, and cycles', () => {
  118. class Exotic {
  119. readonly value = 1
  120. }
  121. class ExoticArray extends Array<number> {}
  122. const sparse = new Array<number>(1)
  123. const cyclic: Record<string, unknown> = {}
  124. cyclic.self = cyclic
  125. expect(isJsonValue(sparse)).toBe(false)
  126. expect(isJsonValue(new ExoticArray(1))).toBe(false)
  127. expect(isJsonValue([undefined])).toBe(false)
  128. expect(isJsonValue({ value: undefined })).toBe(false)
  129. expect(isJsonValue(new Exotic())).toBe(false)
  130. expect(isJsonValue(cyclic)).toBe(false)
  131. })
  132. })