worker-json.spec.ts 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132
  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('rejects exotic containers, sparse arrays, cycles, and invalid children', () => {
  58. class ExoticObject {
  59. readonly value = 1
  60. }
  61. class ExoticArray extends Array<number> {}
  62. const cyclic: Record<string, unknown> = {}
  63. cyclic.self = cyclic
  64. const decorated = [1]
  65. Object.defineProperty(decorated, 'extra', { value: true })
  66. const compensatedSparse = new Array(1)
  67. Object.defineProperty(compensatedSparse, 'extra', { value: true })
  68. const symbolDecorated = [1]
  69. Object.defineProperty(symbolDecorated, Symbol('extra'), { value: true })
  70. const hiddenObject = Object.defineProperty({}, 'hidden', { value: true })
  71. const symbolObject = { [Symbol('extra')]: true }
  72. const customPrototype = Object.create(null) as Record<string, unknown>
  73. const customPrototypeObject = Object.assign(Object.create(customPrototype) as Record<string, unknown>, { value: 1 })
  74. const forgedPrototype: unknown[] = []
  75. Object.setPrototypeOf(forgedPrototype, null)
  76. const forgedArray = [1]
  77. Object.setPrototypeOf(forgedArray, forgedPrototype)
  78. for (const value of [
  79. new ExoticObject(),
  80. new Map([['value', 1]]),
  81. new ExoticArray(1),
  82. new Array(1),
  83. decorated,
  84. compensatedSparse,
  85. symbolDecorated,
  86. hiddenObject,
  87. symbolObject,
  88. customPrototypeObject,
  89. forgedArray,
  90. cyclic,
  91. [undefined],
  92. { value: undefined },
  93. ]) {
  94. expect(snapshotCodeJsonValue(value)).toBeUndefined()
  95. }
  96. })
  97. it('rejects an array whose getter mutates the validated length', () => {
  98. const array = [0, 2]
  99. Object.defineProperty(array, 0, {
  100. enumerable: true,
  101. get: () => {
  102. array.length = 1
  103. return 1
  104. },
  105. })
  106. expect(snapshotCodeJsonValue(array)).toBeUndefined()
  107. })
  108. it('propagates a throwing getter and releases its recursion guard', () => {
  109. const failure = new Error('getter failed')
  110. const source = Object.defineProperty({}, 'value', {
  111. enumerable: true,
  112. get: () => { throw failure },
  113. })
  114. expect(() => snapshotCodeJsonValue(source)).toThrow(failure)
  115. expect(snapshotCodeJsonValue({ after: true })).toEqual({ after: true })
  116. })
  117. })