import { describe, expect, it } from 'vitest'; import { byPull, fileSummary, fixesOf, mergedRun, place, pullKey, pullRkey, replacedTips, runsForCopies, subjectOf, } from '../src/pull-requests.js'; /** @param {string} name */ const sha = (name) => name.repeat(40).slice(0, 40); /** @param {string} name @param {string} [message] */ const commit = (name, message = '') => ({ sha: sha(name), message }); describe('pullKey', () => { it('names a pull request by the account and the branch', () => { expect(pullKey('did:plc:bob', 'agent/docs')).toBe('did:plc:bob agent/docs'); }); it('tells two branches of one account apart', () => { expect(pullKey('did:plc:bob', 'a')).not.toBe(pullKey('did:plc:bob', 'b')); }); }); describe('subjectOf', () => { it('is the first line', () => { expect(subjectOf('add farewell()\n\nA body.\n')).toBe('add farewell()'); }); }); describe('place', () => { const main = [sha('d'), sha('c'), sha('b'), sha('a')]; it('counts the commits main does not have, and names the merge base', () => { const placed = place([commit('f'), commit('e'), commit('c')], main); expect(placed.ahead).toBe(2); expect(placed.base).toBe(sha('c')); expect(placed.merged).toBe(false); expect(placed.rooted).toBe(true); }); it('counts the commits main has that the branch does not', () => { // Branched at b, which main has moved two commits past. expect(place([commit('e'), commit('b')], main).behind).toBe(2); expect(place([commit('e'), commit('d')], main).behind).toBe(0); }); it('reports a tip main already reaches as merged', () => { const placed = place([commit('c'), commit('b')], main); expect(placed.merged).toBe(true); expect(placed.ahead).toBe(0); expect(placed.behind).toBe(0); }); it('reports a branch sharing no commit with main as unrooted', () => { const placed = place([commit('z'), commit('y')], main); expect(placed.rooted).toBe(false); expect(placed.merged).toBe(false); // Nothing places it, so a distance would be a guess. expect(placed.behind).toBe(0); }); it('treats an empty branch as merged', () => { expect(place([], main).merged).toBe(true); }); }); describe('mergedRun', () => { /** @param {string} name @param {string[]} parents */ const entry = (name, parents = []) => ({ sha: sha(name), parents: parents.map(sha), }); // main: m (merge of t and p) over p over o. The branch is t over s over p. const branch = [entry('t', ['s']), entry('s', ['p']), entry('p', ['o'])]; const mainCommits = [entry('m', ['p', 't']), entry('p', ['o']), entry('o')]; it('takes the commits the merge brought in, and where they began', () => { const run = mergedRun(branch, mainCommits); expect(run.ahead).toBe(2); expect(run.base).toBe(sha('p')); }); it('takes the tip alone where no merge commit names it', () => { const run = mergedRun(branch, [entry('p', ['o']), entry('o')]); expect(run.ahead).toBe(1); expect(run.base).toBe(sha('s')); }); it('reports no base at the root of a history', () => { expect(mergedRun([entry('a')], []).base).toBe(null); }); }); describe('fileSummary', () => { it('counts the paths per directory, most first', () => { const summary = fileSummary([ 'packages/core/src/http.js', 'packages/core/src/auth.js', 'README.md', 'packages/git/src/pack.js', ]); expect(summary.count).toBe(4); expect(summary.dirs).toEqual([ { dir: 'packages/core/src', count: 2 }, { dir: '', count: 1 }, { dir: 'packages/git/src', count: 1 }, ]); }); it('counts nothing for no paths', () => { expect(fileSummary([])).toEqual({ count: 0, dirs: [] }); }); }); describe('runsForCopies', () => { // Each copy under its own holder's key, which is what a run names. const copies = [ 'at://did:plc:alice/dev.pdsjs.git.repo/project', 'at://did:plc:bob/dev.pdsjs.git.repo/project', ]; /** @param {string} did @param {string} startedAt */ const run = (did, startedAt, rkey = 'project') => ({ value: { subject: { uri: `at://${did}/dev.pdsjs.git.repo/${rkey}` }, sha: sha('a'), startedAt, }, }); it('keeps the runs about any named copy, newest first', () => { const runs = runsForCopies( [ run('did:plc:alice', '2026-08-01T00:00:00Z'), run('did:plc:bob', '2026-08-02T00:00:00Z'), ], copies, ); expect(runs.map((entry) => entry.startedAt)).toEqual([ '2026-08-02T00:00:00Z', '2026-08-01T00:00:00Z', ]); }); it('drops another repository, and an account nobody named', () => { const runs = runsForCopies( [ run('did:plc:alice', '2026-08-01T00:00:00Z', 'other'), run('did:plc:mallory', '2026-08-01T00:00:00Z'), { value: { subject: { uri: 42 } } }, {}, ], copies, ); expect(runs).toEqual([]); }); }); describe('pullRkey', () => { it('derives a safe record key from the repository and the full ref', () => { expect(pullRkey('project', 'refs/heads/feature/x')).toBe( 'project:refs_heads_feature_x', ); expect(pullRkey('pds.js', 'refs/heads/main')).toBe( 'pds.js:refs_heads_main', ); }); }); describe('fixesOf', () => { it('reads each Fixes trailer as an issue address', () => { const message = [ 'refuse a non-string name', '', 'Fixes: at://did:plc:dan/dev.pdsjs.git.issue/3kissue', ].join('\n'); expect(fixesOf(message)).toEqual([ 'at://did:plc:dan/dev.pdsjs.git.issue/3kissue', ]); }); it('ignores prose mentioning a fix and messages without one', () => { expect(fixesOf('this fixes the bug')).toEqual([]); expect(fixesOf('Fixes: #123')).toEqual([]); expect(fixesOf('')).toEqual([]); }); }); describe('byPull', () => { it('indexes a review under its pull request and its sha', () => { const statement = { author: 'did:plc:bob', ref: 'refs/heads/agent/docs', sha: sha('a'), }; const index = byPull([statement]); expect(index.get(pullKey('did:plc:bob', 'agent/docs'))).toEqual([ statement, ]); expect(index.get(sha('a'))).toEqual([statement]); }); it("reads a check's pull request off the copy it ran against", () => { const run = { subject: { uri: 'at://did:plc:bob/dev.pdsjs.git.repo/project' }, ref: 'refs/heads/agent/docs', sha: sha('a'), }; const index = byPull([run]); expect(index.get(pullKey('did:plc:bob', 'agent/docs'))).toEqual([run]); }); it('indexes a statement naming no branch under its sha alone', () => { const index = byPull([{ sha: sha('a') }]); expect([...index.keys()]).toEqual([sha('a')]); }); }); describe('replacedTips', () => { /** Only what replacedTips reads of a pull request. @param {string} name */ const tip = (name) => /** @type {any} */ ({ sha: sha(name) }); it('names every tip the branch has carried but the one it carries now', () => { const replaced = replacedTips(tip('c'), [ { sha: sha('b'), startedAt: '2026-08-02T00:00:00Z' }, { sha: sha('a'), reviewedAt: '2026-08-01T00:00:00Z' }, { sha: sha('c'), startedAt: '2026-08-03T00:00:00Z' }, ]); expect([...replaced].sort()).toEqual([sha('a'), sha('b')]); // The tip is not something the branch replaced. expect(replaced.has(sha('c'))).toBe(false); }); it('names nothing where the branch has never been rewritten', () => { expect(replacedTips(tip('a'), []).size).toBe(0); expect( replacedTips(tip('a'), [{ sha: sha('a'), reviewedAt: '' }]).size, ).toBe(0); }); });