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

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
2265 lines · 69.4 KB Raw History
// 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: [] });
  });
});