mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-08-29 04:26:38 +00:00
fix(i18n): resolve merge and Markdown edge cases
This commit is contained in:
@@ -27,6 +27,8 @@ function fixture(): string {
|
||||
writeFileSync(join(root, 'docs/guide.zh.md'), '# 指南\n')
|
||||
writeFileSync(join(root, 'docs/reference.md'), '# Overview\n')
|
||||
writeFileSync(join(root, 'docs/reference.zh.md'), '# 概览\n')
|
||||
writeFileSync(join(root, 'docs/a#b.md'), '# Reserved\n')
|
||||
writeFileSync(join(root, 'docs/a#b.zh.md'), '# 保留字符\n')
|
||||
writeFileSync(join(root, 'docs/unpaired.md'), '# Only\n')
|
||||
writeFileSync(join(root, 'docs/section/index.md'), '# Section\n')
|
||||
writeFileSync(join(root, 'docs/section/index.zh.md'), '# 章节\n')
|
||||
@@ -85,6 +87,15 @@ describe('translation link locale validation', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps URL-reserved filename bytes escaped in an encoded exact path', () => {
|
||||
const root = fixture()
|
||||
const input = '[保留](a%23b%2Emd?view=full#section)\n'
|
||||
expect(rewriteTranslationLinkLocales(input, linkContext(root, 'docs/guide.zh.md'))).toEqual({
|
||||
content: '[保留](a%23b.zh.md?view=full#section)\n',
|
||||
rewritten: 1,
|
||||
})
|
||||
})
|
||||
|
||||
it('accepts the target-locale sibling and an out-of-scope target with its own sibling', () => {
|
||||
const root = fixture()
|
||||
expect(translationLinkLocaleViolations(
|
||||
@@ -190,6 +201,14 @@ describe('translation link rewriting and normalization', () => {
|
||||
).content).toBe('[概览][ref]\n\n[ref]: <reference.zh.md#overview> "title"\n')
|
||||
})
|
||||
|
||||
it('uses only the first duplicate reference definition', () => {
|
||||
const root = fixture()
|
||||
expect(translationLinkLocaleViolations(
|
||||
'[概览][ref]\n\n[ref]: reference.zh.md\n[ref]: reference.md\n',
|
||||
linkContext(root, 'docs/guide.zh.md'),
|
||||
)).toEqual([])
|
||||
})
|
||||
|
||||
it('does not treat an image-only definition as a document link', () => {
|
||||
const root = fixture()
|
||||
const input = '![preview][asset]\n\n[asset]: reference.zh.md#overview\n'
|
||||
|
||||
@@ -149,7 +149,7 @@ function relativeExpectedPath(
|
||||
rawPath: string,
|
||||
): string {
|
||||
const relative = posix.relative(posix.dirname(context.sourcePath), expectedPath)
|
||||
const encoded = encodeURI(relative)
|
||||
const encoded = relative.split('/').map(segment => encodeURIComponent(segment)).join('/')
|
||||
return rawPath.startsWith('./') && !encoded.startsWith('.') ? `./${encoded}` : encoded
|
||||
}
|
||||
|
||||
@@ -227,12 +227,18 @@ function visitDocumentLinkNodes(
|
||||
const tree = parseMarkdown(markdown)
|
||||
const switcherOffset = languageSwitcherLinkOffset(tree, markdown, skipTargets)
|
||||
const referencedIdentifiers = new Set<string>()
|
||||
const visitedDefinitions = new Set<string>()
|
||||
visitMarkdown(tree, (node) => {
|
||||
if (node.type === 'linkReference') referencedIdentifiers.add(node.identifier)
|
||||
})
|
||||
visitMarkdown(tree, (node) => {
|
||||
if (node.type === 'link' && node.position?.start.offset === switcherOffset) return
|
||||
if (node.type === 'link' || (node.type === 'definition' && referencedIdentifiers.has(node.identifier))) {
|
||||
if (node.type === 'link') {
|
||||
visitor(node)
|
||||
} else if (node.type === 'definition'
|
||||
&& referencedIdentifiers.has(node.identifier)
|
||||
&& !visitedDefinitions.has(node.identifier)) {
|
||||
visitedDefinitions.add(node.identifier)
|
||||
visitor(node)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -63,6 +63,29 @@ export function gitIndexPaths(root: string): Set<string> {
|
||||
return paths
|
||||
}
|
||||
|
||||
/**
|
||||
* Paths visible to a custom merge driver from the current index plus every
|
||||
* merge head Git advertises through `GITHEAD_<oid>` environment entries.
|
||||
*
|
||||
* Git invokes custom drivers before it writes clean additions from the other
|
||||
* heads into stage zero. The explicit post-conflict resolver has no GITHEAD
|
||||
* entries and therefore uses the already-merged index alone.
|
||||
*/
|
||||
export function gitMergeInputPaths(root: string, environment: NodeJS.ProcessEnv = process.env): Set<string> {
|
||||
const paths = gitIndexPaths(root)
|
||||
const heads = Object.keys(environment)
|
||||
.flatMap(key => /^GITHEAD_([0-9a-f]{40})$/.exec(key)?.[1] ?? [])
|
||||
.sort()
|
||||
for (const head of heads) {
|
||||
const files = runGit(root, ['ls-tree', '-r', '--name-only', '-z', head], `listing merge-head ${head} paths`)
|
||||
.toString('utf8')
|
||||
.split('\0')
|
||||
.filter(Boolean)
|
||||
for (const file of files) paths.add(file)
|
||||
}
|
||||
return paths
|
||||
}
|
||||
|
||||
/**
|
||||
* Read one path from the Git index without consulting working-tree bytes.
|
||||
*
|
||||
|
||||
@@ -491,6 +491,49 @@ describe('translation pairing merge composition', { timeout: 15_000 }, () => {
|
||||
expectMergedPair(fixture)
|
||||
})
|
||||
|
||||
it('sees a paired link target added by the other branch', () => {
|
||||
const fixture = createFixture()
|
||||
commitPair(fixture, baseSource, baseZh, 'base')
|
||||
git(fixture, ['switch', '-c', 'current'])
|
||||
commitPair(fixture, currentSource, currentZh, 'current guide')
|
||||
git(fixture, ['switch', 'master'])
|
||||
record(
|
||||
fixture.root,
|
||||
'docs/guide.md',
|
||||
baseSource.replace('Beta base.', '[Reference](reference.md#overview)'),
|
||||
baseZh.replace('乙基础。', '[参考](reference.zh.md#overview)'),
|
||||
)
|
||||
record(
|
||||
fixture.root,
|
||||
'docs/reference.md',
|
||||
'# Reference\n\nEnglish | [中文](reference.zh.md)\n\nOverview.\n',
|
||||
'# 参考\n\n[English](reference.md) | 中文\n\n概览。\n',
|
||||
)
|
||||
git(fixture, ['add', '.'])
|
||||
git(fixture, ['commit', '-m', 'other guide and target'])
|
||||
git(fixture, ['switch', 'current'])
|
||||
installFixtureRuntime(fixture.root)
|
||||
git(fixture, [
|
||||
'config',
|
||||
'merge.dsh-translation-pairing.driver',
|
||||
'scripts/merge-translation-pairing-driver.sh %O %A %B %P',
|
||||
])
|
||||
|
||||
const merge = spawnSync('git', ['-C', fixture.root, 'merge', '--no-edit', 'master'], {
|
||||
encoding: 'utf8',
|
||||
env: fixture.env,
|
||||
})
|
||||
|
||||
expect(merge.status, merge.stderr).toBe(0)
|
||||
expect(git(fixture, ['diff', '--name-only', '--diff-filter=U'])).toBe('')
|
||||
expect(readFileSync(join(fixture.root, 'docs/guide.md'), 'utf8')).toContain(
|
||||
'[Reference](reference.md#overview)',
|
||||
)
|
||||
expect(readFileSync(join(fixture.root, 'docs/guide.zh.md'), 'utf8')).toContain(
|
||||
'[参考](reference.zh.md#overview)',
|
||||
)
|
||||
})
|
||||
|
||||
it('leaves an ordinary recoverable conflict when the configured runtime is unavailable', () => {
|
||||
const fixture = createFixture()
|
||||
const records = createDivergedPair(fixture)
|
||||
|
||||
@@ -7,7 +7,7 @@ import { basename, isAbsolute, join, relative, resolve, sep } from 'node:path'
|
||||
import {
|
||||
GIT_COMMAND_MAX_BUFFER,
|
||||
gitBlobHash,
|
||||
gitIndexPaths,
|
||||
gitMergeInputPaths,
|
||||
readGitIndexBlob,
|
||||
runGit,
|
||||
storeGitBlob,
|
||||
@@ -179,7 +179,7 @@ function assertMergedPairStructure(
|
||||
const zhText = zh.toString('utf8')
|
||||
const sourceTree = parseTranslationMarkdown(sourceText)
|
||||
const zhTree = parseTranslationMarkdown(zhText)
|
||||
const indexFiles = gitIndexPaths(root)
|
||||
const indexFiles = gitMergeInputPaths(root)
|
||||
const repositoryFileExists = (path: string): boolean => indexFiles.has(path)
|
||||
const sourceSwitcherTargets = languageSwitcherTargets(paths.source)
|
||||
const zhSwitcherTargets = languageSwitcherTargets(paths.zh)
|
||||
|
||||
@@ -368,6 +368,39 @@ describe('translation structural signature', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('compares the first duplicate reference definition that CommonMark resolves', () => {
|
||||
const root = mkdtempSync(join(tmpdir(), 'dsh-translation-structure-'))
|
||||
try {
|
||||
for (const name of ['reference', 'different', 'other']) {
|
||||
writeFileSync(join(root, `${name}.md`), `# ${name}\n`)
|
||||
writeFileSync(join(root, `${name}.zh.md`), `# ${name} zh\n`)
|
||||
}
|
||||
const sourceMarkdown = '[Reference][ref]\n\n[ref]: reference.md\n[ref]: other.md\n'
|
||||
const counterpartMarkdown = '[参考][ref]\n\n[ref]: different.zh.md\n[ref]: other.zh.md\n'
|
||||
const source = translationStructureSignature(
|
||||
parseTranslationMarkdown(sourceMarkdown),
|
||||
'guide.zh.md',
|
||||
{
|
||||
repoRoot: root, sourcePath: 'guide.md',
|
||||
isTranslationPairSource: fixturePairSource, markdown: sourceMarkdown,
|
||||
},
|
||||
)
|
||||
const counterpart = translationStructureSignature(
|
||||
parseTranslationMarkdown(counterpartMarkdown),
|
||||
'guide.md',
|
||||
{
|
||||
repoRoot: root, sourcePath: 'guide.zh.md',
|
||||
isTranslationPairSource: fixturePairSource, markdown: counterpartMarkdown,
|
||||
},
|
||||
)
|
||||
expect(translationStructureDiff(source, counterpart)).toEqual([
|
||||
'link target #1 diverges between the pair: "dsh-translation-target:reference.md" vs "dsh-translation-target:different.md"',
|
||||
])
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
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')
|
||||
|
||||
@@ -362,7 +362,9 @@ export function translationStructureSignature(
|
||||
const sig: TranslationStructureSignature = { headings: [], code: [], tables: [], lists: [], links: [] }
|
||||
const definitions = new Map<string, Extract<Nodes, { type: 'definition' }>>()
|
||||
const collectDefinitions = (node: Nodes): void => {
|
||||
if (node.type === 'definition') definitions.set(node.identifier, node)
|
||||
if (node.type === 'definition' && !definitions.has(node.identifier)) {
|
||||
definitions.set(node.identifier, node)
|
||||
}
|
||||
if ('children' in node) for (const child of node.children) collectDefinitions(child)
|
||||
}
|
||||
collectDefinitions(tree)
|
||||
|
||||
Reference in New Issue
Block a user