Something went wrong. Try again.
A collaborative coding-agent orchestrator for atproto radl.app
Something went wrong. Try again.
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335import { GOAL_SOURCE_GITHUB_ISSUE, GOAL_SOURCE_TANGLED_ISSUE, type GoalView } from '@radial/core'import { describe, expect, it } from 'vitest'import { newGoalArgs } from './goal.js'import { asGitHubIssue, discoverGitHubIssues, githubGoalSeed, githubIssueRows, githubRepo, issuesPageUrl, listGitHubIssues,} from './github-issues.js'import { importedIssues } from './issues.js'
// `issues.test.ts`'s twin, and the differences between them are the feature's argument in miniature.//// There is no index-versus-authority pair to test here: GitHub's API IS the repository's copy, so// the fan-out and the re-read that dominate the tangled tests have nothing to attach to. What// replaces them is everything GitHub does that tangled does not — a pull request answered by the// issues endpoint, a nullable body, a login where a DID would be, no version to pin, and the two// failures an unauthenticated caller actually meets.
const REPO = { owner: 'acme', repo: 'widget' }
const urlOf = (number: number, owner = 'acme', repo = 'widget'): string => `https://github.com/${owner}/${repo}/issues/${number}`
const issue = (number: number, over: Record<string, unknown> = {}) => ({ html_url: urlOf(number), number, state: 'open', title: `Issue ${number}`, body: `The body of ${number}`, created_at: '2026-07-01T00:00:00Z', user: { login: 'stranger', html_url: 'https://github.com/stranger' }, ...over,})
/** An API that answers with whatever it is handed, one page per call. */const api = (pages: unknown[][], init: ResponseInit = { status: 200 }) => { const seen: string[] = [] const fetcher = async (url: string): Promise<Response> => { seen.push(url) const page = pages[Math.min(seen.length - 1, pages.length - 1)] ?? [] return new Response(JSON.stringify(page), init) } return { fetcher, seen }}
describe('reading a project remote', () => { it('takes every spelling of a GitHub repository', () => { expect(githubRepo('https://github.com/acme/widget')).toEqual(REPO) expect(githubRepo('https://github.com/acme/widget.git')).toEqual(REPO) expect(githubRepo('ssh://git@github.com/acme/widget.git')).toEqual(REPO) expect(githubRepo('git@github.com:acme/widget.git')).toEqual(REPO) // The daemon's `parseGitHubRepo` takes this host, so a project whose remote was written with it // must not be told it has no issues. expect(githubRepo('https://www.github.com/acme/widget')).toEqual(REPO) })
it('has no opinion about a remote that is not GitHub', () => { // The mirror image of `tangledRepo`'s own case: not an error, just no. expect(githubRepo('https://tangled.org/disnetdev.com/radial')).toBeUndefined() expect(githubRepo('https://gitlab.com/acme/widget.git')).toBeUndefined() expect(githubRepo('https://github.com/acme')).toBeUndefined() expect(githubRepo('https://github.com/acme/widget/tree/main')).toBeUndefined() expect(githubRepo('not a url')).toBeUndefined() })
it('links the repository issue list', () => { expect(issuesPageUrl(REPO)).toBe('https://github.com/acme/widget/issues') })})
describe('asking GitHub', () => { it('asks for open issues oldest first, and hands back what it got', async () => { const { fetcher, seen } = api([[issue(1)]]) const entries = await listGitHubIssues({ repo: REPO, fetcher }) expect(entries).toHaveLength(1) expect(seen[0]).toContain('/repos/acme/widget/issues?') expect(seen[0]).toContain('state=open') expect(seen[0]).toContain('sort=created') expect(seen[0]).toContain('direction=asc') })
it('reads a response that is not a list as an empty page rather than throwing', async () => { const fetcher = async () => new Response(JSON.stringify({ message: 'nope' }), { status: 200 }) expect(await listGitHubIssues({ repo: REPO, fetcher })).toEqual([]) })
it('names the rate limit rather than saying nothing could be read', async () => { // The failure this feature will actually meet — 60/hour/IP, unauthenticated by design. "GitHub // answered 403" would send a reader looking for a permission they do not need. const fetcher = async () => new Response('rate limited', { status: 403, headers: { 'x-ratelimit-remaining': '0' } }) await expect(listGitHubIssues({ repo: REPO, fetcher })).rejects.toThrow(/hourly limit/) })
it('keeps a plain 403 as a plain 403', async () => { const fetcher = async () => new Response('forbidden', { status: 403 }) await expect(listGitHubIssues({ repo: REPO, fetcher })).rejects.toThrow(/answered 403/) })
it('says a repository is invisible rather than that it is empty', async () => { const fetcher = async () => new Response('not found', { status: 404 }) await expect(listGitHubIssues({ repo: REPO, fetcher })).rejects.toThrow(/private, renamed, or gone/) })})
describe('turning an entry into a candidate', () => { it('accepts an open issue on this repository', () => { expect(asGitHubIssue(issue(7), REPO)).toEqual({ uri: urlOf(7), number: 7, title: 'Issue 7', body: 'The body of 7', createdAt: '2026-07-01T00:00:00Z', login: 'stranger', profileUrl: 'https://github.com/stranger', }) })
it('refuses a pull request, which this endpoint answers with too', () => { // GitHub models a pull as an issue. A pull is work already in flight, not a goal somebody could // take up, and importing one would file a goal against a branch that exists. expect(asGitHubIssue(issue(7, { pull_request: { url: 'https://api.github.com/x' } }), REPO)).toBeUndefined() })
it('takes an issue with no body — GitHub nulls it rather than omitting it', () => { expect(asGitHubIssue(issue(7, { body: null }), REPO)?.body).toBe('') expect(asGitHubIssue(issue(7, { body: undefined }), REPO)?.body).toBe('') })
it('refuses an entry whose html_url is not a canonical issue URL', () => { expect(asGitHubIssue(issue(7, { html_url: 'https://github.com/acme/widget/pull/7' }), REPO)).toBeUndefined() expect(asGitHubIssue(issue(7, { html_url: 'http://github.com/acme/widget/issues/7' }), REPO)).toBeUndefined() expect(asGitHubIssue(issue(7, { html_url: undefined }), REPO)).toBeUndefined() })
it('refuses an entry that names a different repository', () => { // The one thing the index-versus-authority rule protected that survives GitHub being its own // authority: whatever is drawn under this repository was filed on it. expect(asGitHubIssue(issue(7, { html_url: urlOf(7, 'acme', 'other') }), REPO)).toBeUndefined() expect(asGitHubIssue(issue(7, { html_url: urlOf(7, 'other', 'widget') }), REPO)).toBeUndefined() // GitHub owner and repository names are case-insensitive, and a project remote written in another // case is the same repository. expect(asGitHubIssue(issue(7, { html_url: urlOf(7, 'Acme', 'Widget') }), REPO)?.number).toBe(7) })
it('refuses an entry missing what a row is drawn from', () => { expect(asGitHubIssue(issue(7, { title: undefined }), REPO)).toBeUndefined() expect(asGitHubIssue(issue(7, { created_at: undefined }), REPO)).toBeUndefined() expect(asGitHubIssue(issue(7, { user: {} }), REPO)).toBeUndefined() expect(asGitHubIssue(issue(7, { state: 'closed' }), REPO)).toBeUndefined() expect(asGitHubIssue('a string', REPO)).toBeUndefined() })
it('takes a filer with no profile URL rather than losing the issue', () => { const candidate = asGitHubIssue(issue(7, { user: { login: 'stranger' } }), REPO) expect(candidate?.login).toBe('stranger') expect(candidate?.profileUrl).toBeUndefined() })})
describe('discovering candidates', () => { it('orders oldest first and reports nothing dropped', async () => { const { fetcher } = api([ [issue(2, { created_at: '2026-07-01T00:00:00Z' }), issue(1, { created_at: '2026-06-01T00:00:00Z' })], ]) const result = await discoverGitHubIssues({ repo: REPO, taken: new Set(), fetcher }) expect(result.candidates.map((candidate) => candidate.uri)).toEqual([urlOf(1), urlOf(2)]) expect(result).toMatchObject({ failed: 0, truncated: false }) })
it('never counts a pull request as an issue it could not read', async () => { // A repository with an active branch queue would otherwise announce that a dozen issues went // missing. `failed` is for what a reader should be told about. const { fetcher } = api([[issue(1), issue(2, { pull_request: {} }), issue(3, { pull_request: {} })]]) const result = await discoverGitHubIssues({ repo: REPO, taken: new Set(), fetcher }) expect(result.candidates.map((candidate) => candidate.number)).toEqual([1]) expect(result.failed).toBe(0) })
it('counts an entry it could not read', async () => { const { fetcher } = api([[issue(1), { html_url: urlOf(2, 'someone', 'else') }, 'nonsense']]) const result = await discoverGitHubIssues({ repo: REPO, taken: new Set(), fetcher }) expect(result.candidates.map((candidate) => candidate.number)).toEqual([1]) expect(result.failed).toBe(2) })
it('offers an issue the space already took up, so a cold page can still draw its row', async () => { // `discoverIssues`'s regression, and the same one: the fold holds an imported issue's URL and // nothing else, so a look that skipped it could draw the badge, the goal link and "import again" // only in the tab that did the importing, seconds afterwards. const { fetcher } = api([[issue(1), issue(2)]]) const result = await discoverGitHubIssues({ repo: REPO, taken: new Set([urlOf(2)]), fetcher }) expect(result.candidates.map((candidate) => candidate.uri)).toEqual([urlOf(1), urlOf(2)]) })
it('spends a bounded budget on the issues nobody has taken up first', async () => { const { fetcher } = api([[issue(1), issue(2)]]) const result = await discoverGitHubIssues({ repo: REPO, taken: new Set([urlOf(1)]), fetcher, limit: 1, }) expect(result.candidates.map((candidate) => candidate.uri)).toEqual([urlOf(2)]) expect(result.truncated).toBe(true) })
it('walks numbered pages while they come back full, and says when the budget ran out first', async () => { const { fetcher, seen } = api([[issue(1), issue(2)], [issue(3), issue(4)], [issue(5), issue(6)]]) const result = await discoverGitHubIssues({ repo: REPO, taken: new Set(), fetcher, pages: 3, perPage: 2, }) expect(seen).toHaveLength(3) expect(seen[1]).toContain('page=2') expect(seen[2]).toContain('page=3') expect(result.candidates).toHaveLength(6) expect(result.truncated).toBe(true) })
it('stops at a short page — GitHub has no cursor to run out', async () => { const { fetcher, seen } = api([[issue(1)]]) const result = await discoverGitHubIssues({ repo: REPO, taken: new Set(), fetcher, pages: 3, perPage: 2 }) expect(seen).toHaveLength(1) expect(result.truncated).toBe(false) })
it('stops offering at the ceiling and says it did', async () => { const { fetcher } = api([[issue(1), issue(2), issue(3)]]) const result = await discoverGitHubIssues({ repo: REPO, taken: new Set(), fetcher, limit: 2 }) expect(result.candidates).toHaveLength(2) expect(result.truncated).toBe(true) })
it('lets a failed look through as a failure, not as an empty repository', async () => { const fetcher = async () => new Response('nope', { status: 502 }) await expect(discoverGitHubIssues({ repo: REPO, taken: new Set(), fetcher })).rejects.toThrow(/502/) })})
describe('what the space has already taken up', () => { const goal = (rkey: string, source?: Record<string, unknown>): GoalView => ({ target: { uri: `at://did:plc:member/com.disnetdev.radial.goal/${rkey}`, value: { title: rkey, ...(source ? { source } : {}) }, }, }) as unknown as GoalView
it('matches on the kind, so one forge never badges the other', () => { // The join is `importedIssues` with a kind rather than a second function, and the kind is what // keeps the two pages apart: neither reads the other's rows, and a forge this build has never // heard of is simply not ours. const goals = [ goal('gh', { kind: GOAL_SOURCE_GITHUB_ISSUE, uri: urlOf(7) }), goal('tangled', { kind: GOAL_SOURCE_TANGLED_ISSUE, uri: 'at://did:plc:stranger/sh.tangled.repo.issue/3mrx', }), goal('plain'), ] expect([...importedIssues(goals, GOAL_SOURCE_GITHUB_ISSUE).keys()]).toEqual([urlOf(7)]) expect([...importedIssues(goals).keys()]).toEqual([ 'at://did:plc:stranger/sh.tangled.repo.issue/3mrx', ]) })
it('keeps every goal that claimed an issue when two members raced', () => { const first = goal('first', { kind: GOAL_SOURCE_GITHUB_ISSUE, uri: urlOf(7) }) const second = goal('second', { kind: GOAL_SOURCE_GITHUB_ISSUE, uri: urlOf(7) }) expect(importedIssues([first, second], GOAL_SOURCE_GITHUB_ISSUE).get(urlOf(7))).toEqual([ first, second, ]) })
it('joins a look to the fold by URL, and answers nothing for an issue nobody took up', () => { const taken = importedIssues( [goal('one', { kind: GOAL_SOURCE_GITHUB_ISSUE, uri: urlOf(7) })], GOAL_SOURCE_GITHUB_ISSUE, ) const candidates = [asGitHubIssue(issue(7), REPO)!, asGitHubIssue(issue(8), REPO)!] expect(githubIssueRows(taken, candidates).map((row) => row.imported.length)).toEqual([1, 0]) expect(githubIssueRows(taken, candidates)[0]?.title).toBe('Issue 7') })})
describe('importing one', () => { it("seeds the composer with the issue's own words, its URL, and no version to pin", () => { expect(githubGoalSeed(asGitHubIssue(issue(7), REPO)!)).toEqual({ title: 'Issue 7', body: 'The body of 7', filer: { kind: 'label', label: 'stranger', href: 'https://github.com/stranger' }, source: { kind: GOAL_SOURCE_GITHUB_ISSUE, uri: urlOf(7) }, }) })
it("truncates at the goal lexicon's ceilings rather than refusing to open", () => { const seed = githubGoalSeed( asGitHubIssue(issue(7, { title: 'x'.repeat(400), body: 'y'.repeat(100_001) }), REPO)!, ) expect(seed.title).toHaveLength(300) expect(seed.body).toHaveLength(100_000) })
it('writes the pointer through the same `goal create` every other goal goes through', () => { // No `--source-cid`: GitHub content is not content-addressed, so there is no version of the issue // to pin and the pointer says which issue rather than which draft of it. const seed = githubGoalSeed(asGitHubIssue(issue(7), REPO)!) expect( newGoalArgs({ project: 'at://p', title: seed.title, body: seed.body, source: seed.source }), ).toEqual([ 'goal', 'create', '--project', 'at://p', '--title', 'Issue 7', '--body', 'The body of 7', '--source-kind', GOAL_SOURCE_GITHUB_ISSUE, '--source-uri', urlOf(7), ]) })})