execution-mode.spec.ts 5.2 KB

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