import { describe, expect, it } from 'vitest'; import { parseMarkdownReviewIssues } from '../tools/prr/github/api.js'; import { isNonReviewContent } from '../tools/prr/github/review-ingestion-filters.js'; import { parseBranchSpec, normalizeCompareBranch } from '../tools/prr/github/types.js'; describe('parseBranchSpec', () => { it('parses owner/repo@branch shorthand', () => { const result = parseBranchSpec('owner/repo@feature-branch'); expect(result).toEqual({ owner: 'owner', repo: 'repo', branch: 'feature-branch' }); }); it('parses owner/repo:branch shorthand', () => { const result = parseBranchSpec('owner/repo:feature-branch'); expect(result).toEqual({ owner: 'owner', repo: 'repo', branch: 'feature-branch' }); }); it('parses branch names with slashes', () => { const result = parseBranchSpec('owner/repo@feature/siwe/auth'); expect(result).toEqual({ owner: 'owner', repo: 'repo', branch: 'feature/siwe/auth' }); }); it('parses GitHub tree URL', () => { const result = parseBranchSpec('https://github.com/owner/repo/tree/main'); expect(result).toEqual({ owner: 'owner', repo: 'repo', branch: 'main' }); }); it('parses tree URL with branch containing slashes', () => { const result = parseBranchSpec('https://github.com/owner/repo/tree/feature/siwe'); expect(result).toEqual({ owner: 'owner', repo: 'repo', branch: 'feature/siwe' }); }); it('strips query parameters from tree URL', () => { const result = parseBranchSpec('https://github.com/owner/repo/tree/main?tab=readme'); expect(result).toEqual({ owner: 'owner', repo: 'repo', branch: 'main' }); }); it('strips fragment from tree URL', () => { const result = parseBranchSpec('https://github.com/owner/repo/tree/main#installation'); expect(result).toEqual({ owner: 'owner', repo: 'repo', branch: 'main' }); }); it('strips trailing slashes from branch name', () => { const result = parseBranchSpec('https://github.com/owner/repo/tree/main/'); expect(result).toEqual({ owner: 'owner', repo: 'repo', branch: 'main' }); }); it('returns null for plain branch name (no repo context)', () => { const result = parseBranchSpec('feature-branch'); expect(result).toBeNull(); }); it('returns null for invalid input', () => { const result = parseBranchSpec('not-a-valid-spec'); expect(result).toBeNull(); }); }); describe('normalizeCompareBranch', () => { it('returns plain branch name as-is', () => { const result = normalizeCompareBranch('v1-develop'); expect(result).toBe('v1-develop'); }); it('extracts branch from owner/repo@branch format', () => { const result = normalizeCompareBranch('owner/repo@v1-develop'); expect(result).toBe('v1-develop'); }); it('extracts branch from tree URL', () => { const result = normalizeCompareBranch('https://github.com/owner/repo/tree/v1-develop'); expect(result).toBe('v1-develop'); }); it('extracts branch with slashes from tree URL', () => { const result = normalizeCompareBranch('https://github.com/owner/repo/tree/feature/auth/siwe'); expect(result).toBe('feature/auth/siwe'); }); it('strips query parameters when extracting branch', () => { const result = normalizeCompareBranch('https://github.com/owner/repo/tree/main?foo=bar'); expect(result).toBe('main'); }); it('throws on repo mismatch when currentOwner/currentRepo provided', () => { expect(() => { normalizeCompareBranch('other/repo@branch', 'owner', 'repo'); }).toThrow('--compare repo (other/repo) does not match branch repo (owner/repo)'); }); it('allows matching repo when currentOwner/currentRepo provided', () => { const result = normalizeCompareBranch('owner/repo@v1-develop', 'owner', 'repo'); expect(result).toBe('v1-develop'); }); it('throws on invalid structured input', () => { expect(() => { normalizeCompareBranch('https://github.com/invalid-url'); }).toThrow('Invalid --compare value'); }); }); describe('parseMarkdownReviewIssues', () => { describe('parseBranchSpec', () => { it('parses valid branch specifications', () => { expect(parseBranchSpec('owner/repo@branch')).toEqual({ owner: 'owner', repo: 'repo', branch: 'branch' }); expect(parseBranchSpec('owner/repo:branch')).toEqual({ owner: 'owner', repo: 'repo', branch: 'branch' }); expect(parseBranchSpec('https://github.com/owner/repo/tree/branch')).toEqual({ owner: 'owner', repo: 'repo', branch: 'branch' }); }); it('handles special cases', () => { expect(parseBranchSpec('owner/repo@feature/branch')).toEqual({ owner: 'owner', repo: 'repo', branch: 'feature/branch' }); expect(parseBranchSpec('https://github.com/owner/repo/tree/branch?foo=bar#readme')).toEqual({ owner: 'owner', repo: 'repo', branch: 'branch' }); expect(parseBranchSpec('https://github.com/owner/repo/tree/branch/')).toEqual({ owner: 'owner', repo: 'repo', branch: 'branch' }); }); it('returns null for invalid inputs', () => { expect(parseBranchSpec('invalid')).toBeNull(); expect(parseBranchSpec('owner/repo')).toBeNull(); expect(parseBranchSpec('https://github.com/owner/repo')).toBeNull(); }); }); describe('normalizeCompareBranch', () => { it('returns plain branch name unchanged', () => { expect(normalizeCompareBranch('develop')).toBe('develop'); expect(normalizeCompareBranch('feature/branch')).toBe('feature/branch'); }); it('extracts branch from valid specifications', () => { expect(normalizeCompareBranch('owner/repo@branch')).toBe('branch'); expect(normalizeCompareBranch('https://github.com/owner/repo/tree/branch')).toBe('branch'); }); it('validates matching repo', () => { expect(normalizeCompareBranch('owner/repo@branch', 'owner', 'repo')).toBe('branch'); expect(() => normalizeCompareBranch('other/repo@branch', 'owner', 'repo')).toThrow( '--compare repo (other/repo) does not match branch repo (owner/repo)' ); }); it('throws for invalid formats', () => { expect(() => normalizeCompareBranch('https://github.com/owner/repo')).toThrow('Invalid --compare value'); }); it('treats owner/repo without @ or : as plain branch name', () => { // Implementation allows plain branch names; owner/repo is returned as-is when it does not match branch spec (no @ or :). expect(normalizeCompareBranch('owner/repo')).toBe('owner/repo'); }); }); it('skips summary/status recap table items', () => { const markdown = `## Findings | Location | Suggestion | | --- | --- | | logger.ts | Add JSDoc | | reply.ts | Still missing tests |`; expect(parseMarkdownReviewIssues(markdown)).toEqual([]); }); it('keeps explicit bare-file issues when they are written as actionable items', () => { const markdown = `## Blocking Issues ### 1. Missing tests Add tests for \`reply.ts:106\` so the new path is covered.`; expect(parseMarkdownReviewIssues(markdown)).toEqual([ expect.objectContaining({ path: 'reply.ts', line: 106, }), ]); }); }); describe('isNonReviewContent', () => { it('detects README-style dumps with multiple setup headings', () => { const readme = `## Stack Bun ## Commands bun install ## Project Structure apps/ ## Getting Started `; expect(isNonReviewContent(readme)).toBe(true); }); it('returns false for a normal review with Issues section', () => { const review = `## Issues ### 1. Bug in \`foo.ts\` The handler should validate input.`; expect(isNonReviewContent(review)).toBe(false); }); it('filters ultra-short non-actionable text', () => { expect(isNonReviewContent('test')).toBe(true); }); it('keeps short text when it contains issue keywords', () => { expect(isNonReviewContent('Fix the bug in deploy')).toBe(false); }); });