mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-18 13:23:33 +08:00
Desktop memory settings already resolves project memory through the current sanitized project directory, while spawned CLI sessions could compute an older path variant and fail to read indexed memory files. Pin the child runtime to the same memory directory and let memory preview links open related markdown files directly from the rendered panel. Constraint: Existing memory files may already live under both legacy underscore and current hyphenated project directories. Rejected: Symlink or merge memory directories | it could mix stale legacy memory entries into the active project index. Confidence: high Scope-risk: moderate Directive: Keep Settings memory discovery and spawned CLI memory context on the same project identity before changing either sanitizer. Tested: bun test src/server/__tests__/conversation-service.test.ts src/server/__tests__/memory.test.ts Tested: bun run check:server Tested: cd desktop && bun run test -- MemorySettings MarkdownRenderer Tested: cd desktop && bun run build Tested: Browser smoke on local Vite/server for memory preview link navigation Not-tested: bun run check:desktop still has pre-existing vite-config color-mix guard failure in desktop/src/theme/globals.css
185 lines
6.4 KiB
TypeScript
185 lines
6.4 KiB
TypeScript
import { describe, it, expect, vi } from 'vitest'
|
|
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
|
import '@testing-library/jest-dom'
|
|
|
|
vi.mock('../chat/CodeViewer', () => ({
|
|
CodeViewer: ({ code, language }: { code: string; language?: string }) => (
|
|
<div data-testid="code-viewer" data-language={language ?? ''}>
|
|
{code}
|
|
</div>
|
|
),
|
|
}))
|
|
|
|
vi.mock('../chat/MermaidRenderer', () => ({
|
|
MermaidRenderer: ({ code }: { code: string }) => (
|
|
<div data-testid="mermaid-renderer">{code}</div>
|
|
),
|
|
}))
|
|
|
|
import { MarkdownRenderer } from './MarkdownRenderer'
|
|
|
|
describe('MarkdownRenderer', () => {
|
|
it('applies document prose classes and custom width classes', () => {
|
|
const { container } = render(
|
|
<MarkdownRenderer
|
|
content={'# Skill Title\n\nReadable paragraph text.'}
|
|
variant="document"
|
|
className="mx-auto max-w-[72ch]"
|
|
/>,
|
|
)
|
|
|
|
const root = container.firstChild as HTMLDivElement
|
|
expect(root).toBeInTheDocument()
|
|
expect(root.className).toContain('prose-p:text-[15px]')
|
|
expect(root.className).toContain('prose-h2:border-b')
|
|
expect(root.className).toContain('mx-auto')
|
|
expect(root.className).toContain('max-w-[72ch]')
|
|
expect(screen.getByText('Skill Title')).toBeInTheDocument()
|
|
expect(screen.getByText('Readable paragraph text.')).toBeInTheDocument()
|
|
})
|
|
|
|
it('keeps default variant free of document-only typography classes', () => {
|
|
const { container } = render(
|
|
<MarkdownRenderer content={'## Default Heading\n\nBody copy.'} />,
|
|
)
|
|
|
|
const root = container.firstChild as HTMLDivElement
|
|
expect(root).toBeInTheDocument()
|
|
expect(root.className).not.toContain('prose-p:text-[15px]')
|
|
expect(root.className).not.toContain('prose-h2:border-b')
|
|
expect(screen.getByText('Default Heading')).toBeInTheDocument()
|
|
expect(screen.getByText('Body copy.')).toBeInTheDocument()
|
|
})
|
|
|
|
it('applies compact prose classes for dense surfaces', () => {
|
|
const { container } = render(
|
|
<MarkdownRenderer
|
|
content={'**Done**\n\n- One\n- Two'}
|
|
variant="compact"
|
|
/>,
|
|
)
|
|
|
|
const root = container.firstChild as HTMLDivElement
|
|
expect(root).toBeInTheDocument()
|
|
expect(root.className).toContain('prose-p:text-xs')
|
|
expect(root.className).toContain('prose-li:text-xs')
|
|
expect(screen.getByText('Done')).toBeInTheDocument()
|
|
expect(screen.getByText('One')).toBeInTheDocument()
|
|
})
|
|
|
|
it('uses semantic code colors for inline code so both themes stay readable', () => {
|
|
const { container } = render(
|
|
<MarkdownRenderer content={'Use `claude-sonnet-4-6` for balanced speed.'} />,
|
|
)
|
|
|
|
const root = container.firstChild as HTMLDivElement
|
|
expect(root).toBeInTheDocument()
|
|
expect(root.className).toContain('prose-code:text-[var(--color-code-fg)]')
|
|
expect(root.className).toContain('prose-code:bg-[var(--color-code-bg)]')
|
|
expect(root.className).not.toContain('prose-code:text-[var(--color-primary-fixed)]')
|
|
expect(screen.getByText('claude-sonnet-4-6')).toBeInTheDocument()
|
|
})
|
|
|
|
it('renders mermaid fenced blocks with the Mermaid renderer', () => {
|
|
render(<MarkdownRenderer content={'```mermaid\ngraph TB\nA-->B\n```'} />)
|
|
|
|
expect(screen.getByTestId('mermaid-renderer')).toHaveTextContent(
|
|
/graph TB\s+A-->B/,
|
|
)
|
|
expect(screen.queryByTestId('code-viewer')).not.toBeInTheDocument()
|
|
})
|
|
|
|
it('detects mermaid diagrams even when the fence has no language tag', () => {
|
|
render(<MarkdownRenderer content={'```\ngraph TB\nA-->B\n```'} />)
|
|
|
|
expect(screen.getByTestId('mermaid-renderer')).toHaveTextContent(
|
|
/graph TB\s+A-->B/,
|
|
)
|
|
expect(screen.queryByTestId('code-viewer')).not.toBeInTheDocument()
|
|
})
|
|
|
|
it('keeps non-mermaid code fences in the normal code viewer', () => {
|
|
render(<MarkdownRenderer content={'```ts\nconst value = 1\n```'} />)
|
|
|
|
expect(screen.getByTestId('code-viewer')).toHaveAttribute(
|
|
'data-language',
|
|
'ts',
|
|
)
|
|
expect(screen.queryByTestId('mermaid-renderer')).not.toBeInTheDocument()
|
|
})
|
|
|
|
it('wraps markdown tables for horizontal overflow handling', () => {
|
|
const { container } = render(
|
|
<MarkdownRenderer
|
|
content={'| Name | Value |\n| --- | --- |\n| `index.html` | Ready |'}
|
|
/>,
|
|
)
|
|
|
|
expect(container.querySelector('.md-table-wrap')).toBeInTheDocument()
|
|
expect(screen.getByText('index.html')).toBeInTheDocument()
|
|
})
|
|
|
|
it('opens markdown links in a new tab safely', () => {
|
|
render(<MarkdownRenderer content={'[OpenAI](https://openai.com)'} />)
|
|
|
|
const link = screen.getByRole('link', { name: 'OpenAI' })
|
|
expect(link).toHaveAttribute('target', '_blank')
|
|
expect(link).toHaveAttribute('rel', expect.stringContaining('noopener'))
|
|
})
|
|
|
|
it('lets callers intercept markdown link clicks', () => {
|
|
const onLinkClick = vi.fn().mockReturnValue(true)
|
|
render(
|
|
<MarkdownRenderer
|
|
content={'[Manual](notes/manual.md)'}
|
|
onLinkClick={onLinkClick}
|
|
/>,
|
|
)
|
|
|
|
fireEvent.click(screen.getByRole('link', { name: 'Manual' }))
|
|
|
|
expect(onLinkClick).toHaveBeenCalledWith(
|
|
'notes/manual.md',
|
|
expect.objectContaining({ type: 'click' }),
|
|
)
|
|
})
|
|
|
|
it('copies enhanced markdown button text with the legacy clipboard fallback', async () => {
|
|
const originalClipboard = navigator.clipboard
|
|
const originalExecCommand = document.execCommand
|
|
Object.defineProperty(document, 'execCommand', {
|
|
configurable: true,
|
|
value: vi.fn().mockReturnValue(true),
|
|
})
|
|
const execCommand = vi.mocked(document.execCommand)
|
|
Object.defineProperty(navigator, 'clipboard', {
|
|
configurable: true,
|
|
value: {
|
|
writeText: vi.fn().mockRejectedValue(new Error('clipboard blocked')),
|
|
},
|
|
})
|
|
const writeText = vi.mocked(navigator.clipboard.writeText)
|
|
|
|
try {
|
|
render(<MarkdownRenderer content={'<button data-copy-code="npm run verify">Copy</button>'} />)
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: 'Copy' }))
|
|
|
|
await waitFor(() => {
|
|
expect(execCommand).toHaveBeenCalledWith('copy')
|
|
})
|
|
expect(writeText).toHaveBeenCalledWith('npm run verify')
|
|
expect(screen.getByRole('button', { name: 'Copied' })).toBeInTheDocument()
|
|
} finally {
|
|
Object.defineProperty(document, 'execCommand', {
|
|
configurable: true,
|
|
value: originalExecCommand,
|
|
})
|
|
Object.defineProperty(navigator, 'clipboard', {
|
|
configurable: true,
|
|
value: originalClipboard,
|
|
})
|
|
}
|
|
})
|
|
})
|