Files
board-viewer/tests/reader.test.ts
vitya d093693795 feat(reader): Gitea client + config + board orchestrator
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>
2026-05-22 12:48:37 +03:00

113 lines
3.6 KiB
TypeScript

import { describe, expect, test, vi } from 'vitest';
import type { GiteaClient } from '../src/gitea.ts';
import { readBoard } from '../src/reader.ts';
function fakeClient(overrides: Partial<GiteaClient> = {}): GiteaClient {
return {
getFile: vi.fn().mockResolvedValue(null),
getLatestCommitIso: vi.fn().mockResolvedValue(null),
rawUrl: vi.fn().mockReturnValue('http://example/raw/x'),
...overrides,
};
}
const STATUS_MD_ONE = `# Task Board
_Updated: 2026-05-22_
## 🔴 [task-a] — first task
**Status:** active
**Where I stopped:** doing A.
**Next action:** continue A.
**Branch:** master
---
`;
describe('readBoard', () => {
test('merges parser output with project metadata and Gitea last-commit + raw URL', async () => {
const client = fakeClient({
getFile: vi.fn().mockResolvedValue(STATUS_MD_ONE),
getLatestCommitIso: vi.fn().mockResolvedValue('2026-05-22T09:00:00Z'),
rawUrl: vi.fn().mockReturnValue(
'https://git.kzntsv.site/OpeItcLoc03/board-viewer/raw/branch/master/.tasks/task-a.md',
),
});
const records = await readBoard(client, [{ owner: 'OpeItcLoc03', repo: 'board-viewer' }]);
expect(records).toHaveLength(1);
expect(records[0]).toEqual({
slug: 'task-a',
project: 'board-viewer',
project_owner: 'OpeItcLoc03',
status: 'in_progress',
status_emoji: '🔴',
title: 'first task',
where_stopped: 'doing A.',
next_action: 'continue A.',
blocker: null,
branch: 'master',
last_commit_iso: '2026-05-22T09:00:00Z',
raw_url:
'https://git.kzntsv.site/OpeItcLoc03/board-viewer/raw/branch/master/.tasks/task-a.md',
});
});
test('skips repo silently when STATUS.md is missing (returns no records for that repo)', async () => {
const client = fakeClient({
getFile: vi.fn().mockResolvedValue(null), // 404
});
const records = await readBoard(client, [{ owner: 'o', repo: 'no-tasks' }]);
expect(records).toEqual([]);
});
test('aggregates blocks across multiple repos', async () => {
const repoFiles: Record<string, string> = {
'repo-a': STATUS_MD_ONE,
'repo-b': STATUS_MD_ONE.replace('task-a', 'task-b').replace('first task', 'second task'),
};
const client = fakeClient({
getFile: vi.fn(async (_owner: string, repo: string) => repoFiles[repo] ?? null),
getLatestCommitIso: vi.fn().mockResolvedValue('2026-05-22T09:00:00Z'),
});
const records = await readBoard(client, [
{ owner: 'o', repo: 'repo-a' },
{ owner: 'o', repo: 'repo-b' },
]);
expect(records.map((r) => `${r.project}:${r.slug}`)).toEqual([
'repo-a:task-a',
'repo-b:task-b',
]);
});
test('builds raw_url per-task file using <slug>.md path under .tasks/', async () => {
const rawUrl = vi.fn((_o: string, _r: string, path: string) => `RAW:${path}`);
const client = fakeClient({
getFile: vi.fn().mockResolvedValue(STATUS_MD_ONE),
getLatestCommitIso: vi.fn().mockResolvedValue(null),
rawUrl,
});
const records = await readBoard(client, [{ owner: 'o', repo: 'r' }]);
expect(records[0]!.raw_url).toBe('RAW:.tasks/task-a.md');
expect(rawUrl).toHaveBeenCalledWith('o', 'r', '.tasks/task-a.md');
});
test('queries last_commit_iso for the per-task file path', async () => {
const getLatestCommitIso = vi.fn().mockResolvedValue('2026-05-22T10:00:00Z');
const client = fakeClient({
getFile: vi.fn().mockResolvedValue(STATUS_MD_ONE),
getLatestCommitIso,
});
await readBoard(client, [{ owner: 'o', repo: 'r' }]);
expect(getLatestCommitIso).toHaveBeenCalledWith('o', 'r', '.tasks/task-a.md');
});
});