~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
651 lines · 22.5 KB Raw History
// 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]);
  });
});