@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.
- package/.codex-plugin/plugin.json +13 -0
- package/.codex-plugin/resources.json +1827 -0
- package/README.md +22 -0
- package/package.json +9 -2
- package/skills/app-dev/airflow/SKILL.md +1 -1
- package/skills/app-dev/airflow/release-check.mjs +3 -1
- package/skills/app-dev/android-foundation/SKILL.md +1 -1
- package/skills/app-dev/android-foundation/release-check.mjs +3 -1
- package/skills/app-dev/app-debug/SKILL.md +1 -1
- package/skills/app-dev/app-debug/release-check.mjs +3 -1
- package/skills/app-dev/better-auth/SKILL.md +1 -1
- package/skills/app-dev/better-auth/release-check.mjs +3 -1
- package/skills/app-dev/data-catalog/SKILL.md +1 -1
- package/skills/app-dev/data-catalog/release-check.mjs +3 -1
- package/skills/app-dev/data-quality/SKILL.md +1 -1
- package/skills/app-dev/data-quality/release-check.mjs +3 -1
- package/skills/app-dev/ios-foundation/SKILL.md +1 -1
- package/skills/app-dev/ios-foundation/release-check.mjs +3 -1
- package/skills/app-dev/monorepo/SKILL.md +1 -1
- package/skills/app-dev/monorepo/release-check.mjs +3 -1
- package/skills/app-dev/nextjs-fullstack/SKILL.md +1 -1
- package/skills/app-dev/nextjs-fullstack/release-check.mjs +3 -1
- package/skills/app-dev/prisma-setup/SKILL.md +1 -1
- package/skills/app-dev/prisma-setup/release-check.mjs +3 -1
- package/skills/app-dev/pyspark/SKILL.md +1 -1
- package/skills/app-dev/pyspark/release-check.mjs +3 -1
- package/skills/app-dev/python-backend/SKILL.md +1 -1
- package/skills/app-dev/python-backend/release-check.mjs +3 -1
- package/skills/app-dev/shadcn/SKILL.md +1 -1
- package/skills/app-dev/shadcn/release-check.mjs +3 -1
- package/skills/app-dev/zod-v4/SKILL.md +1 -1
- package/skills/app-dev/zod-v4/release-check.mjs +3 -1
- package/skills/cicd/cloud-build/SKILL.md +1 -1
- package/skills/cicd/cloud-build/release-check.mjs +3 -1
- package/skills/cicd/cloud-debug/SKILL.md +1 -1
- package/skills/cicd/cloud-debug/release-check.mjs +3 -1
- package/skills/cicd/cloud-deploy/SKILL.md +1 -1
- package/skills/cicd/cloud-deploy/release-check.mjs +3 -1
- package/skills/cicd/docker/SKILL.md +1 -1
- package/skills/cicd/docker/release-check.mjs +3 -1
- package/skills/cicd/github-actions/SKILL.md +1 -1
- package/skills/cicd/github-actions/release-check.mjs +3 -1
- package/skills/cicd/github-flow/SKILL.md +1 -1
- package/skills/cicd/github-flow/release-check.mjs +3 -1
- package/skills/cicd/google-cloud/SKILL.md +1 -1
- package/skills/cicd/google-cloud/release-check.mjs +3 -1
- package/skills/cicd/mergify/SKILL.md +1 -1
- package/skills/cicd/mergify/release-check.mjs +3 -1
- package/skills/cicd/podman/SKILL.md +1 -1
- package/skills/cicd/podman/release-check.mjs +3 -1
- package/skills/content/article-review/SKILL.md +1 -1
- package/skills/content/article-review/release-check.mjs +3 -1
- package/skills/content/article-to-image/SKILL.md +1 -1
- package/skills/content/article-to-image/release-check.mjs +3 -1
- package/skills/content/flowchart-svg/SKILL.md +1 -1
- package/skills/content/flowchart-svg/release-check.mjs +3 -1
- package/skills/content/image-gen/SKILL.md +1 -1
- package/skills/content/image-gen/release-check.mjs +3 -1
- package/skills/content/mermaid/SKILL.md +1 -1
- package/skills/content/mermaid/release-check.mjs +3 -1
- package/skills/content/wechat-article-writer/SKILL.md +1 -1
- package/skills/content/wechat-article-writer/release-check.mjs +3 -1
- package/skills/content/wechat-publisher/SKILL.md +1 -1
- package/skills/content/wechat-publisher/release-check.mjs +3 -1
- package/skills/content/wechat-search/SKILL.md +1 -1
- package/skills/content/wechat-search/release-check.mjs +3 -1
- package/skills/content/xai-search/SKILL.md +88 -13
- package/skills/content/xai-search/release-check.mjs +3 -1
- package/skills/content/xai-search/scripts/xai_search.sh +125 -149
- package/skills/core/distill-writing/SKILL.md +1 -1
- package/skills/core/distill-writing/release-check.mjs +3 -1
- package/skills/core/doc-to-md/SKILL.md +1 -1
- package/skills/core/doc-to-md/release-check.mjs +3 -1
- package/skills/core/editorial-review/SKILL.md +1 -1
- package/skills/core/editorial-review/release-check.mjs +3 -1
- package/skills/core/house-style/SKILL.md +1 -1
- package/skills/core/house-style/release-check.mjs +3 -1
- package/skills/core/plain-language/SKILL.md +1 -1
- package/skills/core/plain-language/release-check.mjs +3 -1
- package/skills/core/polish-cn/SKILL.md +1 -1
- package/skills/core/polish-cn/release-check.mjs +3 -1
- package/skills/core/reg-parser/SKILL.md +1 -1
- package/skills/core/reg-parser/release-check.mjs +3 -1
- package/skills/core/terminology/SKILL.md +1 -1
- package/skills/core/terminology/release-check.mjs +3 -1
- package/skills/core/write-doc/SKILL.md +1 -1
- package/skills/core/write-doc/release-check.mjs +3 -1
- package/skills/engineering/better-review/SKILL.md +1 -1
- package/skills/engineering/better-review/release-check.mjs +3 -1
- package/skills/engineering/contributing/SKILL.md +1 -1
- package/skills/engineering/contributing/release-check.mjs +3 -1
- package/skills/engineering/e2e-test/SKILL.md +1 -1
- package/skills/engineering/e2e-test/release-check.mjs +3 -1
- package/skills/engineering/engineering-runtime/SKILL.md +1 -1
- package/skills/engineering/engineering-runtime/coherence/workflow.json +13 -13
- package/skills/engineering/engineering-runtime/release-check.mjs +3 -1
- package/skills/engineering/engineering-runtime/scripts/installation-resolver.mjs +191 -0
- package/skills/engineering/engineering-runtime/scripts/workflow-coherence.mjs +51 -10
- package/skills/engineering/ensure-coverage/SKILL.md +1 -1
- package/skills/engineering/ensure-coverage/release-check.mjs +3 -1
- package/skills/engineering/golden-path/SKILL.md +1 -1
- package/skills/engineering/golden-path/release-check.mjs +3 -1
- package/skills/engineering/harness-runtime/SKILL.md +1 -1
- package/skills/engineering/harness-runtime/release-check.mjs +3 -1
- package/skills/engineering/resolve-issues/SKILL.md +1 -1
- package/skills/engineering/resolve-issues/references/preflight.md +7 -0
- package/skills/engineering/resolve-issues/references/shared-modules.md +27 -0
- package/skills/engineering/resolve-issues/references/task-plan.md +5 -1
- package/skills/engineering/resolve-issues/release-check.mjs +3 -1
- package/skills/engineering/resolve-issues/scripts/active-installation.mjs +32 -0
- package/skills/engineering/resolve-issues/scripts/install-hooks.mjs +3 -0
- package/skills/engineering/resolve-issues/scripts/rehydrate.mjs +17 -1
- package/skills/engineering/resolve-release/SKILL.md +1 -1
- package/skills/engineering/resolve-release/release-check.mjs +3 -1
- package/skills/engineering/rush-issues/SKILL.md +1 -1
- package/skills/engineering/rush-issues/release-check.mjs +3 -1
- package/skills/engineering/rush-release/SKILL.md +1 -1
- package/skills/engineering/rush-release/release-check.mjs +3 -1
- package/skills/engineering/test-quality/SKILL.md +1 -1
- package/skills/engineering/test-quality/release-check.mjs +3 -1
- package/skills/risk-compliance/compliance-audit/SKILL.md +1 -1
- package/skills/risk-compliance/compliance-audit/release-check.mjs +3 -1
- package/skills/risk-compliance/contract-compare/SKILL.md +1 -1
- package/skills/risk-compliance/contract-compare/release-check.mjs +3 -1
- package/skills/risk-compliance/contract-review/SKILL.md +1 -1
- package/skills/risk-compliance/contract-review/release-check.mjs +3 -1
- package/skills/risk-compliance/doc-authenticity/SKILL.md +1 -1
- package/skills/risk-compliance/doc-authenticity/release-check.mjs +3 -1
- package/skills/risk-compliance/fraud-analysis/SKILL.md +1 -1
- package/skills/risk-compliance/fraud-analysis/release-check.mjs +3 -1
- package/skills/risk-compliance/payment-analysis/SKILL.md +1 -1
- package/skills/risk-compliance/payment-analysis/release-check.mjs +3 -1
- package/skills/risk-compliance/similar-domain/SKILL.md +1 -1
- package/skills/risk-compliance/similar-domain/release-check.mjs +3 -1
- package/skills/risk-compliance/tld-detection/SKILL.md +1 -1
- package/skills/risk-compliance/tld-detection/release-check.mjs +3 -1
- package/skills/app-dev/android-foundation/scripts/device-run.test.mjs +0 -436
- package/skills/app-dev/data-catalog/scripts/data-catalog.test.mjs +0 -36
- package/skills/app-dev/data-quality/scripts/data-quality.test.mjs +0 -12
- package/skills/cicd/github-actions/scripts/ci-run-audit.test.mjs +0 -637
- package/skills/cicd/github-flow/scripts/flow.test.mjs +0 -114
- package/skills/cicd/github-flow/scripts/persist-conventions.test.mjs +0 -108
- package/skills/cicd/mergify/scripts/audit.test.mjs +0 -201
- package/skills/cicd/mergify/scripts/entrypoints.test.mjs +0 -106
- package/skills/cicd/mergify/scripts/pr-620-mergify-comments.fixture.json +0 -30
- package/skills/cicd/mergify/scripts/stack-push.test.mjs +0 -233
- package/skills/cicd/mergify/scripts/watch-pr-delivery.test.mjs +0 -1826
- package/skills/content/image-gen/scripts/credentials.test.mjs +0 -212
- package/skills/content/image-gen/scripts/options.test.mjs +0 -245
- package/skills/content/xai-search/tests/smoke_test.sh +0 -36
- package/skills/core/polish-cn/scripts/polish-cn.test.mjs +0 -244
- package/skills/core/reg-parser/scripts/reg-parser.test.mjs +0 -165
- package/skills/engineering/better-review/scripts/pr-context.test.mjs +0 -168
- package/skills/engineering/better-review/scripts/rereview-scope.test.mjs +0 -587
- package/skills/engineering/better-review/scripts/scan-diff.test.mjs +0 -414
- package/skills/engineering/better-review/scripts/vendor-sync.test.mjs +0 -96
- package/skills/engineering/contributing/scripts/persist-conventions.test.mjs +0 -118
- package/skills/engineering/e2e-test/scripts/presentation-checks.test.mjs +0 -106
- package/skills/engineering/e2e-test/scripts/presentation-perceivability.test.mjs +0 -117
- package/skills/engineering/e2e-test/scripts/presentation-reachability.test.mjs +0 -69
- package/skills/engineering/e2e-test/scripts/presentation-render-health.test.mjs +0 -98
- package/skills/engineering/e2e-test/scripts/presentation-sweep.test.mjs +0 -71
- package/skills/engineering/e2e-test/scripts/presentation-temporal.test.mjs +0 -59
- package/skills/engineering/e2e-test/scripts/project-route-manifest.test.mjs +0 -76
- package/skills/engineering/engineering-runtime/engineering-runtime.test.mjs +0 -68
- package/skills/engineering/engineering-runtime/scripts/invocation-policy.test.mjs +0 -95
- package/skills/engineering/engineering-runtime/scripts/main-module.test.mjs +0 -288
- package/skills/engineering/engineering-runtime/scripts/required-check-plan.test.mjs +0 -96
- package/skills/engineering/ensure-coverage/scripts/adapters/nextjs-inventory.test.mjs +0 -96
- package/skills/engineering/ensure-coverage/scripts/check-quarantine-expiry.test.mjs +0 -47
- package/skills/engineering/ensure-coverage/scripts/ci-audit.test.mjs +0 -86
- package/skills/engineering/ensure-coverage/scripts/coverage-ledger.test.mjs +0 -447
- package/skills/engineering/ensure-coverage/scripts/design-parity.test.mjs +0 -552
- package/skills/engineering/ensure-coverage/scripts/lint-tests.test.mjs +0 -289
- package/skills/engineering/ensure-coverage/scripts/mock-policy-config.test.mjs +0 -119
- package/skills/engineering/golden-path/scripts/golden-path.test.mjs +0 -1457
- package/skills/engineering/harness-runtime/discover-models.test.mjs +0 -363
- package/skills/engineering/harness-runtime/harness-runtime.test.mjs +0 -967
- package/skills/engineering/harness-runtime/model-catalog-seed.test.mjs +0 -829
- package/skills/engineering/harness-runtime/model-catalog.test.mjs +0 -366
- package/skills/engineering/resolve-issues/scripts/cleanup-runs.test.mjs +0 -280
- package/skills/engineering/resolve-issues/scripts/dropbox.test.mjs +0 -346
- package/skills/engineering/resolve-issues/scripts/findings-reuse.test.mjs +0 -88
- package/skills/engineering/resolve-issues/scripts/github-reviews.test.mjs +0 -264
- package/skills/engineering/resolve-issues/scripts/install-hooks.test.mjs +0 -79
- package/skills/engineering/resolve-issues/scripts/issue-tree.test.mjs +0 -107
- package/skills/engineering/resolve-issues/scripts/manual-invocation.test.mjs +0 -14
- package/skills/engineering/resolve-issues/scripts/next-actions-capacity.test.mjs +0 -149
- package/skills/engineering/resolve-issues/scripts/next-actions.test.mjs +0 -915
- package/skills/engineering/resolve-issues/scripts/plan-sync.test.mjs +0 -156
- package/skills/engineering/resolve-issues/scripts/preflight-models.test.mjs +0 -448
- package/skills/engineering/resolve-issues/scripts/ready-check.test.mjs +0 -217
- package/skills/engineering/resolve-issues/scripts/rehydrate.test.mjs +0 -91
- package/skills/engineering/resolve-issues/scripts/review-verdict.test.mjs +0 -1060
- package/skills/engineering/resolve-issues/scripts/size-check.test.mjs +0 -458
- package/skills/engineering/resolve-release/scripts/apply.test.mjs +0 -99
- package/skills/engineering/resolve-release/scripts/dark-flags.test.mjs +0 -100
- package/skills/engineering/resolve-release/scripts/flow.test.mjs +0 -340
- package/skills/engineering/resolve-release/scripts/green-head.test.mjs +0 -101
- package/skills/engineering/resolve-release/scripts/npm-version-absence.test.mjs +0 -426
- package/skills/engineering/resolve-release/scripts/plan-npm.test.mjs +0 -659
- package/skills/engineering/resolve-release/scripts/plan-toml-boundary.test.mjs +0 -317
- package/skills/engineering/resolve-release/scripts/plan-uv.test.mjs +0 -291
- package/skills/engineering/resolve-release/scripts/plan.test.mjs +0 -428
- package/skills/engineering/test-quality/scripts/lint-tests.test.mjs +0 -591
- package/skills/engineering/test-quality/scripts/mock-policy-config.test.mjs +0 -119
- package/skills/engineering/test-quality/scripts/persist-conventions.test.mjs +0 -104
- package/skills/risk-compliance/compliance-audit/scripts/compliance-audit.test.mjs +0 -14
- package/skills/risk-compliance/doc-authenticity/manual-invocation.test.mjs +0 -12
- package/skills/risk-compliance/doc-authenticity/scripts/validate_verdict.test.mjs +0 -212
- package/skills/risk-compliance/tld-detection/scripts/detect_ladders.test.mjs +0 -14
- package/skills/risk-compliance/tld-detection/scripts/detect_streams.test.mjs +0 -14
- package/skills/risk-compliance/tld-detection/tests/test_detect_ladders.py +0 -134
- 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 笔销售未能匹配授权,覆盖六个月。详见 。', /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
|
-
})
|