2026-07-16 00:19:25 -07:00
|
|
|
/** Unit tests for the prompt-v4 renderer and three-section response parser. */
|
2026-07-14 23:09:01 +08:00
|
|
|
|
2026-07-16 00:19:25 -07:00
|
|
|
import { readFileSync } from 'node:fs'
|
|
|
|
|
import { join, resolve } from 'node:path'
|
2026-07-14 23:09:01 +08:00
|
|
|
import { describe, expect, it } from 'vitest'
|
|
|
|
|
import {
|
|
|
|
|
parseTranslationResponse,
|
|
|
|
|
renderTranslationPrompt,
|
|
|
|
|
renderTranslationResponse,
|
|
|
|
|
} from './translation-prompt.ts'
|
|
|
|
|
|
2026-07-16 00:19:25 -07:00
|
|
|
const root = resolve(import.meta.dirname, '..')
|
|
|
|
|
const document = readFileSync(join(root, 'docs/i18n/translation-prompt.md'), 'utf8')
|
|
|
|
|
const terminology = '| English | 中文 |\n|---|---|\n| agent | agent |'
|
2026-07-14 23:09:01 +08:00
|
|
|
|
|
|
|
|
describe('translation prompt rendering', () => {
|
2026-07-16 00:19:25 -07:00
|
|
|
it('renders both directions with every placeholder resolved', () => {
|
|
|
|
|
const en = renderTranslationPrompt(document, { sourceLanguage: 'English', terminology })
|
|
|
|
|
expect(en).toContain('from English to Chinese')
|
|
|
|
|
expect(en).toContain(terminology)
|
|
|
|
|
expect(en).not.toContain('{{')
|
2026-07-23 21:44:41 +08:00
|
|
|
expect(en).toContain('plain source stays plain (必须)')
|
|
|
|
|
expect(en).toContain('When the target language is English, use the "English" column without a Chinese gloss')
|
2026-07-23 21:55:07 +08:00
|
|
|
expect(en).toContain('for a Chinese target, use an established Chinese rendering')
|
|
|
|
|
expect(en).toContain('for an English target, use the established English technical term')
|
|
|
|
|
expect(en).toContain('does an English target use established English terminology')
|
2026-07-23 21:44:41 +08:00
|
|
|
expect(en).toContain('The parser removes exactly one framing escape')
|
2026-07-16 00:19:25 -07:00
|
|
|
const zh = renderTranslationPrompt(document, { sourceLanguage: 'Chinese', terminology })
|
|
|
|
|
expect(zh).toContain('from Chinese to English')
|
2026-07-14 23:09:01 +08:00
|
|
|
})
|
2026-07-15 20:39:24 +08:00
|
|
|
|
2026-07-16 00:19:25 -07:00
|
|
|
it('rejects a template with unknown or missing placeholders', () => {
|
|
|
|
|
const alien = document.replaceAll('{{terminology}}', '{{terms_prompt}}')
|
|
|
|
|
expect(() => renderTranslationPrompt(alien, { sourceLanguage: 'English', terminology })).toThrow(/unsupported placeholder/)
|
|
|
|
|
const missing = document.replaceAll('{{terminology}}', '')
|
|
|
|
|
expect(() => renderTranslationPrompt(missing, { sourceLanguage: 'English', terminology })).toThrow(/required placeholder/)
|
2026-07-15 20:39:24 +08:00
|
|
|
})
|
2026-07-14 23:09:01 +08:00
|
|
|
})
|
|
|
|
|
|
2026-07-16 00:19:25 -07:00
|
|
|
describe('translation response sections', () => {
|
|
|
|
|
it('round-trips Markdown bodies', () => {
|
|
|
|
|
const response = { translation: '# 标题\n\n正文 **加粗**。', review: '- [Tone] 修正一处。\n- 无修正', final: '# 标题\n\n定稿。' }
|
2026-07-14 23:09:01 +08:00
|
|
|
expect(parseTranslationResponse(renderTranslationResponse(response))).toEqual(response)
|
|
|
|
|
})
|
|
|
|
|
|
2026-07-16 00:19:25 -07:00
|
|
|
it('tolerates a fenced xml wrapper around the whole response', () => {
|
|
|
|
|
const fenced = '```xml\n<translation>\nA\n</translation>\n\n<review>\n- 无修正\n</review>\n\n<final>\nA\n</final>\n```'
|
|
|
|
|
expect(parseTranslationResponse(fenced).final).toBe('A')
|
|
|
|
|
})
|
|
|
|
|
|
2026-07-20 19:46:46 -07:00
|
|
|
it('keeps an inline close tag inside prose from terminating the section', () => {
|
|
|
|
|
const doc = { translation: 'the wire format uses </translation> as its close tag', review: '- 无修正', final: 'F' }
|
|
|
|
|
expect(parseTranslationResponse(renderTranslationResponse(doc))).toEqual(doc)
|
|
|
|
|
})
|
|
|
|
|
|
2026-07-23 21:44:41 +08:00
|
|
|
it('round-trips wrapper-tag lines inside Markdown bodies', () => {
|
|
|
|
|
const doc = {
|
|
|
|
|
translation: '```xml\n</translation>\n```',
|
|
|
|
|
review: '- [Structure] Preserved `<final>` on its own line.',
|
|
|
|
|
final: 'literal delimiters\n</final>\n\\</final>',
|
|
|
|
|
}
|
|
|
|
|
const rendered = renderTranslationResponse(doc)
|
|
|
|
|
expect(parseTranslationResponse(rendered)).toEqual(doc)
|
|
|
|
|
expect(() => parseTranslationResponse(rendered.replace('\\</translation>', '</translation>'))).toThrow(/duplicate <translation>/)
|
|
|
|
|
})
|
|
|
|
|
|
2026-07-20 19:46:46 -07:00
|
|
|
it('rejects a duplicate section appearing before final', () => {
|
|
|
|
|
const early = '<translation>\nA\n</translation>\n<translation>\nB\n</translation>\n<review>\nR\n</review>\n<final>\nF\n</final>'
|
|
|
|
|
expect(() => parseTranslationResponse(early)).toThrow(/duplicate <translation>/)
|
|
|
|
|
})
|
|
|
|
|
|
2026-07-16 00:19:25 -07:00
|
|
|
it('rejects missing, unterminated, or duplicated sections', () => {
|
2026-07-20 19:46:46 -07:00
|
|
|
expect(() => parseTranslationResponse('<translation>\nA\n</translation>')).toThrow(/missing or unterminated <review>/)
|
|
|
|
|
expect(() => parseTranslationResponse('<translation>\nA')).toThrow(/missing or unterminated <translation>/)
|
2026-07-16 00:19:25 -07:00
|
|
|
const dup = '<translation>\nA\n</translation>\n<review>\nR\n</review>\n<final>\nF\n</final>\n<final>\nG\n</final>'
|
|
|
|
|
expect(() => parseTranslationResponse(dup)).toThrow(/duplicate <final>/)
|
2026-07-23 21:44:41 +08:00
|
|
|
expect(() => parseTranslationResponse(`${renderTranslationResponse({ translation: 'A', review: 'R', final: 'F' })}\nstray`))
|
|
|
|
|
.toThrow(/content is not allowed outside/)
|
2026-07-14 23:09:01 +08:00
|
|
|
})
|
|
|
|
|
})
|