chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
// The pull request list as the server reads it: the owner's own copy through
// its own browser, every listed collaborator's copy through theirs, and the
// strangers the discovery port names, each read from their own server.
import { describe, expect, it } from 'vitest';
import { createPullRequestReader } from '../src/pull-request-reader.js';
const ALICE = 'did:plc:alice';
const BOB = 'did:plc:bob';
const CAROL = 'did:plc:carol';
const DAN = 'did:plc:dan';
const EVE = 'did:plc:eve';
const RUNNER = 'did:plc:runner';
const REPO = 'demo';
const SUBJECT = `at://${ALICE}/dev.pdsjs.git.repo/${REPO}`;
/** @param {string} name */
const sha = (name) => name.repeat(40).slice(0, 40);
/**
* @param {string} name
* @param {string[]} parents
* @param {string} message
* @param {number} at
*/
const commit = (name, parents, message, at) => ({
sha: sha(name),
parents: parents.map(sha),
message,
committedAt: at,
author: 'Someone <someone@example.com>',
authoredAt: at,
});
/** @type {Record<string, {did: string, handle: string, pds: string}>} */
const PEOPLE = {
[ALICE]: { did: ALICE, handle: 'alice.test', pds: 'https://alice.test' },
[BOB]: { did: BOB, handle: 'bob.test', pds: 'https://bob.test' },
[CAROL]: { did: CAROL, handle: 'carol.test', pds: 'https://carol.test' },
[DAN]: { did: DAN, handle: 'dan.test', pds: 'https://dan.test' },
[EVE]: { did: EVE, handle: 'eve.test', pds: 'https://eve.test' },
[RUNNER]: { did: RUNNER, handle: 'runner.test', pds: 'https://runner.test' },
};
// main: m3 -> m2 -> m1. Bob's feature: f2 -> f1 -> m2. Dan's fix: d1 -> m3.
const MAIN = [
commit('3', ['2'], 'third', 3000),
commit('2', ['1'], 'second', 2000),
commit('1', [], 'first', 1000),
];
const FEATURE = [
commit(
'f',
['e'],
'feature done\n\nFixes: at://did:plc:x/dev.pdsjs.git.issue/1',
5000,
),
commit('e', ['2'], 'feature started', 4000),
...MAIN.slice(1),
];
const FIX = [commit('d', ['3'], 'fix it', 6000), ...MAIN];
/**
* A world of records and copies, answered by URL.
* @param {Object} [options]
* @param {Array<{did: string, collection: string, rkey: string}>} [options.about] - what discovery answers for every collection
* @param {string[]} [options.unreachable] - DIDs whose servers refuse
* @param {string[]} [options.refusing] - DIDs whose servers answer 500
* @param {Record<string, unknown>} [options.config] - the repository's config record
* @param {{commits: unknown[]}|null} [options.mainLog] - what the owner's browser answers for main
* @param {string[]} [options.unresolvable] - DIDs with no document
* @param {string} [options.branchReadme] - README.md at bob's tip
* @param {string} [options.baseReadme] - README.md at the base
* @param {string} [options.mainReadme] - README.md at main's tip
* @param {string[]} [options.drift] - paths main changed since the base
*/
function world({
about = [],
unreachable = [],
refusing = [],
config = /** @type {Record<string, unknown>} */ ({
collaborators: [BOB],
reviewers: [CAROL],
runner: RUNNER,
}),
mainLog = /** @type {{commits: unknown[]}|null} */ ({ commits: MAIN }),
unresolvable = /** @type {string[]} */ ([]),
// README.md on bob's branch. Main changed the first line since the base;
// a change to the last line merges, one to the first does not. Lines apart,
// because git reads a change to the line beside one as touching it.
branchReadme = 'a\nb\nc\nD\n',
baseReadme = 'a\nb\nc\nd\n',
mainReadme = 'A\nb\nc\nd\n',
drift = ['README.md'],
} = {}) {
/** @type {string[]} */
const asked = [];
/** @type {Array<{did: string, branch: string}>} */
const walked = [];
/** @type {Record<string, Record<string, unknown[]>>} records per did per collection */
const records = {
[BOB]: {
// A collaborator's copy is their own record, found by the name the two
// copies share.
'dev.pdsjs.git.repo': [
{
uri: `at://${BOB}/dev.pdsjs.git.repo/${REPO}`,
cid: 'br1',
value: {
name: REPO,
refs: [],
bundles: [],
createdAt: '2026-08-01T00:00:00Z',
},
},
],
'dev.pdsjs.git.pull': [
{
uri: `at://${BOB}/dev.pdsjs.git.pull/demo:refs_heads_feature`,
cid: 'b1',
value: {
subject: { uri: SUBJECT },
repo: REPO,
ref: 'refs/heads/feature',
sha: sha('f'),
title: 'Bob named it',
note: 'a note',
createdAt: '2026-08-02T00:00:00Z',
},
},
],
'dev.pdsjs.git.review': [],
},
[CAROL]: {
'dev.pdsjs.git.review': [
{
uri: `at://${CAROL}/dev.pdsjs.git.review/1`,
cid: 'c1',
value: {
subject: { uri: SUBJECT },
author: BOB,
ref: 'refs/heads/feature',
sha: sha('f'),
verdict: 'approve',
reviewedAt: '2026-08-03T00:00:00Z',
},
},
{
uri: `at://${CAROL}/dev.pdsjs.git.review/2`,
cid: 'c2',
value: {
subject: { uri: `at://${ALICE}/dev.pdsjs.git.repo/other` },
author: BOB,
ref: 'refs/heads/feature',
sha: sha('f'),
verdict: 'approve',
reviewedAt: '2026-08-03T00:00:00Z',
},
},
// Written against bob's own copy, which is where the branch is.
{
uri: `at://${CAROL}/dev.pdsjs.git.review/3`,
cid: 'c3',
value: {
subject: { uri: `at://${BOB}/dev.pdsjs.git.repo/${REPO}` },
author: BOB,
ref: 'refs/heads/feature',
sha: sha('f'),
verdict: 'comment',
note: 'on the copy',
reviewedAt: '2026-08-04T00:00:00Z',
},
},
],
'dev.pdsjs.git.pull': [],
},
[DAN]: {
'dev.pdsjs.git.repo': [
{
uri: `at://${DAN}/dev.pdsjs.git.repo/${REPO}`,
cid: 'dr1',
value: {
name: REPO,
upstream: SUBJECT,
refs: [],
bundles: [],
createdAt: '2026-08-01T00:00:00Z',
},
},
],
'dev.pdsjs.git.pull': [
{
// The current word, at the key the branch derives. Legacy records
// with server-generated keys are stale.
uri: `at://${DAN}/dev.pdsjs.git.pull/demo:refs_heads_fix`,
cid: 'd4',
value: {
subject: { uri: SUBJECT },
repo: REPO,
ref: 'refs/heads/fix',
sha: sha('d'),
title: 'Dan renamed it',
note: 'the update',
createdAt: '2026-08-05T00:00:00Z',
},
},
{
uri: `at://${DAN}/dev.pdsjs.git.pull/3d`,
cid: 'd1',
value: {
subject: { uri: SUBJECT },
repo: REPO,
ref: 'refs/heads/fix',
sha: sha('d'),
title: 'Dan named it',
status: 'draft',
createdAt: '2026-08-01T00:00:00Z',
},
},
{
uri: `at://${DAN}/dev.pdsjs.git.pull/elsewhere`,
cid: 'd2',
value: {
subject: { uri: `at://${ALICE}/dev.pdsjs.git.repo/other` },
repo: 'other',
ref: 'refs/heads/fix',
sha: sha('d'),
},
},
],
},
[EVE]: {
'dev.pdsjs.git.review': [
{
uri: `at://${EVE}/dev.pdsjs.git.review/1`,
cid: 'e1',
value: {
subject: { uri: SUBJECT },
author: DAN,
ref: 'refs/heads/fix',
sha: sha('d'),
verdict: 'changesRequested',
reviewedAt: '2026-08-04T00:00:00Z',
},
},
],
},
[RUNNER]: {
'dev.pdsjs.git.check': [
{
uri: `at://${RUNNER}/dev.pdsjs.git.check/r1`,
cid: 'r1',
value: {
subject: { uri: `at://${BOB}/dev.pdsjs.git.repo/${REPO}` },
ref: 'refs/heads/feature',
sha: sha('f'),
status: 'success',
startedAt: '2026-08-05T00:00:00Z',
},
},
{
// A copy the config does not name, which runsForCopies drops.
uri: `at://${RUNNER}/dev.pdsjs.git.check/r2`,
cid: 'r2',
value: {
subject: { uri: `at://${DAN}/dev.pdsjs.git.repo/${REPO}` },
ref: 'refs/heads/fix',
sha: sha('d'),
status: 'failure',
startedAt: '2026-08-06T00:00:00Z',
},
},
],
},
};
/** @type {typeof fetch} */
const send = async (input) => {
const url = new URL(String(input));
asked.push(url.href);
const who = Object.values(PEOPLE).find((one) => one.pds === url.origin);
if (!who || unreachable.includes(who.did)) {
throw new TypeError('fetch failed');
}
if (refusing.includes(who.did)) return new Response('no', { status: 500 });
const collection = url.searchParams.get('collection') ?? '';
const held = records[who.did]?.[collection] ?? [];
if (url.pathname.endsWith('listRecords')) {
// The runner's runs come in two pages, to prove the cursor is followed.
if (who.did === RUNNER) {
const cursor = url.searchParams.get('cursor');
return cursor
? Response.json({ records: held.slice(1), cursor: null })
: Response.json({ records: held.slice(0, 1), cursor: 'more' });
}
return Response.json({ records: held, cursor: null });
}
if (url.pathname.endsWith('getRecord')) {
const rkey = url.searchParams.get('rkey');
const row = /** @type {{uri: string}[]} */ (held).find((one) =>
one.uri.endsWith(`/${rkey}`),
);
return row ? Response.json(row) : new Response('no', { status: 404 });
}
return new Response('no', { status: 404 });
};
/** @type {Record<string, Record<string, unknown[]>>} branches per did */
const copies = {
[BOB]: { main: MAIN, feature: FEATURE },
[DAN]: { fix: FIX, other: [commit('o', ['3'], 'not asked for', 7000)] },
};
/** @param {{did: string}} who */
const openCopy = (who) => ({
refNames: async () =>
Object.keys(copies[who.did] ?? {}).map((name) => `refs/heads/${name}`),
/** @param {string} _did @param {string} _repo @param {string} branch */
listCommits: async (_did, _repo, branch) => {
walked.push({ did: who.did, branch });
const log = copies[who.did]?.[branch];
return log ? { commits: log } : null;
},
/**
* @param {string} _did @param {string} _repo @param {string} _base
* @param {string} tip
*/
changedPaths: async (_did, _repo, _base, tip) =>
tip === sha('f')
? [...new Set(['README.md', 'src/a.js', ...drift])]
: ['src/a.js'],
/** @param {string} _did @param {string} _repo @param {string} _sha @param {string} _path */
readFile: async (_did, _repo, _sha, _path) => ({
text: branchReadme,
binary: false,
}),
});
const reader = createPullRequestReader({
resolveContributor: async (did) =>
unresolvable.includes(did) ? null : (PEOPLE[did] ?? null),
readRecord: async (_did, collection, rkey) => {
if (rkey !== REPO) return null;
if (collection === 'dev.pdsjs.git.repo') {
return {
name: REPO,
defaultBranch: 'main',
refs: [{ name: 'refs/heads/main', sha: sha('3') }],
};
}
return config;
},
browser: /** @type {any} */ ({
listCommits: async () => mainLog,
// What main changed since the merge base, for the drift.
changedPaths: async () => drift,
// Every drifted path reads the same base/main text here.
/** @param {string} _did @param {string} _repo @param {string} at */
readFile: async (_did, _repo, at) => ({
text: at === sha('3') ? mainReadme : baseReadme,
binary: false,
}),
}),
discovery: {
about: async (_subject, collection) =>
about.filter((entry) => entry.collection === collection),
},
fetch: send,
openCopy: /** @type {any} */ (openCopy),
});
return { reader, asked, walked };
}
describe('createPullRequestReader', () => {
it("places a collaborator's branch against main and takes their own word on it", async () => {
const { reader } = world();
const list = await reader.listPulls(ALICE, REPO);
expect(list).not.toBeNull();
const pulls = /** @type {NonNullable<typeof list>} */ (list);
expect(pulls.open.map((pull) => pull.id)).toEqual([`${BOB} feature`]);
expect(pulls.merged).toEqual([]);
const [feature] = pulls.open;
expect(feature).toMatchObject({
author: PEOPLE[BOB],
branch: 'feature',
sha: sha('f'),
ahead: 2,
behind: 1,
base: sha('2'),
merged: false,
rooted: true,
files: ['README.md', 'src/a.js'],
fixes: ['at://did:plc:x/dev.pdsjs.git.issue/1'],
// The author's record renames it and carries their note.
title: 'Bob named it',
note: 'a note',
noteAt: '2026-08-02T00:00:00Z',
// Main changed README.md since the branch point, and so did the branch,
// on different lines, so the two go together.
drift: ['README.md'],
conflicts: [],
});
expect(feature.fork).toBeUndefined();
expect(pulls.collaborators.map((who) => who.did)).toEqual([ALICE, BOB]);
expect(pulls.configured).toBe(true);
});
it('leaves a branch drifting more files than it reads for conflicts as unknown, not clean', async () => {
// Main and the branch both change 40 files, the same lines, but the list
// reads only the first 25 for conflicts. Here the two change different
// lines, so none of the read subset conflicts; the rest are unread.
const paths = Array.from({ length: 40 }, (_, at) => `f${at}.txt`);
const { reader } = world({
drift: paths,
// The branch's version differs from main's on a line apart, so a read
// path merges; whether the unread ones do is unknown.
branchReadme: 'a\nb\nc\nD\n',
mainReadme: 'A\nb\nc\nd\n',
baseReadme: 'a\nb\nc\nd\n',
});
const pulls =
/** @type {NonNullable<Awaited<ReturnType<typeof reader.listPulls>>>} */ (
await reader.listPulls(ALICE, REPO)
);
const feature = pulls.open.find((pull) => pull.author.did === BOB);
expect(feature?.drift).toHaveLength(40);
expect(feature?.conflicts).toEqual([]);
expect(feature?.conflictsUnknown).toBe(true);
});
it('names the path where the branch and main changed the same line', async () => {
const { reader } = world({ branchReadme: 'x\nb\nc\nd\n' });
const pulls =
/** @type {NonNullable<Awaited<ReturnType<typeof reader.listPulls>>>} */ (
await reader.listPulls(ALICE, REPO)
);
expect(pulls.open[0].conflicts).toEqual(['README.md']);
});
it('reads a stranger the discovery port names, and only the branch their record names', async () => {
const { reader, walked } = world({
about: [
{
did: DAN,
collection: 'dev.pdsjs.git.pull',
rkey: 'demo:refs_heads_fix',
},
// A record about another repository, which the record itself refutes.
{ did: DAN, collection: 'dev.pdsjs.git.pull', rkey: 'elsewhere' },
// A collaborator, already read whole, is not read twice.
{
did: BOB,
collection: 'dev.pdsjs.git.pull',
rkey: 'demo:refs_heads_feature',
},
],
});
const pulls =
/** @type {NonNullable<Awaited<ReturnType<typeof reader.listPulls>>>} */ (
await reader.listPulls(ALICE, REPO)
);
const fix = pulls.open.find((pull) => pull.author.did === DAN);
expect(fix).toMatchObject({
id: `${DAN} fix`,
fork: true,
title: 'Dan renamed it',
note: 'the update',
ahead: 1,
behind: 0,
base: sha('3'),
files: ['src/a.js'],
});
// Dan's other branch is not this repository's business.
expect(walked.filter((one) => one.did === DAN)).toEqual([
{ did: DAN, branch: 'fix' },
]);
// Newest tip first.
expect(pulls.open.map((pull) => pull.branch)).toEqual(['fix', 'feature']);
});
it("prefers the record at the branch's derived key over another discovery names", async () => {
// Discovery names the derived-key record and an older one. Either order,
// the record the branch's key derives is the current word.
const { reader } = world({
about: [
{ did: DAN, collection: 'dev.pdsjs.git.pull', rkey: '3d' },
{
did: DAN,
collection: 'dev.pdsjs.git.pull',
rkey: 'demo:refs_heads_fix',
},
],
});
const pulls =
/** @type {NonNullable<Awaited<ReturnType<typeof reader.listPulls>>>} */ (
await reader.listPulls(ALICE, REPO)
);
const fix = pulls.open.find((pull) => pull.author.did === DAN);
expect(fix).toMatchObject({
title: 'Dan renamed it',
note: 'the update',
status: undefined,
});
});
it('reads every branch of a copy that names this repository as its upstream', async () => {
const { reader, walked } = world({
about: [{ did: DAN, collection: 'dev.pdsjs.git.repo', rkey: REPO }],
});
const pulls =
/** @type {NonNullable<Awaited<ReturnType<typeof reader.listPulls>>>} */ (
await reader.listPulls(ALICE, REPO)
);
const fromDan = pulls.open.filter((pull) => pull.author.did === DAN);
expect(fromDan.map((pull) => pull.branch).sort()).toEqual(['fix', 'other']);
expect(fromDan.every((pull) => pull.fork)).toBe(true);
// No pull request record, so the tip's own subject names it.
expect(fromDan.find((pull) => pull.branch === 'fix')?.title).toBe('fix it');
expect(walked.filter((one) => one.did === DAN)).toHaveLength(2);
});
it('gathers the reviews from the listed voices and from whoever the port names', async () => {
const { reader } = world({
about: [{ did: EVE, collection: 'dev.pdsjs.git.review', rkey: '1' }],
});
const pulls =
/** @type {NonNullable<Awaited<ReturnType<typeof reader.listPulls>>>} */ (
await reader.listPulls(ALICE, REPO)
);
const reviews = new Map(pulls.reviews);
// Carol's reviews of bob's branch, whether they name alice's record or
// bob's copy, and not her review of another repository.
const onFeature = reviews.get(`${BOB} feature`) ?? [];
expect(onFeature.map((one) => one.uri)).toEqual([
`at://${CAROL}/dev.pdsjs.git.review/1`,
`at://${CAROL}/dev.pdsjs.git.review/3`,
]);
expect(onFeature[0].by).toEqual(PEOPLE[CAROL]);
// Eve is nobody the config names; her word arrives through the port,
// read from her own server, and says so.
const onFix = reviews.get(`${DAN} fix`) ?? [];
expect(onFix).toHaveLength(1);
expect(onFix[0]).toMatchObject({
uri: `at://${EVE}/dev.pdsjs.git.review/1`,
verdict: 'changesRequested',
by: PEOPLE[EVE],
indexed: true,
});
});
it("reads the runner's runs across pages, and keeps only the copies the config names", async () => {
const { reader, asked } = world();
const pulls =
/** @type {NonNullable<Awaited<ReturnType<typeof reader.listPulls>>>} */ (
await reader.listPulls(ALICE, REPO)
);
const checks = new Map(pulls.checks);
expect(checks.get(`${BOB} feature`)?.map((run) => run.status)).toEqual([
'success',
]);
expect(checks.get(`${BOB} feature`)?.[0].rkey).toBe('r1');
// Dan's copy is nobody's collaborator here, so the run on it is not shown.
expect(checks.has(`${DAN} fix`)).toBe(false);
const runnerPages = asked.filter(
(url) =>
url.startsWith('https://runner.test/') && url.includes('listRecords'),
);
expect(runnerPages).toHaveLength(2);
expect(runnerPages[1]).toContain('cursor=more');
});
it('costs an unreachable collaborator their branches and nothing else', async () => {
const { reader } = world({
unreachable: [BOB],
about: [{ did: DAN, collection: 'dev.pdsjs.git.pull', rkey: '3d' }],
});
const pulls =
/** @type {NonNullable<Awaited<ReturnType<typeof reader.listPulls>>>} */ (
await reader.listPulls(ALICE, REPO)
);
// Where a collaborator's copy is, is on their own server: the record is
// theirs and carries their own key. A server that cannot be reached
// costs their branches, and costs nobody else anything.
expect(pulls.open.find((pull) => pull.author.did === BOB)).toBeUndefined();
expect(pulls.open.find((pull) => pull.author.did === DAN)).toBeDefined();
expect(pulls.reviews.length).toBeGreaterThan(0);
});
it('answers null for a repository this account does not hold', async () => {
const { reader } = world();
expect(await reader.listPulls(ALICE, 'nothing')).toBeNull();
});
});
describe('createPullRequestReader, when a server does not answer', () => {
/** @param {ReturnType<typeof world>} made */
const list = async ({ reader }) =>
/** @type {NonNullable<Awaited<ReturnType<typeof reader.listPulls>>>} */ (
await reader.listPulls(ALICE, REPO)
);
it('shows no checks for a repository naming no runner, or one that does not resolve', async () => {
expect(
(await list(world({ config: { collaborators: [BOB] } }))).checks,
).toEqual([]);
expect((await list(world({ unresolvable: [RUNNER] }))).checks).toEqual([]);
});
it('lists nothing from a server that refuses, and reads no record from one', async () => {
const pulls = await list(
world({
refusing: [CAROL, DAN],
about: [{ did: DAN, collection: 'dev.pdsjs.git.pull', rkey: '3d' }],
}),
);
expect(pulls.reviews).toEqual([]);
expect(pulls.open.map((pull) => pull.author.did)).toEqual([BOB]);
});
it('skips a stranger the port names whose account does not resolve', async () => {
const pulls = await list(
world({
unresolvable: [DAN],
about: [{ did: DAN, collection: 'dev.pdsjs.git.pull', rkey: '3d' }],
}),
);
expect(pulls.open.map((pull) => pull.author.did)).toEqual([BOB]);
});
it('reads a repository whose main has no commits as unrooted work, with nothing to drift from', async () => {
const pulls = await list(
world({ mainLog: null, config: { collaborators: [BOB] } }),
);
const [feature] = pulls.open;
expect(feature.rooted).toBe(false);
expect(feature.files).toEqual([]);
expect(feature.drift).toEqual([]);
expect(pulls.configured).toBe(true);
});
it('says so when the config names nobody', async () => {
const pulls = await list(world({ config: {} }));
expect(pulls.configured).toBe(false);
expect(pulls.collaborators.map((who) => who.did)).toEqual([ALICE]);
});
});