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 { 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 = { '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 .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'); }); });