chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
/**
* Placing a collaborator's branch against canonical main, and the statements
* other accounts publish about it. Everything here is decidable from commit
* lists and records alone; the reads that gather them are exercised in the
* browser.
*/
import { afterEach, describe, expect, it } from 'vitest';
import { diffHref, loadPulls, parsePullPath } from '../src/lib/collab.js';
import {
anchorKey,
inSpan,
lineKey,
placeComments,
threadStack,
threadSummaries,
} from '../src/lib/comments.js';
import { account } from '../src/lib/git.js';
/** @param {string} name */
const sha = (name) => name.repeat(40).slice(0, 40);
describe('diffHref and parsePullPath', () => {
const DID = 'did:plc:w4i54sx7ci3skbfzcsq3xrbw';
/**
* The trip an address actually makes: built by a card, put through a
* browser, split and decoded by the router the way app.jsx does.
* @param {string} branch @param {string} [tab]
*/
const around = (branch, tab) => {
const href = diffHref('demo-app', { author: { did: DID }, branch }, tab);
// What the router does with window.location.pathname.
const rest = new URL(href, 'https://site.example').pathname
.split('/')
.filter(Boolean)
.map(decodeURIComponent)
.slice(2);
return { href, ...parsePullPath(rest) };
};
it('carries a branch back whole, slashes and all', () => {
expect(around('fix/contributing')).toMatchObject({
did: DID,
branch: 'fix/contributing',
tab: '',
});
expect(around('agent/rename/the/ports').branch).toBe(
'agent/rename/the/ports',
);
expect(around('main').branch).toBe('main');
});
it('carries a branch whose name needs escaping', () => {
// Git allows these; a path does not carry them raw.
expect(around('fix/a b').branch).toBe('fix/a b');
expect(around('fix/#1').branch).toBe('fix/#1');
expect(around('fix/100%').branch).toBe('fix/100%');
expect(around('feature/über').branch).toBe('feature/über');
});
it('keeps the DID whole, which is what the branch is paired with', () => {
expect(around('fix/contributing').did).toBe(DID);
// A did:web with a port carries a literal %3A of its own, so the trip
// has to survive being encoded twice and decoded once.
const web = 'did:web:example.com%3A3000';
const href = diffHref(
'demo-app',
{ author: { did: web }, branch: 'fix' },
'',
);
expect(href).toContain('%253A');
const rest = new URL(href, 'https://site.example').pathname
.split('/')
.filter(Boolean)
.map(decodeURIComponent)
.slice(2);
expect(parsePullPath(rest).did).toBe(web);
});
it('reads a tab at the end without eating the branch', () => {
expect(around('fix/contributing', 'files')).toMatchObject({
branch: 'fix/contributing',
tab: 'files',
});
expect(around('fix/contributing', 'checks')).toMatchObject({
branch: 'fix/contributing',
tab: 'checks',
});
});
it('keeps a branch whose own name holds a tab word', () => {
// The divider is what makes these readable. Without it each of them read
// as a tab and the branch was unreachable.
expect(around('fix/files').branch).toBe('fix/files');
expect(around('fix/checks').branch).toBe('fix/checks');
expect(around('files/fix').branch).toBe('files/fix');
expect(around('checks').branch).toBe('checks');
expect(around('fix/checks', 'checks')).toMatchObject({
branch: 'fix/checks',
tab: 'checks',
});
});
it('divides on a segment git refuses in a ref name', () => {
// The whole scheme rests on this: `~` cannot be part of a branch, so a
// segment of it is always the divider. `git check-ref-format` refuses
// `refs/heads/fix/~/checks` and allows `refs/heads/fix/-/checks`, which
// is why the divider is not GitLab's `-`.
expect(around('fix/contributing', 'checks').href).toContain('/~/checks');
// Unreserved in a URL, so it passes through without being escaped.
expect(around('fix', 'files').href).not.toContain('%7E');
});
it('reads one of its commits, addressed under it', () => {
const sha = 'a'.repeat(40);
const href = around('fix/contributing', `commit/${sha}`);
expect(href.href).toContain(`/~/commit/${sha}`);
expect(href).toMatchObject({
branch: 'fix/contributing',
tab: 'commit',
commit: sha,
});
});
it('answers empty for an address naming nothing', () => {
expect(parsePullPath([])).toEqual({
did: '',
branch: '',
tab: '',
commit: '',
});
});
});
describe('threadStack', () => {
/** @param {Partial<Record<string, unknown>>} value */
const entry = (value) => ({ verdict: 'comment', ...value });
it('orders oldest first, each reply under what it answers', () => {
const stack = threadStack([
entry({ uri: 'at://b', reviewedAt: '2026-08-02T00:00:00Z' }),
entry({
uri: 'at://c',
reviewedAt: '2026-08-03T00:00:00Z',
replyTo: { uri: 'at://a' },
}),
entry({ uri: 'at://a', reviewedAt: '2026-08-01T00:00:00Z' }),
]);
expect(stack.map((s) => s.uri)).toEqual(['at://a', 'at://c', 'at://b']);
expect(stack.map((s) => s.depth)).toEqual([0, 1, 0]);
});
it('keeps a reply whose parent is not in the stack, at the top level', () => {
const stack = threadStack([
entry({
uri: 'at://c',
reviewedAt: '2026-08-03T00:00:00Z',
replyTo: { uri: 'at://gone' },
}),
]);
expect(stack).toHaveLength(1);
expect(stack[0].depth).toBe(0);
});
});
describe('threadSummaries', () => {
/** @param {Partial<Record<string, unknown>>} value */
const entry = (value) => ({ verdict: 'comment', ...value });
it('shows each thread once, sorted by its latest word', () => {
const summaries = threadSummaries([
entry({ uri: 'at://old', reviewedAt: '2026-08-01T00:00:00Z' }),
entry({ uri: 'at://young', reviewedAt: '2026-08-02T00:00:00Z' }),
// A fresh reply bubbles the older thread up without becoming its face.
entry({
uri: 'at://answer',
reviewedAt: '2026-08-03T00:00:00Z',
replyTo: { uri: 'at://old' },
}),
]);
expect(summaries.map((t) => t.root.uri)).toEqual([
'at://old',
'at://young',
]);
expect(summaries[0].replies).toBe(1);
expect(summaries[1].replies).toBe(0);
// The whole conversation rides along, for the page that shows it.
expect(summaries[0].thread.map((s) => s.uri)).toEqual([
'at://old',
'at://answer',
]);
});
it('roots a reply whose parent is missing at itself', () => {
const summaries = threadSummaries([
entry({
uri: 'at://orphan',
reviewedAt: '2026-08-01T00:00:00Z',
replyTo: { uri: 'at://gone' },
}),
]);
expect(summaries).toHaveLength(1);
expect(summaries[0].replies).toBe(0);
});
});
describe('placeComments, on a range', () => {
const version = { sha: sha('t'), author: null, ref: null };
const review = (n, anchor) => ({
uri: `at://x/dev.pdsjs.git.review/${n}`,
sha: sha('t'),
verdict: 'comment',
reviewedAt: `2026-08-0${n}T00:00:00Z`,
anchor,
by: { did: 'did:plc:a' },
});
it('hangs a range thread on its last line and marks the span between', () => {
const { byFile, spans } = placeComments(
[
review(1, { path: 'a.js', line: 3, endLine: 6, side: 'new' }),
review(2, { path: 'a.js', line: 9, side: 'new' }),
],
version,
);
const lines = byFile.get('a.js');
// The range sits on line 6, the single comment on line 9.
expect([...lines.keys()].sort()).toEqual(['new:6', 'new:9']);
expect(spans.get('a.js')).toEqual([{ side: 'new', start: 3, end: 6 }]);
});
it('reads a row as inside a span on its own side only', () => {
const spans = [{ side: 'new', start: 3, end: 6 }];
expect(inSpan(spans, { oldLine: null, newLine: 4 })).toBe(true);
expect(inSpan(spans, { oldLine: null, newLine: 6 })).toBe(true);
expect(inSpan(spans, { oldLine: null, newLine: 7 })).toBe(false);
// Same number, other side: not in the span.
expect(inSpan(spans, { oldLine: 4, newLine: null })).toBe(false);
expect(inSpan(undefined, { oldLine: null, newLine: 4 })).toBe(false);
});
});
describe('placeComments', () => {
const version = {
sha: sha('a'),
author: 'did:plc:bob',
ref: 'refs/heads/agent/docs',
};
/** @param {Partial<Record<string, unknown>>} value */
const statement = (value) => ({
sha: sha('a'),
author: 'did:plc:bob',
ref: 'refs/heads/agent/docs',
verdict: 'comment',
by: { did: 'did:plc:nova', handle: 'nova' },
...value,
});
it('places a comment under its line, keyed like a diff row', () => {
const { byFile } = placeComments(
[statement({ anchor: { path: 'index.js', line: 2, side: 'new' } })],
version,
);
expect(byFile.get('index.js')?.get('new:2')).toHaveLength(1);
expect(anchorKey({ line: 2 })).toBe('new:2');
expect(lineKey({ oldLine: 7, newLine: null })).toBe('old:7');
});
it('counts a comment on another version rather than placing it', () => {
const { byFile, elsewhere } = placeComments(
[
statement({
sha: sha('b'),
anchor: { path: 'index.js', line: 2 },
}),
],
version,
);
expect(byFile.size).toBe(0);
expect(elsewhere).toBe(1);
});
it('places on one commit only the reviews written against it', () => {
// A commit read under its pull request names no branch of its own, so
// the account-and-ref match is off and the sha is the whole test. That is
// the rule a forge uses: a review shows on the commit it answered.
const onThisCommit = { sha: sha('a'), author: null, ref: null };
const { byFile, elsewhere } = placeComments(
[
statement({ anchor: { path: 'index.js', line: 2 } }),
// Written against a different commit of the same branch. Its line
// number is true of that commit and says nothing about this one.
statement({ sha: sha('b'), anchor: { path: 'index.js', line: 2 } }),
],
onThisCommit,
);
expect(byFile.get('index.js')?.get('new:2')).toHaveLength(1);
// The other review is passed over rather than counted as behind. Behind
// is a fact about a branch that moved on; a commit does not move, so a
// review of a different commit is simply about something else.
expect(elsewhere).toBe(0);
});
it('ignores unanchored reviews and other pull requests entirely', () => {
const { byFile, elsewhere } = placeComments(
[
statement({}),
statement({
sha: sha('z'),
ref: 'refs/heads/agent/other',
anchor: { path: 'x', line: 1 },
}),
],
version,
);
expect(byFile.size).toBe(0);
expect(elsewhere).toBe(0);
});
});
describe('loadPulls', () => {
const original = globalThis.fetch;
const originalLocation = globalThis.location;
afterEach(() => {
globalThis.fetch = original;
globalThis.location = originalLocation;
account.did = '';
account.pds = '';
account.handle = '';
});
it('reads the list from the server, with the client and not its promise', async () => {
// serverClient() is async. Calling a method on it without awaiting it
// first asks the promise, which holds no methods.
globalThis.location = new URL('https://forge.example/');
account.pds = 'https://pds.example';
/** @type {string[]} */
const asked = [];
globalThis.fetch = async (/** @type {string|URL|Request} */ input) => {
const url = String(input);
asked.push(url);
if (url.includes('dev.pdsjs.git.listPulls')) {
return Response.json({
open: [
{
branch: 'refs/heads/fix',
sha: 'a'.repeat(40),
author: { did: 'did:plc:friend' },
},
],
merged: [],
collaborators: [],
reviews: [],
checks: [],
configured: true,
});
}
if (url.includes('app.bsky.actor.getProfiles')) {
return Response.json({ profiles: [] });
}
return Response.json({ repos: [] });
};
const pulls = await loadPulls('demo-app');
expect(pulls.open).toHaveLength(1);
expect(asked.some((url) => url.includes('dev.pdsjs.git.listPulls'))).toBe(
true,
);
});
});