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>
This commit is contained in:
112
tests/reader.test.ts
Normal file
112
tests/reader.test.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
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');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user