agent-core.spec.ts 3.5 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879
  1. import { describe, expect, it } from 'vitest'
  2. import { Context } from 'cordis'
  3. import Loader from '@cordisjs/plugin-loader'
  4. import * as agentCore from '../src/index.ts'
  5. import { AgentId } from '@deepseek-ai/dsh-agent'
  6. /**
  7. * Unit coverage for the @deepseek-ai/dsh-agent-core bundle: mounting it brings
  8. * up the whole providerless spine in one `ctx.plugin`, and the forwarded
  9. * `agents` config reaches the loop (default `[]`, or a pre-created agent).
  10. *
  11. * The bundle is exercised through `ctx.plugin(agentCore, …)` — the NAMESPACE
  12. * import, the same shape the Loader builds from `unwrapExports`. The real
  13. * Loader-path guard (export shape, `unwrapExports`) is the app packages' keyless
  14. * bin smokes; here we assert the composition + config forwarding.
  15. */
  16. async function mount(config?: agentCore.Config): Promise<Context> {
  17. const ctx = new Context()
  18. await ctx.plugin(agentCore, config)
  19. // The bundle mounts its children inside apply() (not awaited there); let their
  20. // fibers settle so the spine services and any pre-created agent are ready.
  21. await new Promise(resolve => setTimeout(resolve, 50))
  22. return ctx
  23. }
  24. describe('dsh-agent-core bundle', () => {
  25. it('brings up the full providerless spine', async () => {
  26. const ctx = await mount()
  27. // One service from each layer of the spine proves the children loaded.
  28. expect(ctx.get('timer')).toBeDefined()
  29. expect(ctx.get('llm')).toBeDefined()
  30. expect(ctx.get('sessions')).toBeDefined()
  31. expect(ctx.get('systemPrompt')).toBeDefined()
  32. expect(ctx.get('tools')).toBeDefined()
  33. expect(ctx.get('agents')).toBeDefined()
  34. expect(ctx.get('agentLoop')).toBeDefined()
  35. await ctx.fiber.dispose()
  36. })
  37. it('defaults the agents list to empty (no pre-created agents)', async () => {
  38. const ctx = await mount()
  39. expect(ctx.get('agents')?.get(AgentId('main'))).toBeUndefined()
  40. await ctx.fiber.dispose()
  41. })
  42. it('forwards a pre-created agent to the loop', async () => {
  43. const ctx = await mount({
  44. agents: [{ id: AgentId('main'), model: 'mock', systemPrompt: 'hi' }],
  45. })
  46. expect(ctx.get('agents')?.get(AgentId('main'))).toBeDefined()
  47. await ctx.fiber.dispose()
  48. })
  49. it('re-exports the loop config schema as its own', () => {
  50. expect(agentCore.Config).toBeDefined()
  51. expect(agentCore.name).toBe('agent-core')
  52. })
  53. it('has the namespace-plugin export shape (no stray default) so the Loader keeps name/Config/apply', () => {
  54. // Postmortem 0001 guard: a stray `export default apply` makes the Loader's
  55. // `unwrapExports` (`exports.default ?? exports`) collapse the module to the
  56. // bare `apply` function, DROPPING the named `name`/`Config`. This package has
  57. // no `inject` export (it mounts children that carry their own), so that
  58. // collapse would NOT crash at load — the plugin would boot but silently lose
  59. // its config schema. This bundle is also never Loader-unwrapped by any smoke
  60. // (the apps import it directly; the mount test namespace-mounts it), so this
  61. // is its ONLY export-shape guard. Assert directly AND through the real
  62. // `unwrapExports` so adding `export default` to src/index.ts fails here.
  63. expect('default' in agentCore).toBe(false)
  64. expect(typeof agentCore.apply).toBe('function')
  65. const loader = Object.create(Loader.prototype) as Loader
  66. const unwrapped = loader.unwrapExports(agentCore) as Record<string, unknown>
  67. expect(unwrapped).toBe(agentCore)
  68. expect(unwrapped.name).toBe('agent-core')
  69. expect(unwrapped.Config).toBeDefined()
  70. expect(typeof unwrapped.apply).toBe('function')
  71. })
  72. })