import { afterEach, describe, expect, it, vi } from 'vitest'; import { resolveGithubSource } from '#/app/plugin/github-resolver'; describe('resolveGithubSource', () => { afterEach(() => { vi.unstubAllGlobals(); }); it('resolves explicit refs without network and encodes ref paths', async () => { const fetchMock = vi.fn(); vi.stubGlobal('fetch', fetchMock); await expect( resolveGithubSource({ kind: 'github', owner: 'owner', repo: 'repo', ref: { kind: 'tag', value: 'release#1' } }), ).resolves.toEqual({ tarballUrl: 'https://codeload.github.com/owner/repo/zip/refs/tags/release%231', displayVersion: 'release#1', ref: { kind: 'tag', value: 'release#1' }, }); expect(fetchMock).not.toHaveBeenCalled(); }); it('uses latest release redirect for bare github urls', async () => { vi.stubGlobal( 'fetch', vi.fn().mockResolvedValue({ status: 302, headers: new Headers({ location: 'https://github.com/owner/repo/releases/tag/v1.2.3' }), }), ); await expect(resolveGithubSource({ kind: 'github', owner: 'owner', repo: 'repo' })).resolves.toEqual({ tarballUrl: 'https://codeload.github.com/owner/repo/zip/refs/tags/v1.2.3', displayVersion: 'v1.2.3', ref: { kind: 'tag', value: 'v1.2.3' }, }); }); it('falls back to HEAD when there is no latest release', async () => { vi.stubGlobal( 'fetch', vi .fn() .mockResolvedValueOnce({ status: 404, ok: false, headers: new Headers() }) .mockResolvedValueOnce({ status: 200, ok: true, headers: new Headers() }), ); await expect(resolveGithubSource({ kind: 'github', owner: 'owner', repo: 'repo' })).resolves.toEqual({ tarballUrl: 'https://codeload.github.com/owner/repo/zip/HEAD', displayVersion: 'HEAD', ref: { kind: 'branch', value: 'HEAD' }, }); }); it('branch-kind ref carrying a tag value (e.g. /tree/v5.1.0) still resolves via short form', async () => { const fetchMock = vi.fn(); vi.stubGlobal('fetch', fetchMock); const result = await resolveGithubSource({ kind: 'github', owner: 'obra', repo: 'superpowers', ref: { kind: 'branch', value: 'v5.1.0' }, }); // Parser cannot distinguish branch from tag in `/tree/`, but codeload's // short form resolves either — so no `/refs/heads/` 404. expect(result.tarballUrl).toBe('https://codeload.github.com/obra/superpowers/zip/v5.1.0'); expect(fetchMock).not.toHaveBeenCalled(); }); it('bare URL: 302 with /releases/tag/X resolves to that tag', async () => { vi.stubGlobal( 'fetch', vi.fn().mockResolvedValue({ status: 302, headers: new Headers({ location: 'https://github.com/obra/superpowers/releases/tag/v5.1.0', }), }), ); const result = await resolveGithubSource({ kind: 'github', owner: 'obra', repo: 'superpowers' }); expect(result.tarballUrl).toBe( 'https://codeload.github.com/obra/superpowers/zip/refs/tags/v5.1.0', ); expect(result.ref).toEqual({ kind: 'tag', value: 'v5.1.0' }); expect(result.displayVersion).toBe('v5.1.0'); }); it('does not call api.github.com on bare URL (API bypass)', async () => { const calls: string[] = []; vi.stubGlobal( 'fetch', vi.fn(async (input: Parameters[0]) => { const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url; calls.push(url); if (url.includes('github.com') && url.includes('/releases/latest')) { return new Response(null, { status: 302, headers: { location: 'https://github.com/obra/superpowers/releases/tag/v5.1.0' }, }); } throw new Error(`unexpected url: ${url}`); }) as typeof fetch, ); await resolveGithubSource({ kind: 'github', owner: 'obra', repo: 'superpowers' }); expect(calls.every((u) => !u.startsWith('https://api.github.com'))).toBe(true); }); it('release-lookup error message hints at the /tree/ escape hatch', async () => { vi.stubGlobal( 'fetch', vi.fn().mockResolvedValue({ status: 502, statusText: 'Bad Gateway', headers: new Headers() }), ); await expect( resolveGithubSource({ kind: 'github', owner: 'obra', repo: 'superpowers' }), ).rejects.toThrow(/\/tree\//); }); });