output-json.spec.ts 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109
  1. import { describe, expect, it, vi } from 'vitest'
  2. import type { CodeJsonValue } from '@deepseek-ai/dsh-code-runtime'
  3. import { jsonStringBytesUpTo, jsonValueBytesUpTo, truncateJsonStringBytes } from '../src/output-json.ts'
  4. describe('truncateJsonStringBytes', () => {
  5. it('returns a fitting string whole and rejects budgets without JSON quotes', () => {
  6. expect(truncateJsonStringBytes('fits', 6)).toBe('fits')
  7. expect(truncateJsonStringBytes('x', 1)).toBe('')
  8. expect(jsonStringBytesUpTo('fits', 6)).toBe(6)
  9. expect(jsonStringBytesUpTo('fits', 5)).toBeUndefined()
  10. })
  11. it('accounts every JSON escape and cuts only between complete code points', () => {
  12. const prefix = '"\\\b\t\n\f\r\u0000😀\ud800€a'
  13. const text = `${prefix}z`
  14. const budget = Buffer.byteLength(JSON.stringify(prefix), 'utf8')
  15. expect(truncateJsonStringBytes(text, budget)).toBe(prefix)
  16. expect(Buffer.byteLength(JSON.stringify(truncateJsonStringBytes(text, budget)), 'utf8')).toBe(budget)
  17. })
  18. it('bounds hostile strings without materializing their complete escaped form', () => {
  19. const stringify = vi.spyOn(JSON, 'stringify').mockImplementation(() => { throw new Error('must not stringify') })
  20. try {
  21. expect(jsonStringBytesUpTo('"'.repeat(10_000), 32)).toBeUndefined()
  22. expect(truncateJsonStringBytes('"'.repeat(10_000), 32)).toBe('"'.repeat(15))
  23. } finally {
  24. stringify.mockRestore()
  25. }
  26. })
  27. })
  28. describe('jsonValueBytesUpTo', () => {
  29. it('matches JSON serialization for every lossless value branch and stops at the cap', () => {
  30. const value = {
  31. empty: {},
  32. nil: null,
  33. yes: true,
  34. no: false,
  35. number: 1.5,
  36. text: '"\n😀',
  37. array: [1, 'x'],
  38. }
  39. const bytes = Buffer.byteLength(JSON.stringify(value), 'utf8')
  40. expect(jsonValueBytesUpTo(value, bytes)).toBe(bytes)
  41. expect(jsonValueBytesUpTo(value, bytes - 1)).toBeUndefined()
  42. expect(jsonValueBytesUpTo({}, 1)).toBeUndefined()
  43. expect(jsonValueBytesUpTo([], 1)).toBeUndefined()
  44. expect(jsonValueBytesUpTo([], 2)).toBe(2)
  45. expect(jsonValueBytesUpTo(null, 3)).toBeUndefined()
  46. expect(jsonValueBytesUpTo(10, 1)).toBeUndefined()
  47. expect(jsonValueBytesUpTo(false, 4)).toBeUndefined()
  48. expect(jsonValueBytesUpTo(new Array<never>(1), 10)).toBeUndefined()
  49. expect(jsonValueBytesUpTo([null], 5)).toBeUndefined()
  50. expect(jsonValueBytesUpTo([0, 0], 3)).toBeUndefined()
  51. expect(jsonValueBytesUpTo({ a: null, b: null }, 10)).toBeUndefined()
  52. expect(jsonValueBytesUpTo({ long: null }, 2)).toBeUndefined()
  53. expect(jsonValueBytesUpTo({ '': null }, 4)).toBeUndefined()
  54. expect(jsonValueBytesUpTo({ a: null }, 9)).toBeUndefined()
  55. expect(jsonValueBytesUpTo({ a: undefined } as unknown as CodeJsonValue, 100)).toBeUndefined()
  56. })
  57. it('meters deeply nested arrays without recursive stack growth', () => {
  58. let value: CodeJsonValue = null
  59. for (let depth = 0; depth < 5_000; depth++) value = [value]
  60. expect(jsonValueBytesUpTo(value, 10_004)).toBe(10_004)
  61. expect(jsonValueBytesUpTo(value, 10_003)).toBeUndefined()
  62. })
  63. it('uses module-captured intrinsics after model-visible globals are mutated', () => {
  64. const value: CodeJsonValue = { payload: ['€', 42] }
  65. const bytes = Buffer.byteLength(JSON.stringify(value), 'utf8')
  66. const arrayIsArrayDescriptor = Object.getOwnPropertyDescriptor(Array, 'isArray')!
  67. const arrayPopDescriptor = Object.getOwnPropertyDescriptor(Array.prototype, 'pop')!
  68. const arrayPushDescriptor = Object.getOwnPropertyDescriptor(Array.prototype, 'push')!
  69. const byteLengthDescriptor = Object.getOwnPropertyDescriptor(Buffer, 'byteLength')!
  70. const objectKeysDescriptor = Object.getOwnPropertyDescriptor(Object, 'keys')!
  71. const charCodeAtDescriptor = Object.getOwnPropertyDescriptor(String.prototype, 'charCodeAt')!
  72. const codePointAtDescriptor = Object.getOwnPropertyDescriptor(String.prototype, 'codePointAt')!
  73. const sliceDescriptor = Object.getOwnPropertyDescriptor(String.prototype, 'slice')!
  74. let measured: number | undefined
  75. let prefix = ''
  76. try {
  77. Array.isArray = (_value: unknown): _value is never[] => false
  78. Array.prototype.pop = () => { throw new Error('mutated pop') }
  79. Array.prototype.push = () => { throw new Error('mutated push') }
  80. Buffer.byteLength = () => 0
  81. Object.keys = () => []
  82. String.prototype.charCodeAt = () => { throw new Error('mutated charCodeAt') }
  83. String.prototype.codePointAt = () => { throw new Error('mutated codePointAt') }
  84. String.prototype.slice = () => { throw new Error('mutated slice') }
  85. measured = jsonValueBytesUpTo(value, bytes)
  86. prefix = truncateJsonStringBytes('€x', 5)
  87. } finally {
  88. Object.defineProperty(Array, 'isArray', arrayIsArrayDescriptor)
  89. Object.defineProperty(Array.prototype, 'pop', arrayPopDescriptor)
  90. Object.defineProperty(Array.prototype, 'push', arrayPushDescriptor)
  91. Object.defineProperty(Buffer, 'byteLength', byteLengthDescriptor)
  92. Object.defineProperty(Object, 'keys', objectKeysDescriptor)
  93. Object.defineProperty(String.prototype, 'charCodeAt', charCodeAtDescriptor)
  94. Object.defineProperty(String.prototype, 'codePointAt', codePointAtDescriptor)
  95. Object.defineProperty(String.prototype, 'slice', sliceDescriptor)
  96. }
  97. expect(measured).toBe(bytes)
  98. expect(prefix).toBe('€')
  99. })
  100. })