| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413 |
- import assert from 'node:assert/strict'
- import test from 'node:test'
- import {
- countVisibleUnits,
- nextResolvingIssueStatus,
- parseReferences,
- retainIssueReferences,
- resolvingIssueStatusCommand,
- requiresPullRequestPolicy,
- validateBody,
- validateIssue,
- validatePullRequest,
- } from './policy.mjs'
- const withDetails = (summary) =>
- `${summary}\n\n<details><summary>验收与细节</summary>待补充。</details>`
- const legalIssue = {
- title: '完成议题管理校验',
- body: withDetails('完成议题管理校验。'),
- assignees: [],
- 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('counts only text outside details', () => {
- assert.deepEqual(countVisibleUnits('支持 GitHub Project。<details>隐藏文字</details>'), {
- units: 4,
- balanced: true,
- detailsCount: 1,
- allCollapsed: true,
- })
- })
- test('requires a balanced default-collapsed details region', () => {
- assert.deepEqual(validateBody({ body: '完成工作。', assignees: [] }), [
- '正文必须包含默认收起的 <details> 区域',
- ])
- assert.deepEqual(
- validateBody({
- body: '完成工作。\n\n<details open><summary>细节</summary>待补充。</details>',
- assignees: [],
- }),
- ['details 必须默认收起,不得设置 open'],
- )
- assert.deepEqual(
- validateBody({ body: '完成工作。\n\n<details><summary>细节</summary>', assignees: [] }),
- ['details 标签必须成对闭合'],
- )
- })
- test('requires Owner for multiple assignees', () => {
- assert.deepEqual(
- validateBody({
- body: withDetails('完成工作。'),
- assignees: ['tianyicui', 'tianyicui-bot'],
- }),
- ['多个 Assignees 时首个非空行必须是 Owner: @login'],
- )
- })
- test('accepts an intended Owner while assignment permission is pending', () => {
- assert.deepEqual(
- validateBody({
- body: withDetails('Owner: @octocat\n\n完成工作。'),
- assignees: [],
- }),
- [],
- )
- assert.deepEqual(
- validateBody({
- body: withDetails('Owner: @octocat\n\n完成工作。'),
- assignees: ['hubot'],
- }),
- ['零或一个 Assignee 时不得写 Owner 行'],
- )
- })
- 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('rejects metadata prefixes in an Issue title', () => {
- const errors = validateIssue({ ...legalIssue, title: '[Bug] 修复恢复错误' })
- assert.ok(errors.includes('Issue 标题不得带 Type、Priority、Status、area 或 Owner 前缀'))
- })
- 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('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('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/* 仅用于 Issue:source/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',
- ),
- )
- })
|