execution-mode.spec.ts 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
  1. /** Covers fail-closed per-call classification and model-schema isolation. */
  2. import { describe, expect, expectTypeOf, it } from 'vitest'
  3. import { Context } from 'cordis'
  4. import { CallId } from '@deepseek-ai/dsh-llm'
  5. import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
  6. import ToolRegistry, {
  7. defineTool,
  8. type ToolDefinition,
  9. type ToolExecutionInput,
  10. type ToolExecutionMode,
  11. } from '@deepseek-ai/dsh-tools'
  12. async function setup() {
  13. const ctx = new Context()
  14. await ctx.plugin(SystemPrompt)
  15. await ctx.plugin(ToolRegistry)
  16. return ctx
  17. }
  18. function exec(name: string, args: unknown): ToolExecutionInput {
  19. return { callId: CallId('c1'), name, arguments: args }
  20. }
  21. describe('ToolRegistry.executionMode', () => {
  22. it('returns parallel only for an explicit true classifier', async () => {
  23. const ctx = await setup()
  24. ctx.tools.register(defineTool({
  25. name: 'safe',
  26. description: 'parallel-safe',
  27. parameters: {},
  28. isConcurrencySafe: () => true,
  29. async execute() { return [] },
  30. }))
  31. expect(ctx.tools.executionMode(exec('safe', {}))).toEqual({ kind: 'parallel' })
  32. })
  33. it('defaults to exclusive for a tool with no isConcurrencySafe declaration', async () => {
  34. const ctx = await setup()
  35. ctx.tools.register(defineTool({
  36. name: 'plain',
  37. description: 'no declaration',
  38. parameters: {},
  39. async execute() { return [] },
  40. }))
  41. expect(ctx.tools.executionMode(exec('plain', {}))).toEqual({ kind: 'exclusive' })
  42. })
  43. it('returns exclusive for an unknown tool', async () => {
  44. const ctx = await setup()
  45. expect(ctx.tools.executionMode(exec('nonexistent', {}))).toEqual({ kind: 'exclusive' })
  46. })
  47. it('returns exclusive when the classifier returns false for these args', async () => {
  48. const ctx = await setup()
  49. ctx.tools.register(defineTool({
  50. name: 'rw',
  51. description: 'read or write',
  52. parameters: { mode: { type: 'string', required: true } },
  53. isConcurrencySafe: args => args.mode === 'read',
  54. async execute() { return [] },
  55. }))
  56. expect(ctx.tools.executionMode(exec('rw', { mode: 'read' }))).toEqual({ kind: 'parallel' })
  57. expect(ctx.tools.executionMode(exec('rw', { mode: 'write' }))).toEqual({ kind: 'exclusive' })
  58. })
  59. it('classifies invalid defineTool arguments as exclusive without throwing', async () => {
  60. const ctx = await setup()
  61. ctx.tools.register(defineTool({
  62. name: 'needs-mode',
  63. description: 'requires mode',
  64. parameters: { mode: { type: 'string', required: true } },
  65. isConcurrencySafe: () => true,
  66. async execute() { return [] },
  67. }))
  68. expect(ctx.tools.executionMode(exec('needs-mode', {}))).toEqual({ kind: 'exclusive' })
  69. })
  70. it('treats a throwing raw classifier as exclusive', async () => {
  71. const ctx = await setup()
  72. const raw: ToolDefinition = {
  73. name: 'thrower',
  74. description: 'classifier throws',
  75. parameters: { type: 'object', properties: {} },
  76. isConcurrencySafe() { throw new Error('boom') },
  77. async execute() { return [] },
  78. }
  79. ctx.tools.register(raw)
  80. expect(ctx.tools.executionMode(exec('thrower', {}))).toEqual({ kind: 'exclusive' })
  81. })
  82. it('treats a truthy non-boolean raw result as exclusive', async () => {
  83. const ctx = await setup()
  84. const raw = {
  85. name: 'truthy',
  86. description: 'classifier returns a truthy string',
  87. parameters: { type: 'object', properties: {} },
  88. isConcurrencySafe() { return 'yes' },
  89. async execute() { return [] },
  90. } as unknown as ToolDefinition
  91. ctx.tools.register(raw)
  92. expect(ctx.tools.executionMode(exec('truthy', {}))).toEqual({ kind: 'exclusive' })
  93. })
  94. it('passes parsed arguments directly to a raw definition', async () => {
  95. const ctx = await setup()
  96. let seen: unknown
  97. ctx.tools.register({
  98. name: 'raw-safe',
  99. description: 'raw',
  100. parameters: { type: 'object', properties: {} },
  101. isConcurrencySafe(args) { seen = args; return true },
  102. async execute() { return [] },
  103. })
  104. expect(ctx.tools.executionMode(exec('raw-safe', { anything: 1 }))).toEqual({ kind: 'parallel' })
  105. expect(seen).toEqual({ anything: 1 })
  106. })
  107. it('isConcurrencySafe never reaches the model-facing schemas() projection', async () => {
  108. const ctx = await setup()
  109. ctx.tools.register(defineTool({
  110. name: 'safe',
  111. description: 'parallel-safe',
  112. parameters: { x: { type: 'string', required: true } },
  113. isConcurrencySafe: () => true,
  114. async execute() { return [] },
  115. }))
  116. const schema = ctx.tools.schemas()[0] as unknown as Record<string, unknown>
  117. expect(Object.keys(schema).sort()).toEqual(['description', 'name', 'parameters'])
  118. expect(schema.isConcurrencySafe).toBeUndefined()
  119. })
  120. it('ToolExecutionMode is the object-tagged union', () => {
  121. expectTypeOf<ToolExecutionMode>().toEqualTypeOf<{ kind: 'parallel' } | { kind: 'exclusive' }>()
  122. })
  123. })