Files
deepseek-harness/.github/issue-management/policy.test.mjs
T

774 lines
25 KiB
JavaScript
Raw 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.
import assert from 'node:assert/strict'
import { readFileSync, readdirSync } from 'node:fs'
import test from 'node:test'
import {
auditIssue,
initializeIssueStartDate,
initializePullRequestStartDates,
issueSnapshot,
nextResolvingIssueStatus,
parseReferences,
projectDate,
repairIssueLabels,
retainIssueReferences,
resolvingIssueStatusCommand,
requiresPullRequestPolicy,
validateIssue,
validatePullRequest,
} from './policy.mjs'
const projectGraphqlData = ({
projectItem = true,
priority = null,
priorityField = true,
priorityType = 'SINGLE_SELECT',
priorityIsIssueField = false,
startDate = null,
startDateField = true,
startDateType = 'DATE',
startDateIsIssueField = false,
} = {}) => ({
organization: {
projectV2: {
id: 'project-id',
title: 'DSH Issue Management',
fields: {
nodes: [
{
id: 'status-field-id',
name: 'Status',
dataType: 'SINGLE_SELECT',
isIssueField: false,
options: [],
},
...(priorityField
? [
{
id: 'priority-project-field-id',
name: 'Priority',
dataType: priorityType,
isIssueField: priorityIsIssueField,
options: [],
},
]
: []),
...(startDateField
? [
{
id: 'start-date-field-id',
name: 'Start Date',
dataType: startDateType,
isIssueField: startDateIsIssueField,
},
]
: []),
],
},
},
},
repository: {
issue: {
id: 'issue-id',
projectItems: {
nodes: projectItem
? [
{
id: 'item-id',
project: { id: 'project-id' },
fieldValueByName: { name: 'Inbox', optionId: 'inbox-option-id' },
priorityValue:
priority === null ? null : { name: priority, optionId: `${priority}-option-id` },
startDateValue: startDate === null ? null : { date: startDate },
},
]
: [],
},
},
},
})
const mockGraphql = (t, resolve) => {
const requests = []
const previousToken = process.env.GH_TOKEN
process.env.GH_TOKEN = 'test-token'
t.after(() => {
if (previousToken === undefined) delete process.env.GH_TOKEN
else process.env.GH_TOKEN = previousToken
})
t.mock.method(globalThis, 'fetch', async (url, options) => {
assert.equal(url, 'https://api.github.com/graphql')
assert.equal(options.headers.Authorization, 'Bearer test-token')
const request = JSON.parse(options.body)
requests.push(request)
return Response.json({ data: resolve(request, requests.length - 1) })
})
return requests
}
const legalIssue = {
labels: [],
type: 'Idea',
priority: null,
status: 'In review',
state: 'open',
stateReason: null,
}
const canonicalKinds = [
'kind/feature',
'kind/bug-fix',
'kind/doc',
'kind/testing',
'kind/cleanup',
'kind/dependency',
]
// Keep an independent oracle rather than importing the implementation's reserved set.
const legacyLabels = [
'kind/bug',
'kind/documentation',
'feature',
'bug-fix',
'doc',
'cleanup',
'testing',
'dependencies',
'ci',
'cli',
'llm',
'web-search',
]
const reviewedPull = (labels) => ({
isDraft: false,
authorType: 'User',
reviewRequestCount: 1,
reviewCount: 0,
labels,
references: { all: [2], resolving: [], related: [2] },
issues: new Map([[2, { priority: null }]]),
})
test('keeps only Bug, Feature, and Task Issue templates with used frontmatter', () => {
const directory = new URL('../ISSUE_TEMPLATE/', import.meta.url)
assert.deepEqual(readdirSync(directory).sort(), ['bug.md', 'config.yml', 'feature.md', 'task.md'])
for (const file of ['bug.md', 'feature.md', 'task.md']) {
const source = readFileSync(new URL(file, directory), 'utf8')
const frontmatter = source.match(/^---\n([\s\S]*?)\n---/)?.[1] ?? ''
const keys = frontmatter
.split('\n')
.filter(Boolean)
.map((line) => line.slice(0, line.indexOf(':')))
.sort()
assert.deepEqual(keys, ['about', 'name', 'type'], file)
assert.match(source, /^## /m, file)
assert.match(source, /<!-- [^\n]+ -->/, file)
assert.doesNotMatch(source, /<details\b/i, file)
}
assert.equal(
readFileSync(new URL('config.yml', directory), 'utf8'),
'blank_issues_enabled: false\n',
)
})
test('keeps Feature Issues limited to motivation and behavior', () => {
const source = readFileSync(
new URL('../ISSUE_TEMPLATE/feature.md', import.meta.url),
'utf8',
)
assert.deepEqual(
[...source.matchAll(/^## .+$/gm)].map(([heading]) => heading),
['## Motivation', '## Behavior'],
)
})
test('keeps Bug Issues limited to the problem report', () => {
const source = readFileSync(new URL('../ISSUE_TEMPLATE/bug.md', import.meta.url), 'utf8')
assert.deepEqual(
[...source.matchAll(/^## .+$/gm)].map(([heading]) => heading),
['## Summary', '## Reproduction', '## Current behavior', '## Expected behavior', '## Environment'],
)
})
test('keeps Task Issues limited to summary and deliverables', () => {
const source = readFileSync(new URL('../ISSUE_TEMPLATE/task.md', import.meta.url), 'utf8')
assert.deepEqual(
[...source.matchAll(/^## .+$/gm)].map(([heading]) => heading),
['## Summary', '## Deliverables'],
)
})
test('structures the pull request template around motivation, changes, and testing', () => {
const source = readFileSync(new URL('../pull_request_template.md', import.meta.url), 'utf8')
for (const heading of ['## Motivation', '## Changes', '## Testing']) {
assert.match(source, new RegExp(`^${heading.replaceAll('#', '\\#')}$`, 'm'))
}
assert.doesNotMatch(source, /^### /m)
assert.match(
source,
/<!-- 高层次说明命令[^\n]+ -->\n<!-- 高层次说明用户[^\n]+ -->/,
)
assert.match(source, /- <!-- [^\n]+ -->\n\n <details>\n <summary>Proof<\/summary>/)
assert.equal(source.match(/<details>/g)?.length, 1)
assert.equal(source.match(/<\/details>/g)?.length, 1)
})
test('ignores Issue title, body presentation, and assignee ownership', () => {
assert.deepEqual(
validateIssue({
...legalIssue,
title: '[Bug] English title',
body: `Owner: @octocat\n\n${'visible '.repeat(60)}<details open>unclosed`,
assignees: ['octocat', 'hubot'],
}),
[],
)
})
test('allows optional metadata in every open Status', () => {
assert.deepEqual(validateIssue(legalIssue), [])
for (const status of ['Inbox', 'Backlog', 'Ready', 'In progress', 'In review']) {
assert.deepEqual(validateIssue({ ...legalIssue, status }), [])
}
})
test('reserves PR kind and legacy labels for pull requests', () => {
for (const label of [
...canonicalKinds,
'kind/experimental',
...legacyLabels,
]) {
assert.ok(
validateIssue({ ...legalIssue, labels: [label] }).some((error) =>
error.startsWith('Issue 不得使用 PR kind 或旧版标签:'),
),
label,
)
}
assert.deepEqual(validateIssue({ ...legalIssue, labels: ['area/web', 'source/member'] }), [])
})
test('removes reserved labels from Issues before validation', async (t) => {
const previousToken = process.env.GH_TOKEN
process.env.GH_TOKEN = 'test-token'
t.after(() => {
if (previousToken === undefined) delete process.env.GH_TOKEN
else process.env.GH_TOKEN = previousToken
})
const requests = []
t.mock.method(globalThis, 'fetch', async (url, options) => {
requests.push({ url, method: options.method })
assert.equal(options.headers.Authorization, 'Bearer test-token')
if (url.endsWith('/labels/bug-fix')) {
return Response.json({ message: 'Label does not exist' }, { status: 404 })
}
return Response.json([])
})
const issue = {
...legalIssue,
number: 42,
labels: ['area/web', 'kind/bug-fix', 'bug-fix', 'source/member'],
}
const repaired = await repairIssueLabels(issue)
assert.deepEqual(repaired.labels, ['area/web', 'source/member'])
assert.deepEqual(issue.labels, ['area/web', 'kind/bug-fix', 'bug-fix', 'source/member'])
assert.deepEqual(validateIssue(repaired), [])
assert.deepEqual(requests, [
{
url: 'https://api.github.com/repos/deepseek-harness/deepseek-harness/issues/42/labels/kind%2Fbug-fix',
method: 'DELETE',
},
{
url: 'https://api.github.com/repos/deepseek-harness/deepseek-harness/issues/42/labels/bug-fix',
method: 'DELETE',
},
])
})
test('deletes a stale audit comment after repairing its only violation', async (t) => {
const previousToken = process.env.GH_TOKEN
process.env.GH_TOKEN = 'test-token'
t.after(() => {
if (previousToken === undefined) delete process.env.GH_TOKEN
else process.env.GH_TOKEN = previousToken
})
const requests = []
t.mock.method(globalThis, 'fetch', async (url, options) => {
requests.push({ url, method: options.method ?? 'GET' })
if (url.endsWith('/issues/42')) {
return Response.json({
node_id: 'issue-id',
labels: [{ name: 'area/web' }, { name: 'kind/bug-fix' }],
type: { name: 'Bug' },
state: 'open',
state_reason: null,
})
}
if (url.endsWith('/graphql')) return Response.json({ data: projectGraphqlData() })
if (url.endsWith('/labels/kind%2Fbug-fix')) return Response.json([{ name: 'area/web' }])
if (url.endsWith('/issues/42/comments?per_page=100')) {
return Response.json([
{
id: 99,
user: { type: 'Bot' },
body: '<!-- dsh-issue-policy -->\nold audit',
},
])
}
if (url.endsWith('/issues/comments/99')) return new Response(null, { status: 204 })
return Response.json({ message: 'unexpected request' }, { status: 500 })
})
assert.deepEqual(await auditIssue(42), [])
assert.deepEqual(
requests.map(({ url, method }) => ({ path: new URL(url).pathname + new URL(url).search, method })),
[
{ path: '/repos/deepseek-harness/deepseek-harness/issues/42', method: 'GET' },
{ path: '/graphql', method: 'POST' },
{
path: '/repos/deepseek-harness/deepseek-harness/issues/42/labels/kind%2Fbug-fix',
method: 'DELETE',
},
{
path: '/repos/deepseek-harness/deepseek-harness/issues/42/comments?per_page=100',
method: 'GET',
},
{
path: '/repos/deepseek-harness/deepseek-harness/issues/comments/99',
method: 'DELETE',
},
],
)
})
test('keeps terminal Status aligned with the native close reason', () => {
assert.deepEqual(
validateIssue({ ...legalIssue, status: 'Done', state: 'closed', stateReason: 'completed' }),
[],
)
assert.deepEqual(
validateIssue({
...legalIssue,
status: 'No action',
state: 'closed',
stateReason: 'not_planned',
}),
[],
)
assert.ok(validateIssue({ ...legalIssue, status: 'Done' }).includes('Done 必须对应 Completed 关闭原因'))
})
test('separates resolving and informational references', () => {
assert.deepEqual(
parseReferences({
body: 'Fixes #12\nRelated to #4\nRefs deepseekharness/dsh-test#7',
repository: 'deepseekharness/dsh-test',
}),
{ all: [4, 7, 12], resolving: [12], related: [4, 7] },
)
})
test('converts PR creation timestamps to Shanghai Project dates', () => {
assert.equal(projectDate('2026-08-27T15:59:59Z', 'Asia/Shanghai'), '2026-08-27')
assert.equal(projectDate('2026-08-27T16:00:00Z', 'Asia/Shanghai'), '2026-08-28')
assert.throws(() => projectDate('invalid', 'Asia/Shanghai'), /无效的 PR 创建时间/)
})
test('initializes every referenced Issue only for a PR opened event', async () => {
const writes = []
const pull = {
createdAt: '2026-08-27T16:00:00Z',
references: { all: [4, 7, 12] },
}
const initialize = async (number, date) => writes.push({ number, date })
await initializePullRequestStartDates(pull, 'opened', initialize)
assert.deepEqual(writes, [
{ number: 4, date: '2026-08-28' },
{ number: 7, date: '2026-08-28' },
{ number: 12, date: '2026-08-28' },
])
for (const action of ['edited', 'synchronize', 'reopened']) {
await initializePullRequestStartDates(pull, action, initialize)
}
assert.equal(writes.length, 3)
})
test('reads Priority and Status from Project custom fields', async (t) => {
const previousGhToken = process.env.GH_TOKEN
const previousGithubToken = process.env.GITHUB_TOKEN
const previousProjectToken = process.env.PROJECT_TOKEN
delete process.env.GH_TOKEN
process.env.GITHUB_TOKEN = 'repository-token'
process.env.PROJECT_TOKEN = 'project-token'
t.after(() => {
if (previousGhToken === undefined) delete process.env.GH_TOKEN
else process.env.GH_TOKEN = previousGhToken
if (previousGithubToken === undefined) delete process.env.GITHUB_TOKEN
else process.env.GITHUB_TOKEN = previousGithubToken
if (previousProjectToken === undefined) delete process.env.PROJECT_TOKEN
else process.env.PROJECT_TOKEN = previousProjectToken
})
const urls = []
t.mock.method(globalThis, 'fetch', async (url, options) => {
urls.push(url)
if (url.endsWith('/issues/42')) {
assert.equal(options.headers.Authorization, 'Bearer repository-token')
return Response.json({
node_id: 'issue-id',
title: 'Project metadata',
body: null,
assignees: [],
labels: [],
type: { name: 'Task' },
state: 'open',
state_reason: null,
})
}
assert.equal(url, 'https://api.github.com/graphql')
assert.equal(options.headers.Authorization, 'Bearer project-token')
return Response.json({ data: projectGraphqlData({ priority: 'P1' }) })
})
const issue = await issueSnapshot(42)
assert.equal(issue.priority, 'P1')
assert.equal(issue.status, 'Inbox')
assert.deepEqual(urls, [
'https://api.github.com/repos/deepseek-harness/deepseek-harness/issues/42',
'https://api.github.com/graphql',
])
})
test('writes an empty Project Start Date with the configured field', async (t) => {
const requests = mockGraphql(t, (request) => {
if (request.query.includes('query(')) return projectGraphqlData()
return { updateProjectV2ItemFieldValue: { projectV2Item: { id: 'item-id' } } }
})
await initializeIssueStartDate(42, '2026-08-28')
assert.equal(requests.length, 2)
assert.match(requests[0].query, /isIssueField/)
assert.doesNotMatch(requests[0].query, /issueField\s*\{/)
assert.match(requests[0].query, /priorityValue: fieldValueByName/)
assert.equal(requests[0].variables.priorityField, 'Priority')
assert.match(requests[0].query, /ProjectV2ItemFieldDateValue/)
assert.match(requests[1].query, /updateProjectV2ItemFieldValue/)
assert.match(requests[1].query, /value: \{date: \$date\}/)
assert.deepEqual(requests[1].variables, {
projectId: 'project-id',
itemId: 'item-id',
fieldId: 'start-date-field-id',
date: '2026-08-28',
})
})
test('preserves an existing Project Start Date', async (t) => {
const requests = mockGraphql(t, () => projectGraphqlData({ startDate: '2026-08-01' }))
await initializeIssueStartDate(42, '2026-08-28')
assert.equal(requests.length, 1)
})
test('adds a referenced Issue to the Project before setting Start Date', async (t) => {
const requests = mockGraphql(t, (request) => {
if (request.query.includes('query(')) return projectGraphqlData({ projectItem: false })
if (request.query.includes('addProjectV2ItemById')) {
return { addProjectV2ItemById: { item: { id: 'new-item-id' } } }
}
return { updateProjectV2ItemFieldValue: { projectV2Item: { id: 'new-item-id' } } }
})
await initializeIssueStartDate(42, '2026-08-28')
assert.equal(requests.length, 3)
assert.deepEqual(requests[1].variables, { projectId: 'project-id', contentId: 'issue-id' })
assert.deepEqual(requests[2].variables, {
projectId: 'project-id',
itemId: 'new-item-id',
fieldId: 'start-date-field-id',
date: '2026-08-28',
})
})
test('rejects a missing, non-Date, or Issue-level Start Date field', async (t) => {
let response = projectGraphqlData({ startDateField: false })
const requests = mockGraphql(t, () => response)
await assert.rejects(initializeIssueStartDate(42, '2026-08-28'), /Project 缺少 Start Date 字段/)
response = projectGraphqlData({ startDateType: 'TEXT' })
await assert.rejects(initializeIssueStartDate(42, '2026-08-28'), /Start Date 字段必须为 Date/)
response = projectGraphqlData({ startDateIsIssueField: true })
await assert.rejects(
initializeIssueStartDate(42, '2026-08-28'),
/Start Date 字段必须为 Project Date 字段/,
)
assert.equal(requests.length, 3)
})
test('rejects a missing, non-select, or Issue-level Priority field', async (t) => {
let response = projectGraphqlData({ priorityField: false })
const requests = mockGraphql(t, () => response)
await assert.rejects(initializeIssueStartDate(42, '2026-08-28'), /Project 缺少 Priority 字段/)
response = projectGraphqlData({ priorityType: 'TEXT' })
await assert.rejects(
initializeIssueStartDate(42, '2026-08-28'),
/Priority 字段必须为 Single Select/,
)
response = projectGraphqlData({ priorityIsIssueField: true })
await assert.rejects(
initializeIssueStartDate(42, '2026-08-28'),
/Priority 字段必须为 Project custom field/,
)
assert.equal(requests.length, 3)
})
test('does not treat pull request references as Issue associations', () => {
const references = {
all: [123, 1180, 1181],
resolving: [123, 1180],
related: [1181],
}
const issues = new Map([
[1180, {}],
[1181, {}],
])
assert.deepEqual(retainIssueReferences(references, issues), {
all: [1180, 1181],
resolving: [1180],
related: [1181],
})
})
test('allows informational references without cross-object constraints', () => {
const errors = validatePullRequest({
isDraft: false,
authorType: 'User',
reviewRequestCount: 1,
reviewCount: 0,
labels: ['kind/cleanup', 'area/infra'],
references: { all: [4], resolving: [], related: [4] },
issues: new Map([[4, { type: 'Bug', priority: 'P0', labels: ['area/web'] }]]),
})
assert.deepEqual(errors, [])
})
test('enforces highest resolving Priority without Type or area synchronization', () => {
const pull = {
isDraft: false,
authorType: 'User',
reviewRequestCount: 0,
reviewCount: 1,
labels: ['kind/cleanup', 'p0', 'area/web'],
references: { all: [2, 3], resolving: [2, 3], related: [] },
issues: new Map([
[2, { type: 'Feature', priority: 'P2', labels: ['area/web'] }],
[3, { type: 'Bug', priority: 'P0', labels: ['area/session'] }],
]),
}
assert.deepEqual(validatePullRequest(pull), [])
assert.ok(
validatePullRequest({ ...pull, labels: ['kind/cleanup', 'p2', 'area/web'] }).includes(
'PR Priority 应为 p0',
),
)
})
test('requires policy only after a human PR enters review', () => {
assert.equal(
requiresPullRequestPolicy({
isDraft: false,
authorType: 'User',
reviewRequestCount: 1,
reviewCount: 0,
}),
true,
)
assert.equal(
requiresPullRequestPolicy({
isDraft: false,
authorType: 'User',
reviewRequestCount: 0,
reviewCount: 0,
}),
false,
)
})
test('maps only explicit review handoffs to review status commands', () => {
assert.equal(
resolvingIssueStatusCommand('pull_request', {
action: 'review_requested',
}),
'review-requested',
)
assert.equal(
resolvingIssueStatusCommand('pull_request_review', {
action: 'submitted',
review: { state: 'changes_requested' },
}),
'changes-requested',
)
for (const state of ['approved', 'commented']) {
assert.equal(
resolvingIssueStatusCommand('pull_request_review', {
action: 'submitted',
review: { state },
}),
null,
)
}
assert.equal(
resolvingIssueStatusCommand('pull_request_review', {
action: 'dismissed',
review: { state: 'changes_requested' },
}),
null,
)
})
test('keeps ordinary pull request events as forward-only implementation signals', () => {
for (const action of ['opened', 'edited', 'synchronize', 'reopened', 'labeled', 'unlabeled']) {
assert.equal(resolvingIssueStatusCommand('pull_request', { action }), 'implementation')
}
assert.equal(
resolvingIssueStatusCommand('pull_request', { action: 'review_request_removed' }),
null,
)
})
test('toggles automation-owned work on request changes and repeated review request', () => {
for (const status of ['Inbox', 'Backlog', 'Ready']) {
assert.equal(nextResolvingIssueStatus(status, 'implementation'), 'In progress')
assert.equal(nextResolvingIssueStatus(status, 'review-requested'), 'In review')
assert.equal(nextResolvingIssueStatus(status, 'changes-requested'), 'In progress')
}
let status = nextResolvingIssueStatus(
'In review',
'changes-requested',
'dsh-issue-management',
)
assert.equal(status, 'In progress')
status = nextResolvingIssueStatus(status, 'review-requested')
assert.equal(status, 'In review')
})
test('preserves human review status and terminal Issues', () => {
assert.equal(nextResolvingIssueStatus('In progress', 'implementation'), null)
assert.equal(nextResolvingIssueStatus('In review', 'implementation'), null)
assert.equal(nextResolvingIssueStatus('In review', 'review-requested'), null)
assert.equal(nextResolvingIssueStatus('In review', 'changes-requested', 'tianyicui'), null)
assert.equal(nextResolvingIssueStatus('In review', 'changes-requested'), null)
assert.equal(nextResolvingIssueStatus('Done', 'review-requested'), null)
assert.equal(nextResolvingIssueStatus('No action', 'changes-requested'), null)
assert.equal(nextResolvingIssueStatus(null, 'review-requested'), null)
})
test('keeps lifecycle projection independent of PR metadata enforcement', () => {
const pull = {
isDraft: false,
authorType: 'User',
reviewRequestCount: 1,
reviewCount: 0,
labels: [],
references: { all: [2], resolving: [2], related: [] },
issues: new Map([[2, { priority: null }]]),
}
assert.ok(validatePullRequest(pull).length > 0)
assert.equal(nextResolvingIssueStatus('Inbox', 'review-requested'), 'In review')
})
test('exempts Draft, Bot, and App PRs', () => {
const invalid = {
isDraft: false,
labels: [],
references: { all: [], resolving: [], related: [] },
issues: new Map(),
reviewRequestCount: 1,
reviewCount: 0,
}
assert.deepEqual(validatePullRequest({ ...invalid, authorType: 'Bot' }), [])
assert.deepEqual(validatePullRequest({ ...invalid, authorType: 'App' }), [])
assert.deepEqual(validatePullRequest({ ...invalid, authorType: 'User', isDraft: true }), [])
assert.ok(validatePullRequest({ ...invalid, authorType: 'User' }).length > 0)
})
test('requires repository PR labels in the enforcement scope', () => {
const errors = validatePullRequest({
isDraft: false,
authorType: 'User',
reviewRequestCount: 1,
reviewCount: 0,
labels: [],
references: { all: [2], resolving: [], related: [2] },
issues: new Map([[2, { priority: null }]]),
})
assert.ok(errors.includes('PR 必须恰好有一个允许的 kind/*,当前为 0'))
assert.ok(errors.includes('PR 必须至少有一个 area/*'))
})
test('accepts exactly the canonical kinds with extensible areas', () => {
for (const kind of canonicalKinds) {
assert.deepEqual(validatePullRequest(reviewedPull([kind, 'area/future-domain'])), [], kind)
}
})
test('rejects multiple, unknown, legacy, and Issue-source PR labels', () => {
assert.ok(
validatePullRequest(
reviewedPull(['kind/feature', 'kind/doc', 'area/web']),
).includes('PR 必须恰好有一个允许的 kind/*,当前为 2'),
)
assert.ok(
validatePullRequest(reviewedPull(['kind/experimental', 'area/web'])).includes(
'PR 含不支持的 kind/*kind/experimental',
),
)
for (const label of legacyLabels) {
assert.ok(
validatePullRequest(reviewedPull(['kind/feature', 'area/web', label])).some((error) =>
error.startsWith('PR 含旧版标签:'),
),
label,
)
}
assert.ok(
validatePullRequest(
reviewedPull(['kind/feature', 'area/web', 'source/internal-pr']),
).includes('source/* 仅用于 Issuesource/internal-pr'),
)
})
test('allows missing Priority only when resolving Issues are also unprioritized', () => {
const pull = {
isDraft: false,
authorType: 'User',
reviewRequestCount: 1,
reviewCount: 0,
labels: ['kind/feature', 'area/web'],
references: { all: [2], resolving: [2], related: [] },
issues: new Map([[2, { priority: null }]]),
}
assert.deepEqual(validatePullRequest(pull), [])
assert.ok(
validatePullRequest({ ...pull, issues: new Map([[2, { priority: 'P2' }]]) }).includes(
'PR Priority 应为 p2',
),
)
assert.ok(
validatePullRequest({ ...pull, labels: [...pull.labels, 'p2'] }).includes(
'有 Priority 的解决型 PR 要求每个被解决 Issue 都设置 Priority',
),
)
})