translation-prompt.spec.ts 3.2 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576
  1. /** Regression tests for the executable translation prompt contract. */
  2. import { describe, expect, it } from 'vitest'
  3. import {
  4. parseTranslationResponse,
  5. renderTranslationPrompt,
  6. renderTranslationResponse,
  7. } from './translation-prompt.ts'
  8. const document = `# Wrapper
  9. ## 模板正文
  10. \`\`\`\`text
  11. {{source_lang}} to {{target_lang}}
  12. {{translation_rules}}
  13. {{terminology}}
  14. [English]({{source_filename}}) | [中文]({{source_filename_zh}})
  15. \`\`\`\`
  16. `
  17. describe('translation prompt rendering', () => {
  18. it('renders every supported placeholder without recursively rewriting injected rules', () => {
  19. const rendered = renderTranslationPrompt(document, {
  20. sourceLanguage: 'English',
  21. sourceFilename: 'guide.md',
  22. translationRules: 'A literal {{source_lang}} in injected rules.',
  23. terminology: '| English | 中文 |',
  24. })
  25. expect(rendered).toContain('English to Chinese')
  26. expect(rendered).toContain('A literal {{source_lang}} in injected rules.')
  27. expect(rendered).toContain('[English](guide.md) | [中文](guide.zh.md)')
  28. })
  29. it('rejects a filename whose suffix contradicts the source language', () => {
  30. expect(() => renderTranslationPrompt(document, {
  31. sourceLanguage: 'Chinese',
  32. sourceFilename: 'guide.md',
  33. translationRules: 'rules',
  34. terminology: 'terms',
  35. })).toThrow('does not match source language Chinese')
  36. })
  37. it('rejects malformed template placeholders before injecting rule contents', () => {
  38. expect(() => renderTranslationPrompt(document.replace('{{source_lang}}', '{{source-lang}}'), {
  39. sourceLanguage: 'English',
  40. sourceFilename: 'guide.md',
  41. translationRules: 'A literal {{source_lang}} in injected rules.',
  42. terminology: '| English | 中文 |',
  43. })).toThrow('template contains malformed placeholder syntax')
  44. })
  45. })
  46. describe('translation response XML', () => {
  47. it('round-trips Markdown and the CDATA terminator', () => {
  48. const response = {
  49. translation: '# Draft\n\nA ]]> marker.',
  50. review: '- [Tone] Fixed.',
  51. final: '# Final\n\nA ]]> marker.',
  52. }
  53. expect(parseTranslationResponse(renderTranslationResponse(response))).toEqual(response)
  54. })
  55. it('rejects missing, reordered, nested, attributed, or non-CDATA children', () => {
  56. expect(() => parseTranslationResponse('<dsh-translation-response version="1"/>')).toThrow('translation, review, and final')
  57. expect(() => parseTranslationResponse('<dsh-translation-response version="1"><review><![CDATA[x]]></review></dsh-translation-response>'))
  58. .toThrow('expected translation, got review')
  59. expect(() => parseTranslationResponse(renderTranslationResponse({ translation: 'x', review: 'y', final: 'z' })
  60. .replace('<translation><![CDATA[x]]></translation>', '<translation><b><![CDATA[x]]></b></translation>')))
  61. .toThrow('nested element b is not allowed')
  62. expect(() => parseTranslationResponse(renderTranslationResponse({ translation: 'x', review: 'y', final: 'z' }).replace('<review>', '<review lang="en">')))
  63. .toThrow('review must not have attributes')
  64. expect(() => parseTranslationResponse(renderTranslationResponse({ translation: 'x', review: 'y', final: 'z' }).replace('<![CDATA[x]]>', 'x')))
  65. .toThrow('all response field content must be inside CDATA')
  66. })
  67. })