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(async (_o: string, _r: string, path: string) => { if (path === '.tasks/STATUS.md') return STATUS_MD_ONE; if (path === '.tasks/task-a.md') return '# task-a\n\ndeep context body.'; return null; }), 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', md_content: '# task-a\n\ndeep context body.', }); }); test('per-task .md missing (404) → md_content is null, record still emitted', async () => { const client = fakeClient({ getFile: vi.fn(async (_o: string, _r: string, path: string) => { if (path === '.tasks/STATUS.md') return STATUS_MD_ONE; return null; }), getLatestCommitIso: vi.fn().mockResolvedValue(null), }); const records = await readBoard(client, [{ owner: 'o', repo: 'r' }]); expect(records).toHaveLength(1); expect(records[0]!.md_content).toBeNull(); }); test('fetches per-task .md alongside STATUS.md (.tasks/.md path)', async () => { const getFile = vi.fn(async (_o: string, _r: string, path: string) => { if (path === '.tasks/STATUS.md') return STATUS_MD_ONE; return 'body'; }); const client = fakeClient({ getFile, getLatestCommitIso: vi.fn().mockResolvedValue(null), }); await readBoard(client, [{ owner: 'o', repo: 'r' }]); expect(getFile).toHaveBeenCalledWith('o', 'r', '.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'); }); });