gen-tool-catalog.spec.ts 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107
  1. /**
  2. * Guarantee tests for the tool-schema catalog generator (`scripts/gen-tool-catalog.ts`).
  3. */
  4. import { describe, expect, it } from 'vitest'
  5. import {
  6. assertManifestComplete,
  7. collectToolCatalog,
  8. render,
  9. type ToolCatalog,
  10. } from '../../../../scripts/gen-tool-catalog.ts'
  11. /** JSON Schema shape enough to reach the values AST extraction can't. */
  12. interface JsonSchema {
  13. type: string
  14. properties?: Record<string, JsonSchema>
  15. items?: JsonSchema
  16. enum?: string[]
  17. required?: string[]
  18. }
  19. describe('gen-tool-catalog collectToolCatalog', () => {
  20. it('boots every shipped tool package and harvests its model-facing schemas', async () => {
  21. const catalog = await collectToolCatalog()
  22. const names = catalog.flatMap(entry => entry.schemas.map(s => s.name)).sort()
  23. expect(names).toEqual(['ask_user_question', 'bash', 'cordis_inspect', 'cordis_mount', 'cordis_unmount', 'create_goal', 'edit', 'exit_plan_mode', 'get_goal', 'glob', 'grep', 'lsp', 'ralph', 'read', 'run_code', 'session_event_read', 'session_event_search', 'session_event_trace', 'session_search', 'session_trace', 'skill', 'subagent', 'task_kill', 'task_list', 'task_output', 'terminal_close', 'terminal_list', 'terminal_open', 'terminal_read', 'terminal_send', 'terminal_signal', 'todo_write', 'update_goal', 'web_fetch', 'web_search', 'workflow', 'write'])
  24. // Every tool carries a JSON-Schema `parameters` object (what the model sees).
  25. for (const entry of catalog) {
  26. for (const schema of entry.schemas) {
  27. expect((schema.parameters as unknown as JsonSchema).type).toBe('object')
  28. }
  29. }
  30. })
  31. it('resolves a runtime-spread enum to its literal members (the payoff over AST)', async () => {
  32. const catalog = await collectToolCatalog()
  33. const todo = catalog
  34. .flatMap(entry => entry.schemas)
  35. .find(s => s.name === 'todo_write')
  36. // `todo-todo` writes `enum: [...STATUSES]` — a source AST would see the
  37. // spread, not the values. Booting yields the shipped enum literals.
  38. const status = (((todo?.parameters as unknown as JsonSchema).properties?.todos)?.items)?.properties?.status
  39. expect(status?.enum).toEqual(['pending', 'in_progress', 'completed'])
  40. })
  41. it('attributes each package with a source pointer that names its index', async () => {
  42. const catalog = await collectToolCatalog()
  43. const bash = catalog.find(entry => entry.pkg === '@deepseek-ai/dsh-tool-bash')
  44. expect(bash?.source).toBe('packages/bash/tool-bash/src/index.ts')
  45. })
  46. it('harvests search tools without depending on the generator process PATH', async () => {
  47. const oldPath = process.env.PATH
  48. try {
  49. process.env.PATH = ''
  50. const catalog = await collectToolCatalog()
  51. const search = catalog.find(entry => entry.pkg === '@deepseek-ai/dsh-tool-fs-search')
  52. expect(search?.schemas.map(s => s.name).sort()).toEqual(['glob', 'grep'])
  53. } finally {
  54. if (oldPath === undefined) delete process.env.PATH
  55. else process.env.PATH = oldPath
  56. }
  57. })
  58. it('records the shipped `subagent_fork` alias in a note (config-driven tool name)', async () => {
  59. // `tool-subagent`'s registered name is the load-time `toolName` config, so the shipped
  60. // agents surface this one package as both `subagent` and `subagent_fork`.
  61. const catalog = await collectToolCatalog()
  62. const subagent = catalog.find(entry => entry.pkg === '@deepseek-ai/dsh-tool-subagent')
  63. expect(subagent?.schemas.map(s => s.name)).toEqual(['subagent'])
  64. expect(subagent?.note).toMatch(/subagent_fork/)
  65. })
  66. })
  67. describe('gen-tool-catalog assertManifestComplete', () => {
  68. it('passes when the manifest lists every on-disk tool package (the default)', () => {
  69. expect(() => { assertManifestComplete() }).not.toThrow()
  70. })
  71. it('throws, naming the omitted package, when a tool package is missing from the manifest', () => {
  72. // An empty manifest scanned against the real tree: every `tool-*` package
  73. // is unlisted, so the guard must fire and name them.
  74. expect(() => { assertManifestComplete([]) }).toThrow(/not in the boot manifest/)
  75. expect(() => { assertManifestComplete([]) }).toThrow(/tool-bash/)
  76. })
  77. })
  78. describe('gen-tool-catalog render', () => {
  79. it('emits a package heading, a tool heading, and a json schema fence', () => {
  80. const catalog: ToolCatalog = [
  81. {
  82. pkg: '@deepseek-ai/dsh-tool-demo',
  83. source: 'packages/demo/tool-demo/src/index.ts',
  84. requires: ['ctx.tools'],
  85. writes: ['tool/result'],
  86. schemas: [{ name: 'demo', description: 'A demo tool.', parameters: { type: 'object', properties: {} } }],
  87. },
  88. ]
  89. const md = render(catalog)
  90. expect(md).toContain('| `@deepseek-ai/dsh-tool-demo` | `demo` | `ctx.tools` | `tool/result` |')
  91. expect(md).toContain('## `@deepseek-ai/dsh-tool-demo`')
  92. expect(md).toContain('### `demo`')
  93. expect(md).toContain('A demo tool.')
  94. expect(md).toContain('```json')
  95. expect(md).toContain('Source: [`packages/demo/tool-demo/src/index.ts`]')
  96. })
  97. })