service.spec.ts 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101
  1. import { describe, expect, it } from 'vitest'
  2. import { Context } from 'cordis'
  3. import { CodeRuntime } from '@deepseek-ai/dsh-code-runtime'
  4. import type { CodeRunRequest, CodeRunResult } from '@deepseek-ai/dsh-code-runtime'
  5. /**
  6. * Minimal concrete runtime: records requests, "executes" by invoking every
  7. * binding once in declaration order, and lets tests script the outcome. The
  8. * seam package ships no implementation, so the contract is exercised through
  9. * the smallest subclass that honors it.
  10. */
  11. class StubRuntime extends CodeRuntime {
  12. readonly language = 'typescript'
  13. readonly isolation = 'in-process-stub'
  14. requests: CodeRunRequest[] = []
  15. nextResult: CodeRunResult = { logs: [] }
  16. async run(request: CodeRunRequest): Promise<CodeRunResult> {
  17. this.requests.push(request)
  18. if (request.signal?.aborted) {
  19. return { logs: [], error: { kind: 'abort', message: String(request.signal.reason) } }
  20. }
  21. for (const namespace of request.bindings) {
  22. for (const fn of Object.values(namespace.functions)) {
  23. await fn({ from: 'stub' })
  24. }
  25. }
  26. return this.nextResult
  27. }
  28. }
  29. async function setup() {
  30. const ctx = new Context()
  31. await ctx.plugin(StubRuntime)
  32. const runtime = ctx.codeRuntime as StubRuntime
  33. return { ctx, runtime }
  34. }
  35. describe('CodeRuntime service seam', () => {
  36. it('registers as ctx.codeRuntime and serves the abstract API', async () => {
  37. const { runtime } = await setup()
  38. expect(runtime.language).toBe('typescript')
  39. expect(runtime.isolation).toBe('in-process-stub')
  40. const calls: unknown[] = []
  41. const result = await runtime.run({
  42. program: 'return 1',
  43. bindings: [{ global: 'tools', functions: { probe: async args => void calls.push(args) } }],
  44. })
  45. expect(result).toEqual({ logs: [] })
  46. expect(calls).toEqual([{ from: 'stub' }])
  47. expect(runtime.requests).toHaveLength(1)
  48. })
  49. it('reports a failed run as an error field on a resolved result, never a rejection', async () => {
  50. const { runtime } = await setup()
  51. runtime.nextResult = {
  52. logs: ['boom'],
  53. error: { kind: 'exception', message: 'boom' },
  54. }
  55. const result = await runtime.run({ program: 'throw new Error("boom")', bindings: [] })
  56. expect(result.error).toEqual({ kind: 'exception', message: 'boom' })
  57. expect(result.value).toBeUndefined()
  58. })
  59. it('reports a pre-aborted signal as an abort failure', async () => {
  60. const { runtime } = await setup()
  61. const controller = new AbortController()
  62. controller.abort('cancelled')
  63. const result = await runtime.run({ program: 'return 1', bindings: [], signal: controller.signal })
  64. expect(result.error).toEqual({ kind: 'abort', message: 'cancelled' })
  65. })
  66. it('is removed from the context when the providing fiber disposes (HMR safety)', async () => {
  67. const ctx = new Context()
  68. const fiber = await ctx.plugin(StubRuntime)
  69. expect(ctx.get('codeRuntime')).toBeInstanceOf(StubRuntime)
  70. await fiber.dispose()
  71. expect(ctx.get('codeRuntime')).toBeUndefined()
  72. })
  73. it('rejects a second implementation in the same context (duplicate service)', async () => {
  74. const { ctx } = await setup()
  75. await expect(ctx.plugin(StubRuntime)).rejects.toThrow(/registered/)
  76. })
  77. it.each([
  78. [{ language: 'typescript', isolation: 'worker' }, /must expose method "run"/],
  79. [{ language: 'TypeScript', isolation: 'worker', run() {} }, /must be lowercase identifiers/],
  80. ])('rejects an invalid runtime implementation through the package invariant', async (value, message) => {
  81. const ctx = new Context()
  82. const invalidRuntime = {
  83. name: 'invalid-code-runtime',
  84. apply(child: Context) {
  85. child.provide('codeRuntime', value as unknown as CodeRuntime)
  86. },
  87. }
  88. await expect(ctx.plugin(invalidRuntime)).rejects.toThrow(message)
  89. })
  90. })