schema.client.spec.ts 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101
  1. import { Context } from '@deepseek-ai/cordis'
  2. import Schema from '@deepseek-ai/schemastery'
  3. import { describe, expect, it } from 'vitest'
  4. import type { SchemaNode } from '../src/client/schema.ts'
  5. import { SettingsSchemaService } from '../src/client/schema.ts'
  6. const service = new SettingsSchemaService(new Context())
  7. const wire = (schema: Schema): unknown => JSON.parse(JSON.stringify(schema.toJSON()))
  8. describe('SettingsSchemaService validation', () => {
  9. it('rehydrates a serialized envelope into a working validator', () => {
  10. const root = service.rehydrate(wire(Schema.object({ name: Schema.string().required() })))
  11. expect(service.validate(root, { name: 'ok' })).toBeUndefined()
  12. expect(service.validate(root, { name: 42 })).toContain('name')
  13. })
  14. it('stringifies non-Error validation throws', () => {
  15. const hostile = (() => {
  16. throw 'plain-string failure'
  17. }) as unknown as SchemaNode
  18. expect(service.validate(hostile, {})).toBe('plain-string failure')
  19. })
  20. })
  21. describe('SettingsSchemaService path operations', () => {
  22. const root = { providers: { openai: { baseURL: 'https://x' } }, models: [{ id: 'a' }] }
  23. it('reads nested object and array paths', () => {
  24. expect(service.getPath(root, [])).toBe(root)
  25. expect(service.getPath(root, ['providers', 'openai', 'baseURL'])).toBe('https://x')
  26. expect(service.getPath(root, ['models', '0', 'id'])).toBe('a')
  27. expect(service.getPath(root, ['providers', 'missing', 'x'])).toBeUndefined()
  28. expect(service.getPath(root, ['providers', 'openai', 'baseURL', 'deep'])).toBeUndefined()
  29. })
  30. it('reports presence by key existence rather than value truthiness', () => {
  31. expect(service.hasPath({ flag: false }, ['flag'])).toBe(true)
  32. expect(service.hasPath({ nested: { key: undefined } }, ['nested', 'key'])).toBe(true)
  33. expect(service.hasPath({}, ['missing'])).toBe(false)
  34. expect(service.hasPath({ leaf: 'x' }, ['leaf', 'deeper'])).toBe(false)
  35. expect(service.hasPath({ models: ['a'] }, ['models', '0'])).toBe(true)
  36. expect(service.hasPath({ models: ['a'] }, ['models', '1'])).toBe(false)
  37. expect(service.hasPath({ root: true }, [])).toBe(true)
  38. expect(service.hasPath(undefined, [])).toBe(false)
  39. })
  40. it('sets nested paths immutably and materializes missing containers', () => {
  41. const draft = {}
  42. const next = service.setPath(draft, ['providers', 'openai', 'baseURL'], 'https://y')
  43. expect(draft).toEqual({})
  44. expect(next).toEqual({ providers: { openai: { baseURL: 'https://y' } } })
  45. const withArray = service.setPath(next, ['models', '0'], { id: 'a' })
  46. expect(withArray).toEqual({ providers: { openai: { baseURL: 'https://y' } }, models: [{ id: 'a' }] })
  47. const replaced = service.setPath(withArray, ['models', '0', 'id'], 'b')
  48. expect(replaced.models).toEqual([{ id: 'b' }])
  49. expect((withArray as { models: unknown[] }).models).toEqual([{ id: 'a' }])
  50. expect(() => service.setPath({}, [], 'x')).toThrow(/non-empty path/)
  51. })
  52. it('deletes nested paths immutably and splices array indexes', () => {
  53. const draft = { providers: { openai: { baseURL: 'https://x', apiKey: 'k' } }, models: ['a', 'b'] }
  54. const withoutKey = service.deletePath(draft, ['providers', 'openai', 'apiKey'])
  55. expect(withoutKey).toEqual({ providers: { openai: { baseURL: 'https://x' } }, models: ['a', 'b'] })
  56. expect(draft.providers.openai.apiKey).toBe('k')
  57. const withoutModel = service.deletePath(withoutKey, ['models', '0'])
  58. expect(withoutModel.models).toEqual(['b'])
  59. expect(service.deletePath(draft, ['providers', 'missing', 'x'])).toBe(draft)
  60. expect(() => service.deletePath({}, [])).toThrow(/non-empty path/)
  61. })
  62. it('deletes keys through array intermediates immutably', () => {
  63. const draft = { models: [{ id: 'a', contextWindow: 1 }] }
  64. const next = service.deletePath(draft, ['models', '0', 'contextWindow'])
  65. expect(next).toEqual({ models: [{ id: 'a' }] })
  66. expect(draft.models[0]).toEqual({ id: 'a', contextWindow: 1 })
  67. })
  68. })
  69. describe('SettingsSchemaService node traversal', () => {
  70. const rootSchema = Schema.object({
  71. providers: Schema.dict(Schema.object({ baseURL: Schema.string() })),
  72. models: Schema.array(Schema.object({ id: Schema.string() })),
  73. leaf: Schema.string(),
  74. })
  75. it('resolves object, dict, and array positions', () => {
  76. const root = service.rehydrate(wire(rootSchema))
  77. expect(service.nodeAtPath(root, [])).toBe(root)
  78. expect(service.nodeAtPath(root, ['providers', 'openai'])?.type).toBe('object')
  79. expect(service.nodeAtPath(root, ['providers', 'openai', 'baseURL'])?.type).toBe('string')
  80. expect(service.nodeAtPath(root, ['models', '0', 'id'])?.type).toBe('string')
  81. expect(service.nodeAtPath(root, ['missing'])).toBeUndefined()
  82. expect(service.nodeAtPath(root, ['missing', 'deeper'])).toBeUndefined()
  83. expect(service.nodeAtPath(root, ['leaf', 'below'])).toBeUndefined()
  84. })
  85. it('tolerates structural nodes missing their relation maps', () => {
  86. expect(service.nodeAtPath({ type: 'object' } as SchemaNode, ['x'])).toBeUndefined()
  87. expect(service.nodeAtPath({ type: 'dict' } as SchemaNode, ['x'])).toBeUndefined()
  88. })
  89. })