closes board-viewer-gitea-reader - src/gitea.ts: HTTP client (getFile, getLatestCommitIso, rawUrl); DI'd fetch - src/reader.ts: readBoard(client, repos) → TaskRecord[] - src/config.ts: TOML config loader with board_viewer_repos whitelist - 25 tests total (9 parser incl. 3 real-repo fixtures, 7 gitea, 5 reader, 4 config) - emoji contract ⚪🔴🟡🔵🟢 (open/in_progress/paused/blocked/done) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
73 lines
2.6 KiB
TypeScript
73 lines
2.6 KiB
TypeScript
import { describe, expect, test } from 'vitest';
|
|
import { readFileSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import { parseStatus } from '../src/parser.ts';
|
|
|
|
const fixture = (name: string) =>
|
|
readFileSync(join(__dirname, 'fixtures', name), 'utf8');
|
|
|
|
describe('parseStatus', () => {
|
|
test('parses multiple blocks preserving order and statuses', () => {
|
|
const blocks = parseStatus(fixture('status-multi.md'));
|
|
|
|
expect(blocks).toHaveLength(3);
|
|
expect(blocks.map((b) => b.slug)).toEqual(['task-a', 'task-b', 'task-c']);
|
|
expect(blocks.map((b) => b.status)).toEqual(['in_progress', 'blocked', 'open']);
|
|
});
|
|
|
|
test('parses Blocker field when present', () => {
|
|
const blocks = parseStatus(fixture('status-multi.md'));
|
|
const blocked = blocks.find((b) => b.slug === 'task-b');
|
|
|
|
expect(blocked?.blocker).toBe('task-a');
|
|
});
|
|
|
|
test('Blocker field is null when absent', () => {
|
|
const blocks = parseStatus(fixture('status-multi.md'));
|
|
const ready = blocks.find((b) => b.slug === 'task-c');
|
|
|
|
expect(ready?.blocker).toBeNull();
|
|
});
|
|
|
|
test('returns empty array for STATUS.md without task blocks', () => {
|
|
expect(parseStatus('# Task Board\n_Updated: 2026-05-22_\n\nNo tasks yet.\n')).toEqual([]);
|
|
});
|
|
|
|
test.each([
|
|
'real-books-STATUS.md',
|
|
'real-claude-skills-STATUS.md',
|
|
'real-projects-meta-mcp-STATUS.md',
|
|
])('parses real STATUS.md fixture %s without crashing and yields ≥1 block', (name) => {
|
|
const blocks = parseStatus(fixture(name));
|
|
|
|
expect(blocks.length).toBeGreaterThan(0);
|
|
for (const b of blocks) {
|
|
expect(b.slug).toMatch(/^[a-z0-9][a-z0-9-]*$/);
|
|
expect(b.status).toMatch(/^(open|in_progress|paused|blocked|done)$/);
|
|
expect(b.status_emoji).toMatch(/^(⚪|🔴|🟡|🔵|🟢)$/u);
|
|
expect(b.title.length).toBeGreaterThan(0);
|
|
}
|
|
});
|
|
|
|
test('ignores blocks whose H2 has no known status emoji', () => {
|
|
const text = '## 🚀 [some-task] — title\n**Status:** custom\n**Branch:** master\n---\n';
|
|
expect(parseStatus(text)).toEqual([]);
|
|
});
|
|
|
|
test('parses a single active block with all fields', () => {
|
|
const blocks = parseStatus(fixture('status-single-block.md'));
|
|
|
|
expect(blocks).toHaveLength(1);
|
|
expect(blocks[0]).toEqual({
|
|
slug: 'board-viewer-pointers',
|
|
title: 'pre-impl: fill Domain conventions with design-context pointers',
|
|
status: 'in_progress',
|
|
status_emoji: '🔴',
|
|
where_stopped: 'только что промочен дизайн.',
|
|
next_action: 'вставить блок в `.wiki/CLAUDE.md`.',
|
|
blocker: null,
|
|
branch: 'master',
|
|
});
|
|
});
|
|
});
|