service.spec.ts 3.2 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788
  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) => { calls.push(args); return null } } }],
  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. })