worker-json.spec.ts 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. import { runInNewContext } from 'node:vm'
  2. import { describe, expect, it } from 'vitest'
  3. import { snapshotJsonValue } from '@deepseek-ai/dsh-session'
  4. import { 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. for (const value of [
  89. new ExoticObject(),
  90. new Map([['value', 1]]),
  91. new ExoticArray(1),
  92. new Array(1),
  93. decorated,
  94. compensatedSparse,
  95. symbolDecorated,
  96. hiddenObject,
  97. symbolObject,
  98. customPrototypeObject,
  99. forgedArray,
  100. cyclic,
  101. [undefined],
  102. { value: undefined },
  103. ]) {
  104. expect(snapshotCodeJsonValue(value)).toBeUndefined()
  105. }
  106. })
  107. it('rejects an array whose getter mutates the validated length', () => {
  108. const array = [0, 2]
  109. Object.defineProperty(array, 0, {
  110. enumerable: true,
  111. get: () => {
  112. array.length = 1
  113. return 1
  114. },
  115. })
  116. expect(snapshotCodeJsonValue(array)).toBeUndefined()
  117. })
  118. it('propagates a throwing getter and releases its recursion guard', () => {
  119. const failure = new Error('getter failed')
  120. const source = Object.defineProperty({}, 'value', {
  121. enumerable: true,
  122. get: () => { throw failure },
  123. })
  124. expect(() => snapshotCodeJsonValue(source)).toThrow(failure)
  125. expect(snapshotCodeJsonValue({ after: true })).toEqual({ after: true })
  126. })
  127. })