policy.test.mjs 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  1. import assert from 'node:assert/strict'
  2. import test from 'node:test'
  3. import {
  4. countVisibleUnits,
  5. parseReferences,
  6. retainIssueReferences,
  7. requiresPullRequestPolicy,
  8. validateBody,
  9. validateIssue,
  10. validatePullRequest,
  11. } from './policy.mjs'
  12. const withDetails = (summary) =>
  13. `${summary}\n\n<details><summary>验收与细节</summary>待补充。</details>`
  14. const legalIssue = {
  15. title: '完成议题管理校验',
  16. body: withDetails('完成议题管理校验。'),
  17. assignees: [],
  18. labels: [],
  19. type: 'Idea',
  20. priority: null,
  21. status: 'In review',
  22. state: 'open',
  23. stateReason: null,
  24. }
  25. test('counts only text outside details', () => {
  26. assert.deepEqual(countVisibleUnits('支持 GitHub Project。<details>隐藏文字</details>'), {
  27. units: 4,
  28. balanced: true,
  29. detailsCount: 1,
  30. allCollapsed: true,
  31. })
  32. })
  33. test('requires a balanced default-collapsed details region', () => {
  34. assert.deepEqual(validateBody({ body: '完成工作。', assignees: [] }), [
  35. '正文必须包含默认收起的 <details> 区域',
  36. ])
  37. assert.deepEqual(
  38. validateBody({
  39. body: '完成工作。\n\n<details open><summary>细节</summary>待补充。</details>',
  40. assignees: [],
  41. }),
  42. ['details 必须默认收起,不得设置 open'],
  43. )
  44. assert.deepEqual(
  45. validateBody({ body: '完成工作。\n\n<details><summary>细节</summary>', assignees: [] }),
  46. ['details 标签必须成对闭合'],
  47. )
  48. })
  49. test('requires Owner for multiple assignees', () => {
  50. assert.deepEqual(
  51. validateBody({
  52. body: withDetails('完成工作。'),
  53. assignees: ['tianyicui', 'tianyicui-bot'],
  54. }),
  55. ['多个 Assignees 时首个非空行必须是 Owner: @login'],
  56. )
  57. })
  58. test('accepts an intended Owner while assignment permission is pending', () => {
  59. assert.deepEqual(
  60. validateBody({
  61. body: withDetails('Owner: @octocat\n\n完成工作。'),
  62. assignees: [],
  63. }),
  64. [],
  65. )
  66. assert.deepEqual(
  67. validateBody({
  68. body: withDetails('Owner: @octocat\n\n完成工作。'),
  69. assignees: ['hubot'],
  70. }),
  71. ['零或一个 Assignee 时不得写 Owner 行'],
  72. )
  73. })
  74. test('allows optional metadata in every open Status', () => {
  75. assert.deepEqual(validateIssue(legalIssue), [])
  76. for (const status of ['Inbox', 'Backlog', 'Ready', 'In progress', 'In review']) {
  77. assert.deepEqual(validateIssue({ ...legalIssue, status }), [])
  78. }
  79. })
  80. test('rejects metadata prefixes in an Issue title', () => {
  81. const errors = validateIssue({ ...legalIssue, title: '[Bug] 修复恢复错误' })
  82. assert.ok(errors.includes('Issue 标题不得带 Type、Priority、Status、area 或 Owner 前缀'))
  83. })
  84. test('keeps terminal Status aligned with the native close reason', () => {
  85. assert.deepEqual(
  86. validateIssue({ ...legalIssue, status: 'Done', state: 'closed', stateReason: 'completed' }),
  87. [],
  88. )
  89. assert.deepEqual(
  90. validateIssue({
  91. ...legalIssue,
  92. status: 'No action',
  93. state: 'closed',
  94. stateReason: 'not_planned',
  95. }),
  96. [],
  97. )
  98. assert.ok(validateIssue({ ...legalIssue, status: 'Done' }).includes('Done 必须对应 Completed 关闭原因'))
  99. })
  100. test('separates resolving and informational references', () => {
  101. assert.deepEqual(
  102. parseReferences({
  103. body: 'Fixes #12\nRelated to #4\nRefs deepseekharness/dsh-test#7',
  104. repository: 'deepseekharness/dsh-test',
  105. }),
  106. { all: [4, 7, 12], resolving: [12], related: [4, 7] },
  107. )
  108. })
  109. test('does not treat pull request references as Issue associations', () => {
  110. const references = {
  111. all: [123, 1180, 1181],
  112. resolving: [123, 1180],
  113. related: [1181],
  114. }
  115. const issues = new Map([
  116. [1180, {}],
  117. [1181, {}],
  118. ])
  119. assert.deepEqual(retainIssueReferences(references, issues), {
  120. all: [1180, 1181],
  121. resolving: [1180],
  122. related: [1181],
  123. })
  124. })
  125. test('allows informational references without cross-object constraints', () => {
  126. const errors = validatePullRequest({
  127. isDraft: false,
  128. authorType: 'User',
  129. reviewRequestCount: 1,
  130. reviewCount: 0,
  131. labels: ['kind/cleanup', 'area/infra'],
  132. references: { all: [4], resolving: [], related: [4] },
  133. issues: new Map([[4, { type: 'Bug', priority: 'P0', labels: ['area/web'] }]]),
  134. })
  135. assert.deepEqual(errors, [])
  136. })
  137. test('enforces highest resolving Priority without Type or area synchronization', () => {
  138. const pull = {
  139. isDraft: false,
  140. authorType: 'User',
  141. reviewRequestCount: 0,
  142. reviewCount: 1,
  143. labels: ['kind/cleanup', 'p0', 'area/web'],
  144. references: { all: [2, 3], resolving: [2, 3], related: [] },
  145. issues: new Map([
  146. [2, { type: 'Feature', priority: 'P2', labels: ['area/web'] }],
  147. [3, { type: 'Bug', priority: 'P0', labels: ['area/session'] }],
  148. ]),
  149. }
  150. assert.deepEqual(validatePullRequest(pull), [])
  151. assert.ok(
  152. validatePullRequest({ ...pull, labels: ['kind/cleanup', 'p2', 'area/web'] }).includes(
  153. 'PR Priority 应为 p0',
  154. ),
  155. )
  156. })
  157. test('requires policy only after a human PR enters review', () => {
  158. assert.equal(
  159. requiresPullRequestPolicy({
  160. isDraft: false,
  161. authorType: 'User',
  162. reviewRequestCount: 1,
  163. reviewCount: 0,
  164. }),
  165. true,
  166. )
  167. assert.equal(
  168. requiresPullRequestPolicy({
  169. isDraft: false,
  170. authorType: 'User',
  171. reviewRequestCount: 0,
  172. reviewCount: 0,
  173. }),
  174. false,
  175. )
  176. })
  177. test('exempts Draft, Bot, and App PRs', () => {
  178. const invalid = {
  179. isDraft: false,
  180. labels: [],
  181. references: { all: [], resolving: [], related: [] },
  182. issues: new Map(),
  183. reviewRequestCount: 1,
  184. reviewCount: 0,
  185. }
  186. assert.deepEqual(validatePullRequest({ ...invalid, authorType: 'Bot' }), [])
  187. assert.deepEqual(validatePullRequest({ ...invalid, authorType: 'App' }), [])
  188. assert.deepEqual(validatePullRequest({ ...invalid, authorType: 'User', isDraft: true }), [])
  189. assert.ok(validatePullRequest({ ...invalid, authorType: 'User' }).length > 0)
  190. })
  191. test('requires repository PR labels in the enforcement scope', () => {
  192. const errors = validatePullRequest({
  193. isDraft: false,
  194. authorType: 'User',
  195. reviewRequestCount: 1,
  196. reviewCount: 0,
  197. labels: [],
  198. references: { all: [2], resolving: [], related: [2] },
  199. issues: new Map([[2, { priority: null }]]),
  200. })
  201. assert.ok(errors.includes('PR 必须恰好有一个 kind/*,当前为 0'))
  202. assert.ok(errors.includes('PR 必须至少有一个 area/*'))
  203. })
  204. test('accepts repository-extensible kind labels', () => {
  205. assert.deepEqual(
  206. validatePullRequest({
  207. isDraft: false,
  208. authorType: 'User',
  209. reviewRequestCount: 1,
  210. reviewCount: 0,
  211. labels: ['kind/dependency', 'area/infra'],
  212. references: { all: [2], resolving: [], related: [2] },
  213. issues: new Map([[2, { priority: null }]]),
  214. }),
  215. [],
  216. )
  217. })
  218. test('allows missing Priority only when resolving Issues are also unprioritized', () => {
  219. const pull = {
  220. isDraft: false,
  221. authorType: 'User',
  222. reviewRequestCount: 1,
  223. reviewCount: 0,
  224. labels: ['kind/feature', 'area/web'],
  225. references: { all: [2], resolving: [2], related: [] },
  226. issues: new Map([[2, { priority: null }]]),
  227. }
  228. assert.deepEqual(validatePullRequest(pull), [])
  229. assert.ok(
  230. validatePullRequest({ ...pull, issues: new Map([[2, { priority: 'P2' }]]) }).includes(
  231. 'PR Priority 应为 p2',
  232. ),
  233. )
  234. assert.ok(
  235. validatePullRequest({ ...pull, labels: [...pull.labels, 'p2'] }).includes(
  236. '有 Priority 的解决型 PR 要求每个被解决 Issue 都设置 Priority',
  237. ),
  238. )
  239. })