json.spec.ts 3.6 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273
  1. import { describe, expect, it } from 'vitest'
  2. import {
  3. inspectSessionFormatVersion,
  4. sessionFormatCount,
  5. sessionFormatSafeInteger,
  6. snapshotSessionFormatArtifact,
  7. snapshotSessionFormatHeader,
  8. snapshotSessionFormatJson,
  9. } from '../src/index.ts'
  10. describe('lossless Session format JSON snapshots', () => {
  11. it.each([
  12. ['negative zero', -0],
  13. ['non-finite number', Number.POSITIVE_INFINITY],
  14. ['undefined member', { value: undefined }],
  15. ['sparse array', Array(1)],
  16. ['symbol member', { [Symbol('hidden')]: true }],
  17. ['non-enumerable member', Object.defineProperty({}, 'hidden', { value: true })],
  18. ['accessor member', Object.defineProperty({}, 'value', { enumerable: true, get: () => 1 })],
  19. ['array property', Object.assign([], { extra: true })],
  20. ['array accessor', Object.defineProperty([1], '0', { enumerable: true, get: () => 1 })],
  21. ])('refuses %s that JSON cannot preserve', (_name, value) => {
  22. expect(() => snapshotSessionFormatJson(value)).toThrow(/JSON|sparse|member|property|number/)
  23. })
  24. it('detaches, freezes, and retains repeated non-cyclic values and __proto__ keys', () => {
  25. const shared = { value: 1 }
  26. const source = JSON.parse('{"__proto__":{"safe":true}}') as Record<string, unknown>
  27. source['values'] = [shared, shared]
  28. const snapshot = snapshotSessionFormatJson(source) as Record<string, unknown>
  29. expect(snapshot).toEqual(source)
  30. expect(snapshot).not.toBe(source)
  31. expect(Object.isFrozen(snapshot)).toBe(true)
  32. expect(Object.isFrozen(snapshot['values'])).toBe(true)
  33. expect(Object.getPrototypeOf(snapshot)).toBe(Object.prototype)
  34. })
  35. it('refuses invalid scalar coordinates, cycles, and custom prototypes', () => {
  36. const cyclic: { self?: unknown } = {}
  37. cyclic.self = cyclic
  38. class RecordValue { value = 1 }
  39. class ArrayValue extends Array<number> {}
  40. expect(() => sessionFormatCount(-1, 'count')).toThrow(/non-negative/)
  41. expect(() => sessionFormatSafeInteger(1.5, 'integer')).toThrow(/safe integer/)
  42. expect(() => inspectSessionFormatVersion([])).toThrow(/header/)
  43. expect(() => snapshotSessionFormatJson(cyclic)).toThrow(/cycle/)
  44. expect(() => snapshotSessionFormatJson(new RecordValue())).toThrow(/non-plain/)
  45. expect(() => snapshotSessionFormatJson(new ArrayValue(1))).toThrow(/non-intrinsic/)
  46. })
  47. it.each([
  48. ['non-object header', { header: null, inheritedEventCount: 0, events: [] }],
  49. ['non-array events', { header: { version: 1 }, inheritedEventCount: 0, events: null }],
  50. ['non-object event', { header: { version: 1 }, inheritedEventCount: 0, events: [null] }],
  51. ['non-dense seq', { header: { version: 1 }, inheritedEventCount: 0, events: [{ type: 'x', seq: 1, time: 1, data: {} }] }],
  52. ['empty type', { header: { version: 1 }, inheritedEventCount: 0, events: [{ type: '', seq: 0, time: 1, data: {} }] }],
  53. ['invalid time', { header: { version: 1 }, inheritedEventCount: 0, events: [{ type: 'x', seq: 0, time: 1.5, data: {} }] }],
  54. ['missing data', { header: { version: 1 }, inheritedEventCount: 0, events: [{ type: 'x', seq: 0, time: 1 }] }],
  55. ['oversized cut', { header: { version: 1 }, inheritedEventCount: 1, events: [] }],
  56. ])('refuses an artifact with %s', (_name, artifact) => {
  57. expect(() => snapshotSessionFormatArtifact(artifact as never)).toThrow()
  58. })
  59. it('refuses a non-object header snapshot', () => {
  60. expect(() => snapshotSessionFormatHeader(null as never)).toThrow(/header|object/)
  61. expect(() => snapshotSessionFormatHeader({
  62. version: 1, id: 'missing-seeded', createdAt: 1, delegationDepth: 0,
  63. } as never)).toThrow(/isSeeded/)
  64. })
  65. })