Files
shiro-neko/test/markdown.test.ts
T
Muhammad Zakir Ramadhan 5b8503fcd9 Initial commit: shiro-neko 0.1.0-beta.1
Agentic coding CLI on Bun, Ink, and the AI SDK.

Core: streamText loop with SDK-level tool approval so a denied call provably never executes; endpoint fallback for OpenAI reasoning models; retry with backoff.

Tools: read/write/edit/glob/grep/bash, path-jailed, gitignore-aware, ripgrep with a JS fallback, binary rejection, live bash streaming.

Agents: five variants crossing thinking level with tool restriction; plan and review withhold mutating tools from the model.

Extensibility: frontmatter skills with on-demand bodies, plugin host with blocking hooks, MCP stdio and HTTP, read-only subagents.

State: durable per-project memory, session task lists, session persistence, compaction that repairs provider-item dependencies.

Distribution: five-platform cross-compiled binaries with checksums, install scripts, CI on three operating systems.

404 tests, typecheck clean.
2026-09-02 17:30:18 +07:00

181 lines
6.1 KiB
TypeScript

import { expect, test } from 'bun:test';
import { parseInline, parseMarkdown, toPlainText } from '../src/markdown';
const kinds = (input: string) => parseMarkdown(input).map((b) => b.kind);
test('plain text is one paragraph', () => {
expect(parseMarkdown('hello world')).toEqual([{ kind: 'paragraph', spans: [{ text: 'hello world' }] }]);
});
test('headings carry their level', () => {
const blocks = parseMarkdown('# One\n## Two\n###### Six');
expect(blocks.map((b) => (b.kind === 'heading' ? b.level : 0))).toEqual([1, 2, 6]);
});
test('seven hashes is not a heading', () => {
expect(kinds('####### too many')).toEqual(['paragraph']);
});
test('a fenced block keeps its language and lines verbatim', () => {
const blocks = parseMarkdown('```ts\nconst a = 1;\n\nconst b = 2;\n```');
expect(blocks).toHaveLength(1);
const block = blocks[0]!;
if (block.kind !== 'code') throw new Error('expected a code block');
expect(block.language).toBe('ts');
expect(block.lines).toEqual(['const a = 1;', '', 'const b = 2;']);
});
test('markup inside a fence is never interpreted', () => {
const blocks = parseMarkdown('```\n# not a heading\n- not a bullet\n**not bold**\n```');
const block = blocks[0]!;
if (block.kind !== 'code') throw new Error('expected a code block');
expect(block.lines).toEqual(['# not a heading', '- not a bullet', '**not bold**']);
});
test('an unclosed fence still yields a code block, for partial streams', () => {
const blocks = parseMarkdown('```js\nconst partial = ');
const block = blocks[0]!;
if (block.kind !== 'code') throw new Error('expected a code block');
expect(block.lines).toEqual(['const partial = ']);
});
test('tilde fences work too', () => {
expect(kinds('~~~\ncode\n~~~')).toEqual(['code']);
});
test('bullets keep their indent and marker', () => {
const blocks = parseMarkdown('- one\n - nested\n* star\n1. first\n2) second');
const bullets = blocks.filter((b) => b.kind === 'bullet');
expect(bullets).toHaveLength(5);
expect(bullets.map((b) => (b.kind === 'bullet' ? b.indent : -1))).toEqual([0, 1, 0, 0, 0]);
expect(bullets.map((b) => (b.kind === 'bullet' ? b.marker : ''))).toEqual(['-', '-', '-', '1.', '2)']);
});
test('quotes and rules are recognised', () => {
expect(kinds('> quoted')).toEqual(['quote']);
expect(kinds('---')).toEqual(['rule']);
expect(kinds('***')).toEqual(['rule']);
expect(kinds('___')).toEqual(['rule']);
});
test('two hyphens are not a rule', () => {
expect(kinds('--')).toEqual(['paragraph']);
});
test('consecutive lines join into one paragraph', () => {
const blocks = parseMarkdown('first line\nsecond line');
expect(blocks).toHaveLength(1);
expect(toPlainText(blocks)).toBe('first line second line');
});
test('a blank line separates paragraphs and collapses runs', () => {
expect(kinds('one\n\n\n\ntwo')).toEqual(['paragraph', 'blank', 'paragraph']);
});
test('trailing blanks are trimmed', () => {
expect(kinds('text\n\n\n')).toEqual(['paragraph']);
});
test('bold, italic, strike, and code spans are marked', () => {
expect(parseInline('**bold**')).toEqual([{ text: 'bold', bold: true }]);
expect(parseInline('*italic*')).toEqual([{ text: 'italic', italic: true }]);
expect(parseInline('__also bold__')).toEqual([{ text: 'also bold', bold: true }]);
expect(parseInline('~~gone~~')).toEqual([{ text: 'gone', strike: true }]);
expect(parseInline('`code`')).toEqual([{ text: 'code', code: true }]);
});
test('a code span is never re-scanned for markup', () => {
expect(parseInline('`**not bold**`')).toEqual([{ text: '**not bold**', code: true }]);
});
test('bold wins over italic on the same run', () => {
expect(parseInline('**both**')).toEqual([{ text: 'both', bold: true }]);
});
test('nesting inside bold is preserved', () => {
expect(parseInline('**bold `code`**')).toEqual([
{ text: 'bold ', bold: true },
{ text: 'code', code: true, bold: true },
]);
});
test('a link keeps its text and is flagged', () => {
expect(parseInline('see [the docs](https://example.com)')).toEqual([
{ text: 'see ' },
{ text: 'the docs', link: true },
]);
});
test('surrounding text is kept around inline markup', () => {
expect(parseInline('before **mid** after')).toEqual([
{ text: 'before ' },
{ text: 'mid', bold: true },
{ text: ' after' },
]);
});
test('an underscore inside a word is not italic', () => {
expect(parseInline('snake_case_name')).toEqual([{ text: 'snake_case_name' }]);
});
test('unmatched markers stay literal', () => {
expect(parseInline('a ** dangling')).toEqual([{ text: 'a ** dangling' }]);
expect(parseInline('unclosed `code')).toEqual([{ text: 'unclosed `code' }]);
});
test('inline markup inside a heading is parsed', () => {
const blocks = parseMarkdown('# A `code` heading');
const block = blocks[0]!;
if (block.kind !== 'heading') throw new Error('expected a heading');
expect(block.spans.some((s) => s.code)).toBe(true);
});
test('toPlainText round-trips the structure without markup', () => {
const text = '# Title\n\nSome **bold** text.\n\n- item one\n- item two\n\n```ts\ncode();\n```';
const plain = toPlainText(parseMarkdown(text));
expect(plain).toContain('# Title');
expect(plain).toContain('Some bold text.');
expect(plain).toContain('- item one');
expect(plain).toContain('code();');
expect(plain).not.toContain('**');
});
test('windows line endings are handled', () => {
expect(kinds('# One\r\n\r\ntext')).toEqual(['heading', 'blank', 'paragraph']);
});
test('an empty document yields no blocks', () => {
expect(parseMarkdown('')).toEqual([]);
expect(parseMarkdown(' \n\n ')).toEqual([]);
});
test('a realistic agent reply parses into the expected shape', () => {
const reply = [
'Fixed the off-by-one in `paginate()`.',
'',
'## What changed',
'',
'- `src/users.ts:42` now uses `<=` instead of `<`',
'- added a boundary test',
'',
'```ts',
'if (offset <= total) next();',
'```',
'',
'> Note: the old behaviour dropped the last row.',
].join('\n');
expect(kinds(reply)).toEqual([
'paragraph',
'blank',
'heading',
'blank',
'bullet',
'bullet',
'blank',
'code',
'blank',
'quote',
]);
});