@godv61/dsh-task-engine 0.25.0 → 0.26.0
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/.acceptance.mjs +150 -150
- package/.assessment-batch1.mjs +20 -20
- package/.enforce-test.mjs +138 -138
- package/.evidence-test.mjs +118 -119
- package/.filter-test.mjs +92 -92
- package/.hook-test.mjs +195 -195
- package/.p0-test.mjs +59 -59
- package/.preset-test.mjs +160 -160
- package/.revision-test.mjs +30 -16
- package/.roundtrip-test.mjs +178 -63
- package/.workflow-test.mjs +86 -0
- package/README.md +7 -7
- package/cordis.patch.yml +10 -10
- package/defaults/eng.json +70 -22
- package/docs/BRIEF-FOR-REVIEW.md +162 -162
- package/docs/CHANGELOG.md +12 -0
- package/docs/README.md +40 -40
- package/docs/configuration.md +12 -9
- package/docs/development.md +1 -1
- package/docs/faq.md +4 -4
- package/docs/listing/godv61__dsh-task-engine.yml +5 -5
- package/docs/listing/submission.md +83 -83
- package/docs/manual.html +31 -26
- package/docs/releases/0.23.2.md +21 -21
- package/docs/roadmap.md +34 -34
- package/docs/testing/0.23.2/R02/344/270/232/345/212/241/346/265/213/350/257/225/346/230/216/347/273/206.md +56 -56
- package/docs/testing/0.23.2/R03/344/270/232/345/212/241/346/265/213/350/257/225/346/230/216/347/273/206.md +38 -38
- package/docs/testing/0.23.2//346/265/213/350/257/225/346/212/245/345/221/212.md +65 -65
- package/hooks/commit-msg +70 -1
- package/lib/client.js +825 -570
- package/lib/client.js.map +3 -3
- package/lib/controller.d.ts +2 -1
- package/lib/controller.js +6 -3
- package/lib/controller.js.map +1 -1
- package/lib/dev-task.js +79 -11
- package/lib/dev-task.js.map +1 -1
- package/lib/engine.d.ts +15 -0
- package/lib/engine.js +29 -5
- package/lib/engine.js.map +1 -1
- package/lib/hook.js +3 -0
- package/lib/hook.js.map +1 -1
- package/lib/skill-audit.js +3 -3
- package/lib/skill-audit.js.map +1 -1
- package/lib/workflows.d.ts +4 -1
- package/lib/workflows.js +90 -2
- package/lib/workflows.js.map +1 -1
- package/package.json +1 -1
- package/preset/agent.cordis.yml +21 -21
- package/preset/enable.mjs +87 -87
- package/preset/persona.md +4 -4
- package/preset/preset.yml +1 -1
- package/rules/coding-conventions.md +6 -6
- package/rules/commit-conventions.md +6 -6
- package/rules/security-redlines.md +5 -5
- package/scripts/verify-dsh-compat.mjs +144 -144
- package/skills/code-verify/SKILL.md +3 -3
package/.acceptance.mjs
CHANGED
|
@@ -1,151 +1,151 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Independent acceptance check for the skeleton/recommendation design.
|
|
3
|
-
*
|
|
4
|
-
* Reads the SHIPPED config and the built engine, not the test files, so a pass
|
|
5
|
-
* cannot come from an assertion written to match the implementation.
|
|
6
|
-
*
|
|
7
|
-
* The criteria are the product principles rather than implementation details:
|
|
8
|
-
* 1. a flow is state control only — adopting a setup is a separate act
|
|
9
|
-
* 2. adoption yields a config the user fully owns, editable and removable
|
|
10
|
-
* 3. a node references skills; a rule belongs to the skill that carries it
|
|
11
|
-
* 4. nothing is merged back after adoption, and an upgrade never overwrites
|
|
12
|
-
*/
|
|
13
|
-
import { readFileSync } from 'node:fs'
|
|
14
|
-
import { resolveFlow, FLOW_PRESETS, adoptRecommendation } from './lib/workflows.js'
|
|
15
|
-
import { validateWorkflow, formatResourceRef, completionBlockers, terminalRequirements, invalidatedBy } from './lib/engine.js'
|
|
16
|
-
|
|
17
|
-
let failures = 0
|
|
18
|
-
const check = (label, ok, detail = '') => {
|
|
19
|
-
console.log(' ' + (ok ? '✅' : '❌') + ' ' + label + (detail ? ' — ' + detail : ''))
|
|
20
|
-
if (!ok) failures++
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
console.log('=== 1. 流程只做状态控制 ===')
|
|
24
|
-
for (const [id, preset] of Object.entries(FLOW_PRESETS)) {
|
|
25
|
-
const skeleton = resolveFlow(id, { flow: id }).config
|
|
26
|
-
check(`${id} 骨架不带技能`, Object.keys(skeleton.stage_bindings ?? {}).length === 0)
|
|
27
|
-
check(`${id} 骨架不带产物`, skeleton.artifacts.length === 0)
|
|
28
|
-
check(`${id} 骨架保留状态机`, preset.config.transitions.length > 0 && preset.config.stages.length > 0)
|
|
29
|
-
check(`${id} 提交文本约定不在骨架里`, skeleton.commit.message_pattern === '', JSON.stringify(skeleton.commit.message_pattern))
|
|
30
|
-
}
|
|
31
|
-
// When a commit gate fires and whether the file scope is protected are flow
|
|
32
|
-
// control, so they must stay on the skeleton: they are a safety floor, not a
|
|
33
|
-
// writing convention, and must not disappear because a user declined a setup.
|
|
34
|
-
check('standard 骨架保留提交检查点', FLOW_PRESETS.standard.config.commit.checkpoints.length > 0,
|
|
35
|
-
JSON.stringify(FLOW_PRESETS.standard.config.commit.checkpoints))
|
|
36
|
-
check('standard 骨架保留文件范围保护', FLOW_PRESETS.standard.config.commit.file_scope === true)
|
|
37
|
-
|
|
38
|
-
console.log('')
|
|
39
|
-
console.log('=== 2. 采用是显式动作 ===')
|
|
40
|
-
check('推荐配置单独存在', Object.values(FLOW_PRESETS).every(p => p.recommendation !== undefined))
|
|
41
|
-
check('选流程不产生绑定', Object.keys(resolveFlow('standard', { flow: 'standard' }).config.stage_bindings ?? {}).length === 0)
|
|
42
|
-
|
|
43
|
-
console.log('')
|
|
44
|
-
console.log('=== 3. 采用后完全可编辑、可删除 ===')
|
|
45
|
-
for (const id of ['standard', 'agile', 'minimal']) {
|
|
46
|
-
const adopted = adoptRecommendation(id)
|
|
47
|
-
check(`${id} 采用后带推荐技能`, Object.keys(adopted.stage_bindings ?? {}).length > 0)
|
|
48
|
-
const replaced = resolveFlow(id, { ...adopted, stage_bindings: {} }).config
|
|
49
|
-
check(`${id} 清空后为空(不补回)`, Object.keys(replaced.stage_bindings ?? {}).length === 0)
|
|
50
|
-
const mine = resolveFlow(id, {
|
|
51
|
-
...adopted,
|
|
52
|
-
stage_bindings: { 开发: { skills: [{ skill: { source: 'project', name: 'mine' }, rules: [] }] } },
|
|
53
|
-
}).config
|
|
54
|
-
const dev = mine.stage_bindings['开发'].skills ?? []
|
|
55
|
-
check(`${id} 用户技能被原样采纳`, dev.length === 1 && dev[0].skill.name === 'mine',
|
|
56
|
-
JSON.stringify(dev.map(s => s.skill.name)))
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
console.log('')
|
|
60
|
-
console.log('=== 4. 规则归属技能,节点只引用 ===')
|
|
61
|
-
for (const id of ['standard', 'agile', 'minimal']) {
|
|
62
|
-
const cfg = resolveFlow(id, adoptRecommendation(id)).config
|
|
63
|
-
let stageRuleList = false
|
|
64
|
-
let badSkill = false
|
|
65
|
-
for (const binding of Object.values(cfg.stage_bindings ?? {})) {
|
|
66
|
-
if (binding.rules !== undefined) stageRuleList = true
|
|
67
|
-
for (const entry of binding.skills ?? []) {
|
|
68
|
-
if (!Array.isArray(entry.rules)) badSkill = true
|
|
69
|
-
if (entry.skill?.source === undefined) badSkill = true
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
check(`${id} 节点无规则列表`, !stageRuleList)
|
|
73
|
-
check(`${id} 每个技能自带规则数组`, !badSkill)
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
console.log('')
|
|
77
|
-
console.log('=== 5. 同一技能引用在任何节点规则一致 ===')
|
|
78
|
-
for (const id of ['standard', 'agile', 'minimal']) {
|
|
79
|
-
const cfg = resolveFlow(id, adoptRecommendation(id)).config
|
|
80
|
-
const bySkill = new Map()
|
|
81
|
-
for (const binding of Object.values(cfg.stage_bindings ?? {})) {
|
|
82
|
-
for (const entry of binding.skills ?? []) {
|
|
83
|
-
const key = formatResourceRef(entry.skill)
|
|
84
|
-
const sig = entry.rules.map(formatResourceRef).sort().join('|')
|
|
85
|
-
if (!bySkill.has(key)) bySkill.set(key, new Set())
|
|
86
|
-
bySkill.get(key).add(sig)
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
let ok = true
|
|
90
|
-
for (const [skill, sigs] of bySkill) if (sigs.size > 1) { ok = false; console.log(' ' + skill + ' 有 ' + sigs.size + ' 种规则集') }
|
|
91
|
-
check(`${id} 每个技能的规则集唯一`, ok, bySkill.size + ' 个技能')
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
console.log('')
|
|
95
|
-
console.log('=== 6. 配置可被引擎接受 ===')
|
|
96
|
-
for (const id of ['standard', 'agile', 'minimal']) {
|
|
97
|
-
const problems = validateWorkflow(resolveFlow(id, adoptRecommendation(id)).config)
|
|
98
|
-
check(`${id} 采用后配置无问题`, problems.length === 0, problems.join('; '))
|
|
99
|
-
const bare = validateWorkflow(resolveFlow(id, { flow: id }).config)
|
|
100
|
-
check(`${id} 裸骨架也合法`, bare.length === 0, bare.join('; '))
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
console.log('')
|
|
104
|
-
console.log('=== 7. defaults/eng.json 是采用后的配置示例 ===')
|
|
105
|
-
const doc = JSON.parse(readFileSync('defaults/eng.json', 'utf8'))
|
|
106
|
-
const merged = resolveFlow(doc.flow, doc)
|
|
107
|
-
check('defaults/eng.json 可解析', merged.ok)
|
|
108
|
-
if (merged.ok) {
|
|
109
|
-
check('defaults/eng.json 无验证问题', validateWorkflow(merged.config).length === 0)
|
|
110
|
-
check('defaults/eng.json 含技能(是采用后的样子)', Object.keys(merged.config.stage_bindings ?? {}).length > 0)
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
console.log('')
|
|
114
|
-
console.log('=== 8. 三预设都以 completed 收尾,且检查各自声明的节点结果 ===')
|
|
115
|
-
for (const id of ['standard', 'agile', 'minimal']) {
|
|
116
|
-
const cfg = resolveFlow(id, adoptRecommendation(id)).config
|
|
117
|
-
const terminals = cfg.stages.filter(s => !cfg.transitions.some(t => t.from === s))
|
|
118
|
-
// Every conclusion a flow could ask for, so each preset is judged on what IT
|
|
119
|
-
// declared rather than on what one preset happens to need.
|
|
120
|
-
const satisfied = {
|
|
121
|
-
stage: terminals[0], execution_version: 1,
|
|
122
|
-
items: [{ id: 'A', status: 'done', review: { spec: { outcome: 'pass' }, quality: { outcome: 'pass' } } }],
|
|
123
|
-
verification: { passed: true, evidence: [] },
|
|
124
|
-
review: { outcome: 'pass' },
|
|
125
|
-
commits: [{ label: 'TASK', hash: 'abc1234' }],
|
|
126
|
-
}
|
|
127
|
-
check(`${id} 未提交不可完成`, completionBlockers({ ...satisfied, commits: [] }, cfg).length > 0)
|
|
128
|
-
check(`${id} 齐全后可完成`, completionBlockers(satisfied, cfg).length === 0,
|
|
129
|
-
JSON.stringify(completionBlockers(satisfied, cfg)))
|
|
130
|
-
|
|
131
|
-
// The result the assessment found could be missing: a flow that declares a review
|
|
132
|
-
// must not complete with the review blocked, and a flow that declares none must
|
|
133
|
-
// not be made to demand one.
|
|
134
|
-
const wantsReview = terminalRequirements(cfg).includes('review_passed')
|
|
135
|
-
const blocked = completionBlockers({ ...satisfied, review: { outcome: 'blocked' } }, cfg)
|
|
136
|
-
check(`${id} ${wantsReview ? '审核未通过不可完成' : '不发明审核要求'}`,
|
|
137
|
-
wantsReview ? blocked.some(b => b.includes('passing review')) : blocked.length === 0,
|
|
138
|
-
JSON.stringify(blocked))
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
console.log('')
|
|
142
|
-
console.log('=== 9. 返工三种类型范围不同 ===')
|
|
143
|
-
const r = invalidatedBy('requirement'), s = invalidatedBy('solution'), d = invalidatedBy('defect')
|
|
144
|
-
check('requirement 失效需求确认', r.includes('requirement_confirmation'))
|
|
145
|
-
check('solution 保留需求确认', !s.includes('requirement_confirmation'))
|
|
146
|
-
check('defect 保留需求与方案确认', !d.includes('requirement_confirmation') && !d.includes('solution_confirmation'))
|
|
147
|
-
check('三者范围互不相同', new Set([r.join(), s.join(), d.join()]).size === 3)
|
|
148
|
-
|
|
149
|
-
console.log('')
|
|
150
|
-
console.log(failures === 0 ? '✅ 全部验收项通过' : `❌ ${failures} 项未通过`)
|
|
1
|
+
/**
|
|
2
|
+
* Independent acceptance check for the skeleton/recommendation design.
|
|
3
|
+
*
|
|
4
|
+
* Reads the SHIPPED config and the built engine, not the test files, so a pass
|
|
5
|
+
* cannot come from an assertion written to match the implementation.
|
|
6
|
+
*
|
|
7
|
+
* The criteria are the product principles rather than implementation details:
|
|
8
|
+
* 1. a flow is state control only — adopting a setup is a separate act
|
|
9
|
+
* 2. adoption yields a config the user fully owns, editable and removable
|
|
10
|
+
* 3. a node references skills; a rule belongs to the skill that carries it
|
|
11
|
+
* 4. nothing is merged back after adoption, and an upgrade never overwrites
|
|
12
|
+
*/
|
|
13
|
+
import { readFileSync } from 'node:fs'
|
|
14
|
+
import { resolveFlow, FLOW_PRESETS, adoptRecommendation } from './lib/workflows.js'
|
|
15
|
+
import { validateWorkflow, formatResourceRef, completionBlockers, terminalRequirements, invalidatedBy } from './lib/engine.js'
|
|
16
|
+
|
|
17
|
+
let failures = 0
|
|
18
|
+
const check = (label, ok, detail = '') => {
|
|
19
|
+
console.log(' ' + (ok ? '✅' : '❌') + ' ' + label + (detail ? ' — ' + detail : ''))
|
|
20
|
+
if (!ok) failures++
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
console.log('=== 1. 流程只做状态控制 ===')
|
|
24
|
+
for (const [id, preset] of Object.entries(FLOW_PRESETS)) {
|
|
25
|
+
const skeleton = resolveFlow(id, { flow: id }).config
|
|
26
|
+
check(`${id} 骨架不带技能`, Object.keys(skeleton.stage_bindings ?? {}).length === 0)
|
|
27
|
+
check(`${id} 骨架不带产物`, skeleton.artifacts.length === 0)
|
|
28
|
+
check(`${id} 骨架保留状态机`, preset.config.transitions.length > 0 && preset.config.stages.length > 0)
|
|
29
|
+
check(`${id} 提交文本约定不在骨架里`, skeleton.commit.message_pattern === '', JSON.stringify(skeleton.commit.message_pattern))
|
|
30
|
+
}
|
|
31
|
+
// When a commit gate fires and whether the file scope is protected are flow
|
|
32
|
+
// control, so they must stay on the skeleton: they are a safety floor, not a
|
|
33
|
+
// writing convention, and must not disappear because a user declined a setup.
|
|
34
|
+
check('standard 骨架保留提交检查点', FLOW_PRESETS.standard.config.commit.checkpoints.length > 0,
|
|
35
|
+
JSON.stringify(FLOW_PRESETS.standard.config.commit.checkpoints))
|
|
36
|
+
check('standard 骨架保留文件范围保护', FLOW_PRESETS.standard.config.commit.file_scope === true)
|
|
37
|
+
|
|
38
|
+
console.log('')
|
|
39
|
+
console.log('=== 2. 采用是显式动作 ===')
|
|
40
|
+
check('推荐配置单独存在', Object.values(FLOW_PRESETS).every(p => p.recommendation !== undefined))
|
|
41
|
+
check('选流程不产生绑定', Object.keys(resolveFlow('standard', { flow: 'standard' }).config.stage_bindings ?? {}).length === 0)
|
|
42
|
+
|
|
43
|
+
console.log('')
|
|
44
|
+
console.log('=== 3. 采用后完全可编辑、可删除 ===')
|
|
45
|
+
for (const id of ['standard', 'agile', 'minimal']) {
|
|
46
|
+
const adopted = adoptRecommendation(id)
|
|
47
|
+
check(`${id} 采用后带推荐技能`, Object.keys(adopted.stage_bindings ?? {}).length > 0)
|
|
48
|
+
const replaced = resolveFlow(id, { ...adopted, stage_bindings: {} }).config
|
|
49
|
+
check(`${id} 清空后为空(不补回)`, Object.keys(replaced.stage_bindings ?? {}).length === 0)
|
|
50
|
+
const mine = resolveFlow(id, {
|
|
51
|
+
...adopted,
|
|
52
|
+
stage_bindings: { 开发: { skills: [{ skill: { source: 'project', name: 'mine' }, rules: [] }] } },
|
|
53
|
+
}).config
|
|
54
|
+
const dev = mine.stage_bindings['开发'].skills ?? []
|
|
55
|
+
check(`${id} 用户技能被原样采纳`, dev.length === 1 && dev[0].skill.name === 'mine',
|
|
56
|
+
JSON.stringify(dev.map(s => s.skill.name)))
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
console.log('')
|
|
60
|
+
console.log('=== 4. 规则归属技能,节点只引用 ===')
|
|
61
|
+
for (const id of ['standard', 'agile', 'minimal']) {
|
|
62
|
+
const cfg = resolveFlow(id, adoptRecommendation(id)).config
|
|
63
|
+
let stageRuleList = false
|
|
64
|
+
let badSkill = false
|
|
65
|
+
for (const binding of Object.values(cfg.stage_bindings ?? {})) {
|
|
66
|
+
if (binding.rules !== undefined) stageRuleList = true
|
|
67
|
+
for (const entry of binding.skills ?? []) {
|
|
68
|
+
if (!Array.isArray(entry.rules)) badSkill = true
|
|
69
|
+
if (entry.skill?.source === undefined) badSkill = true
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
check(`${id} 节点无规则列表`, !stageRuleList)
|
|
73
|
+
check(`${id} 每个技能自带规则数组`, !badSkill)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
console.log('')
|
|
77
|
+
console.log('=== 5. 同一技能引用在任何节点规则一致 ===')
|
|
78
|
+
for (const id of ['standard', 'agile', 'minimal']) {
|
|
79
|
+
const cfg = resolveFlow(id, adoptRecommendation(id)).config
|
|
80
|
+
const bySkill = new Map()
|
|
81
|
+
for (const binding of Object.values(cfg.stage_bindings ?? {})) {
|
|
82
|
+
for (const entry of binding.skills ?? []) {
|
|
83
|
+
const key = formatResourceRef(entry.skill)
|
|
84
|
+
const sig = entry.rules.map(formatResourceRef).sort().join('|')
|
|
85
|
+
if (!bySkill.has(key)) bySkill.set(key, new Set())
|
|
86
|
+
bySkill.get(key).add(sig)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
let ok = true
|
|
90
|
+
for (const [skill, sigs] of bySkill) if (sigs.size > 1) { ok = false; console.log(' ' + skill + ' 有 ' + sigs.size + ' 种规则集') }
|
|
91
|
+
check(`${id} 每个技能的规则集唯一`, ok, bySkill.size + ' 个技能')
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
console.log('')
|
|
95
|
+
console.log('=== 6. 配置可被引擎接受 ===')
|
|
96
|
+
for (const id of ['standard', 'agile', 'minimal']) {
|
|
97
|
+
const problems = validateWorkflow(resolveFlow(id, adoptRecommendation(id)).config)
|
|
98
|
+
check(`${id} 采用后配置无问题`, problems.length === 0, problems.join('; '))
|
|
99
|
+
const bare = validateWorkflow(resolveFlow(id, { flow: id }).config)
|
|
100
|
+
check(`${id} 裸骨架也合法`, bare.length === 0, bare.join('; '))
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
console.log('')
|
|
104
|
+
console.log('=== 7. defaults/eng.json 是采用后的配置示例 ===')
|
|
105
|
+
const doc = JSON.parse(readFileSync('defaults/eng.json', 'utf8'))
|
|
106
|
+
const merged = resolveFlow(doc.flow, doc)
|
|
107
|
+
check('defaults/eng.json 可解析', merged.ok)
|
|
108
|
+
if (merged.ok) {
|
|
109
|
+
check('defaults/eng.json 无验证问题', validateWorkflow(merged.config).length === 0)
|
|
110
|
+
check('defaults/eng.json 含技能(是采用后的样子)', Object.keys(merged.config.stage_bindings ?? {}).length > 0)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
console.log('')
|
|
114
|
+
console.log('=== 8. 三预设都以 completed 收尾,且检查各自声明的节点结果 ===')
|
|
115
|
+
for (const id of ['standard', 'agile', 'minimal']) {
|
|
116
|
+
const cfg = resolveFlow(id, adoptRecommendation(id)).config
|
|
117
|
+
const terminals = cfg.stages.filter(s => !cfg.transitions.some(t => t.from === s))
|
|
118
|
+
// Every conclusion a flow could ask for, so each preset is judged on what IT
|
|
119
|
+
// declared rather than on what one preset happens to need.
|
|
120
|
+
const satisfied = {
|
|
121
|
+
stage: terminals[0], execution_version: 1,
|
|
122
|
+
items: [{ id: 'A', status: 'done', review: { spec: { outcome: 'pass' }, quality: { outcome: 'pass' } } }],
|
|
123
|
+
verification: { passed: true, evidence: [] },
|
|
124
|
+
review: { outcome: 'pass' },
|
|
125
|
+
commits: [{ label: 'TASK', hash: 'abc1234' }],
|
|
126
|
+
}
|
|
127
|
+
check(`${id} 未提交不可完成`, completionBlockers({ ...satisfied, commits: [] }, cfg).length > 0)
|
|
128
|
+
check(`${id} 齐全后可完成`, completionBlockers(satisfied, cfg).length === 0,
|
|
129
|
+
JSON.stringify(completionBlockers(satisfied, cfg)))
|
|
130
|
+
|
|
131
|
+
// The result the assessment found could be missing: a flow that declares a review
|
|
132
|
+
// must not complete with the review blocked, and a flow that declares none must
|
|
133
|
+
// not be made to demand one.
|
|
134
|
+
const wantsReview = terminalRequirements(cfg).includes('review_passed')
|
|
135
|
+
const blocked = completionBlockers({ ...satisfied, review: { outcome: 'blocked' } }, cfg)
|
|
136
|
+
check(`${id} ${wantsReview ? '审核未通过不可完成' : '不发明审核要求'}`,
|
|
137
|
+
wantsReview ? blocked.some(b => b.includes('passing review')) : blocked.length === 0,
|
|
138
|
+
JSON.stringify(blocked))
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
console.log('')
|
|
142
|
+
console.log('=== 9. 返工三种类型范围不同 ===')
|
|
143
|
+
const r = invalidatedBy('requirement'), s = invalidatedBy('solution'), d = invalidatedBy('defect')
|
|
144
|
+
check('requirement 失效需求确认', r.includes('requirement_confirmation'))
|
|
145
|
+
check('solution 保留需求确认', !s.includes('requirement_confirmation'))
|
|
146
|
+
check('defect 保留需求与方案确认', !d.includes('requirement_confirmation') && !d.includes('solution_confirmation'))
|
|
147
|
+
check('三者范围互不相同', new Set([r.join(), s.join(), d.join()]).size === 3)
|
|
148
|
+
|
|
149
|
+
console.log('')
|
|
150
|
+
console.log(failures === 0 ? '✅ 全部验收项通过' : `❌ ${failures} 项未通过`)
|
|
151
151
|
process.exit(failures === 0 ? 0 : 1)
|
package/.assessment-batch1.mjs
CHANGED
|
@@ -17,21 +17,21 @@ import { registerDevTask } from './lib/dev-task.js'
|
|
|
17
17
|
import { newTask, todosBlockers, completionBlockers, validateWorkflow, formatResourceRef } from './lib/engine.js'
|
|
18
18
|
import { adoptRecommendation, FLOW_PRESETS } from './lib/workflows.js'
|
|
19
19
|
import { resolveFlow } from './lib/workflows.js'
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* The preset as a project would actually run it: the skeleton plus the shipped
|
|
23
|
-
* recommendation, adopted exactly the way a user adopts it. A test that asserts
|
|
24
|
-
* on bindings, commit rules or artifacts wants this, because those no longer come
|
|
25
|
-
* from the preset itself.
|
|
26
|
-
* @param {string} id - preset id.
|
|
27
|
-
* @returns {import('./lib/engine.js').WorkflowConfig} the adopted config.
|
|
28
|
-
*/
|
|
29
|
-
function adoptedFlow(id, extra) {
|
|
30
|
-
const base = adoptRecommendation(id)
|
|
31
|
-
if (base === undefined) throw new Error('unknown preset: ' + id)
|
|
32
|
-
return resolveFlow(id, { ...base, ...extra }).config
|
|
33
|
-
}
|
|
34
|
-
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The preset as a project would actually run it: the skeleton plus the shipped
|
|
23
|
+
* recommendation, adopted exactly the way a user adopts it. A test that asserts
|
|
24
|
+
* on bindings, commit rules or artifacts wants this, because those no longer come
|
|
25
|
+
* from the preset itself.
|
|
26
|
+
* @param {string} id - preset id.
|
|
27
|
+
* @returns {import('./lib/engine.js').WorkflowConfig} the adopted config.
|
|
28
|
+
*/
|
|
29
|
+
function adoptedFlow(id, extra) {
|
|
30
|
+
const base = adoptRecommendation(id)
|
|
31
|
+
if (base === undefined) throw new Error('unknown preset: ' + id)
|
|
32
|
+
return resolveFlow(id, { ...base, ...extra }).config
|
|
33
|
+
}
|
|
34
|
+
|
|
35
35
|
|
|
36
36
|
/** Build a fixture on one preset, mirroring .workflow-test.mjs's contract. */
|
|
37
37
|
function fixture(preset = 'standard', stage, extra = {}, services = {}) {
|
|
@@ -365,11 +365,11 @@ test('P2: reaching the last stage is not completion for any preset', () => {
|
|
|
365
365
|
execution_version: 1,
|
|
366
366
|
items: [{ id: 'A', title: 'a', status: 'done', review: { spec: { outcome: 'pass' }, quality: { outcome: 'pass' } } }],
|
|
367
367
|
verification: { passed: true, evidence: [] },
|
|
368
|
-
// The flows that declare a review guard need a recorded verdict. That is the
|
|
369
|
-
// point of deriving terminal requirements from the flow: standard and agile ask
|
|
370
|
-
// for one, minimal does not, so this supplies what is asked for rather than
|
|
371
|
-
// what one flow happens to need.
|
|
372
|
-
review: { outcome: 'pass' },
|
|
368
|
+
// The flows that declare a review guard need a recorded verdict. That is the
|
|
369
|
+
// point of deriving terminal requirements from the flow: standard and agile ask
|
|
370
|
+
// for one, minimal does not, so this supplies what is asked for rather than
|
|
371
|
+
// what one flow happens to need.
|
|
372
|
+
review: { outcome: 'pass' },
|
|
373
373
|
commits: [],
|
|
374
374
|
}
|
|
375
375
|
assert.ok(
|
package/.enforce-test.mjs
CHANGED
|
@@ -1,139 +1,139 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The three enforcement gaps the assessment found, tested as behaviour.
|
|
3
|
-
*
|
|
4
|
-
* Each of these was previously DISCLOSED but not enforced, which is the same as not
|
|
5
|
-
* being enforced: status reported the problem and the task proceeded anyway.
|
|
6
|
-
*/
|
|
7
|
-
import test from 'node:test'
|
|
8
|
-
import assert from 'node:assert/strict'
|
|
9
|
-
import { join, resolve } from 'node:path'
|
|
10
|
-
import { registerDevTask } from './lib/dev-task.js'
|
|
11
|
-
import { adoptRecommendation, resolveFlow } from './lib/workflows.js'
|
|
12
|
-
import { completionBlockers, terminalRequirements, resourceBlockers } from './lib/engine.js'
|
|
13
|
-
|
|
14
|
-
/** A project on the standard flow, one skill carrying a project rule, at 开发. */
|
|
15
|
-
async function project() {
|
|
16
|
-
const cwd = resolve('enforce-project')
|
|
17
|
-
const adopted = adoptRecommendation('standard')
|
|
18
|
-
const skills = adopted.stage_bindings['开发'].skills.map(entry => ({
|
|
19
|
-
...entry,
|
|
20
|
-
rules: [...entry.rules, { source: 'project', name: 'mine' }],
|
|
21
|
-
}))
|
|
22
|
-
const stage_bindings = { ...adopted.stage_bindings, '开发': { skills } }
|
|
23
|
-
const records = new Map([
|
|
24
|
-
[join(cwd, '.dsh/eng.json'), JSON.stringify({ ...adopted, stage_bindings })],
|
|
25
|
-
[join(cwd, '.dsh/rules/mine.md'), 'RULE'],
|
|
26
|
-
[join(cwd, 'a.js'), 'source'],
|
|
27
|
-
])
|
|
28
|
-
const events = []
|
|
29
|
-
let execute
|
|
30
|
-
const key = (p, options) => join(options?.cwd ?? cwd, p)
|
|
31
|
-
const fs = {
|
|
32
|
-
resolve: async (p, options) => ({ targetKey: key(p, options) }),
|
|
33
|
-
readText: async target => records.get(target.targetKey),
|
|
34
|
-
listDir: async () => [],
|
|
35
|
-
lstat: async (p, options) => records.has(key(p, options)) ? { version: 'v' } : undefined,
|
|
36
|
-
writeText: async (target, content) => { records.set(target.targetKey, content) },
|
|
37
|
-
}
|
|
38
|
-
const ctx = {
|
|
39
|
-
fs,
|
|
40
|
-
tools: { register(tool) { execute = tool.execute; return () => {} } },
|
|
41
|
-
get: name => name === 'sandboxPolicy'
|
|
42
|
-
? { resolve: () => ({ mode: 'workspace-write', workspaceRoot: cwd, sessionId: 'e' }) }
|
|
43
|
-
: undefined,
|
|
44
|
-
}
|
|
45
|
-
registerDevTask(ctx)
|
|
46
|
-
const exec = { agent: { session: { id: 'e', header: { cwd }, snapshotEvents: () => events } }, signal: new AbortController().signal }
|
|
47
|
-
const call = async args => execute(JSON.parse(JSON.stringify({ task_id: 'E-1', ...args })), exec)
|
|
48
|
-
await call({ operation: 'create', title: 't', branch: 'main', files: ['a.js'] })
|
|
49
|
-
const state = JSON.parse(records.get(join(cwd, '.dsh/task-E-1.json')))
|
|
50
|
-
state.stage = '开发'
|
|
51
|
-
state.requirement_confirmed = true
|
|
52
|
-
state.solution_confirmed = true
|
|
53
|
-
records.set(join(cwd, '.dsh/task-E-1.json'), JSON.stringify(state))
|
|
54
|
-
const load = name => {
|
|
55
|
-
const callId = 'c' + events.length
|
|
56
|
-
events.push({ type: 'tool/call', data: { name: 'skill', callId, arguments: JSON.stringify({ name }) } })
|
|
57
|
-
events.push({ type: 'tool/result', data: { message: { content: [{ type: 'tool-result', toolCallId: callId, isError: false }] } } })
|
|
58
|
-
}
|
|
59
|
-
const current = () => JSON.parse(records.get(join(cwd, '.dsh/task-E-1.json')))
|
|
60
|
-
return { call, records, cwd, load, current, rule: () => join(cwd, '.dsh/rules/mine.md') }
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/** Satisfy everything 开发 → 交付 needs except the resource question. */
|
|
64
|
-
async function satistfyDevelopment(f) {
|
|
65
|
-
await f.call({ operation: 'items', items: [{ id: 'A', title: 'a', status: 'doing' }] })
|
|
66
|
-
await f.call({ operation: 'dispatch', item_id: 'A', description: 'd' })
|
|
67
|
-
await f.call({ operation: 'review_item', item_id: 'A', spec_outcome: 'pass', quality_outcome: 'pass' })
|
|
68
|
-
await f.call({ operation: 'items', items: [{ id: 'A', status: 'done' }] })
|
|
69
|
-
for (const entry of f.current().flow.config.stage_bindings['开发'].skills) f.load(entry.skill.name)
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
test('enforce: a task WITHOUT a snapshot cannot advance while a bound rule is unresolvable', async () => {
|
|
73
|
-
// The disclosed-but-unenforced case: status said the rule was missing and advance
|
|
74
|
-
// still succeeded, so the stage completed without the constraint.
|
|
75
|
-
const f = await project()
|
|
76
|
-
const state = f.current()
|
|
77
|
-
delete state.flow.resources
|
|
78
|
-
f.records.set(join(f.cwd, '.dsh/task-E-1.json'), JSON.stringify(state))
|
|
79
|
-
await satistfyDevelopment(f)
|
|
80
|
-
|
|
81
|
-
f.records.delete(f.rule())
|
|
82
|
-
await assert.rejects(
|
|
83
|
-
() => f.call({ operation: 'advance', target_stage: '交付' }),
|
|
84
|
-
/resolves nowhere/u,
|
|
85
|
-
'a stage whose rules cannot be resolved must not be passable',
|
|
86
|
-
)
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
test('enforce: a task WITH a snapshot still advances after its source rule is deleted', async () => {
|
|
90
|
-
// The other side of the same rule: the frozen copy IS the constraint, so a
|
|
91
|
-
// deleted source is drift to report, not a reason to block. Without this the
|
|
92
|
-
// freeze would be pointless — the task would be blocked by a file it no longer
|
|
93
|
-
// needs.
|
|
94
|
-
const f = await project()
|
|
95
|
-
await satistfyDevelopment(f)
|
|
96
|
-
f.records.delete(f.rule())
|
|
97
|
-
await f.call({ operation: 'advance', target_stage: '交付' })
|
|
98
|
-
assert.equal(f.current().stage, '交付', 'the frozen task advances on its own copy')
|
|
99
|
-
})
|
|
100
|
-
|
|
101
|
-
test('enforce: completion requires the review and verification the flow declared', async () => {
|
|
102
|
-
// Arriving at the last stage used to be enough. The agile flow declares a review
|
|
103
|
-
// on the way into 审查, so a blocked review must now prevent completion —
|
|
104
|
-
// without hardcoding review into flows that never asked for it.
|
|
105
|
-
const agile = resolveFlow('agile', adoptRecommendation('agile')).config
|
|
106
|
-
assert.ok(terminalRequirements(agile).length >= 0)
|
|
107
|
-
const base = {
|
|
108
|
-
stage: '审查', execution_version: 1,
|
|
109
|
-
items: [{ id: 'A', status: 'done', review: { spec: { outcome: 'pass' }, quality: { outcome: 'pass' } } }],
|
|
110
|
-
verification: { passed: true, evidence: [] },
|
|
111
|
-
commits: [{ label: 'TASK', hash: 'abc1234' }],
|
|
112
|
-
}
|
|
113
|
-
assert.deepEqual(completionBlockers({ ...base, review: { outcome: 'pass' } }, agile), [],
|
|
114
|
-
'a passing review completes')
|
|
115
|
-
const blocked = completionBlockers({ ...base, review: { outcome: 'blocked' } }, agile)
|
|
116
|
-
assert.ok(blocked.some(b => b.includes('passing review')),
|
|
117
|
-
`a blocked review must prevent completion; got ${JSON.stringify(blocked)}`)
|
|
118
|
-
|
|
119
|
-
// And a flow that declares no verification gate is not made to demand one.
|
|
120
|
-
const minimal = resolveFlow('minimal', adoptRecommendation('minimal')).config
|
|
121
|
-
const minState = {
|
|
122
|
-
stage: '交付', execution_version: 1,
|
|
123
|
-
items: [{ id: 'A', status: 'done', review: { spec: { outcome: 'pass' } } }],
|
|
124
|
-
verification: { passed: false, evidence: [] },
|
|
125
|
-
commits: [{ label: 'TASK', hash: 'abc1234' }],
|
|
126
|
-
}
|
|
127
|
-
assert.deepEqual(completionBlockers(minState, minimal), [],
|
|
128
|
-
'minimal declares no verification gate, so a non-passing verification must not block it')
|
|
129
|
-
})
|
|
130
|
-
|
|
131
|
-
test('enforce: the resource blocker names the stage, the reference and the fix', () => {
|
|
132
|
-
const blockers = resourceBlockers(['project:mine'], '开发')
|
|
133
|
-
assert.equal(blockers.length, 1)
|
|
134
|
-
assert.match(blockers[0], /开发/u)
|
|
135
|
-
assert.match(blockers[0], /project:mine/u)
|
|
136
|
-
assert.match(blockers[0], /Restore the file|remove the binding/u,
|
|
137
|
-
'a blocker must say what to do, not only what is wrong')
|
|
138
|
-
assert.deepEqual(resourceBlockers([], '开发'), [], 'a resolvable stage has no blocker')
|
|
1
|
+
/**
|
|
2
|
+
* The three enforcement gaps the assessment found, tested as behaviour.
|
|
3
|
+
*
|
|
4
|
+
* Each of these was previously DISCLOSED but not enforced, which is the same as not
|
|
5
|
+
* being enforced: status reported the problem and the task proceeded anyway.
|
|
6
|
+
*/
|
|
7
|
+
import test from 'node:test'
|
|
8
|
+
import assert from 'node:assert/strict'
|
|
9
|
+
import { join, resolve } from 'node:path'
|
|
10
|
+
import { registerDevTask } from './lib/dev-task.js'
|
|
11
|
+
import { adoptRecommendation, resolveFlow } from './lib/workflows.js'
|
|
12
|
+
import { completionBlockers, terminalRequirements, resourceBlockers } from './lib/engine.js'
|
|
13
|
+
|
|
14
|
+
/** A project on the standard flow, one skill carrying a project rule, at 开发. */
|
|
15
|
+
async function project() {
|
|
16
|
+
const cwd = resolve('enforce-project')
|
|
17
|
+
const adopted = adoptRecommendation('standard')
|
|
18
|
+
const skills = adopted.stage_bindings['开发'].skills.map(entry => ({
|
|
19
|
+
...entry,
|
|
20
|
+
rules: [...entry.rules, { source: 'project', name: 'mine' }],
|
|
21
|
+
}))
|
|
22
|
+
const stage_bindings = { ...adopted.stage_bindings, '开发': { skills } }
|
|
23
|
+
const records = new Map([
|
|
24
|
+
[join(cwd, '.dsh/eng.json'), JSON.stringify({ ...adopted, stage_bindings })],
|
|
25
|
+
[join(cwd, '.dsh/rules/mine.md'), 'RULE'],
|
|
26
|
+
[join(cwd, 'a.js'), 'source'],
|
|
27
|
+
])
|
|
28
|
+
const events = []
|
|
29
|
+
let execute
|
|
30
|
+
const key = (p, options) => join(options?.cwd ?? cwd, p)
|
|
31
|
+
const fs = {
|
|
32
|
+
resolve: async (p, options) => ({ targetKey: key(p, options) }),
|
|
33
|
+
readText: async target => records.get(target.targetKey),
|
|
34
|
+
listDir: async () => [],
|
|
35
|
+
lstat: async (p, options) => records.has(key(p, options)) ? { version: 'v' } : undefined,
|
|
36
|
+
writeText: async (target, content) => { records.set(target.targetKey, content) },
|
|
37
|
+
}
|
|
38
|
+
const ctx = {
|
|
39
|
+
fs,
|
|
40
|
+
tools: { register(tool) { execute = tool.execute; return () => {} } },
|
|
41
|
+
get: name => name === 'sandboxPolicy'
|
|
42
|
+
? { resolve: () => ({ mode: 'workspace-write', workspaceRoot: cwd, sessionId: 'e' }) }
|
|
43
|
+
: undefined,
|
|
44
|
+
}
|
|
45
|
+
registerDevTask(ctx)
|
|
46
|
+
const exec = { agent: { session: { id: 'e', header: { cwd }, snapshotEvents: () => events } }, signal: new AbortController().signal }
|
|
47
|
+
const call = async args => execute(JSON.parse(JSON.stringify({ task_id: 'E-1', ...args })), exec)
|
|
48
|
+
await call({ operation: 'create', title: 't', branch: 'main', files: ['a.js'] })
|
|
49
|
+
const state = JSON.parse(records.get(join(cwd, '.dsh/task-E-1.json')))
|
|
50
|
+
state.stage = '开发'
|
|
51
|
+
state.requirement_confirmed = true
|
|
52
|
+
state.solution_confirmed = true
|
|
53
|
+
records.set(join(cwd, '.dsh/task-E-1.json'), JSON.stringify(state))
|
|
54
|
+
const load = name => {
|
|
55
|
+
const callId = 'c' + events.length
|
|
56
|
+
events.push({ type: 'tool/call', data: { name: 'skill', callId, arguments: JSON.stringify({ name }) } })
|
|
57
|
+
events.push({ type: 'tool/result', data: { message: { content: [{ type: 'tool-result', toolCallId: callId, isError: false }] } } })
|
|
58
|
+
}
|
|
59
|
+
const current = () => JSON.parse(records.get(join(cwd, '.dsh/task-E-1.json')))
|
|
60
|
+
return { call, records, cwd, load, current, rule: () => join(cwd, '.dsh/rules/mine.md') }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Satisfy everything 开发 → 交付 needs except the resource question. */
|
|
64
|
+
async function satistfyDevelopment(f) {
|
|
65
|
+
await f.call({ operation: 'items', items: [{ id: 'A', title: 'a', status: 'doing' }] })
|
|
66
|
+
await f.call({ operation: 'dispatch', item_id: 'A', description: 'd' })
|
|
67
|
+
await f.call({ operation: 'review_item', item_id: 'A', spec_outcome: 'pass', quality_outcome: 'pass' })
|
|
68
|
+
await f.call({ operation: 'items', items: [{ id: 'A', status: 'done' }] })
|
|
69
|
+
for (const entry of f.current().flow.config.stage_bindings['开发'].skills) f.load(entry.skill.name)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
test('enforce: a task WITHOUT a snapshot cannot advance while a bound rule is unresolvable', async () => {
|
|
73
|
+
// The disclosed-but-unenforced case: status said the rule was missing and advance
|
|
74
|
+
// still succeeded, so the stage completed without the constraint.
|
|
75
|
+
const f = await project()
|
|
76
|
+
const state = f.current()
|
|
77
|
+
delete state.flow.resources
|
|
78
|
+
f.records.set(join(f.cwd, '.dsh/task-E-1.json'), JSON.stringify(state))
|
|
79
|
+
await satistfyDevelopment(f)
|
|
80
|
+
|
|
81
|
+
f.records.delete(f.rule())
|
|
82
|
+
await assert.rejects(
|
|
83
|
+
() => f.call({ operation: 'advance', target_stage: '交付' }),
|
|
84
|
+
/resolves nowhere/u,
|
|
85
|
+
'a stage whose rules cannot be resolved must not be passable',
|
|
86
|
+
)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
test('enforce: a task WITH a snapshot still advances after its source rule is deleted', async () => {
|
|
90
|
+
// The other side of the same rule: the frozen copy IS the constraint, so a
|
|
91
|
+
// deleted source is drift to report, not a reason to block. Without this the
|
|
92
|
+
// freeze would be pointless — the task would be blocked by a file it no longer
|
|
93
|
+
// needs.
|
|
94
|
+
const f = await project()
|
|
95
|
+
await satistfyDevelopment(f)
|
|
96
|
+
f.records.delete(f.rule())
|
|
97
|
+
await f.call({ operation: 'advance', target_stage: '交付' })
|
|
98
|
+
assert.equal(f.current().stage, '交付', 'the frozen task advances on its own copy')
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
test('enforce: completion requires the review and verification the flow declared', async () => {
|
|
102
|
+
// Arriving at the last stage used to be enough. The agile flow declares a review
|
|
103
|
+
// on the way into 审查, so a blocked review must now prevent completion —
|
|
104
|
+
// without hardcoding review into flows that never asked for it.
|
|
105
|
+
const agile = resolveFlow('agile', adoptRecommendation('agile')).config
|
|
106
|
+
assert.ok(terminalRequirements(agile).length >= 0)
|
|
107
|
+
const base = {
|
|
108
|
+
stage: '审查', execution_version: 1,
|
|
109
|
+
items: [{ id: 'A', status: 'done', review: { spec: { outcome: 'pass' }, quality: { outcome: 'pass' } } }],
|
|
110
|
+
verification: { passed: true, evidence: [] },
|
|
111
|
+
commits: [{ label: 'TASK', hash: 'abc1234' }],
|
|
112
|
+
}
|
|
113
|
+
assert.deepEqual(completionBlockers({ ...base, review: { outcome: 'pass' } }, agile), [],
|
|
114
|
+
'a passing review completes')
|
|
115
|
+
const blocked = completionBlockers({ ...base, review: { outcome: 'blocked' } }, agile)
|
|
116
|
+
assert.ok(blocked.some(b => b.includes('passing review')),
|
|
117
|
+
`a blocked review must prevent completion; got ${JSON.stringify(blocked)}`)
|
|
118
|
+
|
|
119
|
+
// And a flow that declares no verification gate is not made to demand one.
|
|
120
|
+
const minimal = resolveFlow('minimal', adoptRecommendation('minimal')).config
|
|
121
|
+
const minState = {
|
|
122
|
+
stage: '交付', execution_version: 1,
|
|
123
|
+
items: [{ id: 'A', status: 'done', review: { spec: { outcome: 'pass' } } }],
|
|
124
|
+
verification: { passed: false, evidence: [] },
|
|
125
|
+
commits: [{ label: 'TASK', hash: 'abc1234' }],
|
|
126
|
+
}
|
|
127
|
+
assert.deepEqual(completionBlockers(minState, minimal), [],
|
|
128
|
+
'minimal declares no verification gate, so a non-passing verification must not block it')
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
test('enforce: the resource blocker names the stage, the reference and the fix', () => {
|
|
132
|
+
const blockers = resourceBlockers(['project:mine'], '开发')
|
|
133
|
+
assert.equal(blockers.length, 1)
|
|
134
|
+
assert.match(blockers[0], /开发/u)
|
|
135
|
+
assert.match(blockers[0], /project:mine/u)
|
|
136
|
+
assert.match(blockers[0], /Restore the file|remove the binding/u,
|
|
137
|
+
'a blocker must say what to do, not only what is wrong')
|
|
138
|
+
assert.deepEqual(resourceBlockers([], '开发'), [], 'a resolvable stage has no blocker')
|
|
139
139
|
})
|