tools.spec.ts 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384
  1. /** Agent controls use the same manager methods as the Web and bound inventory reads. */
  2. import { Context } from '@deepseek-ai/cordis'
  3. import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
  4. import ToolRuntime from '@deepseek-ai/dsh-tools'
  5. import { ToolCallId } from '@deepseek-ai/dsh-llm'
  6. import { expect, it, onTestFinished, vi } from 'vitest'
  7. import type PluginManager from '../src/index.ts'
  8. import * as tool from '../src/tools.ts'
  9. function resultText(result: Awaited<ReturnType<ToolRuntime['execute']>>): string {
  10. if (typeof result.value !== 'string') throw new Error('Expected a serialized manager result')
  11. return result.value
  12. }
  13. async function fixture() {
  14. const ctx = new Context()
  15. onTestFinished(() => ctx.fiber.dispose())
  16. const manager = {
  17. listPlugins: vi.fn(async () => Array.from({ length: 30 }, (_, i) => ({ entryId: `include:${i}`, enabled: true }))),
  18. listBundles: vi.fn(async () => [{ name: 'bundle', enabled: true }]),
  19. setPluginEnabled: vi.fn(async () => ({ changed: true, application: 'applied' })),
  20. setBundleEnabled: vi.fn(async () => ({ changed: true, application: 'applied' })),
  21. installBundle: vi.fn(async () => ({ changed: true, application: 'restart-required' })),
  22. removeBundle: vi.fn(async () => ({ changed: false, application: 'failed' })),
  23. }
  24. ctx.provide('pluginManager', manager as unknown as PluginManager)
  25. await ctx.plugin(SystemPrompt)
  26. await ctx.plugin(ToolRuntime)
  27. const fiber = await ctx.plugin(tool)
  28. const call = (args: unknown) => ctx.tools.execute({ name: 'plugin_manager', arguments: args,
  29. callId: ToolCallId('manager-call'), signal: new AbortController().signal })
  30. return { ctx, manager, call, fiber }
  31. }
  32. it('paginates inventories with an explicit continuation and total', async () => {
  33. const { call } = await fixture()
  34. const first = await call({ action: 'list_plugins' })
  35. expect(first.isError).toBe(false)
  36. expect(JSON.stringify(first.content)).toContain('nextOffset')
  37. expect(JSON.parse(resultText(first))).toMatchObject({ nextOffset: 25, total: 30 })
  38. const last = await call({ action: 'list_plugins', offset: 25, limit: 10 })
  39. expect(JSON.parse(resultText(last))).toMatchObject({ nextOffset: null, total: 30 })
  40. expect(resultText(await call({ action: 'list_bundles' }))).toContain('"name":"bundle"')
  41. })
  42. it('forwards all mutation actions and renders the returned outcome', async () => {
  43. const { call, manager } = await fixture()
  44. await call({ action: 'set_plugin', target: 'include:1', enabled: false })
  45. expect(manager.setPluginEnabled).toHaveBeenCalledWith('include:1', false)
  46. await call({ action: 'set_bundle', target: 'bundle', enabled: true })
  47. expect(manager.setBundleEnabled).toHaveBeenCalledWith('bundle', true)
  48. await call({ action: 'install_bundle', target: 'bundle' })
  49. expect(manager.installBundle).toHaveBeenLastCalledWith('bundle', {})
  50. await call({ action: 'install_bundle', target: 'bundle', enabled: false })
  51. expect(manager.installBundle).toHaveBeenLastCalledWith('bundle', { enabled: false })
  52. await call({ action: 'install_bundle', target: 'bundle', approvedBuilds: ['native'] })
  53. expect(manager.installBundle).toHaveBeenLastCalledWith('bundle', { approvedBuilds: ['native'] })
  54. expect(resultText(await call({ action: 'remove_bundle', target: 'bundle' }))).toContain('"application":"failed"')
  55. expect(manager.removeBundle).toHaveBeenCalledWith('bundle')
  56. })
  57. it.each([
  58. { action: 'unknown_action' },
  59. { action: 'list_plugins', offset: -1 },
  60. { action: 'list_plugins', offset: 0.5 },
  61. { action: 'list_bundles', limit: 101 },
  62. { action: 'list_bundles', limit: 0 },
  63. { action: 'list_bundles', limit: 1.5 },
  64. { action: 'set_plugin', enabled: true },
  65. { action: 'set_bundle', target: 'bundle' },
  66. { action: 'install_bundle' }, { action: 'remove_bundle' },
  67. ])('rejects incomplete or unbounded tool inputs: %j', async (args) => {
  68. const { call } = await fixture()
  69. expect((await call(args)).isError).toBe(true)
  70. })
  71. it('presents reads and changes distinctly and disposes its registration', async () => {
  72. const { ctx, fiber } = await fixture()
  73. const definition = ctx.tools.get('plugin_manager')!
  74. expect(definition.presentCall?.({ action: 'list_plugins' })).toMatchObject({ kind: 'read' })
  75. expect(definition.presentCall?.({ action: 'remove_bundle', target: 'bundle' })).toMatchObject({ kind: 'other' })
  76. await fiber.dispose()
  77. expect(ctx.tools.get('plugin_manager')).toBeUndefined()
  78. })