174 lines
6.5 KiB
TypeScript
174 lines
6.5 KiB
TypeScript
/** Regression tests for the bilingual cutoff and structural signature. */
|
|
|
|
import { describe, expect, it } from 'vitest'
|
|
import {
|
|
datedDocumentDate,
|
|
isIsoDate,
|
|
isTranslationScopeFile,
|
|
parseTranslationMarkdown,
|
|
parseTranslationPairingManifest,
|
|
requiresPairByDate,
|
|
requiresTranslationPair,
|
|
translationDocumentClass,
|
|
translationStructureDiff,
|
|
translationStructureSignature,
|
|
} from './translation-pairing.ts'
|
|
|
|
function signature(markdown: string) {
|
|
return translationStructureSignature(parseTranslationMarkdown(markdown), 'counterpart.zh.md')
|
|
}
|
|
|
|
describe('translation pairing manifest', () => {
|
|
it('accepts a real ISO cutoff and string-array fields', () => {
|
|
expect(parseTranslationPairingManifest(JSON.stringify({
|
|
requiredSince: '2026-07-14',
|
|
required: ['README.md'],
|
|
requiredClasses: ['non-readme'],
|
|
excluded: ['docs/generated/'],
|
|
}))).toEqual({
|
|
requiredSince: '2026-07-14',
|
|
required: ['README.md'],
|
|
requiredClasses: ['non-readme'],
|
|
excluded: ['docs/generated/'],
|
|
})
|
|
})
|
|
|
|
it.each(['2026-7-14', '2026-02-29', '2026-13-01', 'not-a-date'])('rejects invalid cutoff %s', (cutoff) => {
|
|
expect(isIsoDate(cutoff)).toBe(false)
|
|
expect(() => parseTranslationPairingManifest(JSON.stringify({
|
|
requiredSince: cutoff,
|
|
required: [],
|
|
requiredClasses: [],
|
|
excluded: [],
|
|
}))).toThrow('requiredSince must be a valid YYYY-MM-DD date')
|
|
})
|
|
|
|
it('rejects non-string manifest arrays', () => {
|
|
expect(() => parseTranslationPairingManifest(JSON.stringify({
|
|
requiredSince: '2026-07-14',
|
|
required: [42],
|
|
requiredClasses: [],
|
|
excluded: [],
|
|
}))).toThrow('required must be an array of strings')
|
|
})
|
|
|
|
it('rejects unknown and duplicate document classes', () => {
|
|
const manifest = (requiredClasses: string[]) => JSON.stringify({
|
|
requiredSince: '2026-07-14',
|
|
required: [],
|
|
requiredClasses,
|
|
excluded: [],
|
|
})
|
|
expect(() => parseTranslationPairingManifest(manifest(['guide']))).toThrow('requiredClasses must contain only')
|
|
expect(() => parseTranslationPairingManifest(manifest(['readme', 'readme']))).toThrow('requiredClasses must not contain duplicates')
|
|
})
|
|
})
|
|
|
|
describe('document-class pairing frontier', () => {
|
|
const manifest = parseTranslationPairingManifest(JSON.stringify({
|
|
requiredSince: '2026-07-14',
|
|
required: ['docs/legacy/README.md'],
|
|
requiredClasses: ['non-readme'],
|
|
excluded: [],
|
|
}))
|
|
|
|
it('classifies README basenames case-insensitively', () => {
|
|
expect(translationDocumentClass('packages/core/README.md')).toBe('readme')
|
|
expect(translationDocumentClass('missions/readme.md')).toBe('readme')
|
|
expect(translationDocumentClass('docs/readme-guide.md')).toBe('non-readme')
|
|
})
|
|
|
|
it('requires every non-README while retaining explicit README entries', () => {
|
|
expect(requiresTranslationPair('docs/guide.md', manifest)).toBe(true)
|
|
expect(requiresTranslationPair('docs/legacy/README.md', manifest)).toBe(true)
|
|
expect(requiresTranslationPair('docs/new/README.md', manifest)).toBe(false)
|
|
})
|
|
|
|
it('requires both document classes after the README frontier closes', () => {
|
|
const closed = parseTranslationPairingManifest(JSON.stringify({
|
|
...manifest,
|
|
requiredClasses: ['non-readme', 'readme'],
|
|
}))
|
|
expect(requiresTranslationPair('docs/guide.md', closed)).toBe(true)
|
|
expect(requiresTranslationPair('future/subtree/README.md', closed)).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe('translation scope discovery', () => {
|
|
it.each([
|
|
'README.md',
|
|
'apps/cli/README.md',
|
|
'future/subtree/readme.md',
|
|
'packages/example/README.zh.md',
|
|
'native/example/README.i18n.yaml',
|
|
'.agents/notes/proposed/feature.md',
|
|
'docs/guide.md',
|
|
'python/guide.md',
|
|
])('includes %s', (file) => {
|
|
expect(isTranslationScopeFile(file)).toBe(true)
|
|
})
|
|
|
|
it.each([
|
|
'packages/example/guide.md',
|
|
'examples/tutorial.md',
|
|
'website/reference.md',
|
|
'packages/example/README.txt',
|
|
'vendor/example/README.md',
|
|
'packages/example/node_modules/dependency/README.md',
|
|
'packages/example/lib/README.md',
|
|
'coverage/report/README.md',
|
|
'python/sdk-runtime/src/deepseek_harness_runtime/runtime/dsh-jsonrpc-agent-macos-arm64/README.md',
|
|
'python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/README.md',
|
|
])('excludes non-source or non-README path %s', (file) => {
|
|
expect(isTranslationScopeFile(file)).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('date-based pairing frontier', () => {
|
|
const cutoff = '2026-07-14'
|
|
|
|
it('enforces the cutoff day and every later day, but not the preceding day', () => {
|
|
expect(requiresPairByDate('.agents/notes/2026-07-13-before.md', cutoff)).toBe(false)
|
|
expect(requiresPairByDate('.agents/notes/2026-07-14-at-cutoff.md', cutoff)).toBe(true)
|
|
expect(requiresPairByDate('.agents/notes/2026-07-15-after.md', cutoff)).toBe(true)
|
|
})
|
|
|
|
it('matches only a date at the start of the basename', () => {
|
|
expect(datedDocumentDate('.agents/notes/2026-07-14-proposal.md')).toBe('2026-07-14')
|
|
expect(datedDocumentDate('docs/release-notes-2026-07-14-alpha.md')).toBeUndefined()
|
|
expect(requiresPairByDate('docs/release-notes-2026-07-14-alpha.md', cutoff)).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('translation structural signature', () => {
|
|
it('accepts matching list kinds, starts, and item counts', () => {
|
|
const source = signature('3. One\n4. Two\n\n- A\n- B\n')
|
|
const counterpart = signature('3. 一\n4. 二\n\n- 甲\n- 乙\n')
|
|
expect(translationStructureDiff(source, counterpart)).toEqual([])
|
|
})
|
|
|
|
it('rejects an altered ordered-list start', () => {
|
|
const source = signature('3. One\n4. Two\n\n- A\n- B\n')
|
|
const counterpart = signature('1. 一\n2. 二\n\n- 甲\n- 乙\n')
|
|
expect(translationStructureDiff(source, counterpart)).toEqual([
|
|
'list (kind, start, item count) #1 diverges between the pair: "ordered:start=3:items=2" vs "ordered:start=1:items=2"',
|
|
])
|
|
})
|
|
|
|
it('rejects a missing list item', () => {
|
|
const source = signature('- A\n- B\n')
|
|
const counterpart = signature('- 甲\n')
|
|
expect(translationStructureDiff(source, counterpart)).toEqual([
|
|
'list (kind, start, item count) #1 diverges between the pair: "bullet:items=2" vs "bullet:items=1"',
|
|
])
|
|
})
|
|
|
|
it('rejects altered table row or column counts', () => {
|
|
const source = signature('| A | B |\n|---|---|\n| 1 | 2 |\n| 3 | 4 |\n')
|
|
const counterpart = signature('| 甲 | 乙 |\n|---|---|\n| 一 | 二 |\n')
|
|
expect(translationStructureDiff(source, counterpart)).toEqual([
|
|
'table (row x column count) #1 diverges between the pair: "3x2" vs "2x2"',
|
|
])
|
|
})
|
|
})
|