load-path.spec.ts 2.3 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849
  1. /**
  2. * Real-load-path guard for @deepseek-ai/dsh-tool-web. `tool-web` is a NAMESPACE
  3. * plugin with `inject` — so a stray `export default apply` would make the cordis
  4. * Loader's `unwrapExports` (`exports.default ?? exports`) collapse the module to
  5. * the bare `apply` function, DROPPING `inject`. The plugin would then read
  6. * `ctx.web` without having injected it and throw `cannot get property … without
  7. * inject` the moment it loads (postmortem 0001).
  8. *
  9. * A hand-built `ctx.plugin({ apply, inject })` mount CANNOT catch that — it
  10. * bypasses `unwrapExports`. So this test unwraps the module through the REAL
  11. * `Loader.prototype.unwrapExports` and mounts the result over `ctx.web`,
  12. * exercising the exact path the Loader uses. Prove the guard bites: add
  13. * `export default apply` to `src/index.ts`, watch this go red, revert.
  14. */
  15. import { describe, expect, it } from 'vitest'
  16. import { Context } from 'cordis'
  17. import Loader from '@cordisjs/plugin-loader'
  18. import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
  19. import ToolRegistry from '@deepseek-ai/dsh-tools'
  20. import WebService from '@deepseek-ai/dsh-web'
  21. import * as toolWeb from '@deepseek-ai/dsh-tool-web'
  22. describe('dsh-tool-web real-load-path guard', () => {
  23. it('has no default export and keeps name/inject/Config through unwrapExports', () => {
  24. expect('default' in toolWeb).toBe(false)
  25. const loader = Object.create(Loader.prototype) as Loader
  26. const unwrapped = loader.unwrapExports(toolWeb) as Record<string, unknown>
  27. expect(unwrapped).toBe(toolWeb)
  28. expect(unwrapped.name).toBe('tool-web')
  29. expect(unwrapped.inject).toEqual(['tools', 'web', 'systemPrompt'])
  30. expect(typeof unwrapped.apply).toBe('function')
  31. })
  32. it('boots over ctx.web through the unwrapped module without an inject error', async () => {
  33. const ctx = new Context()
  34. await ctx.plugin(SystemPrompt)
  35. await ctx.plugin(ToolRegistry)
  36. await ctx.plugin(WebService, {})
  37. const loader = Object.create(Loader.prototype) as Loader
  38. const unwrapped = loader.unwrapExports(toolWeb) as Parameters<Context['plugin']>[0]
  39. // A collapsed export shape (dropped inject) would throw "without inject" here.
  40. const fiber = await ctx.plugin(unwrapped)
  41. expect(ctx.tools.schemas().map(s => s.name)).toEqual(expect.arrayContaining(['web_search', 'web_fetch']))
  42. await fiber.dispose()
  43. })
  44. })