translation-pairing.spec.ts 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
  1. /** Regression tests for the bilingual corpus scope and structural signature. */
  2. import { describe, expect, it } from 'vitest'
  3. import {
  4. isTranslationScopeFile,
  5. pairAnchorOfArgument,
  6. parseTranslationMarkdown,
  7. parseTranslationPairingCliArgs,
  8. parseTranslationPairingManifest,
  9. translationStructureDiff,
  10. translationStructureSignature,
  11. } from './translation-pairing.ts'
  12. function signature(markdown: string) {
  13. return translationStructureSignature(parseTranslationMarkdown(markdown), 'counterpart.zh.md')
  14. }
  15. describe('translation pairing manifest', () => {
  16. it('accepts an exclusions-only manifest', () => {
  17. expect(parseTranslationPairingManifest(JSON.stringify({
  18. excluded: ['docs/generated/'],
  19. }))).toEqual({
  20. excluded: ['docs/generated/'],
  21. })
  22. })
  23. it.each([
  24. ['required', ['packages/README.md']],
  25. ['requiredClasses', ['readme']],
  26. ['requiredSince', '2026-07-14'],
  27. ] as const)('rejects obsolete policy field %s instead of accepting an inert requirement', (field, value) => {
  28. expect(() => parseTranslationPairingManifest(JSON.stringify({
  29. excluded: [],
  30. [field]: value,
  31. }))).toThrow(`unsupported field(s): ${field}; every in-scope document is required`)
  32. })
  33. it('rejects a missing or non-string exclusion list', () => {
  34. expect(() => parseTranslationPairingManifest('{}')).toThrow('excluded must be an array of strings')
  35. expect(() => parseTranslationPairingManifest(JSON.stringify({
  36. excluded: [42],
  37. }))).toThrow('excluded must be an array of strings')
  38. })
  39. })
  40. describe('translation scope discovery', () => {
  41. it.each([
  42. 'README.md',
  43. 'apps/cli/README.md',
  44. 'future/subtree/readme.md',
  45. 'packages/example/README.zh.md',
  46. 'native/example/README.i18n.yaml',
  47. '.agents/notes/proposed/feature.md',
  48. 'docs/guide.md',
  49. 'python/guide.md',
  50. ])('includes %s', (file) => {
  51. expect(isTranslationScopeFile(file)).toBe(true)
  52. })
  53. it.each([
  54. 'packages/example/guide.md',
  55. 'examples/tutorial.md',
  56. 'website/reference.md',
  57. 'packages/example/README.txt',
  58. 'vendor/example/README.md',
  59. 'packages/example/node_modules/dependency/README.md',
  60. 'packages/example/lib/README.md',
  61. 'coverage/report/README.md',
  62. 'python/sdk-runtime/src/deepseek_harness_runtime/runtime/dsh-jsonrpc-agent-macos-arm64/README.md',
  63. 'python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/README.md',
  64. ])('excludes non-source or non-README path %s', (file) => {
  65. expect(isTranslationScopeFile(file)).toBe(false)
  66. })
  67. })
  68. describe('translation structural signature', () => {
  69. it('accepts matching list kinds, starts, and item counts', () => {
  70. const source = signature('3. One\n4. Two\n\n- A\n- B\n')
  71. const counterpart = signature('3. 一\n4. 二\n\n- 甲\n- 乙\n')
  72. expect(translationStructureDiff(source, counterpart)).toEqual([])
  73. })
  74. it('rejects an altered ordered-list start', () => {
  75. const source = signature('3. One\n4. Two\n\n- A\n- B\n')
  76. const counterpart = signature('1. 一\n2. 二\n\n- 甲\n- 乙\n')
  77. expect(translationStructureDiff(source, counterpart)).toEqual([
  78. 'list (kind, start, item count) #1 diverges between the pair: "ordered:start=3:items=2" vs "ordered:start=1:items=2"',
  79. ])
  80. })
  81. it('rejects a missing list item', () => {
  82. const source = signature('- A\n- B\n')
  83. const counterpart = signature('- 甲\n')
  84. expect(translationStructureDiff(source, counterpart)).toEqual([
  85. 'list (kind, start, item count) #1 diverges between the pair: "bullet:items=2" vs "bullet:items=1"',
  86. ])
  87. })
  88. it('rejects altered table row or column counts', () => {
  89. const source = signature('| A | B |\n|---|---|\n| 1 | 2 |\n| 3 | 4 |\n')
  90. const counterpart = signature('| 甲 | 乙 |\n|---|---|\n| 一 | 二 |\n')
  91. expect(translationStructureDiff(source, counterpart)).toEqual([
  92. 'table (row x column count) #1 diverges between the pair: "3x2" vs "2x2"',
  93. ])
  94. })
  95. })
  96. describe('pair CLI arguments', () => {
  97. it('normalizes any pair file or bare stem to the English anchor', () => {
  98. expect(pairAnchorOfArgument('docs/foo.md')).toBe('docs/foo.md')
  99. expect(pairAnchorOfArgument('docs/foo.zh.md')).toBe('docs/foo.md')
  100. expect(pairAnchorOfArgument('docs/foo.i18n.yaml')).toBe('docs/foo.md')
  101. expect(pairAnchorOfArgument('docs/foo')).toBe('docs/foo.md')
  102. expect(pairAnchorOfArgument('.\\docs\\foo.zh.md')).toBe('docs/foo.md')
  103. })
  104. it('scopes a check to named pairs and dedupes the three spellings', () => {
  105. expect(parseTranslationPairingCliArgs(['docs/foo.zh.md', 'docs/foo.i18n.yaml', 'docs/bar.md'])).toEqual({
  106. mode: 'check',
  107. scope: 'pairs',
  108. anchors: ['docs/bar.md', 'docs/foo.md'],
  109. })
  110. expect(parseTranslationPairingCliArgs([])).toEqual({ mode: 'check', scope: 'corpus', anchors: [] })
  111. })
  112. it('requires --write to name confirmed pairs or opt into --all', () => {
  113. expect(() => parseTranslationPairingCliArgs(['--write'])).toThrow('requires the pair(s) you confirmed')
  114. expect(parseTranslationPairingCliArgs(['--write', 'docs/foo.md'])).toEqual({
  115. mode: 'write',
  116. scope: 'pairs',
  117. anchors: ['docs/foo.md'],
  118. })
  119. expect(parseTranslationPairingCliArgs(['--write', '--all'])).toEqual({ mode: 'write', scope: 'corpus', anchors: [] })
  120. expect(() => parseTranslationPairingCliArgs(['--write', '--all', 'docs/foo.md'])).toThrow('not both')
  121. })
  122. it('keeps --list corpus-only and rejects unknown flags', () => {
  123. expect(parseTranslationPairingCliArgs(['--list'])).toEqual({ mode: 'list', scope: 'corpus', anchors: [] })
  124. expect(() => parseTranslationPairingCliArgs(['--list', 'docs/foo.md'])).toThrow('takes no other flags or paths')
  125. expect(() => parseTranslationPairingCliArgs(['--all'])).toThrow('--all only applies to --write')
  126. expect(() => parseTranslationPairingCliArgs(['--frobnicate'])).toThrow('unknown flag(s): --frobnicate')
  127. })
  128. })