loader-composition.spec.ts 3.9 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798
  1. /**
  2. * REAL-composition proof: the shipped YAML shape (session + projection
  3. * registry + session-turn-outline) boots through the vendored Loader, the
  4. * function plugin's namespace survives (no default export), and a logged turn
  5. * with its prompt serves the outline through the composed registry.
  6. */
  7. import { mkdtemp, rm, writeFile } from 'node:fs/promises'
  8. import { tmpdir } from 'node:os'
  9. import { join } from 'node:path'
  10. import { pathToFileURL } from 'node:url'
  11. import { afterEach, describe, expect, it } from 'vitest'
  12. import { Context } from '@deepseek-ai/cordis'
  13. import Loader from '@deepseek-ai/cordis-plugin-loader'
  14. import Include from '@deepseek-ai/cordis-plugin-include'
  15. import { createAssistantMessage, createUserMessage } from '@deepseek-ai/dsh-llm'
  16. import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
  17. import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
  18. import * as SessionTurnOutlinePlugin from '@deepseek-ai/dsh-session-turn-outline'
  19. let root: string | undefined
  20. let context: Context | undefined
  21. afterEach(async () => {
  22. await context?.fiber.dispose()
  23. context = undefined
  24. if (root !== undefined) await rm(root, { recursive: true, force: true })
  25. root = undefined
  26. })
  27. async function loadYaml(lines: readonly string[]): Promise<Context> {
  28. root = await mkdtemp(join(tmpdir(), 'dsh-session-turn-outline-loader-'))
  29. const configPath = join(root, 'cordis.yml')
  30. await writeFile(configPath, [...lines, ''].join('\n'))
  31. context = new Context()
  32. context.baseUrl = pathToFileURL(root).href + '/'
  33. await context.plugin(Loader)
  34. context.loader.builtins.include = Include
  35. const modules = new Map<string, unknown>([
  36. ['@deepseek-ai/dsh-session', SessionStore],
  37. ['@deepseek-ai/dsh-session-projection', SessionProjectionRegistry],
  38. ['@deepseek-ai/dsh-session-turn-outline', SessionTurnOutlinePlugin],
  39. ])
  40. context.loader.internal = {
  41. version: 'v2',
  42. async import(specifier: string) {
  43. if (!modules.has(specifier)) throw new Error(`unexpected Loader import: ${specifier}`)
  44. return modules.get(specifier)
  45. },
  46. } as unknown as NonNullable<typeof context.loader.internal>
  47. await context.loader.create({
  48. name: 'cordis:include',
  49. config: { path: pathToFileURL(configPath).href },
  50. })
  51. await context.loader.await()
  52. return context
  53. }
  54. describe('real Loader composition', () => {
  55. it('loads the shipped session-turn-outline YAML shape and serves the outline', async () => {
  56. const loaded = await loadYaml([
  57. "- name: '@deepseek-ai/dsh-session'",
  58. "- name: '@deepseek-ai/dsh-session-projection'",
  59. "- name: '@deepseek-ai/dsh-session-turn-outline'",
  60. ])
  61. const unloaded = [...loaded.loader.entries()]
  62. .filter(entry => entry.fiber === undefined && !entry.disabled)
  63. .map(entry => entry.options.name)
  64. expect(unloaded).toEqual([])
  65. const session = loaded.sessions.create(SessionId('composed'))
  66. const boundary = session.append('turn/start', { turn: 1 }).seq
  67. session.append('user/message', createUserMessage({
  68. content: [{ type: 'text', text: 'composed prompt' }],
  69. source: { kind: 'user' },
  70. }), { surfaceOp: 'append' })
  71. session.append('assistant/message', {
  72. stream: [],
  73. turn: 1,
  74. step: 1,
  75. message: createAssistantMessage({
  76. content: [{ type: 'text', text: 'composed answer' }],
  77. source: { provider: 'deepseek-official', model: 'deepseek-v4-flash' },
  78. }),
  79. }, { surfaceOp: 'append' })
  80. session.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
  81. expect(loaded.sessionProjections.snapshot(session).values.turnOutline)
  82. .toEqual([{ turn: 1, seq: boundary, prompt: 'composed prompt', response: 'composed answer' }])
  83. })
  84. it('keeps the function-plugin namespace free of a default export', () => {
  85. // A default export beside the named form makes the Loader discard the
  86. // namespace (postmortem 0001) — pin its absence.
  87. expect('default' in SessionTurnOutlinePlugin).toBe(false)
  88. })
  89. })