translation-pairing.spec.ts 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. /** Regression tests for the bilingual cutoff and structural signature. */
  2. import { describe, expect, it } from 'vitest'
  3. import {
  4. datedDocumentDate,
  5. isIsoDate,
  6. isTranslationScopeFile,
  7. parseTranslationMarkdown,
  8. parseTranslationPairingManifest,
  9. requiresPairByDate,
  10. requiresTranslationPair,
  11. translationDocumentClass,
  12. translationStructureDiff,
  13. translationStructureSignature,
  14. } from './translation-pairing.ts'
  15. function signature(markdown: string) {
  16. return translationStructureSignature(parseTranslationMarkdown(markdown), 'counterpart.zh.md')
  17. }
  18. describe('translation pairing manifest', () => {
  19. it('accepts a real ISO cutoff and string-array fields', () => {
  20. expect(parseTranslationPairingManifest(JSON.stringify({
  21. requiredSince: '2026-07-14',
  22. required: ['README.md'],
  23. requiredClasses: ['non-readme'],
  24. excluded: ['docs/generated/'],
  25. }))).toEqual({
  26. requiredSince: '2026-07-14',
  27. required: ['README.md'],
  28. requiredClasses: ['non-readme'],
  29. excluded: ['docs/generated/'],
  30. })
  31. })
  32. it.each(['2026-7-14', '2026-02-29', '2026-13-01', 'not-a-date'])('rejects invalid cutoff %s', (cutoff) => {
  33. expect(isIsoDate(cutoff)).toBe(false)
  34. expect(() => parseTranslationPairingManifest(JSON.stringify({
  35. requiredSince: cutoff,
  36. required: [],
  37. requiredClasses: [],
  38. excluded: [],
  39. }))).toThrow('requiredSince must be a valid YYYY-MM-DD date')
  40. })
  41. it('rejects non-string manifest arrays', () => {
  42. expect(() => parseTranslationPairingManifest(JSON.stringify({
  43. requiredSince: '2026-07-14',
  44. required: [42],
  45. requiredClasses: [],
  46. excluded: [],
  47. }))).toThrow('required must be an array of strings')
  48. })
  49. it('rejects unknown and duplicate document classes', () => {
  50. const manifest = (requiredClasses: string[]) => JSON.stringify({
  51. requiredSince: '2026-07-14',
  52. required: [],
  53. requiredClasses,
  54. excluded: [],
  55. })
  56. expect(() => parseTranslationPairingManifest(manifest(['guide']))).toThrow('requiredClasses must contain only')
  57. expect(() => parseTranslationPairingManifest(manifest(['readme', 'readme']))).toThrow('requiredClasses must not contain duplicates')
  58. })
  59. })
  60. describe('document-class pairing frontier', () => {
  61. const manifest = parseTranslationPairingManifest(JSON.stringify({
  62. requiredSince: '2026-07-14',
  63. required: ['docs/legacy/README.md'],
  64. requiredClasses: ['non-readme'],
  65. excluded: [],
  66. }))
  67. it('classifies README basenames case-insensitively', () => {
  68. expect(translationDocumentClass('packages/core/README.md')).toBe('readme')
  69. expect(translationDocumentClass('missions/readme.md')).toBe('readme')
  70. expect(translationDocumentClass('docs/readme-guide.md')).toBe('non-readme')
  71. })
  72. it('requires every non-README while retaining explicit README entries', () => {
  73. expect(requiresTranslationPair('docs/guide.md', manifest)).toBe(true)
  74. expect(requiresTranslationPair('docs/legacy/README.md', manifest)).toBe(true)
  75. expect(requiresTranslationPair('docs/new/README.md', manifest)).toBe(false)
  76. })
  77. it('requires both document classes after the README frontier closes', () => {
  78. const closed = parseTranslationPairingManifest(JSON.stringify({
  79. ...manifest,
  80. requiredClasses: ['non-readme', 'readme'],
  81. }))
  82. expect(requiresTranslationPair('docs/guide.md', closed)).toBe(true)
  83. expect(requiresTranslationPair('future/subtree/README.md', closed)).toBe(true)
  84. })
  85. })
  86. describe('translation scope discovery', () => {
  87. it.each([
  88. 'README.md',
  89. 'apps/cli/README.md',
  90. 'future/subtree/readme.md',
  91. 'packages/example/README.zh.md',
  92. 'native/example/README.i18n.yaml',
  93. '.agents/notes/proposed/feature.md',
  94. 'docs/guide.md',
  95. 'python/guide.md',
  96. ])('includes %s', (file) => {
  97. expect(isTranslationScopeFile(file)).toBe(true)
  98. })
  99. it.each([
  100. 'packages/example/guide.md',
  101. 'examples/tutorial.md',
  102. 'website/reference.md',
  103. 'packages/example/README.txt',
  104. 'vendor/example/README.md',
  105. 'packages/example/node_modules/dependency/README.md',
  106. 'packages/example/lib/README.md',
  107. 'coverage/report/README.md',
  108. 'python/sdk-runtime/src/deepseek_harness_runtime/runtime/dsh-jsonrpc-agent-macos-arm64/README.md',
  109. 'python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/README.md',
  110. ])('excludes non-source or non-README path %s', (file) => {
  111. expect(isTranslationScopeFile(file)).toBe(false)
  112. })
  113. })
  114. describe('date-based pairing frontier', () => {
  115. const cutoff = '2026-07-14'
  116. it('enforces the cutoff day and every later day, but not the preceding day', () => {
  117. expect(requiresPairByDate('.agents/notes/2026-07-13-before.md', cutoff)).toBe(false)
  118. expect(requiresPairByDate('.agents/notes/2026-07-14-at-cutoff.md', cutoff)).toBe(true)
  119. expect(requiresPairByDate('.agents/notes/2026-07-15-after.md', cutoff)).toBe(true)
  120. })
  121. it('matches only a date at the start of the basename', () => {
  122. expect(datedDocumentDate('.agents/notes/2026-07-14-proposal.md')).toBe('2026-07-14')
  123. expect(datedDocumentDate('docs/release-notes-2026-07-14-alpha.md')).toBeUndefined()
  124. expect(requiresPairByDate('docs/release-notes-2026-07-14-alpha.md', cutoff)).toBe(false)
  125. })
  126. })
  127. describe('translation structural signature', () => {
  128. it('accepts matching list kinds, starts, and item counts', () => {
  129. const source = signature('3. One\n4. Two\n\n- A\n- B\n')
  130. const counterpart = signature('3. 一\n4. 二\n\n- 甲\n- 乙\n')
  131. expect(translationStructureDiff(source, counterpart)).toEqual([])
  132. })
  133. it('rejects an altered ordered-list start', () => {
  134. const source = signature('3. One\n4. Two\n\n- A\n- B\n')
  135. const counterpart = signature('1. 一\n2. 二\n\n- 甲\n- 乙\n')
  136. expect(translationStructureDiff(source, counterpart)).toEqual([
  137. 'list (kind, start, item count) #1 diverges between the pair: "ordered:start=3:items=2" vs "ordered:start=1:items=2"',
  138. ])
  139. })
  140. it('rejects a missing list item', () => {
  141. const source = signature('- A\n- B\n')
  142. const counterpart = signature('- 甲\n')
  143. expect(translationStructureDiff(source, counterpart)).toEqual([
  144. 'list (kind, start, item count) #1 diverges between the pair: "bullet:items=2" vs "bullet:items=1"',
  145. ])
  146. })
  147. it('rejects altered table row or column counts', () => {
  148. const source = signature('| A | B |\n|---|---|\n| 1 | 2 |\n| 3 | 4 |\n')
  149. const counterpart = signature('| 甲 | 乙 |\n|---|---|\n| 一 | 二 |\n')
  150. expect(translationStructureDiff(source, counterpart)).toEqual([
  151. 'table (row x column count) #1 diverges between the pair: "3x2" vs "2x2"',
  152. ])
  153. })
  154. })