// The hosting operations behind the dev.pdsjs.git methods, driven through // pds.fetch with a cookie session against fake storage, with a fake // browser. methods.test.js covers the transport: anonymous reads, // validators, bearer scopes. import { cborDecode, cborEncodeDagCbor } from '@pdsjs/core/repo'; import { describe, expect, it } from 'vitest'; import { apiGet, apiPost, createActorStorage, createPds as createBasePds, DID, HANDLE, sessionCookie, } from '../../../test/helpers/account-api-harness.js'; import { createFakeSpaceStorage } from '../../../test/helpers/fake-space-storage.js'; import { checkRequestKey, checkRunsOf, createGitHosting, latestCheckEntries, latestCheckKey, mergeGitConfig, pdsEndpointOf, } from '../src/hosting.js'; import { createGitMethods } from '../src/methods.js'; import { createGitWriteGuard } from '../src/rules.js'; /** * @param {Object} [options] * @param {Object} [options.actorStorage] * @param {Object} [options.spaceStorage] - core's own space browser reads it * @param {Object} [options.spaceBrowser] - what the git routes list spaces with * @param {Object} [options.spaceAdmin] * @param {boolean} [options.gitHttpEnabled] * @param {Object} [options.gitBrowser] */ function createPds({ actorStorage, spaceStorage, spaceBrowser, spaceAdmin, gitHttpEnabled = false, gitBrowser, } = {}) { return createBasePds({ actorStorage, spaceStorage, extensions: [ (host) => ({ name: 'git', collections: [ 'dev.pdsjs.git.repo', 'dev.pdsjs.git.config', 'dev.pdsjs.git.identity', 'dev.pdsjs.git.checkRequest', ], writeGuard: createGitWriteGuard({ getRecord: async ( /** @type {unknown} */ _space, /** @type {string} */ collection, /** @type {string} */ rkey, ) => { const row = await host.actorStorage.getRecord( `at://${DID}/${collection}/${rkey}`, ); return row?.value ? cborDecode(row.value) : null; }, getBlob: async () => null, getBlobRange: async () => null, }), routes: createGitMethods({ host, browser: /** @type {any} */ (gitBrowser ?? null), hosting: createGitHosting({ host, httpEnabled: gitHttpEnabled, spaceBrowser: /** @type {any} */ ( spaceBrowser ?? host.spaceBrowser ?? null ), spaceAdmin: /** @type {any} */ (spaceAdmin ?? null), }), }), features: { git: true, gitBrowse: Boolean(gitBrowser) }, stats: (count) => ({ gitRepos: count('dev.pdsjs.git.repo') }), }), ], }); } const REPO_RECORD = { $type: 'dev.pdsjs.git.repo', name: 'myrepo', description: 'a test repository', defaultBranch: 'main', refs: [ { name: 'refs/heads/main', sha: 'a'.repeat(40) }, { name: 'refs/heads/feature', sha: 'b'.repeat(40) }, { name: 'refs/tags/v1', sha: 'c'.repeat(40) }, ], bundles: [ { heads: ['a'.repeat(40)], parts: [ { $type: 'blob', ref: { $link: 'bafkreiaaa' }, mimeType: 'application/octet-stream', size: 700, }, { $type: 'blob', ref: { $link: 'bafkreibbb' }, mimeType: 'application/octet-stream', size: 300, }, ], }, { heads: ['b'.repeat(40)], parts: [ { $type: 'blob', ref: { $link: 'bafkreiccc' }, mimeType: 'application/octet-stream', size: 500, }, ], }, ], createdAt: '2026-08-01T00:00:00.000Z', updatedAt: '2026-08-12T00:00:00.000Z', }; /** * The listing a name is resolved through, for a store whose getRecord is * written by hand. A name reaches its key through the listing, so a fake * that answers one read answers both. * @param {Record} byRkey - encoded records by key */ function repoListing(byRkey) { return async (/** @type {string} */ collection) => collection === 'dev.pdsjs.git.repo' ? { records: Object.entries(byRkey).map(([rkey, value]) => ({ uri: `at://${DID}/dev.pdsjs.git.repo/${rkey}`, cid: `bafy${rkey}`, value, })), cursor: null, } : { records: [], cursor: null }; } function gitActorStorage() { return createActorStorage({ async getRecord(/** @type {string} */ uri) { return uri === `at://${DID}/dev.pdsjs.git.repo/myrepo` ? { cid: 'bafyrepo', value: cborEncodeDagCbor(REPO_RECORD) } : null; }, async listRecords(/** @type {string} */ collection) { if (collection !== 'dev.pdsjs.git.repo') { return { records: [], cursor: null }; } return { records: [ { uri: `at://${DID}/dev.pdsjs.git.repo/myrepo`, cid: 'bafyrepo', rkey: 'myrepo', value: cborEncodeDagCbor(REPO_RECORD), }, ], cursor: null, }; }, }); } describe('git repos endpoint', () => { it('summarizes repositories with clone URLs', async () => { const pds = createPds({ actorStorage: gitActorStorage() }); const response = await apiGet( pds, '/xrpc/dev.pdsjs.git.listRepos', await sessionCookie(), ); expect(response.status).toBe(200); const body = await response.json(); expect(body.httpEnabled).toBe(false); expect(body.repos).toHaveLength(1); const repo = body.repos[0]; expect(repo.name).toBe('myrepo'); expect(repo.description).toBe('a test repository'); expect(repo.defaultBranch).toBe('main'); expect(repo.branches).toBe(2); expect(repo.tags).toBe(1); expect(repo.bundles).toBe(2); expect(repo.parts).toBe(3); expect(repo.size).toBe(1500); expect(repo.refs).toHaveLength(3); expect(repo.cloneUrl).toBe(`atproto://${HANDLE}/myrepo`); expect(repo.httpCloneUrl).toBeNull(); }); it('adds an HTTP clone URL when the platform serves git smart HTTP', async () => { const pds = createPds({ actorStorage: gitActorStorage(), gitHttpEnabled: true, }); const response = await apiGet( pds, '/xrpc/dev.pdsjs.git.listRepos', await sessionCookie(), ); const body = await response.json(); expect(body.httpEnabled).toBe(true); expect(body.repos[0].httpCloneUrl).toBe( `https://pds.example.com/git/${HANDLE}/myrepo`, ); }); it('reports the feature and an empty list without records', async () => { const pds = createPds(); const cookie = await sessionCookie(); const listed = await apiGet(pds, '/xrpc/dev.pdsjs.git.listRepos', cookie); expect(await listed.json()).toEqual({ httpEnabled: false, repos: [] }); const overview = await apiGet(pds, '/account/api/overview', cookie); const data = await overview.json(); expect(data.features.git).toBe(true); expect(data.stats.gitRepos).toBe(0); }); it('counts repositories held in spaces in the overview stat', async () => { const SPACE = `at://${DID}/space/com.example.team/main`; const repo = (/** @type {string} */ rkey) => ({ collection: 'dev.pdsjs.git.repo', rkey, cid: `bafy${rkey}`, value: { name: rkey }, }); const pds = createPds({ spaceStorage: createFakeSpaceStorage({ spaces: [ { uri: SPACE, deletedAt: null }, { uri: `at://${DID}/space/com.example.gone/old`, deletedAt: 'x' }, ], records: { [SPACE]: [ repo('one'), repo('two'), { collection: 'com.example.post', rkey: 'p', cid: 'bafyp', value: {}, }, ], [`at://${DID}/space/com.example.gone/old`]: [repo('gone')], }, }), }); const overview = await apiGet( pds, '/account/api/overview', await sessionCookie(), ); // Two space repos; the deleted space contributes nothing. expect((await overview.json()).stats.gitRepos).toBe(2); }); }); describe('git browse endpoints', () => { /** A browser port that records calls and returns canned listings. */ function fakeBrowser() { /** @type {Array<{method: string, args: unknown[]}>} */ const calls = []; return { calls, async listTree( /** @type {string} */ did, /** @type {string} */ repo, /** @type {string|undefined} */ ref, /** @type {string|undefined} */ path, ) { calls.push({ method: 'listTree', args: [did, repo, ref, path] }); if (repo === 'missing') return null; return { ref: 'refs/heads/main', sha: 'a'.repeat(40), path: path || '', entries: [ { name: 'src', type: 'dir', mode: '40000', sha: 'b'.repeat(40), size: null, }, { name: 'README.md', type: 'file', mode: '100644', sha: 'c'.repeat(40), size: 15, }, ], }; }, async readFile( /** @type {string} */ did, /** @type {string} */ repo, /** @type {string|undefined} */ ref, /** @type {string|undefined} */ path, ) { calls.push({ method: 'readFile', args: [did, repo, ref, path] }); return { ref: 'refs/heads/main', path, size: 15, mode: '100644', mediaType: null, binary: false, truncated: false, text: '# browser test\n', }; }, async readBytes( /** @type {string} */ did, /** @type {string} */ repo, /** @type {string|undefined} */ ref, /** @type {string|undefined} */ path, ) { calls.push({ method: 'readBytes', args: [did, repo, ref, path] }); return { ref: 'refs/heads/main', path, size: 4, mode: '100644', mediaType: 'image/png', binary: true, bytes: new Uint8Array([137, 80, 78, 71]), }; }, }; } it('lists a directory through the injected browser', async () => { const browser = fakeBrowser(); const pds = createPds({ actorStorage: gitActorStorage(), gitBrowser: browser, }); const response = await apiGet( pds, '/xrpc/dev.pdsjs.git.getTree?repo=myrepo&ref=main&path=src', await sessionCookie(), ); expect(response.status).toBe(200); const body = await response.json(); expect(body.entries).toHaveLength(2); expect(browser.calls[0]).toEqual({ method: 'listTree', args: [DID, 'myrepo', 'main', 'src'], }); }); it('reads a file through the injected browser', async () => { const browser = fakeBrowser(); const pds = createPds({ actorStorage: gitActorStorage(), gitBrowser: browser, }); const response = await apiGet( pds, '/xrpc/dev.pdsjs.git.getFile?repo=myrepo&path=README.md', await sessionCookie(), ); const body = await response.json(); expect(body.text).toBe('# browser test\n'); expect(browser.calls[0]).toEqual({ method: 'readFile', args: [DID, 'myrepo', undefined, 'README.md'], }); }); it('serves an image file as bytes', async () => { const browser = fakeBrowser(); const pds = createPds({ actorStorage: gitActorStorage(), gitBrowser: browser, }); const response = await apiGet( pds, '/xrpc/dev.pdsjs.git.getRaw?repo=myrepo&ref=main&path=logo.png', await sessionCookie(), ); expect(response.status).toBe(200); expect(response.headers.get('Content-Type')).toBe('image/png'); expect(response.headers.get('X-Content-Type-Options')).toBe('nosniff'); expect(new Uint8Array(await response.arrayBuffer())).toEqual( new Uint8Array([137, 80, 78, 71]), ); expect(browser.calls[0]).toEqual({ method: 'readBytes', args: [DID, 'myrepo', 'main', 'logo.png'], }); }); it('reports a missing repository on the raw endpoint', async () => { const pds = createPds({ actorStorage: gitActorStorage(), gitBrowser: fakeBrowser(), }); const response = await apiGet( pds, '/xrpc/dev.pdsjs.git.getRaw?repo=missing&path=logo.png', await sessionCookie(), ); expect(response.status).toBe(404); expect((await response.json()).message).toMatch(/No repository named/); }); it('rejects the raw endpoint without required parameters', async () => { const pds = createPds({ actorStorage: gitActorStorage(), gitBrowser: fakeBrowser(), }); const response = await apiGet( pds, '/xrpc/dev.pdsjs.git.getRaw?repo=myrepo', await sessionCookie(), ); expect(response.status).toBe(400); expect((await response.json()).message).toMatch(/path/); }); it('reports the feature unavailable without a browser', async () => { const pds = createPds(); const cookie = await sessionCookie(); const tree = await apiGet( pds, '/xrpc/dev.pdsjs.git.getTree?repo=x', cookie, ); expect(tree.status).toBe(501); const raw = await apiGet( pds, '/xrpc/dev.pdsjs.git.getRaw?repo=x&path=logo.png', cookie, ); expect(raw.status).toBe(501); const overview = await apiGet(pds, '/account/api/overview', cookie); expect((await overview.json()).features.gitBrowse).toBe(false); }); it('surfaces the browse feature flag when a browser is injected', async () => { const pds = createPds({ actorStorage: gitActorStorage(), gitBrowser: fakeBrowser(), }); const overview = await apiGet( pds, '/account/api/overview', await sessionCookie(), ); expect((await overview.json()).features.gitBrowse).toBe(true); }); }); describe('git repos endpoint edge cases', () => { it('tolerates malformed record values with fallbacks', async () => { const storage = createActorStorage({ async listRecords(/** @type {string} */ collection) { if (collection !== 'dev.pdsjs.git.repo') { return { records: [], cursor: null }; } return { records: [ { // No usable name: it falls back to the uri tail uri: `at://${DID}/dev.pdsjs.git.repo/fallback-name`, cid: 'bafyweird', rkey: 'fallback-name', value: cborEncodeDagCbor({ name: 42, description: 7, defaultBranch: null, createdAt: null, updatedAt: null, refs: [ { name: 'refs/heads/main', sha: 'a'.repeat(40) }, { name: 17, sha: 'not-a-ref' }, ], bundles: [ { heads: 'not-an-array', parts: [{ size: 'big' }] }, 'not-an-object', ], }), }, { // Sorts by createdAt when updatedAt is absent uri: `at://${DID}/dev.pdsjs.git.repo/older`, cid: 'bafyolder', rkey: 'older', value: cborEncodeDagCbor({ name: 'older', refs: 'not-an-array', bundles: null, createdAt: '2020-01-01T00:00:00.000Z', }), }, ], cursor: null, }; }, }); const pds = createPds({ actorStorage: storage }); const response = await apiGet( pds, '/xrpc/dev.pdsjs.git.listRepos', await sessionCookie(), ); expect(response.status).toBe(200); const body = await response.json(); // Dated repos sort first; a repo with no dates at all sorts last expect(body.repos.map((/** @type {{name: string}} */ r) => r.name)).toEqual( ['older', 'fallback-name'], ); const weird = body.repos[1]; expect(weird.description).toBeNull(); expect(weird.defaultBranch).toBeNull(); expect(weird.createdAt).toBeNull(); expect(weird.updatedAt).toBeNull(); // The malformed ref entry is filtered; the malformed bundle keeps its // shape but contributes no parts or size expect(weird.refs).toHaveLength(1); expect(weird.branches).toBe(1); expect(weird.tags).toBe(0); expect(weird.parts).toBe(1); expect(weird.size).toBe(0); const older = body.repos[0]; expect(older.refs).toEqual([]); expect(older.bundles).toBe(0); }); }); describe('git browse endpoint edge cases', () => { function stubBrowser() { return { async listTree(/** @type {string} */ _did, /** @type {string} */ repo) { return repo === 'missing' ? null : { entries: [] }; }, async readFile(/** @type {string} */ _did, /** @type {string} */ repo) { return repo === 'missing' ? null : { text: '' }; }, }; } it('rejects a tree request without a repo parameter', async () => { const pds = createPds({ actorStorage: gitActorStorage(), gitBrowser: stubBrowser(), }); const response = await apiGet( pds, '/xrpc/dev.pdsjs.git.getTree', await sessionCookie(), ); expect(response.status).toBe(400); expect((await response.json()).message).toMatch(/repo/); }); it('rejects a tree request for an unknown repository', async () => { const pds = createPds({ actorStorage: gitActorStorage(), gitBrowser: stubBrowser(), }); const response = await apiGet( pds, '/xrpc/dev.pdsjs.git.getTree?repo=missing', await sessionCookie(), ); expect(response.status).toBe(404); expect((await response.json()).message).toMatch(/No repository named/); }); it('rejects a file request without repo and path', async () => { const pds = createPds({ actorStorage: gitActorStorage(), gitBrowser: stubBrowser(), }); const response = await apiGet( pds, '/xrpc/dev.pdsjs.git.getFile?repo=x', await sessionCookie(), ); expect(response.status).toBe(400); expect((await response.json()).message).toMatch(/path/); }); it('rejects a file request for an unknown repository', async () => { const pds = createPds({ actorStorage: gitActorStorage(), gitBrowser: stubBrowser(), }); const response = await apiGet( pds, '/xrpc/dev.pdsjs.git.getFile?repo=missing&path=README.md', await sessionCookie(), ); expect(response.status).toBe(404); expect((await response.json()).message).toMatch(/No repository named/); }); it('reports the file feature unavailable without a browser', async () => { const pds = createPds(); const response = await apiGet( pds, '/xrpc/dev.pdsjs.git.getFile?repo=x&path=y', await sessionCookie(), ); expect(response.status).toBe(501); }); }); describe('git create endpoint', () => { /** A minted record key: a TID, which sorts by when it was made. */ const TID = /^[234567a-z]{13}$/; /** A space browser holding nothing, so a fresh name is free. */ const noRepos = { async listSpaces() { return []; }, async listRecords() { return { records: [], truncated: false }; }, }; it('refuses a bad name and a private create without spaces', async () => { const pds = createPds(); const cookie = await sessionCookie(); const badName = await apiPost( pds, '/xrpc/dev.pdsjs.git.createRepo', { name: 'no spaces allowed' }, cookie, ); expect(badName.status).toBe(400); expect((await badName.json()).message).toMatch(/repository name/); const noSpaces = await apiPost( pds, '/xrpc/dev.pdsjs.git.createRepo', { name: 'ok-name', private: true }, cookie, ); expect(noSpaces.status).toBe(400); expect((await noSpaces.json()).message).toMatch(/not enabled/); }); it('creates a private repository record in its own space', async () => { /** @type {string[]} */ const calls = []; const pds = createPds({ spaceAdmin: { async createSpace(/** @type {{type: string, skey?: string}} */ opts) { return { uri: `at://${DID}/space/${opts.type}/${opts.skey}` }; }, async createRecord( /** @type {string} */ space, /** @type {string} */ collection, /** @type {string} */ rkey, /** @type {{refs: unknown[], bundles: unknown[]}} */ record, ) { calls.push(`${space} ${collection}/${rkey}`); expect(record.refs).toEqual([]); expect(record.bundles).toEqual([]); }, }, }); // The space is the repository's own: one address form means one place a // private repository can be, and the name is what reaches it. const SPACE = `at://${DID}/space/dev.pdsjs.git.space/proj`; const response = await apiPost( pds, '/xrpc/dev.pdsjs.git.createRepo', { name: 'proj', private: true }, await sessionCookie(), ); expect(response.status).toBe(200); expect(await response.json()).toEqual({ name: 'proj', space: SPACE }); expect(calls).toEqual([`${SPACE} dev.pdsjs.git.repo/proj`]); }); it('creates a space of the git type, keyed by the repository', async () => { /** @type {string[]} */ const created = []; /** @type {string[]} */ const writes = []; const TYPE = 'dev.pdsjs.git.space'; const pds = createPds({ spaceAdmin: { async createSpace(/** @type {{type: string, skey?: string}} */ opts) { created.push(`${opts.type}/${opts.skey ?? ''}`); return { uri: `at://${DID}/space/${opts.type}/${opts.skey}` }; }, async createRecord( /** @type {string} */ space, /** @type {string} */ collection, /** @type {string} */ rkey, ) { writes.push(`${space} ${collection}/${rkey}`); }, }, }); const response = await apiPost( pds, '/xrpc/dev.pdsjs.git.createRepo', { name: 'proj', private: true }, await sessionCookie(), ); expect(response.status).toBe(200); expect(await response.json()).toEqual({ name: 'proj', space: `at://${DID}/space/${TYPE}/proj`, }); expect(created).toEqual([`${TYPE}/proj`]); expect(writes).toEqual([ `at://${DID}/space/${TYPE}/proj dev.pdsjs.git.repo/proj`, ]); }); it('refuses a name a private repository already holds', async () => { const pds = createPds({ spaceBrowser: { async listSpaces() { return [{ uri: `at://${DID}/space/dev.pdsjs.git.space/3lheld` }]; }, async listRecords() { return { records: [ { rkey: '3lheld', cid: 'bafyheld', indexedAt: '2026-01-01T00:00:00.000Z', value: { name: 'proj', refs: [], bundles: [] }, }, ], truncated: false, }; }, }, spaceAdmin: { async createSpace() { throw new Error('a name already held must not reach the space'); }, async createRecord() {}, }, }); // The name is the address, so it is one repository's whether that // repository is public or in a space. const response = await apiPost( pds, '/xrpc/dev.pdsjs.git.createRepo', { name: 'proj', private: true }, await sessionCookie(), ); expect(response.status).toBe(400); expect((await response.json()).message).toMatch(/already exists/); }); it('creates a public repository record with the description', async () => { /** @type {Array<{collection: string, rkey: string, value: Uint8Array}>} */ const writes = []; const pds = createPds({ actorStorage: createActorStorage({ async getBlock() { return null; }, async putBlock() {}, async putCommit() {}, async putRecord( /** @type {string} */ _uri, /** @type {string} */ _cid, /** @type {string} */ collection, /** @type {string} */ rkey, /** @type {Uint8Array} */ value, ) { writes.push({ collection, rkey, value }); }, }), }); const response = await apiPost( pds, '/xrpc/dev.pdsjs.git.createRepo', { name: 'proj', description: ' A project. ' }, await sessionCookie(), ); expect(response.status).toBe(200); expect(await response.json()).toEqual({ name: 'proj', space: null }); expect(writes).toHaveLength(1); expect(writes[0].collection).toBe('dev.pdsjs.git.repo'); // The key is minted; the name is the record's own field. expect(writes[0].rkey).toMatch(TID); const record = cborDecode(writes[0].value); expect(record.name).toBe('proj'); expect(record.refs).toEqual([]); expect(record.bundles).toEqual([]); expect(record.description).toBe('A project.'); }); it('leaves the description off a create that names none', async () => { /** @type {Uint8Array[]} */ const values = []; const pds = createPds({ actorStorage: createActorStorage({ async getBlock() { return null; }, async putBlock() {}, async putCommit() {}, async putRecord( /** @type {string} */ _uri, /** @type {string} */ _cid, /** @type {string} */ _collection, /** @type {string} */ _rkey, /** @type {Uint8Array} */ value, ) { values.push(value); }, }), }); const response = await apiPost( pds, '/xrpc/dev.pdsjs.git.createRepo', { name: 'bare', description: ' ' }, await sessionCookie(), ); expect(response.status).toBe(200); const record = cborDecode(values[0]); expect('description' in record).toBe(false); }); it('refuses a public name that already exists', async () => { const taken = cborEncodeDagCbor({ name: 'taken', refs: [], bundles: [] }); const pds = createPds({ actorStorage: createActorStorage({ async getRecord(/** @type {string} */ uri) { return uri === `at://${DID}/dev.pdsjs.git.repo/3ltaken` ? { cid: 'bafyexisting', value: taken } : null; }, listRecords: repoListing({ '3ltaken': taken }), }), }); const response = await apiPost( pds, '/xrpc/dev.pdsjs.git.createRepo', { name: 'taken' }, await sessionCookie(), ); expect(response.status).toBe(400); expect((await response.json()).message).toMatch(/already exists/); }); }); describe('git settings endpoint', () => { it('requires a name and refuses space settings without spaces', async () => { const pds = createPds(); const cookie = await sessionCookie(); const unnamed = await apiPost( pds, '/xrpc/dev.pdsjs.git.updateSettings', {}, cookie, ); expect(unnamed.status).toBe(400); expect((await unnamed.json()).message).toMatch(/Name the repository/); const noSpaces = await apiPost( pds, '/xrpc/dev.pdsjs.git.updateSettings', { name: 'repo', space: 'at://did:plc:x/space/com.example.t/s' }, cookie, ); expect(noSpaces.status).toBe(400); expect((await noSpaces.json()).message).toMatch(/not enabled/); }); it('reads no checks when the repository names no runner', async () => { const pds = createPds({ actorStorage: gitActorStorage() }); const cookie = await sessionCookie(); const body = await ( await apiGet(pds, '/xrpc/dev.pdsjs.git.listChecks?repo=myrepo', cookie) ).json(); expect(body.runner).toBe(null); expect(body.checks).toEqual([]); }); it('names the repository before reading checks', async () => { const pds = createPds(); const cookie = await sessionCookie(); const unnamed = await apiGet(pds, '/xrpc/dev.pdsjs.git.listChecks', cookie); expect(unnamed.status).toBe(400); expect((await unnamed.json()).message).toMatch(/repo/); }); it('reads each branch check from the runner PDS the DID names', async () => { const config = cborEncodeDagCbor({ $type: 'dev.pdsjs.git.config', name: 'proj', runner: 'did:plc:runner', }); const repo = cborEncodeDagCbor({ $type: 'dev.pdsjs.git.repo', name: 'proj', refs: [ { name: 'refs/heads/main', sha: 'a'.repeat(40) }, { name: 'refs/tags/v1', sha: 'b'.repeat(40) }, ], }); const storage = createActorStorage({ async getRecord(/** @type {string} */ uri) { if (uri === `at://${DID}/dev.pdsjs.git.config/proj`) { return { cid: 'bafyconfig', value: config }; } if (uri === `at://${DID}/dev.pdsjs.git.repo/proj`) { return { cid: 'bafyrepo', value: repo }; } return null; }, listRecords: repoListing({ proj: repo }), }); /** @type {string[]} */ const fetched = []; const realFetch = globalThis.fetch; globalThis.fetch = async (/** @type {any} */ input) => { const target = String(input); fetched.push(target); if (target.includes('plc.directory')) { return new Response( JSON.stringify({ service: [ { id: '#atproto_pds', serviceEndpoint: 'https://runner.test' }, ], }), { headers: { 'content-type': 'application/json' } }, ); } return new Response( JSON.stringify({ value: { status: 'success', sha: 'a'.repeat(40), workflow: 'ci' }, }), { headers: { 'content-type': 'application/json' } }, ); }; try { const pds = createPds({ actorStorage: storage }); const cookie = await sessionCookie(); const body = await ( await apiGet(pds, '/xrpc/dev.pdsjs.git.listChecks?repo=proj', cookie) ).json(); expect(body.runner).toBe('did:plc:runner'); expect(body.service).toBe('https://runner.test'); // Only the branch, never the tag. expect(body.checks).toHaveLength(1); expect(body.checks[0].ref).toBe('refs/heads/main'); expect(body.checks[0].status).toBe('success'); const rkey = latestCheckKey(DID, 'proj', 'refs/heads/main'); expect( fetched.some( (target) => target.startsWith('https://runner.test/xrpc/') && target.includes(`rkey=${encodeURIComponent(rkey)}`) && target.includes('collection=dev.pdsjs.git.latestCheck'), ), ).toBe(true); } finally { globalThis.fetch = realFetch; } }); it('refuses a re-run it cannot describe to a runner', async () => { const pds = createPds(); const cookie = await sessionCookie(); const unnamed = await apiPost( pds, '/xrpc/dev.pdsjs.git.requestCheck', {}, cookie, ); expect(unnamed.status).toBe(400); expect((await unnamed.json()).message).toMatch(/Name the repository/); const noRef = await apiPost( pds, '/xrpc/dev.pdsjs.git.requestCheck', { name: 'proj' }, cookie, ); expect(noRef.status).toBe(400); expect((await noRef.json()).message).toMatch(/Name a branch/); // A tag is not a branch, and the daemon's workflows match branch refs. const tag = await apiPost( pds, '/xrpc/dev.pdsjs.git.requestCheck', { name: 'proj', ref: 'refs/tags/v1' }, cookie, ); expect(tag.status).toBe(400); expect((await tag.json()).message).toMatch(/Name a branch/); }); it('refuses a re-run for a repository no runner can clone', async () => { const pds = createPds(); const cookie = await sessionCookie(); const inSpace = await apiPost( pds, '/xrpc/dev.pdsjs.git.requestCheck', { name: 'proj', ref: 'refs/heads/main', space: 'at://did:plc:x/space/com.example.t/s', }, cookie, ); expect(inSpace.status).toBe(400); expect((await inSpace.json()).message).toMatch(/no public clone URL/); }); it('refuses a re-run on a repository that names no runner', async () => { const repo = cborEncodeDagCbor({ name: 'proj', refs: [], bundles: [] }); const pds = createPds({ actorStorage: createActorStorage({ async getRecord(/** @type {string} */ uri) { return uri === `at://${DID}/dev.pdsjs.git.repo/proj` ? { cid: 'bafyrepo', value: repo } : null; }, listRecords: repoListing({ proj: repo }), }), }); const cookie = await sessionCookie(); const noRunner = await apiPost( pds, '/xrpc/dev.pdsjs.git.requestCheck', { name: 'proj', ref: 'refs/heads/main' }, cookie, ); expect(noRunner.status).toBe(400); expect((await noRunner.json()).message).toMatch(/Set a check runner/); }); it('reports the runner as unreachable when its DID does not resolve', async () => { const config = cborEncodeDagCbor({ $type: 'dev.pdsjs.git.config', name: 'proj', runner: 'did:plc:runner', }); const storage = createActorStorage({ async getRecord(/** @type {string} */ uri) { if (uri === `at://${DID}/dev.pdsjs.git.config/proj`) { return { cid: 'bafyconfig', value: config }; } return uri === `at://${DID}/dev.pdsjs.git.repo/proj` ? { cid: 'bafyrepo', value: cborEncodeDagCbor({ name: 'proj' }) } : null; }, listRecords: repoListing({ proj: cborEncodeDagCbor({ name: 'proj' }), }), }); const realFetch = globalThis.fetch; globalThis.fetch = async () => { throw new Error('network down'); }; try { const pds = createPds({ actorStorage: storage }); const cookie = await sessionCookie(); const body = await ( await apiGet(pds, '/xrpc/dev.pdsjs.git.listChecks?repo=proj', cookie) ).json(); expect(body.unreachable).toBe(true); expect(body.checks).toEqual([]); } finally { globalThis.fetch = realFetch; } }); it('refuses a runner that is not a DID', async () => { const pds = createPds(); const cookie = await sessionCookie(); const bad = await apiPost( pds, '/xrpc/dev.pdsjs.git.updateSettings', { name: 'repo', runner: 'ci.example.com' }, cookie, ); expect(bad.status).toBe(400); expect((await bad.json()).message).toMatch(/must be a DID/); }); it('refuses a package the account has no registry for', async () => { const pds = createPds(); const cookie = await sessionCookie(); const bad = await apiPost( pds, '/xrpc/dev.pdsjs.git.updateSettings', { name: 'repo', packages: [{ registry: 'cargo', name: 'lib' }] }, cookie, ); expect(bad.status).toBe(400); expect((await bad.json()).message).toMatch(/names a registry/); const unnamed = await apiPost( pds, '/xrpc/dev.pdsjs.git.updateSettings', { name: 'repo', packages: [{ registry: 'npm' }] }, cookie, ); expect(unnamed.status).toBe(400); expect((await unnamed.json()).message).toMatch(/names a registry/); const notAList = await apiPost( pds, '/xrpc/dev.pdsjs.git.updateSettings', { name: 'repo', packages: 'lib' }, cookie, ); expect(notAList.status).toBe(400); expect((await notAList.json()).message).toMatch(/must be a list/); }); it('links a package to a repository', async () => { /** @type {Array>} */ const writes = []; const pds = createPds({ spaceBrowser: { async listSpaces() { return []; }, async listRecords() { return { records: [ { rkey: 'proj', cid: 'bafyrepo', indexedAt: '2026-01-01T00:00:00.000Z', value: { name: 'proj', refs: [], bundles: [] }, }, ], truncated: false, }; }, }, spaceAdmin: { async getRecord() { return { runner: 'did:plc:runner', createdAt: '2026-01-01T00:00:00Z', }; }, async putRecord( /** @type {string} */ _space, /** @type {string} */ _collection, /** @type {string} */ _rkey, /** @type {Record} */ record, ) { writes.push(record); }, }, }); const linked = await apiPost( pds, '/xrpc/dev.pdsjs.git.updateSettings', { name: 'proj', space: `at://${DID}/space/com.example.team/main`, packages: [{ registry: 'npm', name: ' @scope/lib ' }], }, await sessionCookie(), ); expect(linked.status).toBe(200); expect((await linked.json()).packages).toEqual([ { registry: 'npm', name: '@scope/lib' }, ]); expect(writes[0].packages).toEqual([ { registry: 'npm', name: '@scope/lib' }, ]); // The write names one setting, and the rest of the record survives it. expect(writes[0].runner).toBe('did:plc:runner'); }); it('refuses malformed protected branch lists and missing repos', async () => { const pds = createPds(); const cookie = await sessionCookie(); const malformed = await apiPost( pds, '/xrpc/dev.pdsjs.git.updateSettings', { name: 'repo', protectedBranches: ['ok', 'not ok'] }, cookie, ); expect(malformed.status).toBe(400); expect((await malformed.json()).message).toMatch(/list of branch names/); const missing = await apiPost( pds, '/xrpc/dev.pdsjs.git.updateSettings', { name: 'ghost', description: 'x' }, cookie, ); expect(missing.status).toBe(404); expect((await missing.json()).message).toMatch(/No repository named/); }); }); describe('git rename endpoint', () => { const PROJ = cborEncodeDagCbor({ $type: 'dev.pdsjs.git.repo', name: 'proj', refs: [{ name: 'refs/heads/main', sha: 'a'.repeat(40) }], bundles: [], createdAt: '2026-01-01T00:00:00.000Z', }); const CONFIG = cborEncodeDagCbor({ $type: 'dev.pdsjs.git.config', name: 'proj', runner: 'did:plc:runner', createdAt: '2026-01-01T00:00:00.000Z', }); /** * A store holding proj at a minted key, with the writes it takes. * @param {Uint8Array|null} [config] - the config record beside it, or none */ function storageHolding(config = CONFIG) { /** @type {Array<{collection: string, rkey: string, value: Uint8Array}>} */ const writes = []; const storage = createActorStorage({ async getRecord(/** @type {string} */ uri) { if (uri === `at://${DID}/dev.pdsjs.git.repo/3lprojkey`) { return { cid: 'bafyrepo', value: PROJ }; } if (config && uri === `at://${DID}/dev.pdsjs.git.config/3lprojkey`) { return { cid: 'bafyconfig', value: config }; } return null; }, listRecords: repoListing({ '3lprojkey': PROJ }), async getBlock() { return null; }, async putBlock() {}, async putCommit() {}, async putRecord( /** @type {string} */ _uri, /** @type {string} */ _cid, /** @type {string} */ collection, /** @type {string} */ rkey, /** @type {Uint8Array} */ value, ) { writes.push({ collection, rkey, value }); }, }); return { storage, writes }; } it('names both the repository and the name to give it', async () => { const pds = createPds(); const answer = await apiPost( pds, '/xrpc/dev.pdsjs.git.renameRepo', { name: 'proj' }, await sessionCookie(), ); expect(answer.status).toBe(400); expect((await answer.json()).message).toMatch(/name to give it/); }); it('refuses a name outside the charset an address permits', async () => { const { storage } = storageHolding(); const pds = createPds({ actorStorage: storage }); const answer = await apiPost( pds, '/xrpc/dev.pdsjs.git.renameRepo', { name: 'proj', newName: 'no spaces allowed' }, await sessionCookie(), ); expect(answer.status).toBe(400); expect((await answer.json()).message).toMatch(/repository name/); }); it('writes the new name and leaves the record key alone', async () => { const { storage, writes } = storageHolding(); const pds = createPds({ actorStorage: storage }); const answer = await apiPost( pds, '/xrpc/dev.pdsjs.git.renameRepo', { name: 'proj', newName: 'renamed' }, await sessionCookie(), ); expect(answer.status).toBe(200); expect(await answer.json()).toEqual({ name: 'renamed', rkey: '3lprojkey' }); // Both records keep their key, which is what every statement about the // repository names. expect(writes.map((write) => write.rkey)).toEqual([ '3lprojkey', '3lprojkey', ]); const record = cborDecode(writes[0].value); expect(record.name).toBe('renamed'); expect(record.refs).toEqual([ { name: 'refs/heads/main', sha: 'a'.repeat(40) }, ]); // The config carries the name it was written under. expect(cborDecode(writes[1].value).name).toBe('renamed'); expect(cborDecode(writes[1].value).runner).toBe('did:plc:runner'); }); it('leaves a repository with no config alone about one', async () => { const { storage, writes } = storageHolding(null); const pds = createPds({ actorStorage: storage }); const answer = await apiPost( pds, '/xrpc/dev.pdsjs.git.renameRepo', { name: 'proj', newName: 'renamed' }, await sessionCookie(), ); expect(answer.status).toBe(200); expect(writes).toHaveLength(1); }); it('writes nothing when the name it is given is the name it has', async () => { const { storage, writes } = storageHolding(); const pds = createPds({ actorStorage: storage }); const answer = await apiPost( pds, '/xrpc/dev.pdsjs.git.renameRepo', { name: 'proj', newName: 'proj' }, await sessionCookie(), ); expect(answer.status).toBe(200); expect(writes).toEqual([]); }); it('refuses a name another repository holds', async () => { const other = cborEncodeDagCbor({ $type: 'dev.pdsjs.git.repo', name: 'taken', refs: [], bundles: [], createdAt: '2026-01-01T00:00:00.000Z', }); const { writes } = storageHolding(); const pds = createPds({ actorStorage: createActorStorage({ async getRecord(/** @type {string} */ uri) { if (uri === `at://${DID}/dev.pdsjs.git.repo/3lprojkey`) { return { cid: 'bafyrepo', value: PROJ }; } if (uri === `at://${DID}/dev.pdsjs.git.repo/3ltakenkey`) { return { cid: 'bafytaken', value: other }; } return null; }, listRecords: repoListing({ '3lprojkey': PROJ, '3ltakenkey': other }), }), }); const answer = await apiPost( pds, '/xrpc/dev.pdsjs.git.renameRepo', { name: 'proj', newName: 'taken' }, await sessionCookie(), ); expect(answer.status).toBe(400); expect((await answer.json()).message).toMatch(/already exists/); expect(writes).toEqual([]); }); it('refuses a repository in a space, which its space addresses', async () => { const pds = createPds(); const answer = await apiPost( pds, '/xrpc/dev.pdsjs.git.renameRepo', { name: 'proj', newName: 'renamed', space: `at://${DID}/space/dev.pdsjs.git.space/proj`, }, await sessionCookie(), ); expect(answer.status).toBe(400); expect((await answer.json()).message).toMatch(/keyed by its name/); }); }); describe('git delete endpoint', () => { const REPO = cborEncodeDagCbor({ $type: 'dev.pdsjs.git.repo', name: 'proj', refs: [{ name: 'refs/heads/main', sha: 'a'.repeat(40) }], bundles: [], createdAt: '2026-01-01T00:00:00.000Z', }); const CONFIG = cborEncodeDagCbor({ $type: 'dev.pdsjs.git.config', name: 'proj', createdAt: '2026-01-01T00:00:00.000Z', }); /** * A store holding one repository, with the deletes it is asked for. * @param {Uint8Array|null} config - the config record beside it, or none */ function storageHolding(config) { /** @type {string[]} */ const deleted = []; /** @type {string[]} */ const unlinked = []; const storage = createActorStorage({ async getRecord(/** @type {string} */ uri) { if (uri === `at://${DID}/dev.pdsjs.git.repo/proj`) { return { cid: 'bafyrepo', value: REPO }; } if (config && uri === `at://${DID}/dev.pdsjs.git.config/proj`) { return { cid: 'bafyconfig', value: config }; } return null; }, async deleteRecord(/** @type {string} */ uri) { deleted.push(uri); }, async unlinkBlobsFromRecord(/** @type {string} */ uri) { unlinked.push(uri); }, async getOrphanedBlobs() { return []; }, async getBlock() { return null; }, async putBlock() {}, async putCommit() {}, listRecords: repoListing({ proj: REPO }), }); return { storage, deleted, unlinked }; } it('requires a name and refuses a space delete without spaces', async () => { const pds = createPds(); const cookie = await sessionCookie(); const unnamed = await apiPost( pds, '/xrpc/dev.pdsjs.git.deleteRepo', {}, cookie, ); expect(unnamed.status).toBe(400); expect((await unnamed.json()).message).toMatch(/Name the repository/); const noSpaces = await apiPost( pds, '/xrpc/dev.pdsjs.git.deleteRepo', { name: 'proj', space: 'at://did:plc:x/space/com.example.t/s' }, cookie, ); expect(noSpaces.status).toBe(400); expect((await noSpaces.json()).message).toMatch(/not enabled/); }); it('reports a name it holds no repository under', async () => { const pds = createPds(); const response = await apiPost( pds, '/xrpc/dev.pdsjs.git.deleteRepo', { name: 'ghost' }, await sessionCookie(), ); expect(response.status).toBe(404); expect((await response.json()).message).toMatch(/No repository named/); }); it('deletes the record, the config beside it, and unlinks the blobs', async () => { const { storage, deleted, unlinked } = storageHolding(CONFIG); const pds = createPds({ actorStorage: storage }); const response = await apiPost( pds, '/xrpc/dev.pdsjs.git.deleteRepo', { name: 'proj' }, await sessionCookie(), ); expect(response.status).toBe(200); expect(await response.json()).toEqual({ name: 'proj', space: null }); expect(deleted).toEqual([ `at://${DID}/dev.pdsjs.git.repo/proj`, `at://${DID}/dev.pdsjs.git.config/proj`, ]); // Every record deleted has its blob links dropped, or the bundle bytes // stay held against an address holding nothing. expect(unlinked).toEqual(deleted); }); it('leaves a repository with no config alone about one', async () => { const { storage, deleted } = storageHolding(null); const pds = createPds({ actorStorage: storage }); const response = await apiPost( pds, '/xrpc/dev.pdsjs.git.deleteRepo', { name: 'proj' }, await sessionCookie(), ); expect(response.status).toBe(200); expect(deleted).toEqual([`at://${DID}/dev.pdsjs.git.repo/proj`]); }); it('refuses while the repository holds a protected branch', async () => { const protectedConfig = cborEncodeDagCbor({ $type: 'dev.pdsjs.git.config', name: 'proj', protectedBranches: ['main'], createdAt: '2026-01-01T00:00:00.000Z', }); const { storage, deleted } = storageHolding(protectedConfig); const pds = createPds({ actorStorage: storage }); const response = await apiPost( pds, '/xrpc/dev.pdsjs.git.deleteRepo', { name: 'proj' }, await sessionCookie(), ); expect(response.status).toBe(400); expect((await response.json()).message).toMatch(/remove the protection/); expect(deleted).toEqual([]); }); it('empties the member list of a private repository it deletes', async () => { const SPACE = `at://${DID}/space/dev.pdsjs.git.space/proj`; /** @type {string[]} */ const deleted = []; /** @type {string[]} */ const removed = []; const pds = createPds({ actorStorage: createActorStorage({ async getOrphanedBlobs() { return []; }, }), spaceBrowser: { async listSpaces() { return []; }, async listRecords() { return { records: [ { rkey: 'proj', cid: 'bafyrepo', indexedAt: '2026-01-01T00:00:00.000Z', value: { name: 'proj', refs: [], bundles: [] }, }, ], truncated: false, }; }, async listMembers() { return [DID, 'did:plc:bob', 'did:plc:carol'].map((did) => ({ did, read: true, write: true, })); }, }, spaceAdmin: { async getRecord( /** @type {string} */ _space, /** @type {string} */ collection, ) { return collection === 'dev.pdsjs.git.repo' ? { name: 'proj', refs: [], bundles: [] } : { name: 'proj' }; }, async deleteRecord( /** @type {string} */ space, /** @type {string} */ collection, /** @type {string} */ rkey, ) { deleted.push(`${space} ${collection}/${rkey}`); }, async removeMember( /** @type {string} */ _space, /** @type {string} */ did, ) { removed.push(did); }, }, }); const response = await apiPost( pds, '/xrpc/dev.pdsjs.git.deleteRepo', { name: 'proj', space: SPACE }, await sessionCookie(), ); expect(response.status).toBe(200); expect(await response.json()).toEqual({ name: 'proj', space: SPACE }); expect(deleted).toEqual([ `${SPACE} dev.pdsjs.git.repo/proj`, `${SPACE} dev.pdsjs.git.config/proj`, ]); // The owner stays; the space is theirs and a repository of that name // created later must start with nobody else in it. expect(removed).toEqual(['did:plc:bob', 'did:plc:carol']); }); }); describe('git authors endpoint', () => { const identity = cborEncodeDagCbor({ $type: 'dev.pdsjs.git.identity', idents: [ { email: 'me@example.com' }, { email: 'friend@example.com', did: 'did:plc:friend' }, { name: 'release-bot', label: 'Release bot' }, ], createdAt: '2026-01-01T00:00:00.000Z', }); const storage = () => createActorStorage({ async getRecord(/** @type {string} */ uri) { return uri === `at://${DID}/dev.pdsjs.git.identity/self` ? { cid: 'bafyidentity', value: identity } : null; }, }); /** @param {(url: string) => Response} answer */ function withFetch(answer) { /** @type {string[]} */ const asked = []; const real = globalThis.fetch; globalThis.fetch = async (/** @type {any} */ input) => { asked.push(String(input)); return answer(String(input)); }; return { asked, restore: () => (globalThis.fetch = real) }; } const profiles = () => new Response( JSON.stringify({ profiles: [ { did: DID, handle: 'test.handle', displayName: 'Me', avatar: 'https://cdn/me.jpg', }, { did: 'did:plc:friend', handle: 'friend.test' }, ], }), { headers: { 'content-type': 'application/json' } }, ); it('resolves the claims and the profiles behind them, anonymously', async () => { const { asked, restore } = withFetch(profiles); try { const pds = createPds({ actorStorage: storage() }); const res = await pds.fetch( new Request('https://pds.example.com/xrpc/dev.pdsjs.git.getAuthors'), ); expect(res.status).toBe(200); expect(res.headers.get('Cache-Control')).toBe('public, max-age=300'); const body = await res.json(); expect(body.owner).toEqual({ did: DID, handle: 'test.handle', displayName: 'Me', avatar: 'https://cdn/me.jpg', }); expect(body.claims).toEqual([ { name: '', email: 'me@example.com', label: '', did: '', avatar: '', profile: null, }, { name: '', email: 'friend@example.com', label: '', did: 'did:plc:friend', avatar: '', profile: { did: 'did:plc:friend', handle: 'friend.test', displayName: '', avatar: '', }, }, { name: 'release-bot', email: '', label: 'Release bot', did: '', avatar: '', profile: null, }, ]); const [profileAsk] = asked; expect(profileAsk).toContain('app.bsky.actor.getProfiles'); expect(profileAsk).toContain(`actors=${encodeURIComponent(DID)}`); expect(profileAsk).toContain('actors=did%3Aplc%3Afriend'); // The profiles are kept: a second read asks the AppView nothing. await pds.fetch( new Request('https://pds.example.com/xrpc/dev.pdsjs.git.getAuthors'), ); expect(asked).toHaveLength(1); } finally { restore(); } }); it('answers the claims without faces when the AppView does not', async () => { const { restore } = withFetch(() => { throw new Error('appview down'); }); try { const pds = createPds({ actorStorage: storage() }); const body = await ( await pds.fetch( new Request('https://pds.example.com/xrpc/dev.pdsjs.git.getAuthors'), ) ).json(); expect(body.owner).toBeNull(); expect(body.claims).toHaveLength(3); expect(body.claims[1].profile).toBeNull(); } finally { restore(); } }); it('answers an account with no identity record', async () => { const { restore } = withFetch(profiles); try { const pds = createPds(); const body = await ( await pds.fetch( new Request('https://pds.example.com/xrpc/dev.pdsjs.git.getAuthors'), ) ).json(); expect(body.claims).toEqual([]); expect(body.owner?.handle).toBe('test.handle'); } finally { restore(); } }); }); describe('mergeGitConfig', () => { it('carries a runner through a protected branch write', () => { const existing = { $type: 'dev.pdsjs.git.config', name: 'proj', runner: 'did:plc:runner', createdAt: '2026-01-01T00:00:00.000Z', }; const merged = mergeGitConfig(existing, { name: 'proj', protectedBranches: ['main'], }); expect(merged.runner).toBe('did:plc:runner'); expect(merged.protectedBranches).toEqual(['main']); }); it('carries protected branches through a runner write', () => { const merged = mergeGitConfig( { protectedBranches: ['main'], createdAt: '2026-01-01T00:00:00.000Z' }, { name: 'proj', runner: 'did:plc:runner' }, ); expect(merged.protectedBranches).toEqual(['main']); expect(merged.runner).toBe('did:plc:runner'); }); it('keeps a field this server does not recognize', () => { const merged = mergeGitConfig( { somethingNewer: 42, createdAt: '2026-01-01T00:00:00.000Z' }, { name: 'proj', protectedBranches: [] }, ); expect(merged.somethingNewer).toBe(42); }); it('clears the runner when the write names null', () => { const merged = mergeGitConfig( { runner: 'did:plc:runner', createdAt: '2026-01-01T00:00:00.000Z' }, { name: 'proj', runner: null }, ); expect('runner' in merged).toBe(false); }); it('keeps the original createdAt and stamps updatedAt', () => { const merged = mergeGitConfig( { createdAt: '2026-01-01T00:00:00.000Z' }, { name: 'proj', protectedBranches: ['main'] }, ); expect(merged.createdAt).toBe('2026-01-01T00:00:00.000Z'); expect(merged.updatedAt).toBeTypeOf('string'); }); it('stamps createdAt when there is no record to replace', () => { const merged = mergeGitConfig(null, { name: 'proj', protectedBranches: ['main'], }); expect(merged.createdAt).toBeTypeOf('string'); expect(merged.name).toBe('proj'); }); it('carries linked packages through a runner write', () => { const merged = mergeGitConfig( { packages: [{ registry: 'npm', name: 'lib' }], createdAt: '2026-01-01T00:00:00.000Z', }, { name: 'proj', runner: 'did:plc:runner' }, ); expect(merged.packages).toEqual([{ registry: 'npm', name: 'lib' }]); }); it('clears the packages when the write names an empty list', () => { const merged = mergeGitConfig( { packages: [{ registry: 'npm', name: 'lib' }], createdAt: '2026-01-01T00:00:00.000Z', }, { name: 'proj', packages: [] }, ); expect('packages' in merged).toBe(false); }); }); describe('pdsEndpointOf', () => { it('reads the atproto PDS service from a DID document', () => { expect( pdsEndpointOf({ service: [ { id: '#atproto_labeler', serviceEndpoint: 'https://labeler.test' }, { id: '#atproto_pds', serviceEndpoint: 'https://pds.test/' }, ], }), ).toBe('https://pds.test'); }); it('reports nothing for a document with no PDS', () => { expect(pdsEndpointOf({ service: [] })).toBe(null); expect(pdsEndpointOf({})).toBe(null); expect(pdsEndpointOf(null)).toBe(null); }); }); describe('latestCheckKey', () => { // The runner writes with the copy in @pdsjs/git-ci. If these disagree, a // published check never resolves, so both derivations assert this vector. it('derives the key a runner writes under', () => { expect(latestCheckKey('did:plc:abc', 'proj', 'refs/heads/main')).toBe( 'did:plc:abc:proj:refs_heads_main', ); }); }); describe('latestCheckEntries', () => { // The runner writes with the copy in @pdsjs/git-ci; both normalizations // read the same records. it('reads the per-workflow entries', () => { const entries = latestCheckEntries({ ref: 'refs/heads/main', checks: [ { workflow: 'ci', status: 'success' }, { workflow: 'mirror', status: 'running' }, ], }); expect(entries.map((entry) => entry.workflow)).toEqual(['ci', 'mirror']); }); it('reads a record from before entries existed as one entry', () => { const entries = latestCheckEntries({ ref: 'refs/heads/main', workflow: 'ci', status: 'success', sha: 'a'.repeat(40), }); expect(entries).toHaveLength(1); expect(entries[0].workflow).toBe('ci'); expect(entries[0].status).toBe('success'); }); it('reads an empty or foreign record as no entries', () => { expect(latestCheckEntries({})).toEqual([]); expect(latestCheckEntries({ ref: 'refs/heads/main' })).toEqual([]); }); }); describe('checkRequestKey', () => { // The runner reads with the copy in @pdsjs/git-ci. If these disagree, a // second request writes a second record and the runner checks twice, so // both derivations assert this vector. it('derives the key a runner reads requests under', () => { expect(checkRequestKey('proj', 'refs/heads/main')).toBe( 'proj:refs_heads_main', ); }); }); describe('mergePull', () => { const PATH = '/xrpc/dev.pdsjs.git.mergePull'; it('names what it needs before it reaches for anything', async () => { // What was asked for is answered before whether it can be served, so an // incomplete request reads the same on every deployment. const pds = createPds({ actorStorage: gitActorStorage() }); const answer = await apiPost(pds, PATH, {}, await sessionCookie()); expect(answer.status).toBe(400); expect((await answer.json()).message).toMatch(/Name the repository/); }); it('refuses a repository this account does not hold', async () => { const pds = createPds({ actorStorage: gitActorStorage() }); const answer = await apiPost( pds, PATH, { name: 'nothing', did: 'did:plc:bob', ref: 'refs/heads/fix' }, await sessionCookie(), ); expect(answer.status).toBe(404); expect((await answer.json()).message).toMatch(/No repository named/); }); it('refuses a repository whose canonical branch is not there', async () => { // A repository with no commits has no main to merge onto, and that is // answered before the other account's server is reached for. const empty = { $type: 'dev.pdsjs.git.repo', name: 'empty', defaultBranch: 'main', refs: [], bundles: [], createdAt: '2026-08-01T00:00:00.000Z', }; const pds = createPds({ actorStorage: createActorStorage({ async getRecord(/** @type {string} */ uri) { return uri === `at://${DID}/dev.pdsjs.git.repo/empty` ? { cid: 'bafyempty', value: cborEncodeDagCbor(empty) } : null; }, listRecords: repoListing({ empty: cborEncodeDagCbor(empty) }), }), }); const answer = await apiPost( pds, PATH, { name: 'empty', did: 'did:plc:bob', ref: 'refs/heads/fix' }, await sessionCookie(), ); expect(answer.status).toBe(400); expect((await answer.json()).message).toMatch(/is not a branch here/); }); it('is closed to a request carrying no session', async () => { const pds = createPds({ actorStorage: gitActorStorage() }); const answer = await apiPost( pds, PATH, { name: 'myrepo', did: 'did:plc:bob', ref: 'refs/heads/feature' }, '', ); expect(answer.status).toBe(401); }); }); describe('listPulls and listIssues', () => { /** * The world beyond this server: a directory answering for two accounts, * the owner's own records served from an origin of its own, and a * collaborator whose server does not answer. */ function elsewhere() { const realFetch = globalThis.fetch; /** @type {string[]} */ const asked = []; globalThis.fetch = async (/** @type {any} */ input) => { const url = new URL(String(input)); asked.push(url.href); if (url.hostname === 'plc.directory') { const did = decodeURIComponent(url.pathname.slice(1)); const host = did === DID ? 'https://owner.test' : 'https://bob.test'; return Response.json({ alsoKnownAs: [`at://${did.slice('did:plc:'.length)}.test`], service: [{ id: '#atproto_pds', serviceEndpoint: host }], }); } if (url.origin === 'https://owner.test') { const collection = url.searchParams.get('collection'); if (collection === 'dev.pdsjs.git.issue') { return Response.json({ records: [ { uri: `at://${DID}/dev.pdsjs.git.issue/1`, cid: 'bafyissue', value: { subject: { uri: `at://${DID}/dev.pdsjs.git.repo/myrepo` }, title: 'It breaks', createdAt: '2026-08-01T00:00:00Z', }, }, ], cursor: null, }); } return Response.json({ records: [], cursor: null }); } throw new TypeError('fetch failed'); }; return { asked, restore: () => (globalThis.fetch = realFetch) }; } const configured = () => createActorStorage({ async getRecord(/** @type {string} */ uri) { if (uri === `at://${DID}/dev.pdsjs.git.repo/myrepo`) { return { cid: 'bafyrepo', value: cborEncodeDagCbor(REPO_RECORD) }; } if (uri === `at://${DID}/dev.pdsjs.git.config/myrepo`) { return { cid: 'bafyconfig', value: cborEncodeDagCbor({ $type: 'dev.pdsjs.git.config', collaborators: ['did:plc:bob'], }), }; } return null; }, listRecords: repoListing({ myrepo: cborEncodeDagCbor(REPO_RECORD) }), }); it('lists the pull requests from the copies it can reach, and names every collaborator', async () => { const world = elsewhere(); try { const pds = createPds({ actorStorage: configured(), gitBrowser: { listCommits: async () => ({ commits: [] }), changedPaths: async () => [], }, }); const res = await apiGet( pds, '/xrpc/dev.pdsjs.git.listPulls?repo=myrepo', '', ); expect(res.status).toBe(200); const body = await res.json(); // Bob's server does not answer, which costs his branches and nothing else. expect(body.open).toEqual([]); expect(body.merged).toEqual([]); expect( body.collaborators.map((/** @type {{did: string}} */ who) => who.did), ).toEqual([DID, 'did:plc:bob']); expect(body.collaborators[1].handle).toBe('bob.test'); expect(body.configured).toBe(true); expect( world.asked.some((url) => url.startsWith('https://bob.test/')), ).toBe(true); } finally { world.restore(); } }); it("lists the issue statements from each voice's own server", async () => { const world = elsewhere(); try { const pds = createPds({ actorStorage: configured() }); const res = await apiGet( pds, '/xrpc/dev.pdsjs.git.listIssues?repo=myrepo', '', ); expect(res.status).toBe(200); const { statements } = await res.json(); expect(statements).toHaveLength(1); expect(statements[0]).toMatchObject({ uri: `at://${DID}/dev.pdsjs.git.issue/1`, title: 'It breaks', by: { did: DID, handle: `${DID.slice('did:plc:'.length)}.test` }, }); } finally { world.restore(); } }); it('answers RepoNotFound for a repository this account does not hold', async () => { const world = elsewhere(); try { const pds = createPds({ actorStorage: configured() }); const res = await apiGet( pds, '/xrpc/dev.pdsjs.git.listIssues?repo=nothing', '', ); expect(res.status).toBe(404); expect((await res.json()).error).toBe('RepoNotFound'); } finally { world.restore(); } }); }); describe('checkRunsOf', () => { const subject = `at://${DID}/dev.pdsjs.git.repo/proj`; const row = ( /** @type {string} */ rkey, /** @type {Record} */ value, ) => ({ uri: `at://did:plc:runner/dev.pdsjs.git.check/${rkey}`, cid: `cid-${rkey}`, value: { subject: { uri: subject }, sha: 'a'.repeat(40), status: 'success', ...value, }, }); it('keeps the runs about the repository, newest first, each with its record key', () => { const runs = checkRunsOf( [ row('old', { startedAt: '2026-01-01T00:00:00Z' }), row('new', { startedAt: '2026-02-01T00:00:00Z' }), row('elsewhere', { startedAt: '2026-03-01T00:00:00Z', subject: { uri: `at://${DID}/dev.pdsjs.git.repo/other` }, }), row('malformed', { startedAt: undefined }), null, ], subject, ); expect(runs.map((run) => run.rkey)).toEqual(['new', 'old']); }); }); describe('listCheckRuns', () => { it('lists every run the runner published about the repository, across pages', async () => { const storage = createActorStorage({ async getRecord(/** @type {string} */ uri) { if (uri === `at://${DID}/dev.pdsjs.git.repo/proj`) { return { cid: 'bafyrepo', value: cborEncodeDagCbor({ ...REPO_RECORD, name: 'proj' }), }; } if (uri === `at://${DID}/dev.pdsjs.git.config/proj`) { return { cid: 'bafyconfig', value: cborEncodeDagCbor({ runner: 'did:plc:runner' }), }; } return null; }, listRecords: repoListing({ proj: cborEncodeDagCbor({ ...REPO_RECORD, name: 'proj' }), }), }); const subject = `at://${DID}/dev.pdsjs.git.repo/proj`; const realFetch = globalThis.fetch; /** @type {string[]} */ const asked = []; globalThis.fetch = async (/** @type {any} */ input) => { const url = new URL(String(input)); asked.push(url.href); if (url.pathname.startsWith('/did')) { return Response.json({ service: [ { id: '#atproto_pds', serviceEndpoint: 'https://runner.test' }, ], }); } const page = url.searchParams.get('cursor'); const run = (/** @type {string} */ rkey, /** @type {string} */ at) => ({ uri: `at://did:plc:runner/dev.pdsjs.git.check/${rkey}`, cid: rkey, value: { subject: { uri: subject }, sha: 'a'.repeat(40), status: 'success', startedAt: at, }, }); return page ? Response.json({ records: [run('r1', '2026-01-01T00:00:00Z')], cursor: null, }) : Response.json({ records: [run('r2', '2026-02-01T00:00:00Z')], cursor: 'more', }); }; try { const pds = createPds({ actorStorage: storage }); const res = await apiGet( pds, '/xrpc/dev.pdsjs.git.listCheckRuns?repo=proj', '', ); expect(res.status).toBe(200); const body = await res.json(); expect(body.runner).toBe('did:plc:runner'); expect(body.service).toBe('https://runner.test'); expect( body.runs.map((/** @type {{rkey: string}} */ run) => run.rkey), ).toEqual(['r2', 'r1']); expect(asked.filter((url) => url.includes('listRecords'))).toHaveLength( 2, ); } finally { globalThis.fetch = realFetch; } }); it('answers no runs for a repository naming no runner', async () => { const pds = createPds({ actorStorage: gitActorStorage() }); const res = await apiGet( pds, '/xrpc/dev.pdsjs.git.listCheckRuns?repo=myrepo', '', ); expect(res.status).toBe(200); expect(await res.json()).toEqual({ runner: null, runs: [] }); }); });