@1aboveio/skills 1.0.8 → 1.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (214) hide show
  1. package/.codex-plugin/plugin.json +13 -0
  2. package/.codex-plugin/resources.json +1827 -0
  3. package/README.md +22 -0
  4. package/package.json +9 -2
  5. package/skills/app-dev/airflow/SKILL.md +1 -1
  6. package/skills/app-dev/airflow/release-check.mjs +3 -1
  7. package/skills/app-dev/android-foundation/SKILL.md +1 -1
  8. package/skills/app-dev/android-foundation/release-check.mjs +3 -1
  9. package/skills/app-dev/app-debug/SKILL.md +1 -1
  10. package/skills/app-dev/app-debug/release-check.mjs +3 -1
  11. package/skills/app-dev/better-auth/SKILL.md +1 -1
  12. package/skills/app-dev/better-auth/release-check.mjs +3 -1
  13. package/skills/app-dev/data-catalog/SKILL.md +1 -1
  14. package/skills/app-dev/data-catalog/release-check.mjs +3 -1
  15. package/skills/app-dev/data-quality/SKILL.md +1 -1
  16. package/skills/app-dev/data-quality/release-check.mjs +3 -1
  17. package/skills/app-dev/ios-foundation/SKILL.md +1 -1
  18. package/skills/app-dev/ios-foundation/release-check.mjs +3 -1
  19. package/skills/app-dev/monorepo/SKILL.md +1 -1
  20. package/skills/app-dev/monorepo/release-check.mjs +3 -1
  21. package/skills/app-dev/nextjs-fullstack/SKILL.md +1 -1
  22. package/skills/app-dev/nextjs-fullstack/release-check.mjs +3 -1
  23. package/skills/app-dev/prisma-setup/SKILL.md +1 -1
  24. package/skills/app-dev/prisma-setup/release-check.mjs +3 -1
  25. package/skills/app-dev/pyspark/SKILL.md +1 -1
  26. package/skills/app-dev/pyspark/release-check.mjs +3 -1
  27. package/skills/app-dev/python-backend/SKILL.md +1 -1
  28. package/skills/app-dev/python-backend/release-check.mjs +3 -1
  29. package/skills/app-dev/shadcn/SKILL.md +1 -1
  30. package/skills/app-dev/shadcn/release-check.mjs +3 -1
  31. package/skills/app-dev/zod-v4/SKILL.md +1 -1
  32. package/skills/app-dev/zod-v4/release-check.mjs +3 -1
  33. package/skills/cicd/cloud-build/SKILL.md +1 -1
  34. package/skills/cicd/cloud-build/release-check.mjs +3 -1
  35. package/skills/cicd/cloud-debug/SKILL.md +1 -1
  36. package/skills/cicd/cloud-debug/release-check.mjs +3 -1
  37. package/skills/cicd/cloud-deploy/SKILL.md +1 -1
  38. package/skills/cicd/cloud-deploy/release-check.mjs +3 -1
  39. package/skills/cicd/docker/SKILL.md +1 -1
  40. package/skills/cicd/docker/release-check.mjs +3 -1
  41. package/skills/cicd/github-actions/SKILL.md +1 -1
  42. package/skills/cicd/github-actions/release-check.mjs +3 -1
  43. package/skills/cicd/github-flow/SKILL.md +1 -1
  44. package/skills/cicd/github-flow/release-check.mjs +3 -1
  45. package/skills/cicd/google-cloud/SKILL.md +1 -1
  46. package/skills/cicd/google-cloud/release-check.mjs +3 -1
  47. package/skills/cicd/mergify/SKILL.md +1 -1
  48. package/skills/cicd/mergify/release-check.mjs +3 -1
  49. package/skills/cicd/podman/SKILL.md +1 -1
  50. package/skills/cicd/podman/release-check.mjs +3 -1
  51. package/skills/content/article-review/SKILL.md +1 -1
  52. package/skills/content/article-review/release-check.mjs +3 -1
  53. package/skills/content/article-to-image/SKILL.md +1 -1
  54. package/skills/content/article-to-image/release-check.mjs +3 -1
  55. package/skills/content/flowchart-svg/SKILL.md +1 -1
  56. package/skills/content/flowchart-svg/release-check.mjs +3 -1
  57. package/skills/content/image-gen/SKILL.md +1 -1
  58. package/skills/content/image-gen/release-check.mjs +3 -1
  59. package/skills/content/mermaid/SKILL.md +1 -1
  60. package/skills/content/mermaid/release-check.mjs +3 -1
  61. package/skills/content/wechat-article-writer/SKILL.md +1 -1
  62. package/skills/content/wechat-article-writer/release-check.mjs +3 -1
  63. package/skills/content/wechat-publisher/SKILL.md +1 -1
  64. package/skills/content/wechat-publisher/release-check.mjs +3 -1
  65. package/skills/content/wechat-search/SKILL.md +1 -1
  66. package/skills/content/wechat-search/release-check.mjs +3 -1
  67. package/skills/content/xai-search/SKILL.md +88 -13
  68. package/skills/content/xai-search/release-check.mjs +3 -1
  69. package/skills/content/xai-search/scripts/xai_search.sh +125 -149
  70. package/skills/core/distill-writing/SKILL.md +1 -1
  71. package/skills/core/distill-writing/release-check.mjs +3 -1
  72. package/skills/core/doc-to-md/SKILL.md +1 -1
  73. package/skills/core/doc-to-md/release-check.mjs +3 -1
  74. package/skills/core/editorial-review/SKILL.md +1 -1
  75. package/skills/core/editorial-review/release-check.mjs +3 -1
  76. package/skills/core/house-style/SKILL.md +1 -1
  77. package/skills/core/house-style/release-check.mjs +3 -1
  78. package/skills/core/plain-language/SKILL.md +1 -1
  79. package/skills/core/plain-language/release-check.mjs +3 -1
  80. package/skills/core/polish-cn/SKILL.md +1 -1
  81. package/skills/core/polish-cn/release-check.mjs +3 -1
  82. package/skills/core/reg-parser/SKILL.md +1 -1
  83. package/skills/core/reg-parser/release-check.mjs +3 -1
  84. package/skills/core/terminology/SKILL.md +1 -1
  85. package/skills/core/terminology/release-check.mjs +3 -1
  86. package/skills/core/write-doc/SKILL.md +1 -1
  87. package/skills/core/write-doc/release-check.mjs +3 -1
  88. package/skills/engineering/better-review/SKILL.md +1 -1
  89. package/skills/engineering/better-review/release-check.mjs +3 -1
  90. package/skills/engineering/contributing/SKILL.md +1 -1
  91. package/skills/engineering/contributing/release-check.mjs +3 -1
  92. package/skills/engineering/e2e-test/SKILL.md +1 -1
  93. package/skills/engineering/e2e-test/release-check.mjs +3 -1
  94. package/skills/engineering/engineering-runtime/SKILL.md +1 -1
  95. package/skills/engineering/engineering-runtime/coherence/workflow.json +13 -13
  96. package/skills/engineering/engineering-runtime/release-check.mjs +3 -1
  97. package/skills/engineering/engineering-runtime/scripts/installation-resolver.mjs +191 -0
  98. package/skills/engineering/engineering-runtime/scripts/workflow-coherence.mjs +51 -10
  99. package/skills/engineering/ensure-coverage/SKILL.md +1 -1
  100. package/skills/engineering/ensure-coverage/release-check.mjs +3 -1
  101. package/skills/engineering/golden-path/SKILL.md +1 -1
  102. package/skills/engineering/golden-path/release-check.mjs +3 -1
  103. package/skills/engineering/harness-runtime/SKILL.md +1 -1
  104. package/skills/engineering/harness-runtime/release-check.mjs +3 -1
  105. package/skills/engineering/resolve-issues/SKILL.md +1 -1
  106. package/skills/engineering/resolve-issues/references/preflight.md +7 -0
  107. package/skills/engineering/resolve-issues/references/shared-modules.md +27 -0
  108. package/skills/engineering/resolve-issues/references/task-plan.md +5 -1
  109. package/skills/engineering/resolve-issues/release-check.mjs +3 -1
  110. package/skills/engineering/resolve-issues/scripts/active-installation.mjs +32 -0
  111. package/skills/engineering/resolve-issues/scripts/install-hooks.mjs +3 -0
  112. package/skills/engineering/resolve-issues/scripts/rehydrate.mjs +17 -1
  113. package/skills/engineering/resolve-release/SKILL.md +1 -1
  114. package/skills/engineering/resolve-release/release-check.mjs +3 -1
  115. package/skills/engineering/rush-issues/SKILL.md +1 -1
  116. package/skills/engineering/rush-issues/release-check.mjs +3 -1
  117. package/skills/engineering/rush-release/SKILL.md +1 -1
  118. package/skills/engineering/rush-release/release-check.mjs +3 -1
  119. package/skills/engineering/test-quality/SKILL.md +1 -1
  120. package/skills/engineering/test-quality/release-check.mjs +3 -1
  121. package/skills/risk-compliance/compliance-audit/SKILL.md +1 -1
  122. package/skills/risk-compliance/compliance-audit/release-check.mjs +3 -1
  123. package/skills/risk-compliance/contract-compare/SKILL.md +1 -1
  124. package/skills/risk-compliance/contract-compare/release-check.mjs +3 -1
  125. package/skills/risk-compliance/contract-review/SKILL.md +1 -1
  126. package/skills/risk-compliance/contract-review/release-check.mjs +3 -1
  127. package/skills/risk-compliance/doc-authenticity/SKILL.md +1 -1
  128. package/skills/risk-compliance/doc-authenticity/release-check.mjs +3 -1
  129. package/skills/risk-compliance/fraud-analysis/SKILL.md +1 -1
  130. package/skills/risk-compliance/fraud-analysis/release-check.mjs +3 -1
  131. package/skills/risk-compliance/payment-analysis/SKILL.md +1 -1
  132. package/skills/risk-compliance/payment-analysis/release-check.mjs +3 -1
  133. package/skills/risk-compliance/similar-domain/SKILL.md +1 -1
  134. package/skills/risk-compliance/similar-domain/release-check.mjs +3 -1
  135. package/skills/risk-compliance/tld-detection/SKILL.md +1 -1
  136. package/skills/risk-compliance/tld-detection/release-check.mjs +3 -1
  137. package/skills/app-dev/android-foundation/scripts/device-run.test.mjs +0 -436
  138. package/skills/app-dev/data-catalog/scripts/data-catalog.test.mjs +0 -36
  139. package/skills/app-dev/data-quality/scripts/data-quality.test.mjs +0 -12
  140. package/skills/cicd/github-actions/scripts/ci-run-audit.test.mjs +0 -637
  141. package/skills/cicd/github-flow/scripts/flow.test.mjs +0 -114
  142. package/skills/cicd/github-flow/scripts/persist-conventions.test.mjs +0 -108
  143. package/skills/cicd/mergify/scripts/audit.test.mjs +0 -201
  144. package/skills/cicd/mergify/scripts/entrypoints.test.mjs +0 -106
  145. package/skills/cicd/mergify/scripts/pr-620-mergify-comments.fixture.json +0 -30
  146. package/skills/cicd/mergify/scripts/stack-push.test.mjs +0 -233
  147. package/skills/cicd/mergify/scripts/watch-pr-delivery.test.mjs +0 -1826
  148. package/skills/content/image-gen/scripts/credentials.test.mjs +0 -212
  149. package/skills/content/image-gen/scripts/options.test.mjs +0 -245
  150. package/skills/content/xai-search/tests/smoke_test.sh +0 -36
  151. package/skills/core/polish-cn/scripts/polish-cn.test.mjs +0 -244
  152. package/skills/core/reg-parser/scripts/reg-parser.test.mjs +0 -165
  153. package/skills/engineering/better-review/scripts/pr-context.test.mjs +0 -168
  154. package/skills/engineering/better-review/scripts/rereview-scope.test.mjs +0 -587
  155. package/skills/engineering/better-review/scripts/scan-diff.test.mjs +0 -414
  156. package/skills/engineering/better-review/scripts/vendor-sync.test.mjs +0 -96
  157. package/skills/engineering/contributing/scripts/persist-conventions.test.mjs +0 -118
  158. package/skills/engineering/e2e-test/scripts/presentation-checks.test.mjs +0 -106
  159. package/skills/engineering/e2e-test/scripts/presentation-perceivability.test.mjs +0 -117
  160. package/skills/engineering/e2e-test/scripts/presentation-reachability.test.mjs +0 -69
  161. package/skills/engineering/e2e-test/scripts/presentation-render-health.test.mjs +0 -98
  162. package/skills/engineering/e2e-test/scripts/presentation-sweep.test.mjs +0 -71
  163. package/skills/engineering/e2e-test/scripts/presentation-temporal.test.mjs +0 -59
  164. package/skills/engineering/e2e-test/scripts/project-route-manifest.test.mjs +0 -76
  165. package/skills/engineering/engineering-runtime/engineering-runtime.test.mjs +0 -68
  166. package/skills/engineering/engineering-runtime/scripts/invocation-policy.test.mjs +0 -95
  167. package/skills/engineering/engineering-runtime/scripts/main-module.test.mjs +0 -288
  168. package/skills/engineering/engineering-runtime/scripts/required-check-plan.test.mjs +0 -96
  169. package/skills/engineering/ensure-coverage/scripts/adapters/nextjs-inventory.test.mjs +0 -96
  170. package/skills/engineering/ensure-coverage/scripts/check-quarantine-expiry.test.mjs +0 -47
  171. package/skills/engineering/ensure-coverage/scripts/ci-audit.test.mjs +0 -86
  172. package/skills/engineering/ensure-coverage/scripts/coverage-ledger.test.mjs +0 -447
  173. package/skills/engineering/ensure-coverage/scripts/design-parity.test.mjs +0 -552
  174. package/skills/engineering/ensure-coverage/scripts/lint-tests.test.mjs +0 -289
  175. package/skills/engineering/ensure-coverage/scripts/mock-policy-config.test.mjs +0 -119
  176. package/skills/engineering/golden-path/scripts/golden-path.test.mjs +0 -1457
  177. package/skills/engineering/harness-runtime/discover-models.test.mjs +0 -363
  178. package/skills/engineering/harness-runtime/harness-runtime.test.mjs +0 -967
  179. package/skills/engineering/harness-runtime/model-catalog-seed.test.mjs +0 -829
  180. package/skills/engineering/harness-runtime/model-catalog.test.mjs +0 -366
  181. package/skills/engineering/resolve-issues/scripts/cleanup-runs.test.mjs +0 -280
  182. package/skills/engineering/resolve-issues/scripts/dropbox.test.mjs +0 -346
  183. package/skills/engineering/resolve-issues/scripts/findings-reuse.test.mjs +0 -88
  184. package/skills/engineering/resolve-issues/scripts/github-reviews.test.mjs +0 -264
  185. package/skills/engineering/resolve-issues/scripts/install-hooks.test.mjs +0 -79
  186. package/skills/engineering/resolve-issues/scripts/issue-tree.test.mjs +0 -107
  187. package/skills/engineering/resolve-issues/scripts/manual-invocation.test.mjs +0 -14
  188. package/skills/engineering/resolve-issues/scripts/next-actions-capacity.test.mjs +0 -149
  189. package/skills/engineering/resolve-issues/scripts/next-actions.test.mjs +0 -915
  190. package/skills/engineering/resolve-issues/scripts/plan-sync.test.mjs +0 -156
  191. package/skills/engineering/resolve-issues/scripts/preflight-models.test.mjs +0 -448
  192. package/skills/engineering/resolve-issues/scripts/ready-check.test.mjs +0 -217
  193. package/skills/engineering/resolve-issues/scripts/rehydrate.test.mjs +0 -91
  194. package/skills/engineering/resolve-issues/scripts/review-verdict.test.mjs +0 -1060
  195. package/skills/engineering/resolve-issues/scripts/size-check.test.mjs +0 -458
  196. package/skills/engineering/resolve-release/scripts/apply.test.mjs +0 -99
  197. package/skills/engineering/resolve-release/scripts/dark-flags.test.mjs +0 -100
  198. package/skills/engineering/resolve-release/scripts/flow.test.mjs +0 -340
  199. package/skills/engineering/resolve-release/scripts/green-head.test.mjs +0 -101
  200. package/skills/engineering/resolve-release/scripts/npm-version-absence.test.mjs +0 -426
  201. package/skills/engineering/resolve-release/scripts/plan-npm.test.mjs +0 -659
  202. package/skills/engineering/resolve-release/scripts/plan-toml-boundary.test.mjs +0 -317
  203. package/skills/engineering/resolve-release/scripts/plan-uv.test.mjs +0 -291
  204. package/skills/engineering/resolve-release/scripts/plan.test.mjs +0 -428
  205. package/skills/engineering/test-quality/scripts/lint-tests.test.mjs +0 -591
  206. package/skills/engineering/test-quality/scripts/mock-policy-config.test.mjs +0 -119
  207. package/skills/engineering/test-quality/scripts/persist-conventions.test.mjs +0 -104
  208. package/skills/risk-compliance/compliance-audit/scripts/compliance-audit.test.mjs +0 -14
  209. package/skills/risk-compliance/doc-authenticity/manual-invocation.test.mjs +0 -12
  210. package/skills/risk-compliance/doc-authenticity/scripts/validate_verdict.test.mjs +0 -212
  211. package/skills/risk-compliance/tld-detection/scripts/detect_ladders.test.mjs +0 -14
  212. package/skills/risk-compliance/tld-detection/scripts/detect_streams.test.mjs +0 -14
  213. package/skills/risk-compliance/tld-detection/tests/test_detect_ladders.py +0 -134
  214. package/skills/risk-compliance/tld-detection/tests/test_detect_streams.py +0 -520
