worker-json.spec.ts 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  1. import { runInNewContext } from 'node:vm'
  2. import { describe, expect, it } from 'vitest'
  3. import { snapshotJsonValue } from '@deepseek-ai/dsh-util-values'
  4. import { decodeWorkerJson, encodeWorkerJson, snapshotCodeJsonValue } from '../src/worker-json.ts'
  5. describe('snapshotCodeJsonValue', () => {
  6. it('matches the canonical scalar boundary', () => {
  7. const unsupported = [undefined, 1n, Symbol('value'), () => 1]
  8. for (const value of [null, false, 'text', 1.25, -0, Number.NaN, Number.POSITIVE_INFINITY, ...unsupported]) {
  9. expect(snapshotCodeJsonValue(value)).toEqual(snapshotJsonValue(value))
  10. }
  11. })
  12. it('detaches dense arrays and plain or null-prototype records', () => {
  13. const shared = { value: 1 }
  14. const nullPrototype = Object.assign(Object.create(null) as Record<string, unknown>, { shared })
  15. const source = { list: [nullPrototype, shared], alias: shared }
  16. const snapshot = snapshotCodeJsonValue(source) as Record<string, unknown>
  17. shared.value = 2
  18. expect(snapshot).toEqual({ list: [{ shared: { value: 1 } }, { value: 1 }], alias: { value: 1 } })
  19. expect(snapshot).not.toBe(source)
  20. expect((snapshot.list as unknown[])[0]).not.toBe(nullPrototype)
  21. expect(snapshot.alias).not.toBe(shared)
  22. })
  23. it('accepts intrinsic plain containers from another JavaScript realm', () => {
  24. const foreign = runInNewContext('({ object: { nested: [1] }, array: [2, { ok: true }] })') as {
  25. object: unknown
  26. array: unknown
  27. }
  28. expect(snapshotCodeJsonValue(foreign.object)).toEqual({ nested: [1] })
  29. expect(snapshotCodeJsonValue(foreign.array)).toEqual([2, { ok: true }])
  30. })
  31. it('reads each accepted slot once and preserves a literal __proto__ key', () => {
  32. let objectReads = 0
  33. let arrayReads = 0
  34. const source = Object.create(null) as Record<string, unknown>
  35. Object.defineProperty(source, '__proto__', {
  36. enumerable: true,
  37. get: () => {
  38. objectReads += 1
  39. return { safe: true }
  40. },
  41. })
  42. const array = new Array<unknown>(1)
  43. Object.defineProperty(array, 0, {
  44. enumerable: true,
  45. get: () => {
  46. arrayReads += 1
  47. return arrayReads === 1 ? source : undefined
  48. },
  49. })
  50. const snapshot = snapshotCodeJsonValue(array) as Record<string, unknown>[]
  51. expect(objectReads).toBe(1)
  52. expect(arrayReads).toBe(1)
  53. expect(Object.getPrototypeOf(snapshot[0])).toBe(Object.prototype)
  54. expect(Object.hasOwn(snapshot[0]!, '__proto__')).toBe(true)
  55. expect(snapshot[0]?.['__proto__']).toEqual({ safe: true })
  56. })
  57. it('accepts deeply nested valid JSON without using the JavaScript call stack', () => {
  58. let value: unknown = 'leaf'
  59. for (let depth = 0; depth < 5_000; depth++) value = [value]
  60. let cursor = snapshotCodeJsonValue(value)
  61. for (let depth = 0; depth < 5_000; depth++) {
  62. expect(Array.isArray(cursor)).toBe(true)
  63. cursor = Array.isArray(cursor) ? cursor[0] : undefined
  64. }
  65. expect(cursor).toBe('leaf')
  66. })
  67. it('rejects exotic containers, sparse arrays, cycles, and invalid children', () => {
  68. class ExoticObject {
  69. readonly value = 1
  70. }
  71. class ExoticArray extends Array<number> {}
  72. const cyclic: Record<string, unknown> = {}
  73. cyclic.self = cyclic
  74. const decorated = [1]
  75. Object.defineProperty(decorated, 'extra', { value: true })
  76. const compensatedSparse = new Array(1)
  77. Object.defineProperty(compensatedSparse, 'extra', { value: true })
  78. const symbolDecorated = [1]
  79. Object.defineProperty(symbolDecorated, Symbol('extra'), { value: true })
  80. const hiddenObject = Object.defineProperty({}, 'hidden', { value: true })
  81. const symbolObject = { [Symbol('extra')]: true }
  82. const customPrototype = Object.create(null) as Record<string, unknown>
  83. const customPrototypeObject = Object.assign(Object.create(customPrototype) as Record<string, unknown>, { value: 1 })
  84. const forgedPrototype: unknown[] = []
  85. Object.setPrototypeOf(forgedPrototype, null)
  86. const forgedArray = [1]
  87. Object.setPrototypeOf(forgedArray, forgedPrototype)
  88. const spoofedObjectPrototype = Object.create(null) as Record<string, unknown>
  89. const SpoofedObject = function Object() {}
  90. SpoofedObject.prototype = spoofedObjectPrototype
  91. Object.defineProperty(spoofedObjectPrototype, 'constructor', { value: SpoofedObject })
  92. const spoofedObject = Object.create(spoofedObjectPrototype) as Record<string, unknown>
  93. spoofedObject.value = 1
  94. const revokedPrototype = Object.create(null) as Record<string, unknown>
  95. const RevokedObject = function Object() {}
  96. RevokedObject.prototype = revokedPrototype
  97. const revokedConstructor = Proxy.revocable(RevokedObject, {})
  98. Object.defineProperty(revokedPrototype, 'constructor', { value: revokedConstructor.proxy })
  99. const revokedObject = Object.create(revokedPrototype) as Record<string, unknown>
  100. revokedConstructor.revoke()
  101. const spoofedArrayPrototype: unknown[] = []
  102. Object.setPrototypeOf(spoofedArrayPrototype, Object.prototype)
  103. const SpoofedArray = function Array() {}
  104. SpoofedArray.prototype = spoofedArrayPrototype
  105. Object.defineProperty(spoofedArrayPrototype, 'constructor', { value: SpoofedArray })
  106. const spoofedArray = [1]
  107. Object.setPrototypeOf(spoofedArray, spoofedArrayPrototype)
  108. for (const value of [
  109. new ExoticObject(),
  110. new Map([['value', 1]]),
  111. new ExoticArray(1),
  112. new Array(1),
  113. decorated,
  114. compensatedSparse,
  115. symbolDecorated,
  116. hiddenObject,
  117. symbolObject,
  118. customPrototypeObject,
  119. forgedArray,
  120. spoofedObject,
  121. revokedObject,
  122. spoofedArray,
  123. cyclic,
  124. [undefined],
  125. { value: undefined },
  126. ]) {
  127. const canonical = snapshotJsonValue(value)
  128. expect(canonical).toBeUndefined()
  129. expect(snapshotCodeJsonValue(value)).toEqual(canonical)
  130. }
  131. })
  132. it('rejects an array whose getter mutates the validated length', () => {
  133. const array = [0, 2]
  134. Object.defineProperty(array, 0, {
  135. enumerable: true,
  136. get: () => {
  137. array.length = 1
  138. return 1
  139. },
  140. })
  141. expect(snapshotCodeJsonValue(array)).toBeUndefined()
  142. })
  143. it('propagates a throwing getter and releases its recursion guard', () => {
  144. const failure = new Error('getter failed')
  145. const source = Object.defineProperty({}, 'value', {
  146. enumerable: true,
  147. get: () => { throw failure },
  148. })
  149. expect(() => snapshotCodeJsonValue(source)).toThrow(failure)
  150. expect(snapshotCodeJsonValue({ after: true })).toEqual({ after: true })
  151. })
  152. })
  153. describe('flat worker JSON wire', () => {
  154. it('round-trips every JSON root while preserving object keys and container order', () => {
  155. const withPrototypeKey = Object.create(null) as Record<string, unknown>
  156. withPrototypeKey.__proto__ = { safe: true }
  157. const values = [null, false, true, 1.25, 'text', [], {}, [1, { nested: [2] }], withPrototypeKey]
  158. for (const value of values) {
  159. const snapshot = snapshotCodeJsonValue(value)
  160. expect(snapshot).not.toBeUndefined()
  161. expect(decodeWorkerJson(encodeWorkerJson(snapshot!))).toEqual(snapshot)
  162. }
  163. const decoded = decodeWorkerJson(encodeWorkerJson(snapshotCodeJsonValue(withPrototypeKey)!)) as Record<string, unknown>
  164. expect(Object.hasOwn(decoded, '__proto__')).toBe(true)
  165. expect(decoded.__proto__).toEqual({ safe: true })
  166. })
  167. it('round-trips deep values through a bounded-depth token array', () => {
  168. let value: unknown = 'leaf'
  169. for (let depth = 0; depth < 5_000; depth++) value = [value]
  170. const snapshot = snapshotCodeJsonValue(value)!
  171. const wire = encodeWorkerJson(snapshot)
  172. expect(wire).toHaveLength(5_001)
  173. let cursor = decodeWorkerJson(wire)
  174. for (let depth = 0; depth < 5_000; depth++) {
  175. expect(Array.isArray(cursor)).toBe(true)
  176. cursor = Array.isArray(cursor) ? cursor[0] : undefined
  177. }
  178. expect(cursor).toBe('leaf')
  179. })
  180. it('rejects malformed, incomplete, lossy, sparse, decorated, and throwing wire values', () => {
  181. const sparse = new Array(1)
  182. const compensatedSparse = new Array(1)
  183. Object.defineProperty(compensatedSparse, 'extra', { value: true })
  184. const decorated: unknown[] = [null]
  185. Object.defineProperty(decorated, 'extra', { value: true })
  186. const throwing: unknown[] = []
  187. Object.defineProperty(throwing, 0, { enumerable: true, get: () => { throw new Error('wire getter') } })
  188. const decoratedKeys: unknown[] = ['x']
  189. Object.defineProperty(decoratedKeys, 'extra', { value: true })
  190. const foreignMarker: Record<string, unknown> = { kind: 'array', length: 0 }
  191. Object.setPrototypeOf(foreignMarker, {})
  192. const hiddenMarker = Object.defineProperty({ kind: 'array', length: 0 }, 'hidden', { value: true })
  193. for (const value of [
  194. undefined,
  195. null,
  196. {},
  197. [],
  198. sparse,
  199. compensatedSparse,
  200. decorated,
  201. throwing,
  202. [undefined],
  203. [-0],
  204. [Number.NaN],
  205. [Number.POSITIVE_INFINITY],
  206. [1, 2],
  207. [[]],
  208. [foreignMarker],
  209. [hiddenMarker],
  210. [{ kind: 'unknown' }],
  211. [{ kind: 'array', bogus: 0 }],
  212. [{ kind: 'array' }],
  213. [{ kind: 'array', length: '1' }],
  214. [{ kind: 'array', length: -1 }],
  215. [{ kind: 'array', length: Number.MAX_SAFE_INTEGER + 1 }],
  216. [{ kind: 'array', length: 1 }],
  217. [{ kind: 'array', length: 2 }, { kind: 'array', length: 1 }, null],
  218. [{ kind: 'array', length: 0, extra: true }],
  219. [{ kind: 'object' }],
  220. [{ kind: 'object', keys: 'x' }],
  221. [{ kind: 'object', keys: decoratedKeys }],
  222. [{ kind: 'object', keys: [1] }],
  223. [{ kind: 'object', keys: ['x', 'x'] }, 1, 2],
  224. [{ kind: 'object', keys: ['x'] }],
  225. [{ kind: 'object', keys: [], extra: true }],
  226. ]) {
  227. expect(decodeWorkerJson(value)).toBeUndefined()
  228. }
  229. })
  230. it('rejects invalid values passed through a forged static type', () => {
  231. expect(() => encodeWorkerJson([undefined] as never)).toThrow(/sparse JSON array/)
  232. expect(() => encodeWorkerJson({ value: undefined } as never)).toThrow(/undefined JSON object property/)
  233. })
  234. })