Files
bherbruck 15e7615ce3 PR rules: a PR has to explain itself and show its picture (#11188)
A pull request now starts by saying what it is in plain words, and shows
anything there is to look at in its description. A file in the diff
doesn't count. The new Picture check fails a PR that adds to the showcase
or docs/screenshots without showing a picture. It isn't a required check.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 15:43:22 +00:00

197 lines
9.3 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict';
const test = require('node:test');
const assert = require('node:assert/strict');
const pc = require('./pr-checks.js');
const REPO = 'tangency-labs/solidcraft';
test('parseClosing finds closing keywords only', () => {
assert.deepEqual(pc.parseClosing('Fixes #12', REPO), [12]);
assert.deepEqual(pc.parseClosing('closes: #3\nResolved #4', REPO), [3, 4]);
assert.deepEqual(pc.parseClosing('Fixes #5, #6 and #7', REPO), [5, 6, 7]);
assert.deepEqual(pc.parseClosing('Fix #8', REPO), [8]);
assert.deepEqual(pc.parseClosing('Refs #9\nSee #10\nrelated to #11', REPO), []);
assert.deepEqual(pc.parseClosing('Prefixes #1 and suffix fixes#2', REPO), []);
assert.deepEqual(pc.parseClosing(null, REPO), []);
});
test('parseClosing handles repo-qualified refs, comments and code', () => {
assert.deepEqual(pc.parseClosing(`Fixes ${REPO}#20`, REPO), [20]);
assert.deepEqual(pc.parseClosing('Fixes other/repo#21', REPO), []);
assert.deepEqual(pc.parseClosing('<!-- Fixes #22 -->', REPO), []);
assert.deepEqual(pc.parseClosing('```\nFixes #23\n```\nFixes #24', REPO), [24]);
assert.deepEqual(pc.parseClosing('say `Fixes #25` to close', REPO), []);
});
test('parseDefaults', () => {
assert.equal(pc.parseDefaults('x').status, 'missing');
assert.equal(pc.parseDefaults('<!-- Changes a default: no -->').status, 'missing');
assert.equal(pc.parseDefaults('Changes a default: no').status, 'no');
assert.equal(pc.parseDefaults('- Changes a default: No.\r\nmore').status, 'no');
assert.equal(pc.parseDefaults('**Changes a default:** no').status, 'no');
assert.equal(pc.parseDefaults('Changes a default: <no | yes — <what>, old behaviour kept behind preference <name>>').status, 'malformed');
assert.equal(pc.parseDefaults('Changes a default:').status, 'malformed');
assert.equal(pc.parseDefaults('Changes a default: maybe').status, 'malformed');
assert.equal(pc.parseDefaults('Changes a default: yes').status, 'malformed');
assert.equal(pc.parseDefaults('Changes a default: yes - faster zoom').status, 'malformed');
});
test('parseDefaults yes forms', () => {
for (const sep of ['—', '–', '-', '->', '→', ':']) {
const r = pc.parseDefaults(`Changes a default: yes ${sep} zoom speed doubled, old behaviour kept behind preference viewport.zoom_speed`);
assert.equal(r.status, 'yes', sep);
assert.equal(r.preference, 'viewport.zoom_speed');
assert.equal(r.detail, 'zoom speed doubled');
}
assert.equal(pc.parseDefaults('Changes a default: yes — x, old value kept behind preference `p`').status, 'yes');
assert.equal(pc.parseDefaults('Changes a default: yes — x, old behaviour kept behind preference <name>').status, 'malformed');
});
test('isBot and isDocsOnly', () => {
assert.ok(pc.isBot({ login: 'dependabot[bot]', type: 'Bot' }));
assert.ok(pc.isBot({ login: 'solidcraft-generated[bot]' }));
assert.ok(!pc.isBot({ login: 'alice', type: 'User' }));
assert.ok(pc.isDocsOnly(['README.md', 'docs/a.png']));
assert.ok(!pc.isDocsOnly(['README.md', 'crates/a.rs']));
assert.ok(!pc.isDocsOnly([]));
});
const mk = (number, login, body, extra = {}) => ({
number, state: 'open', user: { login }, body, title: `t${number}`,
html_url: `https://github.com/${REPO}/pull/${number}`, labels: [], ...extra,
});
test('findDuplicates: lower number, other author, same issue', () => {
const pulls = [
mk(10, 'bob', 'Fixes #5'),
mk(11, 'carol', 'Fixes #6'),
mk(12, 'alice', 'Fixes #5'),
mk(13, 'dan', 'Refs #5'),
mk(14, 'erin', 'Closes #5', { draft: true }),
mk(15, 'fay', 'Fixes #5', { labels: [{ name: 'not-a-duplicate' }] }),
mk(30, 'gus', 'Fixes #5'),
mk(16, 'hal', 'Fixes #5', { state: 'closed' }),
];
const d = pc.findDuplicates({ number: 20, author: 'alice', issues: [5], pulls, repoFullName: REPO });
assert.deepEqual(d.map((x) => x.number), [10, 14]);
assert.ok(d[1].draft);
});
function fakes(pull, { pulls = [], files = [] } = {}) {
const calls = [];
const github = {
paginate: async (fn) => (fn === 'list' ? pulls : files),
rest: {
pulls: { list: 'list', listFiles: 'files' },
issues: {
addLabels: async (a) => calls.push(['add', a.labels]),
removeLabel: async (a) => calls.push(['remove', a.name]),
},
},
};
const failures = [];
const core = { setFailed: (m) => failures.push(m), info() {}, notice() {} };
const context = { repo: { owner: 'tangency-labs', repo: 'solidcraft' }, payload: { pull_request: pull } };
return { github, core, context, calls, failures };
}
test('checkDuplicate fails with links, skips on label and bots', async () => {
const me = mk(20, 'alice', 'Fixes #5');
let f = fakes(me, { pulls: [mk(10, 'bob', 'Fixes #5')] });
await pc.checkDuplicate(f);
assert.equal(f.failures.length, 1);
assert.match(f.failures[0], /pull\/10/);
f = fakes({ ...me, labels: [{ name: 'not-a-duplicate' }] }, { pulls: [mk(10, 'bob', 'Fixes #5')] });
await pc.checkDuplicate(f);
assert.equal(f.failures.length, 0);
f = fakes({ ...me, user: { login: 'dependabot[bot]', type: 'Bot' } }, { pulls: [mk(10, 'bob', 'Fixes #5')] });
await pc.checkDuplicate(f);
assert.equal(f.failures.length, 0);
});
test('checkDefaults fails when missing, passes docs-only and bots', async () => {
const me = mk(20, 'alice', 'Fixes #5', { created_at: '2026-10-25T00:00:00Z' });
let f = fakes(me, { files: [{ filename: 'crates/a.rs' }] });
await pc.checkDefaults(f);
assert.equal(f.failures.length, 1);
f = fakes(me, { files: [{ filename: 'docs/a.md' }] });
await pc.checkDefaults(f);
assert.equal(f.failures.length, 0);
f = fakes({ ...me, body: 'Changes a default: no' }, { files: [{ filename: 'crates/a.rs' }] });
await pc.checkDefaults(f);
assert.equal(f.failures.length, 0);
});
test('syncDefaultLabel adds and removes', async () => {
const yes = 'Changes a default: yes — a, old behaviour kept behind preference p';
let f = fakes(mk(1, 'a', yes));
await pc.syncDefaultLabel(f);
assert.deepEqual(f.calls, [['add', ['changes-default']]]);
f = fakes(mk(1, 'a', 'Changes a default: no', { labels: [{ name: 'changes-default' }] }));
await pc.syncDefaultLabel(f);
assert.deepEqual(f.calls, [['remove', 'changes-default']]);
});
test('syncDefaultLabel notes a read-only token instead of failing', async () => {
const yes = 'Changes a default: yes — a, old behaviour kept behind preference p';
const f = fakes(mk(1, 'a', yes));
const notes = [];
f.core.notice = (m) => notes.push(m);
f.github.rest.issues.addLabels = async () => { throw Object.assign(new Error('Resource not accessible by integration'), { status: 403 }); };
await pc.syncDefaultLabel(f);
assert.equal(notes.length, 1);
f.github.rest.issues.addLabels = async () => { throw Object.assign(new Error('server error'), { status: 500 }); };
await assert.rejects(pc.syncDefaultLabel(f));
});
test('checkDefaults only enforces from 2026-10-25', async () => {
const files = [{ filename: 'crates/a.rs' }];
const old = mk(20, 'alice', 'Fixes #5', { created_at: '2026-10-24T23:59:59Z' });
let f = fakes(old, { files });
assert.deepEqual(await pc.checkDefaults(f), { skipped: 'opened before enforcement' });
assert.equal(f.failures.length, 0);
f = fakes({ ...old, created_at: '2026-10-25T00:00:00Z' }, { files });
await pc.checkDefaults(f);
assert.equal(f.failures.length, 1);
});
test('hasPicture needs one a reader can see in the description', () => {
assert.equal(pc.hasPicture('![tray](https://example.com/a.gif)'), true);
assert.equal(pc.hasPicture('<img width="960" alt="x" src="https://github.com/user-attachments/assets/abc" />'), true);
assert.equal(pc.hasPicture('<video src="https://example.com/a.mp4"></video>'), true);
assert.equal(pc.hasPicture('https://github.com/user-attachments/assets/abc'), true);
assert.equal(pc.hasPicture(''), false);
assert.equal(pc.hasPicture(null), false);
assert.equal(pc.hasPicture('The GIF is the change.'), false);
assert.equal(pc.hasPicture('See docs/screenshots/box.gif'), false);
assert.equal(pc.hasPicture('<!-- ![old](https://example.com/a.gif) -->'), false);
assert.equal(pc.hasPicture('```\n![x](https://example.com/a.gif)\n```'), false);
assert.equal(pc.hasPicture('`![x](https://example.com/a.gif)`'), false);
});
test('needsPicture covers the showcase and its media only', () => {
assert.equal(pc.needsPicture(['docs/showcase.md']), true);
assert.equal(pc.needsPicture(['crates/a.rs', 'docs/screenshots/a.gif']), true);
assert.equal(pc.needsPicture(['docs/mcp.md', 'crates/a.rs']), false);
});
test('checkPicture fails a showcase PR with no picture, from 2026-11-02 on', async () => {
const files = [{ filename: 'docs/showcase.md' }];
const pr = mk(30, 'alice', 'The GIF is the change.', { created_at: '2026-11-02T00:00:00Z' });
let f = fakes(pr, { files });
assert.deepEqual(await pc.checkPicture(f), { shown: false });
assert.equal(f.failures.length, 1);
f = fakes({ ...pr, body: '![it](https://example.com/a.gif)' }, { files });
assert.deepEqual(await pc.checkPicture(f), { shown: true });
assert.equal(f.failures.length, 0);
f = fakes({ ...pr, created_at: '2026-11-01T23:59:59Z' }, { files });
assert.deepEqual(await pc.checkPicture(f), { skipped: 'opened before enforcement' });
f = fakes(pr, { files: [{ filename: 'crates/a.rs' }] });
assert.deepEqual(await pc.checkPicture(f), { skipped: 'nothing to show' });
assert.equal(f.failures.length, 0);
});