@@ -1,244 +0,0 @@
1
- import assert from 'node:assert/strict';
2
- import { spawnSync } from 'node:child_process';
3
- import { mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
4
- import { tmpdir } from 'node:os';
5
- import { dirname, join } from 'node:path';
6
- import { fileURLToPath } from 'node:url';
7
- import test from 'node:test';
8
-
9
- const script = join(dirname(fileURLToPath(import.meta.url)), 'polish_blocks.py');
10
-
11
- const REPORT = `---
12
- title: 月报
13
- ---
14
- # 2026 年 10 月报告
15
-
16
- 本期授权尝试 4,946 笔,授权成功率 45.79%。0 笔销售未能匹配授权,覆盖六个月。
17
- 详见 [方法说明](method.zh.md)。
18
-
19
- | 指标 | 值 |
20
- |---|---|
21
- | 授权尝试 | 4,946 |
22
-
23
- - 授权拒绝 1,111 笔,\`C1\` 为高风险。
24
- - 疑似欺诈响应不是已确认损失。
25
-
26
- \`\`\`python
27
- print("本期授权尝试 4,946 笔")
28
- \`\`\`
29
- `;
30
-
31
- function polish(...args) {
32
- return spawnSync('python3', [script, ...args], { encoding: 'utf8' });
33
- }
34
-
35
- // Writes a document, extracts it, and returns helpers. `propose` keeps every block it does not
36
- // rewrite, so the proposal covers all blocks unless `partial` is set.
37
- function setup(content = REPORT) {
38
- const dir = mkdtempSync(join(tmpdir(), 'polish-cn-test-'));
39
- const doc = join(dir, 'report.zh.md');
40
- writeFileSync(doc, content);
41
- const blocksPath = join(dir, 'blocks.json');
42
- const result = polish('extract', doc, '--out', blocksPath);
43
- assert.equal(result.status, 0, result.stderr);
44
- const blocks = JSON.parse(readFileSync(blocksPath, 'utf8')).blocks;
45
- const byText = (needle) => blocks.find((block) => block.text.includes(needle));
46
- const propose = (entries, { partial = false } = {}) => {
47
- const path = join(dir, `proposals-${Math.random().toString(36).slice(2)}.json`);
48
- const rewritten = {};
49
- for (const [needle, text] of entries) {
50
- const block = byText(needle);
51
- rewritten[block.id] = { sha: block.sha, text };
52
- }
53
- const kept = partial ? {} : Object.fromEntries(
54
- blocks.filter((block) => !rewritten[block.id]).map((block) => [block.id, 'already natural']));
55
- writeFileSync(path, JSON.stringify({ blocks: rewritten, kept, notes: ['note kept'] }));
56
- return path;
57
- };
58
- return { dir, doc, blocksPath, blocks, byText, propose };
59
- }
60
-
61
- test('extract returns prose only: front matter, tables and code fences are never blocks', () => {
62
- const { blocks } = setup();
63
- assert.deepEqual(blocks.map((block) => block.kind), ['heading', 'paragraph', 'list-item', 'list-item']);
64
- assert.ok(blocks.every((block) => !block.text.includes('|') && !block.text.includes('print(')));
65
- assert.equal(blocks[0].prefix, '# ');
66
- assert.deepEqual(blocks[1].lines, [6, 7]);
67
- });
68
-
69
- test('extract skips HTML comments and blocks, indented code, pipe tables without edge pipes, and link definitions', () => {
70
- const { blocks } = setup([
71
- '正文一。', '', '<!--', '注释 99', '-->', '', '<div>', '内嵌 HTML 7', '</div>', '',
72
- ' indented_code(3)', '', '指标 | 值', '--- | ---', '尝试 | 12', '', '[ref]: https://example.com/x', '', '正文二。', '',
73
- ].join('\n'));
74
- assert.deepEqual(blocks.map((block) => block.text), ['正文一。', '正文二。']);
75
- });
76
-
77
- test('check accepts a rewrite that keeps every protected token, even in a new order', () => {
78
- const { blocksPath, propose } = setup();
79
- const proposals = propose([[
80
- '本期授权尝试',
81
- '本期授权成功率为 45.79%,授权尝试共 4,946 笔。0 笔销售未能匹配授权,覆盖六个月。详见 [方法](method.zh.md)。',
82
- ]]);
83
- const result = polish('check', blocksPath, proposals);
84
- assert.equal(result.status, 0, result.stdout + result.stderr);
85
- assert.equal(JSON.parse(result.stdout).accepted.length, 1);
86
- });
87
-
88
- test('check fails when a block is neither rewritten nor kept, or is both, unless --partial', () => {
89
- const { dir, blocksPath, byText, propose } = setup();
90
- const partial = propose([['疑似欺诈', '疑似欺诈响应并非已确认的损失。']], { partial: true });
91
- const strict = polish('check', blocksPath, partial);
92
- assert.equal(strict.status, 1);
93
- assert.equal(JSON.parse(strict.stdout).unreviewed.length, 3);
94
- assert.equal(polish('check', blocksPath, partial, '--partial').status, 0);
95
-
96
- const block = byText('疑似欺诈');
97
- const both = JSON.parse(readFileSync(propose([['疑似欺诈', '疑似欺诈响应并非已确认的损失。']]), 'utf8'));
98
- both.kept[block.id] = 'also kept';
99
- const bothPath = join(dir, 'both.json');
100
- writeFileSync(bothPath, JSON.stringify(both));
101
- const result = polish('check', blocksPath, bothPath);
102
- assert.equal(result.status, 1);
103
- assert.deepEqual(JSON.parse(result.stdout).partitionProblems, [block.id]);
104
- });
105
-
106
- test('check rejects changed, added or dropped numbers, signs, currency, links, images, code and protected terms', () => {
107
- const { blocksPath, propose } = setup();
108
- const cases = [
109
- ['本期授权尝试', '本期授权尝试 4,964 笔,授权成功率 45.79%。0 笔销售未能匹配授权,覆盖六个月。详见 [方法说明](method.zh.md)。', /number '4,946'/],
110
- ['本期授权尝试', '本期授权尝试 4,946 笔,授权成功率 45.79%。0 笔销售未能匹配授权,覆盖 6 个月。详见 [方法说明](method.zh.md)。', /added number '6'/],
111
- ['本期授权尝试', '本期授权尝试 4,946 笔,授权成功率 45.79%。0 笔销售未能匹配授权,覆盖六个月。', /link 'method\.zh\.md'/],
112
- ['本期授权尝试', '本期授权尝试 4,946 笔,授权成功率 45.79%。0 笔销售未能匹配授权,覆盖六个月。详见 ![方法说明](method.zh.md)。', /image 'method\.zh\.md'/],
113
- ['本期授权尝试', '本期授权尝试 4,946 笔,授权成功率 +45.79%。0 笔销售未能匹配授权,覆盖六个月。详见 [方法说明](method.zh.md)。', /symbol '\+'/],
114
- ['本期授权尝试', '本期授权尝试 4,946 笔,授权成功率 45.79%,约 ¥0。0 笔销售未能匹配授权,覆盖六个月。详见 [方法说明](method.zh.md)。', /symbol '¥'/],
115
- ['授权拒绝', '拒付 1,111 笔,`C1` 为高风险。', /term '授权拒绝'/],
116
- ['授权拒绝', '授权拒绝 1,111 笔,C2 为高风险。', /code '`C1`'/],
117
- ];
118
- for (const [needle, text, reason] of cases) {
119
- const result = polish('check', blocksPath, propose([[needle, text]]), '--protect', '授权拒绝', '--protect', '拒付');
120
- assert.equal(result.status, 1, `expected rejection for: ${text}`);
121
- const rejected = Object.values(JSON.parse(result.stdout).rejected);
122
- assert.match(rejected[0], reason);
123
- }
124
- });
125
-
126
- test('check reads TERMINOLOGY.md as concepts: mentions are kept, Avoid terms may become preferred but never appear', () => {
127
- const { dir, blocks, blocksPath, propose } = setup('本月拒绝交易 1 笔,授权拒绝 2 笔。\n\n疑似欺诈响应不是已确认损失。\n');
128
- const terms = join(dir, 'TERMINOLOGY.md');
129
- writeFileSync(terms, [
130
- '# Terminology: test', '', '## Disputes', '',
131
- '| Concept | English | 简体 | 繁體(港) | Avoid | Meaning | Status | Source |',
132
- '| --- | --- | --- | --- | --- | --- | --- | --- |',
133
- '| Decline | decline | 授权拒绝 | 授權拒絕 | 拒绝交易、拒付(指授权拒绝) | Issuer refusal | user, 2026-10-20 | user |',
134
- '| Chargeback | chargeback | 拒付 | — | 退单(指 Visa 拒付) | Dispute | proposed | a.md |',
135
- '| Piped | pipe | 甲\\|乙 | — | | Escaped pipe | proposed | a.md |', '',
136
- '## Sources', '', '| id | Source |', '| --- | --- |', '| a.md | 授权拒绝 |', '',
137
- ].join('\n'));
138
- const run = (needle, text) => polish('check', blocksPath, propose([[needle, text]]), '--terminology', terms);
139
-
140
- const normalized = run('拒绝交易', '本月授权拒绝 1 笔,授权拒绝 2 笔。');
141
- assert.equal(normalized.status, 0, normalized.stdout);
142
- assert.deepEqual(Object.keys(JSON.parse(normalized.stdout).avoidedTermsInSource), [blocks[0].id]);
143
-
144
- const added = run('拒绝交易', '本月拒绝交易 1 笔,拒绝交易 2 笔。');
145
- assert.equal(added.status, 1);
146
- assert.match(Object.values(JSON.parse(added.stdout).rejected)[0], /avoids: 拒绝交易/);
147
-
148
- const dropped = run('拒绝交易', '本月有 1 笔,授权拒绝 2 笔。');
149
- assert.equal(dropped.status, 1);
150
- assert.match(Object.values(JSON.parse(dropped.stdout).rejected)[0], /concept 'c1'.*c1 = 授权拒绝/);
151
-
152
- const scopedNote = run('疑似欺诈', '疑似欺诈响应不是已确认损失,与拒付无关。');
153
- assert.equal(scopedNote.status, 1, 'adding 拒付 adds a mention of the chargeback concept');
154
- assert.match(Object.values(JSON.parse(scopedNote.stdout).rejected)[0], /c2 = 拒付/);
155
-
156
- const emDash = run('疑似欺诈', '疑似欺诈响应——不是已确认损失。');
157
- assert.equal(emDash.status, 0, 'an em dash is not a protected term: — means an empty cell');
158
- });
159
-
160
- test('check flags ids in kept that are not blocks', () => {
161
- const { dir, blocksPath, propose } = setup();
162
- const proposals = JSON.parse(readFileSync(propose([]), 'utf8'));
163
- proposals.kept.b999_note = 'invented';
164
- const path = join(dir, 'unknown.json');
165
- writeFileSync(path, JSON.stringify(proposals));
166
- const result = polish('check', blocksPath, path);
167
- assert.equal(result.status, 1);
168
- assert.deepEqual(JSON.parse(result.stdout).partitionProblems, ['b999_note (unknown id in kept)']);
169
- });
170
-
171
- test('check refuses line breaks and new Markdown structure inside a proposal', () => {
172
- const { blocksPath, propose } = setup();
173
- for (const text of ['疑似欺诈响应\n# 新标题', '疑似欺诈响应\r# 注入', '# 疑似欺诈响应不是已确认损失。', '- 疑似欺诈响应不是已确认损失。']) {
174
- const result = polish('check', blocksPath, propose([['疑似欺诈', text]]));
175
- assert.equal(result.status, 1, `expected rejection for: ${JSON.stringify(text)}`);
176
- assert.match(Object.values(JSON.parse(result.stdout).rejected)[0], /one line|block syntax/);
177
- }
178
- });
179
-
180
- test('check refuses deletions, stale blocks and malformed proposal files', () => {
181
- const { dir, blocksPath, byText, propose } = setup();
182
- const empty = polish('check', blocksPath, propose([['疑似欺诈', ' ']]));
183
- assert.equal(empty.status, 1);
184
- assert.match(empty.stdout, /never deletes a block/);
185
-
186
- const block = byText('疑似欺诈');
187
- const stale = JSON.parse(readFileSync(propose([['疑似欺诈', '疑似欺诈响应并非已确认的损失。']]), 'utf8'));
188
- stale.blocks[block.id].sha = 'old';
189
- const stalePath = join(dir, 'stale.json');
190
- writeFileSync(stalePath, JSON.stringify(stale));
191
- const result = polish('check', blocksPath, stalePath);
192
- assert.equal(result.status, 1);
193
- assert.match(result.stdout, /does not match block sha/);
194
-
195
- const nullPath = join(dir, 'null.json');
196
- writeFileSync(nullPath, 'null');
197
- const malformed = polish('check', blocksPath, nullPath);
198
- assert.equal(malformed.status, 2);
199
- assert.match(malformed.stderr, /needs an object/);
200
- });
201
-
202
- test('apply rewrites accepted prose, keeps tables and code byte-identical, and writes the record', () => {
203
- const { dir, doc, blocksPath, byText, propose } = setup();
204
- const proposals = propose([
205
- ['疑似欺诈', '疑似欺诈响应并非已确认的损失。'],
206
- ['授权拒绝', '授权拒绝共 1,111 笔,`C1` 列为高风险。'],
207
- ]);
208
- const record = join(dir, 'record.json');
209
- const result = polish('apply', doc, blocksPath, proposals, '--reject', byText('授权拒绝').id,
210
- '--record', record, '--model', 'deepseek/deepseek-flash');
211
- assert.equal(result.status, 0, result.stderr);
212
- const after = readFileSync(doc, 'utf8');
213
- assert.match(after, /^- 疑似欺诈响应并非已确认的损失。$/m);
214
- assert.match(after, /^- 授权拒绝 1,111 笔,`C1` 为高风险。$/m, 'a caller-rejected block stays unchanged');
215
- assert.ok(after.includes('| 授权尝试 | 4,946 |\n'));
216
- assert.ok(after.includes('print("本期授权尝试 4,946 笔")'));
217
- assert.ok(after.endsWith('```\n'), 'the trailing newline survives');
218
- const written = JSON.parse(readFileSync(record, 'utf8'));
219
- assert.equal(written.model, 'deepseek/deepseek-flash');
220
- assert.equal(written.applied.length, 1);
221
- assert.deepEqual(written.notes, ['note kept']);
222
- });
223
-
224
- test('apply keeps CRLF line endings and leaves HTML comments intact', () => {
225
- const crlf = ['正文 123。', '', '<!--', '注释 99', '-->', '', '结尾。', ''].join('\r\n');
226
- const { doc, blocksPath, propose } = setup(crlf);
227
- const result = polish('apply', doc, blocksPath, propose([['正文', '正文为 123。']]));
228
- assert.equal(result.status, 0, result.stderr);
229
- assert.equal(readFileSync(doc, 'utf8'), crlf.replace('正文 123。', '正文为 123。'));
230
- });
231
-
232
- test('apply refuses an incomplete proposal file and a document changed after extract', () => {
233
- const { doc, blocksPath, propose } = setup();
234
- const incomplete = polish('apply', doc, blocksPath, propose([['疑似欺诈', '疑似欺诈响应并非已确认的损失。']], { partial: true }));
235
- assert.equal(incomplete.status, 1);
236
- assert.match(incomplete.stderr, /every block needs/);
237
-
238
- const proposals = propose([['疑似欺诈', '疑似欺诈响应并非已确认的损失。']]);
239
- writeFileSync(doc, REPORT.replace('疑似欺诈响应不是', '疑似欺诈响应并不是'));
240
- const result = polish('apply', doc, blocksPath, proposals);
241
- assert.equal(result.status, 1);
242
- assert.match(result.stderr, /changed since extract/);
243
- assert.ok(readFileSync(doc, 'utf8').includes('疑似欺诈响应并不是'));
244
- });
@@ -1,165 +0,0 @@
1
- import assert from 'node:assert/strict';
2
- import { spawnSync } from 'node:child_process';
3
- import { mkdtempSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
4
- import { tmpdir } from 'node:os';
5
- import { dirname, join } from 'node:path';
6
- import { fileURLToPath } from 'node:url';
7
- import test from 'node:test';
8
-
9
- const scriptsDir = dirname(fileURLToPath(import.meta.url));
10
- const skillDir = join(scriptsDir, '..');
11
- const checklistsDir = join(skillDir, 'references', 'checklists');
12
- const mandatesDir = join(skillDir, 'references', 'mandates');
13
-
14
- function run(cmd, args) {
15
- return spawnSync(cmd, args, { encoding: 'utf8' });
16
- }
17
-
18
- function py(script, ...args) {
19
- return run('python3', [join(scriptsDir, script), ...args]);
20
- }
21
-
22
- function scratch(name, content) {
23
- const path = join(mkdtempSync(join(tmpdir(), 'reg-parser-test-')), name);
24
- writeFileSync(path, content);
25
- return path;
26
- }
27
-
28
- const HEADER = `\`\`\`yaml
29
- name: "Test Checklist"
30
- regulation: "Directive 2015/849"
31
- version: "1.0"
32
- scope: "Test"
33
- date: "2026-10-19"
34
- summary:
35
- categories: 1
36
- total_items: 1
37
- critical: 1
38
- major: 0
39
- minor: 0
40
- \`\`\`
41
-
42
- ## CAT-01: Customer Due Diligence
43
-
44
- `;
45
-
46
- // The item template in SKILL.md, filled in. If the template drifts from what
47
- // the validator accepts, every checklist an agent builds from it fails.
48
- function templateItem() {
49
- const skill = readFileSync(join(skillDir, 'SKILL.md'), 'utf8');
50
- const block = skill.match(/Each obligation from Step 2 becomes one checklist item:\n\n```markdown\n([\s\S]*?)```/)[1];
51
- return block
52
- .split('\n')
53
- .filter((line) => !line.includes('# source-native scheme'))
54
- .map((line) => line.replace(/\s+# cat scheme$/, ''))
55
- .join('\n')
56
- .replace('CAT-XX-NNN', 'CAT-01-001')
57
- .replace('[Requirement title]', 'Verify identity')
58
- .replace('[plain-language obligation]', 'Verify customer identity before onboarding.')
59
- .replace('section [ref], [regulation]', 'Art. 13(1)(a), Directive 2015/849')
60
- .replace('critical | major | minor', 'critical')
61
- .replace('[Concrete observable step]', 'ID checked against a reliable source')
62
- .replace('[Another step]', 'Check date recorded')
63
- .replace('[Document or record that proves compliance]', 'Verification log');
64
- }
65
-
66
- test('every bundled checklist validates', () => {
67
- for (const name of readdirSync(checklistsDir).filter((f) => f.endsWith('.md'))) {
68
- const result = py('validate_checklist.py', join(checklistsDir, name));
69
- assert.equal(result.status, 0, `${name}\n${result.stdout}${result.stderr}`);
70
- }
71
- });
72
-
73
- test('every bundled mandate validates', () => {
74
- for (const name of readdirSync(mandatesDir).filter((f) => f.endsWith('.md'))) {
75
- const result = py('validate_mandate.py', join(mandatesDir, name));
76
- assert.equal(result.status, 0, `${name}\n${result.stdout}${result.stderr}`);
77
- }
78
- });
79
-
80
- test('every bundled checklist JSON equals a fresh dump of its markdown', () => {
81
- for (const name of readdirSync(checklistsDir).filter((f) => f.endsWith('.json'))) {
82
- const md = join(checklistsDir, name.replace(/\.json$/, '.md'));
83
- const result = py('dump_checklist.py', md);
84
- assert.equal(result.status, 0, result.stderr);
85
- assert.deepEqual(JSON.parse(result.stdout), JSON.parse(readFileSync(join(checklistsDir, name), 'utf8')), name);
86
- }
87
- });
88
-
89
- test('the SKILL.md item template passes the validator', () => {
90
- const result = py('validate_checklist.py', scratch('ok.md', HEADER + templateItem() + '\n'));
91
- assert.equal(result.status, 0, result.stdout + result.stderr);
92
- });
93
-
94
- test('a checklist with colons outside the bold labels fails', () => {
95
- const item = templateItem().replace(/\*\*(\w+):\*\*/g, '**$1**:');
96
- const result = py('validate_checklist.py', scratch('bad.md', HEADER + item + '\n'));
97
- assert.equal(result.status, 1, result.stdout);
98
- });
99
-
100
- test('a checklist item with empty fields fails', () => {
101
- const item = `### CAT-01-001: Empty
102
-
103
- - **Requirement:**
104
- - **Source:**
105
- - **Severity:** critical
106
- - **Verification:**
107
- - **Evidence:**
108
- `;
109
- const result = py('validate_checklist.py', scratch('empty.md', HEADER + item));
110
- assert.equal(result.status, 1, result.stdout);
111
- assert.match(result.stdout + result.stderr, /field Requirement is empty/);
112
- assert.match(result.stdout + result.stderr, /Verification has no '- \[ \]' steps/);
113
- });
114
-
115
- test('a raw preprocess output fails mandate validation', () => {
116
- const source = scratch('src.md', 'Section 1 The licensee must keep records.\n```\nfenced\n```\n');
117
- const out = join(dirname(source), 'mandate.md');
118
- const pre = run('bash', [join(scriptsDir, 'preprocess-mandate.sh'), source, '--source', 'Test', '--out', out]);
119
- assert.equal(pre.status, 0, pre.stderr);
120
- const result = py('validate_mandate.py', out);
121
- assert.equal(result.status, 1, result.stdout);
122
- assert.match(result.stdout + result.stderr, /Status: RAW/);
123
- });
124
-
125
- test('preprocess refuses to overwrite a finished mandate', () => {
126
- const finished = join(mandatesDir, 'visa-vars.md');
127
- const copy = scratch('visa-vars.md', readFileSync(finished, 'utf8'));
128
- const source = scratch('src.md', 'Section 1 must.\n');
129
- const pre = run('bash', [join(scriptsDir, 'preprocess-mandate.sh'), source, '--source', 'Test', '--out', copy]);
130
- assert.equal(pre.status, 1, pre.stdout);
131
- assert.equal(readFileSync(copy, 'utf8'), readFileSync(finished, 'utf8'));
132
- });
133
-
134
- test('a mandate section with only prose or a hollow entry fails', () => {
135
- const sections = (body) => `# Test — Contract Obligation Extract
136
-
137
- > **Source:** Test Act 2026
138
-
139
- ## Mandatory Downstream Agreement Clauses
140
- ${body}
141
-
142
- ## Prohibited Conduct
143
- _None_
144
-
145
- ## Thresholds & Triggers
146
- _None_
147
-
148
- ## Audit & Oversight Rights
149
- _None_
150
-
151
- ## Notes
152
- Jurisdiction: test only. Effective 2026-10-19. Version 1. Caveat: synthetic fixture for the validator.
153
- `;
154
- assert.equal(py('validate_mandate.py', scratch('ok.md', sections('**section 4(1) Records**: Keep records five years.'))).status, 0);
155
- assert.equal(py('validate_mandate.py', scratch('prose.md', sections('This part has no obligations worth noting, n/a mostly.'))).status, 1);
156
- assert.equal(py('validate_mandate.py', scratch('hollow.md', sections('**section **:'))).status, 1);
157
- });
158
-
159
- test('compare_checklist counts item headings, not ID mentions', () => {
160
- const target = join(skillDir, 'references', 'tests', 'vars-checklist.md');
161
- const canonical = py('compare_checklist.py', join(checklistsDir, 'visa-vars-checklist.md'), target);
162
- assert.equal(canonical.status, 0, canonical.stdout);
163
- const ids = [...new Set(readFileSync(target, 'utf8').match(/\b[A-Z]{4}\.C\d+\.\d+\b/g))].join(' ');
164
- assert.equal(py('compare_checklist.py', scratch('fake.md', ids), target).status, 1);
165
- });
@@ -1,168 +0,0 @@
1
- import { test } from 'node:test'
2
- import assert from 'node:assert/strict'
3
- import { threeDotRange, parseNameStatus, parseLog, fastForwardable, isPromotionTarget, isIntegrationBranch, isPromotion, reviewNote, normalizeHeadName } from './pr-context.mjs'
4
-
5
- test('threeDotRange uses three dots (the merge diff, not two-dot)', () => {
6
- assert.equal(threeDotRange('abc123'), 'abc123...HEAD')
7
- assert.equal(threeDotRange('abc', 'feat'), 'abc...feat')
8
- })
9
-
10
- test('parseNameStatus handles add/modify/delete and rename', () => {
11
- const text = ['M\tsrc/a.ts', 'A\tsrc/new.ts', 'D\tsrc/old.ts', 'R100\tsrc/from.ts\tsrc/to.ts'].join('\n')
12
- assert.deepEqual(parseNameStatus(text), [
13
- { status: 'M', path: 'src/a.ts' },
14
- { status: 'A', path: 'src/new.ts' },
15
- { status: 'D', path: 'src/old.ts' },
16
- { status: 'R100', path: 'src/to.ts' },
17
- ])
18
- })
19
-
20
- test('parseNameStatus empty → []', () => {
21
- assert.deepEqual(parseNameStatus(''), [])
22
- })
23
-
24
- test('parseLog splits sha + subject', () => {
25
- const text = '1a2b3c add export\n4d5e6f fix expired coupon'
26
- assert.deepEqual(parseLog(text), [
27
- { sha: '1a2b3c', subject: 'add export' },
28
- { sha: '4d5e6f', subject: 'fix expired coupon' },
29
- ])
30
- })
31
-
32
- test('fastForwardable: empty = FF possible; non-empty (base has commits head lacks) = not FF', () => {
33
- assert.equal(fastForwardable([]), true)
34
- assert.equal(fastForwardable([{ sha: 'h0tf1x', subject: 'hotfix prod' }]), false)
35
- assert.equal(fastForwardable(undefined), true)
36
- })
37
-
38
- test('isPromotionTarget: production branches only (the base half of a promotion)', () => {
39
- assert.equal(isPromotionTarget('main'), true)
40
- assert.equal(isPromotionTarget('master'), true)
41
- assert.equal(isPromotionTarget('production'), true)
42
- assert.equal(isPromotionTarget('origin/main'), true)
43
- assert.equal(isPromotionTarget('dev'), false)
44
- assert.equal(isPromotionTarget('feature/x'), false)
45
- assert.equal(isPromotionTarget('release/1.2'), false)
46
- })
47
-
48
- test('isIntegrationBranch: only integration/release-shaped heads (the head half of a promotion)', () => {
49
- // integration branches
50
- assert.equal(isIntegrationBranch('dev'), true)
51
- assert.equal(isIntegrationBranch('develop'), true)
52
- assert.equal(isIntegrationBranch('origin/dev'), true)
53
- assert.equal(isIntegrationBranch('refs/heads/develop'), true)
54
- // release / hotfix / staging shapes
55
- assert.equal(isIntegrationBranch('release/1.4'), true)
56
- assert.equal(isIntegrationBranch('release/2.0.0'), true)
57
- assert.equal(isIntegrationBranch('hotfix/urgent-fix'), true)
58
- assert.equal(isIntegrationBranch('staging'), true)
59
- assert.equal(isIntegrationBranch('staging/eu'), true)
60
- assert.equal(isIntegrationBranch('origin/release/1.4'), true)
61
- // ordinary feature branches — NOT integration
62
- assert.equal(isIntegrationBranch('feature/x'), false)
63
- assert.equal(isIntegrationBranch('chore/388-doc-code-prose-audit'), false)
64
- assert.equal(isIntegrationBranch('issue-392-promotion-heuristic'), false)
65
- assert.equal(isIntegrationBranch('main'), false)
66
- assert.equal(isIntegrationBranch('develop-tweaks'), false) // not exactly `develop`
67
- assert.equal(isIntegrationBranch('feature/release-notes'), false) // `release` not its own segment
68
- // unresolvable head (a bare SHA, or empty) — NOT integration → safe default
69
- assert.equal(isIntegrationBranch('a1b2c3d'), false)
70
- assert.equal(isIntegrationBranch(''), false)
71
- assert.equal(isIntegrationBranch(undefined), false)
72
- })
73
-
74
- test('isPromotion: true ONLY when base is production AND head is integration/release-shaped', () => {
75
- // the ~15x false-positive this fixes: a feature branch → main is NOT a promotion
76
- assert.equal(isPromotion('main', 'chore/388-doc-code-prose-audit'), false)
77
- assert.equal(isPromotion('main', 'feature/x'), false)
78
- assert.equal(isPromotion('main', 'issue-392-promotion-heuristic'), false)
79
- // genuine promotions: integration/release branch → production
80
- assert.equal(isPromotion('main', 'dev'), true)
81
- assert.equal(isPromotion('main', 'release/1.4'), true)
82
- assert.equal(isPromotion('main', 'hotfix/x'), true)
83
- assert.equal(isPromotion('master', 'develop'), true)
84
- assert.equal(isPromotion('origin/main', 'origin/dev'), true)
85
- // base is not a production branch → never a promotion, even from an integration head
86
- assert.equal(isPromotion('dev', 'feature/x'), false)
87
- assert.equal(isPromotion('release/1.4', 'dev'), false)
88
- // unresolvable head (bare SHA / empty) → false (documented safe default)
89
- assert.equal(isPromotion('main', 'a1b2c3d'), false)
90
- assert.equal(isPromotion('main', ''), false)
91
- assert.equal(isPromotion('main', undefined), false)
92
- })
93
-
94
- test('normalizeHeadName strips ONLY known ref prefixes — never an arbitrary leading segment', () => {
95
- // known ref prefixes → stripped to the bare name
96
- assert.equal(normalizeHeadName('refs/heads/develop'), 'develop')
97
- assert.equal(normalizeHeadName('refs/remotes/origin/release/1.4'), 'release/1.4')
98
- assert.equal(normalizeHeadName('origin/dev'), 'dev')
99
- assert.equal(normalizeHeadName('origin/release/1.4'), 'release/1.4')
100
- assert.equal(normalizeHeadName('upstream/staging'), 'staging')
101
- // NOT a known prefix → left intact (the any-prefix hole this closes)
102
- assert.equal(normalizeHeadName('jsmith/dev'), 'jsmith/dev')
103
- assert.equal(normalizeHeadName('feature/release/notes'), 'feature/release/notes')
104
- assert.equal(normalizeHeadName('team/staging/eu'), 'team/staging/eu')
105
- // plain names pass through
106
- assert.equal(normalizeHeadName('dev'), 'dev')
107
- assert.equal(normalizeHeadName('release/1.4'), 'release/1.4')
108
- assert.equal(normalizeHeadName(''), '')
109
- assert.equal(normalizeHeadName(undefined), '')
110
- })
111
-
112
- // LEFT-BOUNDARY REGRESSION (issue #392, fix round): the integration/release name must
113
- // open the branch (after ref-prefix stripping), never be buried under a namespace. A
114
- // nested-prefix feature branch is an ordinary feature PR, not a promotion — the unsafe
115
- // direction if mis-classified. Removing the `^`/normalization makes these go RED.
116
- test('isIntegrationBranch: nested-prefix feature branches are NOT integration (left-boundary hole)', () => {
117
- // line-79 (release/hotfix/staging path) hole
118
- assert.equal(isIntegrationBranch('feature/release/notes'), false)
119
- assert.equal(isIntegrationBranch('feature/hotfix/x'), false)
120
- assert.equal(isIntegrationBranch('team/staging/eu'), false)
121
- // line-77 (exact dev/develop/staging) hole
122
- assert.equal(isIntegrationBranch('jsmith/dev'), false)
123
- assert.equal(isIntegrationBranch('alice/develop'), false)
124
- assert.equal(isIntegrationBranch('wip/staging'), false)
125
- })
126
-
127
- test('isIntegrationBranch: supported ref prefixes still resolve to TRUE after normalization', () => {
128
- assert.equal(isIntegrationBranch('origin/dev'), true)
129
- assert.equal(isIntegrationBranch('refs/heads/develop'), true)
130
- assert.equal(isIntegrationBranch('refs/remotes/origin/release/1.4'), true)
131
- })
132
-
133
- test('isPromotion: nested-prefix feature branches → main are NOT promotions (left-boundary hole)', () => {
134
- // these all classified promotion=true before the anchor fix (the fail-open direction)
135
- assert.equal(isPromotion('main', 'feature/release/notes'), false)
136
- assert.equal(isPromotion('main', 'feature/hotfix/x'), false)
137
- assert.equal(isPromotion('main', 'team/staging/eu'), false)
138
- assert.equal(isPromotion('main', 'jsmith/dev'), false)
139
- assert.equal(isPromotion('main', 'alice/develop'), false)
140
- assert.equal(isPromotion('main', 'wip/staging'), false)
141
- // supported prefixes remain genuine promotions
142
- assert.equal(isPromotion('main', 'refs/heads/develop'), true)
143
- assert.equal(isPromotion('main', 'refs/remotes/origin/release/1.4'), true)
144
- })
145
-
146
- test('reviewNote: PROMOTION + NOT fast-forwardable note still fires for a genuine promotion that is not FF', () => {
147
- const note = reviewNote({
148
- promotion: true,
149
- fastForwardable: false,
150
- base: 'main',
151
- head: 'release/1.4',
152
- commitsOnBaseNotInHead: [{ sha: 'h0tf1x', subject: 'hotfix prod' }],
153
- })
154
- assert.match(note, /PROMOTION \+ NOT fast-forwardable/)
155
- assert.match(note, /main has 1 commit\(s\) not in release\/1\.4/)
156
- assert.match(note, /HOLD/)
157
- })
158
-
159
- test('reviewNote: fast-forwardable promotion → release-readiness note (not a re-review)', () => {
160
- const note = reviewNote({ promotion: true, fastForwardable: true, base: 'main', head: 'dev', commitsOnBaseNotInHead: [] })
161
- assert.match(note, /PROMOTION \(fast-forwardable\)/)
162
- assert.doesNotMatch(note, /NOT fast-forwardable/)
163
- })
164
-
165
- test('reviewNote: not a promotion → review the NET diff (the common feature-PR path)', () => {
166
- const note = reviewNote({ promotion: false, fastForwardable: true, base: 'main', head: 'feature/x', commitsOnBaseNotInHead: [] })
167
- assert.match(note, /Review the NET diff/)
168
- })