const assert = require('node:assert/strict'); const { test } = require('node:test'); const guard = require('./pr-policy-guard.cjs'); function fixture() { const pr = { number: 100, state: 'open', merged: false, base: { ref: 'dev' }, body: 'Refs #42', created_at: '2026-11-01T10:00:00Z', }; const issue = { number: 42, state: 'open', html_url: 'https://github.com/SnowLuma/SnowLuma/issues/42', created_at: '2026-11-01T09:00:00Z', author_association: 'NONE', labels: [{ name: 'bug' }], }; const state = { pr, issue, issues: new Map([[42, issue]]), comments: [], lookups: [], mutations: [], failures: [], refresh: null, requests: 0, }; const context = { repo: { owner: 'SnowLuma', repo: 'SnowLuma' }, issue: { number: 100 }, serverUrl: 'https://github.com', }; const github = { rest: { pulls: { get: async () => { state.requests += 1; return { data: structuredClone(state.requests > 1 && state.refresh ? state.refresh : pr), }; }, update: async (params) => state.mutations.push({ action: 'close', ...params }), }, issues: { get: async ({ issue_number }) => { state.lookups.push(issue_number); const result = state.issues.get(issue_number); if (result instanceof Error) { throw result; } if (!result) { throw Object.assign(new Error('Not found'), { status: 404 }); } return { data: structuredClone(result) }; }, listComments: () => {}, createComment: async (params) => state.mutations.push({ action: 'comment', ...params }), updateComment: async (params) => state.mutations.push({ action: 'edit-comment', ...params }), }, }, paginate: async () => structuredClone(state.comments), }; const core = { info: () => {}, setFailed: (reason) => state.failures.push(reason), }; return { ...state, state, github, run: () => guard({ github, context, core }) }; } function assertClosed(f, reason) { assert.deepEqual( f.mutations.map((entry) => entry.action), ['comment', 'close'], ); assert.equal(f.mutations[1].pull_number, 100); assert.equal(f.mutations[1].state, 'closed'); assert.match(f.mutations[0].body, reason); assert.equal(f.failures.length, 1); } for (const body of [ 'Refs #42', 'refs: #42', 'Ref #42', 'Close #42', 'Closes #42', 'Closed #42', 'Fix #42', 'Fixes #42', 'Fixed #42', 'Resolve #42', 'Resolves #42', 'Resolved #42', 'This change fixes #42.', 'CLOSES: #42', 'Refs SnowLuma/SnowLuma#42', 'Refs snowluma/snowluma#42', 'Refs https://github.com/SnowLuma/SnowLuma/issues/42', ]) { test(`accepts a real issue reference: ${body}`, async () => { const f = fixture(); f.pr.body = body; await f.run(); assert.deepEqual(f.lookups, [42]); assert.deepEqual(f.mutations, []); assert.deepEqual(f.failures, []); }); } for (const body of [ '', null, '#42', 'prefixRefs #42', 'Refs #0', 'Refs #42abc', 'Refs #42/other', 'Refs #9007199254740993', 'Refs other/repo#42', 'Refs https://github.com/other/repo/issues/42', 'Refs https://example.org/SnowLuma/SnowLuma/issues/42', 'Refs https://github.com/SnowLuma/SnowLuma/pull/42', 'Refs https://%', '', '\nold reason', }); await f.run(); assert.deepEqual( f.mutations.map((entry) => entry.action), ['edit-comment', 'close'], ); assert.equal(f.mutations[0].comment_id, 7); }); test('does not overwrite a contributor comment containing the guard marker', async () => { const f = fixture(); f.pr.body = ''; f.comments.push({ id: 7, user: { login: 'contributor' }, body: '', }); await f.run(); assertClosed(f, /缺少本仓库 issue/); }); test('does not close the PR if its explanation cannot be posted', async () => { const f = fixture(); f.pr.body = ''; const error = Object.assign(new Error('Comment rejected'), { status: 403 }); f.github.rest.issues.createComment = async () => { throw error; }; await assert.rejects(f.run, error); assert.deepEqual(f.mutations, []); }); test('reuses an unchanged explanation without posting or editing another comment', async () => { const first = fixture(); first.pr.body = ''; await first.run(); const reopened = fixture(); reopened.pr.body = ''; reopened.comments.push({ id: 7, user: { login: 'github-actions[bot]' }, body: first.mutations[0].body, }); await reopened.run(); assert.deepEqual( reopened.mutations.map((entry) => entry.action), ['close'], ); });