worker-json.spec.ts 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596
  1. import { describe, expect, it } from 'vitest'
  2. import { snapshotJsonValue } from '@deepseek-ai/dsh-session'
  3. import { snapshotCodeJsonValue } from '../src/worker-json.ts'
  4. describe('snapshotCodeJsonValue', () => {
  5. it('matches the canonical scalar boundary', () => {
  6. const unsupported = [undefined, 1n, Symbol('value'), () => 1]
  7. for (const value of [null, false, 'text', 1.25, -0, Number.NaN, Number.POSITIVE_INFINITY, ...unsupported]) {
  8. expect(snapshotCodeJsonValue(value)).toEqual(snapshotJsonValue(value))
  9. }
  10. })
  11. it('detaches dense arrays and plain or null-prototype records', () => {
  12. const shared = { value: 1 }
  13. const nullPrototype = Object.assign(Object.create(null) as Record<string, unknown>, { shared })
  14. const source = { list: [nullPrototype, shared], alias: shared }
  15. const snapshot = snapshotCodeJsonValue(source) as Record<string, unknown>
  16. shared.value = 2
  17. expect(snapshot).toEqual({ list: [{ shared: { value: 1 } }, { value: 1 }], alias: { value: 1 } })
  18. expect(snapshot).not.toBe(source)
  19. expect((snapshot.list as unknown[])[0]).not.toBe(nullPrototype)
  20. expect(snapshot.alias).not.toBe(shared)
  21. })
  22. it('reads each accepted slot once and preserves a literal __proto__ key', () => {
  23. let objectReads = 0
  24. let arrayReads = 0
  25. const source = Object.create(null) as Record<string, unknown>
  26. Object.defineProperty(source, '__proto__', {
  27. enumerable: true,
  28. get: () => {
  29. objectReads += 1
  30. return { safe: true }
  31. },
  32. })
  33. const array = new Array<unknown>(1)
  34. Object.defineProperty(array, 0, {
  35. enumerable: true,
  36. get: () => {
  37. arrayReads += 1
  38. return arrayReads === 1 ? source : undefined
  39. },
  40. })
  41. const snapshot = snapshotCodeJsonValue(array) as Record<string, unknown>[]
  42. expect(objectReads).toBe(1)
  43. expect(arrayReads).toBe(1)
  44. expect(Object.getPrototypeOf(snapshot[0])).toBe(Object.prototype)
  45. expect(Object.hasOwn(snapshot[0]!, '__proto__')).toBe(true)
  46. expect(snapshot[0]?.['__proto__']).toEqual({ safe: true })
  47. })
  48. it('rejects exotic containers, sparse arrays, cycles, and invalid children', () => {
  49. class ExoticObject {
  50. readonly value = 1
  51. }
  52. class ExoticArray extends Array<number> {}
  53. const cyclic: Record<string, unknown> = {}
  54. cyclic.self = cyclic
  55. const decorated = [1]
  56. Object.defineProperty(decorated, 'extra', { value: true })
  57. const compensatedSparse = new Array(1)
  58. Object.defineProperty(compensatedSparse, 'extra', { value: true })
  59. const symbolDecorated = [1]
  60. Object.defineProperty(symbolDecorated, Symbol('extra'), { value: true })
  61. for (const value of [
  62. new ExoticObject(),
  63. new Map([['value', 1]]),
  64. new ExoticArray(1),
  65. new Array(1),
  66. decorated,
  67. compensatedSparse,
  68. symbolDecorated,
  69. cyclic,
  70. [undefined],
  71. { value: undefined },
  72. ]) {
  73. expect(snapshotCodeJsonValue(value)).toBeUndefined()
  74. }
  75. })
  76. it('propagates a throwing getter and releases its recursion guard', () => {
  77. const failure = new Error('getter failed')
  78. const source = Object.defineProperty({}, 'value', {
  79. enumerable: true,
  80. get: () => { throw failure },
  81. })
  82. expect(() => snapshotCodeJsonValue(source)).toThrow(failure)
  83. expect(snapshotCodeJsonValue({ after: true })).toEqual({ after: true })
  84. })
  85. })