chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
// 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<string, Uint8Array>} 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<Record<string, unknown>>} */
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<string, unknown>} */ 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<string, unknown>} */ 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: [] });
});
});