mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
The chatFileMentions service (provided by ui-deliverables beside its turn-tail entry, reached via ctx.get) resolves inline-code tokens in the closing message against the turn's produced locations: exact path or unique basename links, ambiguity and unknowns stay inert. MarkdownText gains the optional fileMentions seam — settled renders only, never inside anchors.
227 lines
8.8 KiB
TypeScript
227 lines
8.8 KiB
TypeScript
// @vitest-environment jsdom
|
|
// Branch coverage for the mdast renderer that real parses cannot reach: the
|
|
// grammar only emits references whose definitions exist, always stamps
|
|
// positions and align arrays, and never emits bare list items — but the
|
|
// renderer is a pure function over mdast, so hand-built trees exercise its
|
|
// defensive arms directly.
|
|
import { StrictMode } from 'react'
|
|
import { cleanup, render } from '@testing-library/react'
|
|
import { afterEach, describe, expect, it } from 'vitest'
|
|
import type * as Md from 'mdast'
|
|
import { MarkdownText } from '@deepseek-ai/dsh-client-ui-primitives'
|
|
import {
|
|
collectReferenceTargets, createReferenceTargets, renderBlocks, renderFootnoteSection,
|
|
} from '../src/markdown/render.tsx'
|
|
import type { MarkdownRenderContext } from '../src/markdown/render.tsx'
|
|
|
|
afterEach(cleanup)
|
|
|
|
function makeContext(): MarkdownRenderContext {
|
|
return {
|
|
streaming: false,
|
|
codeLabels: undefined,
|
|
fileMentions: undefined,
|
|
targets: createReferenceTargets(),
|
|
footnoteOrder: [],
|
|
footnoteCounts: new Map(),
|
|
}
|
|
}
|
|
|
|
function renderNodes(nodes: Md.RootContent[], context = makeContext()): HTMLElement {
|
|
const { container } = render(
|
|
<div>{renderBlocks(nodes.map((node, key) => ({ node, key })), context)}</div>,
|
|
)
|
|
return container
|
|
}
|
|
|
|
const text = (value: string): Md.Text => ({ type: 'text', value })
|
|
|
|
describe('renderBlocks over hand-built trees', () => {
|
|
it('reverts unresolved references to their bracketed source', () => {
|
|
const container = renderNodes([
|
|
{
|
|
type: 'paragraph',
|
|
children: [
|
|
{ type: 'linkReference', identifier: 'a', referenceType: 'shortcut', children: [text('one')] },
|
|
{ type: 'linkReference', identifier: 'b', referenceType: 'collapsed', children: [text('two')] },
|
|
{ type: 'linkReference', identifier: 'c', label: 'C', referenceType: 'full', children: [text('three')] },
|
|
{ type: 'imageReference', identifier: 'd', referenceType: 'full', alt: 'pic' },
|
|
{ type: 'imageReference', identifier: 'e', referenceType: 'shortcut', alt: null },
|
|
],
|
|
},
|
|
])
|
|
expect(container.textContent).toBe('[one][two][][three][C]![pic][d]![]')
|
|
expect(container.querySelector('a')).toBeNull()
|
|
})
|
|
|
|
it('keeps the first definition when identifiers repeat', () => {
|
|
const targets = createReferenceTargets()
|
|
collectReferenceTargets([
|
|
{ type: 'definition', identifier: 'dup', url: 'https://example.com/first' },
|
|
{ type: 'definition', identifier: 'dup', url: 'https://example.com/second' },
|
|
{ type: 'footnoteDefinition', identifier: 'fn', children: [] },
|
|
{ type: 'footnoteDefinition', identifier: 'fn', children: [{ type: 'paragraph', children: [text('late')] }] },
|
|
], targets)
|
|
expect(targets.definitions.get('DUP')?.url).toBe('https://example.com/first')
|
|
expect(targets.footnotes.get('FN')?.children).toEqual([])
|
|
})
|
|
|
|
it('renders a bare list item, computing looseness from the item itself', () => {
|
|
const item: Md.ListItem = {
|
|
type: 'listItem',
|
|
spread: null,
|
|
children: [
|
|
{ type: 'paragraph', children: [text('alpha')] },
|
|
{ type: 'paragraph', children: [text('beta')] },
|
|
],
|
|
}
|
|
const container = renderNodes([item])
|
|
// Two block children make the parentless item loose: paragraphs stay wrapped.
|
|
expect([...container.querySelectorAll('li > p')].map(p => p.textContent)).toEqual(['alpha', 'beta'])
|
|
})
|
|
|
|
it('renders spread-null lists and align-less tables', () => {
|
|
const container = renderNodes([
|
|
{
|
|
type: 'list',
|
|
ordered: false,
|
|
spread: null,
|
|
children: [{ type: 'listItem', spread: null, children: [{ type: 'paragraph', children: [text('solo')] }] }],
|
|
},
|
|
{
|
|
type: 'table',
|
|
children: [
|
|
{ type: 'tableRow', children: [{ type: 'tableCell', children: [text('h')] }] },
|
|
{ type: 'tableRow', children: [{ type: 'tableCell', children: [text('short')] }] },
|
|
],
|
|
},
|
|
])
|
|
expect(container.querySelector('li')?.textContent).toBe('solo')
|
|
expect(container.querySelector('th')?.getAttribute('style')).toBeNull()
|
|
expect(container.querySelector('td')?.textContent).toBe('short')
|
|
})
|
|
|
|
it('pads rows against the alignment width with empty cells', () => {
|
|
const container = renderNodes([
|
|
{
|
|
type: 'table',
|
|
align: ['left', 'right'],
|
|
children: [
|
|
{ type: 'tableRow', children: [{ type: 'tableCell', children: [text('only')] }] },
|
|
],
|
|
},
|
|
])
|
|
const cells = [...container.querySelectorAll('th')]
|
|
expect(cells).toHaveLength(2)
|
|
expect(cells[1]?.textContent).toBe('')
|
|
})
|
|
|
|
it('renders a checked item without any content as a bare checkbox', () => {
|
|
const container = renderNodes([
|
|
{
|
|
type: 'list',
|
|
ordered: false,
|
|
children: [
|
|
{ type: 'listItem', checked: true, children: [] },
|
|
{ type: 'listItem', checked: false, children: [{ type: 'paragraph', children: [] }] },
|
|
],
|
|
},
|
|
])
|
|
const items = [...container.querySelectorAll('li.task-list-item')]
|
|
expect(items).toHaveLength(2)
|
|
for (const item of items) {
|
|
expect(item.querySelector('input[type="checkbox"]')).not.toBeNull()
|
|
expect(item.textContent?.trim()).toBe('')
|
|
}
|
|
})
|
|
|
|
it('renders images with a null alt as an empty alt attribute', () => {
|
|
const targets = createReferenceTargets()
|
|
targets.definitions.set('R', { type: 'definition', identifier: 'r', url: 'https://example.com/r.png' })
|
|
const container = renderNodes([
|
|
{ type: 'paragraph', children: [{ type: 'image', url: 'https://example.com/x.png', alt: null }] },
|
|
{ type: 'paragraph', children: [{ type: 'imageReference', identifier: 'r', referenceType: 'full', alt: null }] },
|
|
], { ...makeContext(), targets })
|
|
const images = [...container.querySelectorAll('img')]
|
|
expect(images.map(image => image.getAttribute('alt'))).toEqual(['', ''])
|
|
})
|
|
|
|
it('drops a definition nested in a list item without leaving a separator behind', () => {
|
|
const container = renderNodes([
|
|
{
|
|
type: 'list',
|
|
ordered: true,
|
|
start: 3,
|
|
children: [{
|
|
type: 'listItem',
|
|
children: [
|
|
{ type: 'paragraph', children: [text('body')] },
|
|
{ type: 'definition', identifier: 'x', url: 'https://example.com' },
|
|
],
|
|
}],
|
|
},
|
|
])
|
|
expect(container.querySelector('ol')?.getAttribute('start')).toBe('3')
|
|
// The two mdast children make the item loose (wrap newlines around the
|
|
// paragraph); the dropped definition contributes nothing else.
|
|
expect(container.querySelector('li')?.textContent).toBe('\nbody\n')
|
|
})
|
|
|
|
it('renders nothing for node types without a mapping', () => {
|
|
const container = renderNodes([
|
|
{ type: 'yaml', value: 'front: matter' },
|
|
{ type: 'tableRow', children: [] },
|
|
{ type: 'paragraph', children: [text('after')] },
|
|
])
|
|
expect(container.textContent).toBe('after')
|
|
})
|
|
})
|
|
|
|
describe('renderFootnoteSection edge shapes', () => {
|
|
it('skips referenced footnotes without definitions and returns null when none remain', () => {
|
|
const context = makeContext()
|
|
context.footnoteOrder.push('GHOST')
|
|
context.footnoteCounts.set('GHOST', 1)
|
|
expect(renderFootnoteSection(context)).toBeNull()
|
|
})
|
|
|
|
it('renders no back-reference markers for an uncounted footnote', () => {
|
|
const context = makeContext()
|
|
context.targets.footnotes.set('Q', {
|
|
type: 'footnoteDefinition',
|
|
identifier: 'q',
|
|
children: [{ type: 'paragraph', children: [text('quiet')] }],
|
|
})
|
|
context.footnoteOrder.push('Q')
|
|
const { container } = render(<div>{renderFootnoteSection(context)}</div>)
|
|
expect(container.querySelector('li')?.textContent).toBe('\nquiet \n')
|
|
})
|
|
|
|
it('appends back-references after a non-paragraph body', () => {
|
|
const context = makeContext()
|
|
context.targets.footnotes.set('N', {
|
|
type: 'footnoteDefinition',
|
|
identifier: 'n',
|
|
children: [{ type: 'code', value: 'code body', lang: null }],
|
|
})
|
|
context.footnoteOrder.push('N')
|
|
context.footnoteCounts.set('N', 1)
|
|
const { container } = render(<div>{renderFootnoteSection(context)}</div>)
|
|
const item = container.querySelector('li')
|
|
expect(item?.querySelector('.md-code-block')).not.toBeNull()
|
|
expect(item?.textContent).toContain('↩')
|
|
})
|
|
})
|
|
|
|
describe('MarkdownText under StrictMode', () => {
|
|
it('streams identically when React double-invokes render work', () => {
|
|
const doc = 'one\n\ntwo\n\nthree\n\nfour\n\nfive'
|
|
const strict = render(<StrictMode><MarkdownText text={doc.slice(0, 8)} streaming /></StrictMode>)
|
|
strict.rerender(<StrictMode><MarkdownText text={doc} streaming /></StrictMode>)
|
|
const plain = render(<MarkdownText text={doc} streaming />)
|
|
expect(strict.container.innerHTML).toBe(plain.container.innerHTML)
|
|
strict.unmount()
|
|
plain.unmount()
|
|
})
|
|
})
|