Something went wrong. Try again.
A collaborative coding-agent orchestrator for atproto radl.app
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430import { GOAL_SOURCE_TANGLED_ISSUE, type GoalView } from '@radial/core'import { describe, expect, it } from 'vitest'import { newGoalArgs } from './goal.js'import { asIssue, discoverIssues, goalSeed, importedIssues, issueRows, issuesPageUrl, listIssues, resolveRepoDid, tangledRepo, type IssueLocator,} from './issues.js'
// Same shape of scepticism as `guests.test.ts`, because it is the same kind of path: an index says// WHERE a record is, the record says what it is and which repository it belongs to, and when the two// disagree the index loses. What is different here is the identity rule — a tangled repository's DID// is not its owner's — and that getting it wrong is silently total, so it is tested first.
const OWNER = 'did:plc:owner'const REPO = 'did:plc:repository'const FILER = 'did:plc:stranger'const ISSUES = 'sh.tangled.repo.issue'
const uriOf = (rkey: string, did = FILER): string => `at://${did}/${ISSUES}/${rkey}`
const issue = (rkey: string, over: Record<string, unknown> = {}) => ({ uri: uriOf(rkey), cid: `cid-${rkey}`, value: { $type: ISSUES, repo: REPO, title: `Issue ${rkey}`, body: `The body of ${rkey}`, createdAt: '2026-07-01T00:00:00Z', ...over, },})
/** An appview that answers with whatever it is handed, in one page or several. */const appview = (pages: Array<{ items: IssueLocator[]; cursor?: string }>) => { const seen: string[] = [] let call = 0 const fetcher = async (url: string): Promise<Response> => { seen.push(url) const page = pages[Math.min(call, pages.length - 1)] call += 1 return new Response(JSON.stringify(page), { status: 200 }) } return { fetcher, seen }}
const reader = (records: Array<{ uri: string; cid: string; value: unknown }>) => ({ getForeignRecord: async (uri: string) => { const found = records.find((record) => record.uri === uri) if (!found) throw new Error(`no record at ${uri}`) return found },})
const open = (rkey: string, state = 'open'): IssueLocator => ({ uri: uriOf(rkey), cid: `cid-${rkey}`, state })
describe('reading a project remote', () => { it('takes every spelling of a tangled repository', () => { expect(tangledRepo('https://tangled.org/disnetdev.com/radial')).toEqual({ host: 'tangled.org', owner: 'disnetdev.com', repo: 'radial', }) expect(tangledRepo('git@tangled.org:@alice/widget')).toEqual({ host: 'tangled.org', owner: '@alice', repo: 'widget', }) expect(tangledRepo('ssh://git@tangled.org/alice/widget.git')).toEqual({ host: 'tangled.org', owner: 'alice', repo: 'widget', }) })
it('has no opinion about a remote that is not tangled', () => { // Not an error: the question the section asks is "are there issues we could offer?", and a // GitHub project's answer is no. GitHub's issues are reachable, and offering them is a second // adapter, not a bug in this one. expect(tangledRepo('https://github.com/acme/widget.git')).toBeUndefined() expect(tangledRepo('https://tangled.org/alice')).toBeUndefined() expect(tangledRepo('not a url')).toBeUndefined() })
it('links the repository issue list, which is the only page it can name', () => { const location = tangledRepo('https://tangled.org/disnetdev.com/radial')! expect(issuesPageUrl(location)).toBe('https://tangled.org/disnetdev.com/radial/issues') })})
describe('resolving the repository DID', () => { const owned = (records: Array<{ uri: string; cid: string; value: unknown }>) => ({ listForeignRecords: async () => ({ records }), getForeignRecord: async () => { throw new Error('not used') }, }) const repoRecord = (rkey: string, value: Record<string, unknown>) => ({ uri: `at://${OWNER}/sh.tangled.repo/${rkey}`, cid: `cid-${rkey}`, value: { $type: 'sh.tangled.repo', ...value }, })
it('returns the repository DID, which is not the owner DID', async () => { const did = await resolveRepoDid({ location: { host: 'tangled.org', owner: 'alice.example', repo: 'widget' }, reader: owned([repoRecord('widget', { knot: 'knot1', repoDid: REPO })]), resolveHandle: async () => OWNER, }) expect(did).toBe(REPO) expect(did).not.toBe(OWNER) })
it('matches the newer tid-keyed convention on the name in the record', async () => { const did = await resolveRepoDid({ location: { host: 'tangled.org', owner: '@alice.example', repo: 'widget' }, reader: owned([ repoRecord('3ktid1', { name: 'other', repoDid: 'did:plc:other' }), repoRecord('3ktid2', { name: 'widget', repoDid: REPO }), ]), resolveHandle: async () => OWNER, }) expect(did).toBe(REPO) })
it('falls back to the owner for a record written before repoDid existed', async () => { const did = await resolveRepoDid({ location: { host: 'tangled.org', owner: 'alice.example', repo: 'widget' }, reader: owned([repoRecord('widget', { knot: 'knot1' })]), resolveHandle: async () => OWNER, }) expect(did).toBe(OWNER) })
it('resolves no handle when the remote already names a DID', async () => { let asked = false const did = await resolveRepoDid({ location: { host: 'tangled.org', owner: OWNER, repo: 'widget' }, reader: owned([repoRecord('widget', { repoDid: REPO })]), resolveHandle: async () => { asked = true return OWNER }, }) expect(did).toBe(REPO) expect(asked).toBe(false) })
it('says so when the owner holds no such repository', async () => { await expect( resolveRepoDid({ location: { host: 'tangled.org', owner: 'alice.example', repo: 'widget' }, reader: owned([repoRecord('something-else', { repoDid: REPO })]), resolveHandle: async () => OWNER, }), ).rejects.toThrow(/no sh\.tangled\.repo record named "widget"/) })})
describe('asking the appview', () => { it('asks about the repository DID and reads back locators with their state', async () => { const { fetcher, seen } = appview([{ items: [open('one')] }]) const result = await listIssues({ repoDid: REPO, fetcher }) expect(result.items).toEqual([open('one')]) expect(seen[0]).toContain('/xrpc/sh.tangled.repo.listIssues') expect(seen[0]).toContain(`subject=${encodeURIComponent(REPO)}`) })
it('drops a malformed entry rather than losing the page it arrived in', async () => { const { fetcher } = appview([{ items: [{ cid: 'c' } as unknown as IssueLocator, open('two')] }]) expect((await listIssues({ repoDid: REPO, fetcher })).items).toEqual([open('two')]) })
it('says so when the appview refuses', async () => { const fetcher = async () => new Response('nope', { status: 502 }) await expect(listIssues({ repoDid: REPO, fetcher })).rejects.toThrow(/502/) })})
describe('turning a hit into a candidate', () => { it('accepts an issue record that names this repository', () => { expect(asIssue(issue('one'), REPO)).toEqual({ uri: uriOf('one'), cid: 'cid-one', did: FILER, title: 'Issue one', body: 'The body of one', createdAt: '2026-07-01T00:00:00Z', }) })
it('takes an issue with no body — a title can be the whole of one', () => { expect(asIssue(issue('one', { body: undefined }), REPO)?.body).toBe('') })
it('refuses a record that is not an issue', () => { expect(asIssue({ uri: uriOf('one'), cid: 'c', value: { hello: 'world' } }, REPO)).toBeUndefined() expect(asIssue(issue('one', { $type: 'sh.tangled.repo.pull' }), REPO)).toBeUndefined() expect(asIssue(issue('one', { title: undefined }), REPO)).toBeUndefined() expect( asIssue({ uri: `at://${FILER}/sh.tangled.repo.pull/one`, cid: 'c', value: issue('one').value }, REPO), ).toBeUndefined() })
it('refuses an issue the index filed under the wrong repository', () => { // The identity rule, from the reading side: an issue names the REPO did, and an index that could // decide which repository somebody's words were filed against could put text in front of a // reader under false pretences. expect(asIssue(issue('one', { repo: 'did:plc:elsewhere' }), REPO)).toBeUndefined() expect(asIssue(issue('one', { repo: OWNER }), REPO)).toBeUndefined() })})
describe('discovering candidates', () => { it('keeps open issues, re-reads each from its author, and orders oldest first', async () => { const older = issue('older', { createdAt: '2026-06-01T00:00:00Z' }) const newer = issue('newer', { createdAt: '2026-07-01T00:00:00Z' }) const { fetcher } = appview([{ items: [open('newer'), open('older')] }]) const result = await discoverIssues({ repoDid: REPO, taken: new Set(), reader: reader([older, newer]), fetcher, }) expect(result.candidates.map((candidate) => candidate.uri)).toEqual([older.uri, newer.uri]) expect(result).toMatchObject({ failed: 0, truncated: false }) })
it('drops what tangled closed, and anything whose state it did not fold', async () => { const { fetcher } = appview([ { items: [open('one'), open('two', 'closed'), open('three', 'deleted'), { uri: uriOf('four'), cid: 'c' }] }, ]) const result = await discoverIssues({ repoDid: REPO, taken: new Set(), reader: reader([issue('one')]), fetcher, }) expect(result.candidates.map((candidate) => candidate.uri)).toEqual([uriOf('one')]) expect(result.failed).toBe(0) })
it('reads an issue the space already took up, so a cold page can still draw its row', async () => { // The regression this exists for: dropping an imported issue here left the fold holding its URI // and nothing else — no title, no filer, no body — so the badge, the link to the goal and // "import again" existed only for an import made in this tab, seconds ago. A member arriving // afterwards saw the issue vanish, which reads as "somebody closed it on the forge". const asked: string[] = [] const { fetcher } = appview([{ items: [open('one'), open('two')] }]) const result = await discoverIssues({ repoDid: REPO, taken: new Set([uriOf('two')]), reader: { getForeignRecord: async (uri: string) => { asked.push(uri) return issue(uri.slice(uri.lastIndexOf('/') + 1)) }, }, fetcher, }) expect(result.candidates.map((candidate) => candidate.uri)).toEqual([uriOf('one'), uriOf('two')]) expect(asked.sort()).toEqual([uriOf('one'), uriOf('two')]) })
it('spends a bounded budget on the issues nobody has taken up first', async () => { // Both kinds are rows, but a repository with a backlog longer than this view loads should spend // what it has on the ones still waiting for somebody. An imported issue is dropped only when // there is no room left — never on principle. const { fetcher } = appview([{ items: [open('taken'), open('waiting')] }]) const result = await discoverIssues({ repoDid: REPO, taken: new Set([uriOf('taken')]), reader: reader([issue('waiting'), issue('taken')]), fetcher, limit: 1, }) expect(result.candidates.map((candidate) => candidate.uri)).toEqual([uriOf('waiting')]) expect(result.truncated).toBe(true) })
it('counts a hit it could not read rather than showing the index\'s copy of it', async () => { // The reader is shown the author's own bytes or nothing. Bobbin hands out a `value` beside every // locator and it would be one line to render it; then the appview would be deciding what a // stranger said, which is exactly the authority this path refuses it. const { fetcher } = appview([{ items: [open('one'), open('gone'), open('moved')] }]) const result = await discoverIssues({ repoDid: REPO, taken: new Set(), reader: reader([issue('one'), issue('moved', { repo: 'did:plc:elsewhere' })]), fetcher, }) expect(result.candidates.map((candidate) => candidate.uri)).toEqual([uriOf('one')]) expect(result.failed).toBe(2) })
it('walks pages until the cursor runs out, and says when the budget did first', async () => { const { fetcher, seen } = appview([ { items: [open('one')], cursor: 'page-2' }, { items: [open('two')], cursor: 'page-3' }, { items: [open('three')], cursor: 'page-4' }, ]) const result = await discoverIssues({ repoDid: REPO, taken: new Set(), reader: reader([issue('one'), issue('two'), issue('three')]), fetcher, pages: 3, }) expect(seen.length).toBe(3) expect(seen[1]).toContain('cursor=page-2') expect(result.candidates).toHaveLength(3) expect(result.truncated).toBe(true) })
it('stops offering at the ceiling and says it did', async () => { const items = ['a', 'b', 'c'].map((rkey) => open(rkey)) const { fetcher } = appview([{ items }]) const result = await discoverIssues({ repoDid: REPO, taken: new Set(), reader: reader(['a', 'b', 'c'].map((rkey) => issue(rkey))), fetcher, limit: 2, }) expect(result.candidates).toHaveLength(2) expect(result.truncated).toBe(true) })})
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('indexes goals by the issue they pinned, ended ones included', () => { // Another forge's import is not this page's row: the join matches on the kind, and the GitHub // page runs the same function with its own (`github-issues.test.ts`). const taken = importedIssues([ goal('one', { kind: GOAL_SOURCE_TANGLED_ISSUE, uri: uriOf('one'), cid: 'cid-one' }), goal('two'), goal('three', { kind: 'github-issue', uri: 'https://github.com/acme/w/issues/7' }), ]) expect([...taken.keys()]).toEqual([uriOf('one')]) })
it('keeps every goal that claimed an issue when two members raced', () => { const first = goal('first', { kind: GOAL_SOURCE_TANGLED_ISSUE, uri: uriOf('one'), cid: 'cid-one' }) const second = goal('second', { kind: GOAL_SOURCE_TANGLED_ISSUE, uri: uriOf('one'), cid: 'cid-one' }) // Both, in the order the caller listed them: two members taking up one issue is a fact the row // says out loud, which is only possible if the join keeps them — `goalsByOrigin`'s own shape. expect(importedIssues([first, second]).get(uriOf('one'))).toEqual([first, second]) })
it('joins a look to the fold by URI, and answers nothing for an issue nobody took up', () => { const taken = importedIssues([ goal('one', { kind: GOAL_SOURCE_TANGLED_ISSUE, uri: uriOf('one'), cid: 'cid-one' }), ]) const candidates = [asIssue(issue('one'), REPO)!, asIssue(issue('two'), REPO)!] // The goal pinned `cid-one` and the candidate is at `cid-one` here, but the join is by URI on // purpose: a goal written from an earlier draft of an issue was still written from THIS issue, // and a row that hid it would invite a second member to import it again. expect(issueRows(taken, candidates).map((row) => row.imported.length)).toEqual([1, 0]) expect(issueRows(taken, candidates)[0]?.title).toBe('Issue one') })})
describe('importing one', () => { it('seeds the composer with the issue\'s own words and the version it read', () => { // The filer is a DID, which is what tangled has that GitHub does not: the card resolves them // through `identify()` like every other name in the app. expect(goalSeed(asIssue(issue('one'), REPO)!)).toEqual({ title: 'Issue one', body: 'The body of one', filer: { kind: 'did', did: FILER }, source: { kind: GOAL_SOURCE_TANGLED_ISSUE, uri: uriOf('one'), cid: 'cid-one' }, }) })
it('truncates at the goal lexicon\'s ceilings rather than refusing to open', () => { const seed = goalSeed(asIssue(issue('one', { 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', () => { const seed = goalSeed(asIssue(issue('one'), REPO)!) expect( newGoalArgs({ project: 'at://p', title: seed.title, body: seed.body, source: seed.source }), ).toEqual([ 'goal', 'create', '--project', 'at://p', '--title', 'Issue one', '--body', 'The body of one', '--source-kind', GOAL_SOURCE_TANGLED_ISSUE, '--source-uri', uriOf('one'), '--source-cid', 'cid-one', ]) })
it('writes no source for a goal nobody imported', () => { expect(newGoalArgs({ project: 'at://p', title: 'Ship it', body: '' })).toEqual([ 'goal', 'create', '--project', 'at://p', '--title', 'Ship it', ]) })})