validator.test.ts 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. import { describe, expect, it } from 'bun:test'
  2. import { validateAbility, validateInputs } from '../src/validator/index.js'
  3. import type { Ability } from '../src/types/index.js'
  4. describe('validateAbility', () => {
  5. it('should validate a minimal valid ability', () => {
  6. const ability = {
  7. name: 'test-ability',
  8. description: 'A test ability',
  9. steps: [
  10. {
  11. id: 'step1',
  12. type: 'script',
  13. run: 'echo hello',
  14. },
  15. ],
  16. }
  17. const result = validateAbility(ability)
  18. expect(result.valid).toBe(true)
  19. expect(result.errors).toHaveLength(0)
  20. })
  21. it('should reject ability without name', () => {
  22. const ability = {
  23. description: 'A test ability',
  24. steps: [{ id: 'step1', type: 'script', run: 'echo' }],
  25. }
  26. const result = validateAbility(ability)
  27. expect(result.valid).toBe(false)
  28. expect(result.errors.some((e) => e.path.includes('name'))).toBe(true)
  29. })
  30. it('should reject ability without steps', () => {
  31. const ability = {
  32. name: 'test',
  33. description: 'Test',
  34. steps: [],
  35. }
  36. const result = validateAbility(ability)
  37. expect(result.valid).toBe(false)
  38. })
  39. it('should detect circular dependencies', () => {
  40. const ability = {
  41. name: 'circular',
  42. description: 'Has circular deps',
  43. steps: [
  44. { id: 'a', type: 'script', run: 'echo a', needs: ['c'] },
  45. { id: 'b', type: 'script', run: 'echo b', needs: ['a'] },
  46. { id: 'c', type: 'script', run: 'echo c', needs: ['b'] },
  47. ],
  48. }
  49. const result = validateAbility(ability)
  50. expect(result.valid).toBe(false)
  51. expect(result.errors.some((e) => e.code === 'CIRCULAR_DEPENDENCY')).toBe(true)
  52. })
  53. it('should detect missing dependencies', () => {
  54. const ability = {
  55. name: 'missing-dep',
  56. description: 'Missing dep',
  57. steps: [
  58. { id: 'a', type: 'script', run: 'echo', needs: ['nonexistent'] },
  59. ],
  60. }
  61. const result = validateAbility(ability)
  62. expect(result.valid).toBe(false)
  63. expect(result.errors.some((e) => e.code === 'MISSING_DEPENDENCY')).toBe(true)
  64. })
  65. it('should detect duplicate step IDs', () => {
  66. const ability = {
  67. name: 'duplicate',
  68. description: 'Duplicate IDs',
  69. steps: [
  70. { id: 'step1', type: 'script', run: 'echo 1' },
  71. { id: 'step1', type: 'script', run: 'echo 2' },
  72. ],
  73. }
  74. const result = validateAbility(ability)
  75. expect(result.valid).toBe(false)
  76. expect(result.errors.some((e) => e.code === 'DUPLICATE_ID')).toBe(true)
  77. })
  78. it('should validate all step types', () => {
  79. const ability = {
  80. name: 'all-types',
  81. description: 'All step types',
  82. steps: [
  83. { id: 'script', type: 'script', run: 'echo' },
  84. { id: 'agent', type: 'agent', agent: 'oracle', prompt: 'Help' },
  85. { id: 'skill', type: 'skill', skill: 'test-skill' },
  86. { id: 'approval', type: 'approval', prompt: 'Approve?' },
  87. { id: 'workflow', type: 'workflow', workflow: 'sub-workflow' },
  88. ],
  89. }
  90. const result = validateAbility(ability)
  91. expect(result.valid).toBe(true)
  92. })
  93. })
  94. describe('validateInputs', () => {
  95. const ability: Ability = {
  96. name: 'test',
  97. description: 'Test',
  98. inputs: {
  99. version: {
  100. type: 'string',
  101. required: true,
  102. pattern: '^v\\d+\\.\\d+\\.\\d+$',
  103. },
  104. count: {
  105. type: 'number',
  106. required: false,
  107. min: 1,
  108. max: 100,
  109. },
  110. env: {
  111. type: 'string',
  112. required: true,
  113. enum: ['dev', 'staging', 'prod'],
  114. },
  115. },
  116. steps: [{ id: 'test', type: 'script', run: 'echo' }],
  117. }
  118. it('should validate correct inputs', () => {
  119. const errors = validateInputs(ability, {
  120. version: 'v1.2.3',
  121. count: 50,
  122. env: 'staging',
  123. })
  124. expect(errors).toHaveLength(0)
  125. })
  126. it('should reject missing required input', () => {
  127. const errors = validateInputs(ability, {
  128. env: 'dev',
  129. })
  130. expect(errors.some((e) => e.code === 'MISSING_INPUT')).toBe(true)
  131. })
  132. it('should reject invalid pattern', () => {
  133. const errors = validateInputs(ability, {
  134. version: 'invalid',
  135. env: 'dev',
  136. })
  137. expect(errors.some((e) => e.code === 'PATTERN_MISMATCH')).toBe(true)
  138. })
  139. it('should reject invalid enum value', () => {
  140. const errors = validateInputs(ability, {
  141. version: 'v1.0.0',
  142. env: 'invalid',
  143. })
  144. expect(errors.some((e) => e.code === 'ENUM_MISMATCH')).toBe(true)
  145. })
  146. it('should reject number out of range', () => {
  147. const errors = validateInputs(ability, {
  148. version: 'v1.0.0',
  149. env: 'dev',
  150. count: 200,
  151. })
  152. expect(errors.some((e) => e.code === 'MAX_VALUE')).toBe(true)
  153. })
  154. })