loader-composition.spec.ts 3.4 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
  1. import { mkdtemp, rm, writeFile } from 'node:fs/promises'
  2. import { tmpdir } from 'node:os'
  3. import { join } from 'node:path'
  4. import { pathToFileURL } from 'node:url'
  5. import { afterEach, describe, expect, it } from 'vitest'
  6. import { Context } from 'cordis'
  7. import Loader from '@cordisjs/plugin-loader'
  8. import Include from '@cordisjs/plugin-include'
  9. import LlmService from '@deepseek-ai/dsh-llm'
  10. import TokenMeterService from '@deepseek-ai/dsh-token-meter'
  11. import BasicCompactService from '@deepseek-ai/dsh-compact-basic'
  12. let root: string | undefined
  13. let context: Context | undefined
  14. afterEach(async () => {
  15. await context?.fiber.dispose()
  16. context = undefined
  17. if (root !== undefined) await rm(root, { recursive: true, force: true })
  18. root = undefined
  19. })
  20. async function loadYaml(lines: readonly string[]): Promise<Context> {
  21. root = await mkdtemp(join(tmpdir(), 'dsh-token-meter-loader-'))
  22. const configPath = join(root, 'cordis.yml')
  23. await writeFile(configPath, [...lines, ''].join('\n'))
  24. context = new Context()
  25. context.baseUrl = pathToFileURL(root).href + '/'
  26. await context.plugin(Loader)
  27. context.loader.builtins.include = Include
  28. const modules = new Map<string, unknown>([
  29. ['@deepseek-ai/dsh-llm', LlmService],
  30. ['@deepseek-ai/dsh-token-meter', TokenMeterService],
  31. ['@deepseek-ai/dsh-compact-basic', BasicCompactService],
  32. ])
  33. context.loader.internal = {
  34. version: 'v2',
  35. async import(specifier: string) {
  36. if (!modules.has(specifier)) throw new Error(`unexpected Loader import: ${specifier}`)
  37. return modules.get(specifier)
  38. },
  39. } as unknown as NonNullable<typeof context.loader.internal>
  40. await context.loader.create({
  41. name: 'cordis:include',
  42. config: { path: pathToFileURL(configPath).href },
  43. })
  44. await context.loader.await()
  45. return context
  46. }
  47. describe('real Loader composition', () => {
  48. it('loads the flat token-meter and compact-basic YAML shape', async () => {
  49. const loaded = await loadYaml([
  50. "- name: '@deepseek-ai/dsh-llm'",
  51. "- name: '@deepseek-ai/dsh-token-meter'",
  52. ' config:',
  53. ' contextWindow: 4096',
  54. "- name: '@deepseek-ai/dsh-compact-basic'",
  55. ' config:',
  56. ' thresholdRatio: 0.5',
  57. ' retainTokens: 512',
  58. ' auto: false',
  59. ])
  60. const unloaded = [...loaded.loader.entries()]
  61. .filter(entry => entry.fiber === undefined && !entry.disabled)
  62. .map(entry => entry.options.name)
  63. expect(unloaded).toEqual([])
  64. expect(loaded.tokenMeter.contextWindow).toBe(4096)
  65. expect(loaded.get('compact')).toBeInstanceOf(BasicCompactService)
  66. expect((loaded.compact as BasicCompactService).config).toMatchObject({
  67. thresholdRatio: 0.5,
  68. retainTokens: 512,
  69. auto: false,
  70. })
  71. })
  72. it('rejects stale token-meter config after Schemastery normalization', async () => {
  73. context = new Context()
  74. await expect(context.plugin(TokenMeterService, {
  75. models: { legacy: { contextWindow: 4096 } },
  76. } as never)).rejects.toThrow(/TokenMeterConfig: unknown key "models"/)
  77. })
  78. it('rejects stale compact-basic config after Schemastery normalization', async () => {
  79. context = new Context()
  80. await context.plugin(LlmService)
  81. await context.plugin(TokenMeterService)
  82. await expect(context.plugin(BasicCompactService, {
  83. models: { legacy: { thresholdRatio: 0.5 } },
  84. } as never)).rejects.toThrow(/BasicCompactConfig: unknown key "models"/)
  85. })
  86. })