feat(i18n): compose pairing records during merges

This commit is contained in:
Tianyi Cui committed 2026-08-08 21:11:59 +08:00
1 parent 05cbc185f0
commit 542c029c80
13 files changed
+1049 -77

No files matched your search

+86 -5
View File
@@ -1,11 +1,16 @@
/** Regression tests for bilingual snapshots, corpus scope, and structure. */
import { execFileSync, spawnSync } from 'node:child_process'
import { mkdtempSync, rmSync } from 'node:fs'
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { describe, expect, it } from 'vitest'
import { gitBlobHash, storeGitBlob } from './translation-pairing-git.ts'
import { gitBlobHash, readGitIndexBlob, storeGitBlob } from './translation-pairing-git.ts'
import {
parseTranslationPairingRecord,
renderTranslationPairingRecord,
translationPairPaths,
} from './translation-pairing-record.ts'
import {
isTranslationScopeFile,
pairAnchorOfArgument,
@@ -74,6 +79,28 @@ describe('translation pairing snapshots', () => {
}
})
it('reads staged bytes independently of the working tree', () => {
const root = mkdtempSync(join(tmpdir(), 'dsh-translation-pairing-index-'))
try {
execFileSync('git', ['init', '--quiet', root], {
env: { ...process.env, GIT_DEFAULT_HASH: 'sha1' },
})
execFileSync('git', ['-C', root, 'config', 'user.email', 'pairing@example.test'])
execFileSync('git', ['-C', root, 'config', 'user.name', 'Pairing Test'])
writeFileSync(join(root, 'owner.md'), 'staged')
execFileSync('git', ['-C', root, 'add', 'owner.md'])
writeFileSync(join(root, 'owner.md'), 'unstaged')
const indexed = readGitIndexBlob(root, 'owner.md')
expect(indexed?.content.toString('utf8')).toBe('staged')
expect(indexed?.objectId).toBe(gitBlobHash(Buffer.from('staged')))
expect(readGitIndexBlob(root, 'absent.md')).toBeUndefined()
} finally {
rmSync(root, { recursive: true, force: true })
}
})
it.skipIf(!supportsSha256ObjectFormat)('rejects an object format that pairing records cannot represent', () => {
const root = mkdtempSync(join(tmpdir(), 'dsh-translation-pairing-'))
try {
@@ -113,6 +140,32 @@ describe('translation pairing manifest', () => {
})
})
describe('translation pairing records', () => {
const paths = translationPairPaths('docs/foo.md')
const record = {
sourceHash: '1'.repeat(40),
zhHash: '2'.repeat(40),
}
it('round-trips the canonical two-hash record', () => {
expect(parseTranslationPairingRecord(renderTranslationPairingRecord(paths, record), paths)).toEqual(record)
})
it('rejects duplicate or unexpected keys', () => {
expect(parseTranslationPairingRecord([
`foo.md: ${'1'.repeat(40)}`,
`foo.md: ${'3'.repeat(40)}`,
`foo.zh.md: ${'2'.repeat(40)}`,
'',
].join('\n'), paths)).toBeUndefined()
expect(parseTranslationPairingRecord([
`foo.md: ${'1'.repeat(40)}`,
`bar.zh.md: ${'2'.repeat(40)}`,
'',
].join('\n'), paths)).toBeUndefined()
})
})
describe('translation scope discovery', () => {
it.each([
'README.md',
@@ -186,28 +239,56 @@ describe('pair CLI arguments', () => {
it('scopes a check to named pairs and dedupes the three spellings', () => {
expect(parseTranslationPairingCliArgs(['docs/foo.zh.md', 'docs/foo.i18n.yaml', 'docs/bar.md'])).toEqual({
input: 'worktree',
mode: 'check',
scope: 'pairs',
anchors: ['docs/bar.md', 'docs/foo.md'],
})
expect(parseTranslationPairingCliArgs([])).toEqual({ mode: 'check', scope: 'corpus', anchors: [] })
expect(parseTranslationPairingCliArgs([])).toEqual({
input: 'worktree',
mode: 'check',
scope: 'corpus',
anchors: [],
})
})
it('requires --write to name confirmed pairs or opt into --all', () => {
expect(() => parseTranslationPairingCliArgs(['--write'])).toThrow('requires the pair(s) you confirmed')
expect(parseTranslationPairingCliArgs(['--write', 'docs/foo.md'])).toEqual({
input: 'worktree',
mode: 'write',
scope: 'pairs',
anchors: ['docs/foo.md'],
})
expect(parseTranslationPairingCliArgs(['--write', '--all'])).toEqual({ mode: 'write', scope: 'corpus', anchors: [] })
expect(parseTranslationPairingCliArgs(['--write', '--all'])).toEqual({
input: 'worktree',
mode: 'write',
scope: 'corpus',
anchors: [],
})
expect(() => parseTranslationPairingCliArgs(['--write', '--all', 'docs/foo.md'])).toThrow('not both')
})
it('keeps --list corpus-only and rejects unknown flags', () => {
expect(parseTranslationPairingCliArgs(['--list'])).toEqual({ mode: 'list', scope: 'corpus', anchors: [] })
expect(parseTranslationPairingCliArgs(['--list'])).toEqual({
input: 'worktree',
mode: 'list',
scope: 'corpus',
anchors: [],
})
expect(() => parseTranslationPairingCliArgs(['--list', 'docs/foo.md'])).toThrow('takes no other flags or paths')
expect(() => parseTranslationPairingCliArgs(['--all'])).toThrow('--all only applies to --write')
expect(() => parseTranslationPairingCliArgs(['--frobnicate'])).toThrow('unknown flag(s): --frobnicate')
})
it('makes cached verification a named, read-only index check', () => {
expect(parseTranslationPairingCliArgs(['--cached', 'docs/foo.i18n.yaml'])).toEqual({
input: 'index',
mode: 'check',
scope: 'pairs',
anchors: ['docs/foo.md'],
})
expect(() => parseTranslationPairingCliArgs(['--cached'])).toThrow('requires the staged pair paths')
expect(() => parseTranslationPairingCliArgs(['--cached', '--write', 'docs/foo.md'])).toThrow('read-only')
})
})