@godv61/dsh-task-engine 0.23.0 → 0.23.2

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 (53) hide show
  1. package/.p0-test.mjs +739 -734
  2. package/.workflow-test.mjs +290 -0
  3. package/README.md +77 -181
  4. package/defaults/eng.json +10 -10
  5. package/docs/CHANGELOG.md +192 -0
  6. package/docs/README.md +29 -0
  7. package/docs/assets/workflow-banner.svg +29 -0
  8. package/docs/configuration.md +49 -0
  9. package/docs/development.md +53 -0
  10. package/docs/faq.md +47 -0
  11. package/docs/getting-started.md +51 -0
  12. package/docs/manual.html +18 -11
  13. package/docs/release-0.23.0.md +3 -5
  14. package/docs/release-0.23.1.md +58 -0
  15. package/docs/release-0.23.2.md +21 -0
  16. package/docs/resource-install.md +55 -11
  17. package/docs/roadmap.md +33 -0
  18. package/docs/testing/0.23.1//346/265/213/350/257/225/346/212/245/345/221/212.md +34 -0
  19. package/docs/testing/0.23.1//350/207/252/345/212/250/345/214/226/346/265/213/350/257/225/346/230/216/347/273/206.md +41 -0
  20. 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 -0
  21. 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 -0
  22. package/docs/testing/0.23.2//346/265/213/350/257/225/346/212/245/345/221/212.md +65 -0
  23. package/docs/testing/0.23.2//350/207/252/345/212/250/345/214/226/346/265/213/350/257/225/346/230/216/347/273/206.md +43 -0
  24. package/docs/workflow-regression.md +36 -0
  25. package/hooks/commit-msg +4 -4
  26. package/lib/client.js +7 -6
  27. package/lib/client.js.map +2 -2
  28. package/lib/controller.js +5 -15
  29. package/lib/controller.js.map +1 -1
  30. package/lib/custom-flow.d.ts +212 -0
  31. package/lib/custom-flow.js +50 -0
  32. package/lib/custom-flow.js.map +1 -0
  33. package/lib/dev-task.js +202 -40
  34. package/lib/dev-task.js.map +1 -1
  35. package/lib/engine.d.ts +17 -1
  36. package/lib/engine.js +5 -0
  37. package/lib/engine.js.map +1 -1
  38. package/lib/shipped-skills.js +2 -1
  39. package/lib/shipped-skills.js.map +1 -1
  40. package/lib/skill-audit.d.ts +16 -0
  41. package/lib/skill-audit.js +60 -0
  42. package/lib/skill-audit.js.map +1 -0
  43. package/lib/workflows.js +4 -4
  44. package/lib/workflows.js.map +1 -1
  45. package/package.json +4 -3
  46. package/rules/commit-conventions.md +2 -2
  47. package/skills/code-commit/SKILL.md +13 -13
  48. package/skills/code-implement/SKILL.md +24 -23
  49. package/skills/code-review/SKILL.md +13 -13
  50. package/skills/code-verify/SKILL.md +19 -12
  51. package/skills/eng-delivery/SKILL.md +38 -34
  52. package/skills/requirement-analysis/SKILL.md +16 -16
  53. package/skills/solution-design/SKILL.md +19 -16
@@ -0,0 +1,290 @@
1
+ import test from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'
4
+ import { tmpdir } from 'node:os'
5
+ import { join, resolve, sep } from 'node:path'
6
+ import { registerDevTask } from './lib/dev-task.js'
7
+ import { newTask } from './lib/engine.js'
8
+ import { resolveFlow } from './lib/workflows.js'
9
+ import Controller from './lib/controller.js'
10
+ import { registerShippedSkills } from './lib/shipped-skills.js'
11
+
12
+ function fixture(stage = '开发', extra = {}, services = {}) {
13
+ const cwd = resolve('test-project')
14
+ const config = resolveFlow('standard', { stage_bindings: { 完成: { skills: ['software-testing'] } } }).config
15
+ const state = newTask({ id: 'LIVE-1', title: 'EAM regression', branch: 'test', work_size: 'standard', risk_level: 'standard', flow: { flow: 'standard', version: 2, config }, root: cwd })
16
+ Object.assign(state, { stage, execution_version: 1, files: ['app.js'], requirement_confirmed: true, solution_confirmed: true, ...extra })
17
+ const records = new Map([[join(cwd, '.dsh/task-LIVE-1.json'), JSON.stringify(state)], [join(cwd, 'app.js'), 'source']])
18
+ const events = [], runs = []
19
+ const session = { id: 'test-session', header: { cwd }, snapshotEvents: () => events }
20
+ const policy = { mode: 'workspace-write', workspaceRoot: cwd, sessionId: session.id }
21
+ const abort = new AbortController()
22
+ let execute
23
+ const key = (path, options) => join(options?.cwd ?? cwd, path)
24
+ const fs = {
25
+ resolve: async (path, options) => ({ targetKey: key(path, options) }),
26
+ readText: async target => records.get(target.targetKey),
27
+ listDir: async () => [...records.keys()].filter(path => /task-.+\.json$/.test(path)).map(path => ({ name: path.split(/[\\/]/).at(-1) })),
28
+ lstat: async (path, options) => records.has(key(path, options)) ? { version: 'v' } : undefined,
29
+ writeText: async (target, content) => { records.set(target.targetKey, content) },
30
+ }
31
+ const ctx = { fs, tools: { register(tool) { execute = tool.execute; return () => {} } }, get(name) {
32
+ if (Object.hasOwn(services, name)) return services[name]
33
+ if (name === 'sandboxPolicy') return { resolve(request) { assert.equal(request.session, session); return policy } }
34
+ if (name === 'shell') return { resolve(request) { runs.push(request); return request }, async run(request) {
35
+ const fail = request.command === 'fail'
36
+ return { exitCode: fail ? 1 : 0, timedOut: false, aborted: false, sandbox: { mode: 'workspace-write', denied: false }, stdout: { text: request.command.startsWith('git -c core.quotepath=false log') ? 'abcdef1234567890\n【LIVE-1】【TASK】done\n\napp.js\n' : 'checks passed' }, stderr: { text: '' } }
37
+ } }
38
+ return undefined
39
+ } }
40
+ registerDevTask(ctx)
41
+ const exec = { agent: { session }, signal: abort.signal }
42
+ function load(name, success = true) {
43
+ const callId = `call-${events.length}`
44
+ events.push({ type: 'tool/call', data: { name: 'skill', callId, arguments: JSON.stringify({ name }) } })
45
+ events.push({ type: 'tool/result', data: { message: { content: [{ type: 'tool-result', toolCallId: callId, isError: !success }] } } })
46
+ }
47
+ return { call: args => execute(JSON.parse(JSON.stringify({ task_id: 'LIVE-1', ...args })), exec), load, runs, policy, exec, records, cwd,
48
+ state: () => JSON.parse(records.get(join(cwd, '.dsh/task-LIVE-1.json'))) }
49
+ }
50
+
51
+ test('审查通过后更新状态并激活下一项,保留已完成项的审计', async () => {
52
+ const f = fixture()
53
+ await f.call({ operation: 'items', items: [{ id: 'A', title: 'first', status: 'doing' }, { id: 'B', title: 'second' }] })
54
+ await f.call({ operation: 'dispatch', item_id: 'A', description: 'implemented by worker' })
55
+ await f.call({ operation: 'review_item', item_id: 'A', spec_outcome: 'pass', quality_outcome: 'pass' })
56
+ await f.call({ operation: 'items', items: [{ id: 'A', title: 'first', status: 'done' }, { id: 'B', title: 'second', status: 'doing' }] })
57
+ assert.equal(f.state().items[0].review.spec.outcome, 'pass')
58
+ assert.equal(f.state().items[0].dispatch.description, 'implemented by worker')
59
+ await f.call({ operation: 'items', items: [{ id: 'A', title: 'changed', status: 'doing' }, { id: 'B', title: 'second', status: 'todo' }] })
60
+ assert.equal(f.state().items[0].review, undefined)
61
+ await assert.rejects(f.call({ operation: 'items', items: [{ id: 'A', title: 'x' }, { id: 'A', title: 'y' }] }), /unique/)
62
+ })
63
+
64
+ test('追加修复项可按 id 保留旧标题,拒绝已完成项改名后静默丢审核', async () => {
65
+ const f = fixture()
66
+ await f.call({ operation: 'items', items: [{ id: 'A', title: 'reviewed models', status: 'doing' }] })
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' }, { id: 'B', title: 'fix race', status: 'doing' }] })
69
+ assert.equal(f.state().items[0].title, 'reviewed models')
70
+ assert.equal(f.state().items[0].review.quality.outcome, 'pass')
71
+ await assert.rejects(f.call({ operation: 'items', items: [{ id: 'A', title: 'rewritten summary', status: 'done' }, { id: 'B', status: 'doing' }] }), /omit title|reopen/)
72
+ assert.equal(f.state().items[0].review.quality.outcome, 'pass')
73
+ assert.equal(f.state().items.length, 2)
74
+ await assert.rejects(f.call({ operation: 'items', items: [{ id: 'NEW' }] }), /title/)
75
+ })
76
+
77
+ test('派发立即反映进行中,重派不复用旧审核且保持单一进行项', async () => {
78
+ const f = fixture()
79
+ await f.call({ operation: 'items', items: [{ id: 'A', title: 'models' }, { id: 'B', title: 'api' }] })
80
+ await f.call({ operation: 'dispatch', item_id: 'A', description: 'implement models' })
81
+ assert.equal(f.state().items[0].status, 'doing')
82
+ await assert.rejects(f.call({ operation: 'dispatch', item_id: 'B' }), /current doing item/)
83
+ await f.call({ operation: 'review_item', item_id: 'A', spec_outcome: 'pass', quality_outcome: 'pass' })
84
+ await f.call({ operation: 'dispatch', item_id: 'A', description: 'rework models' })
85
+ assert.equal(f.state().items[0].review, undefined)
86
+ })
87
+
88
+ test('无任务 id 的 status 发现当前工作区任务并支持分支过滤', async () => {
89
+ const f = fixture()
90
+ assert.equal(JSON.parse(await f.call({ operation: 'status', task_id: undefined })).tasks[0].id, 'LIVE-1')
91
+ assert.equal(JSON.parse(await f.call({ operation: 'status', task_id: undefined, branch: 'other' })).tasks.length, 0)
92
+ })
93
+
94
+ test('verify 升级审批不可用时不得先执行命令', async () => {
95
+ const f = fixture('交付')
96
+ const before = JSON.stringify(f.state())
97
+ await assert.rejects(f.call({ operation: 'verify', command: 'npm test', sandbox_permissions: 'danger-full-access', justification: 'Retry the denied test command' }), /approval/)
98
+ assert.equal(f.runs.length, 0, 'approval must precede shell execution')
99
+ assert.equal(JSON.stringify(f.state()), before)
100
+ })
101
+
102
+ test('verify 拒绝、取消及无效升级参数均不执行命令或改写台账', async () => {
103
+ for (const outcome of ['rejected', 'cancelled', 'unavailable']) {
104
+ const f = fixture('交付', {}, { approval: { request: async () => outcome } })
105
+ const before = JSON.stringify(f.state())
106
+ await assert.rejects(f.call({ operation: 'verify', command: 'npm test', sandbox_permissions: 'danger-full-access', justification: 'Retry denied command' }), /not approved/)
107
+ assert.equal(f.runs.length, 0)
108
+ assert.equal(JSON.stringify(f.state()), before)
109
+ }
110
+ for (const args of [{ sandbox_permissions: 'danger-full-access' }, { justification: 'orphan justification' }]) {
111
+ const f = fixture('交付')
112
+ await assert.rejects(f.call({ operation: 'verify', command: 'npm test', ...args }), /invalid escalation/)
113
+ assert.equal(f.runs.length, 0)
114
+ }
115
+ })
116
+
117
+ test('验证命令审批先于执行,只授权本次命令且回执写入不重复审批', async () => {
118
+ for (const operation of ['verify', 'skill_result']) {
119
+ const approvals = []
120
+ const f = fixture('完成', {}, { approval: { request: async request => {
121
+ assert.equal(f.runs.length, 0)
122
+ approvals.push(request)
123
+ return 'allowed-once'
124
+ } } })
125
+ f.load('software-testing')
126
+ await f.call({ operation, command: 'npm test', skill_name: 'software-testing', evidence: ['real checks'], sandbox_permissions: 'danger-full-access', justification: 'Retry denied test command' })
127
+ assert.equal(approvals.length, 1)
128
+ assert.match(approvals[0].reason, /npm test/)
129
+ assert.equal(f.runs[0].sandboxPolicy.mode, 'danger-full-access')
130
+ assert.equal(f.runs[0].sandboxPolicy.workspaceRoot, f.cwd)
131
+ assert.equal(f.runs[0].sandboxPolicy.sessionId, f.policy.sessionId)
132
+ assert.equal(f.runs[0].signal, f.exec.signal)
133
+ assert.equal(f.policy.mode, 'workspace-write')
134
+ await f.call({ operation: 'verify', command: 'npm test' })
135
+ assert.equal(f.runs[1].sandboxPolicy, f.policy)
136
+ assert.equal(approvals.length, 1)
137
+ }
138
+ })
139
+
140
+ test('skill_result 审批拒绝时不执行命令、不写入技能回执', async () => {
141
+ const f = fixture('完成', {}, { approval: { request: async () => 'rejected' } })
142
+ f.load('software-testing')
143
+ const before = JSON.stringify(f.state())
144
+ await assert.rejects(f.call({ operation: 'skill_result', skill_name: 'software-testing', command: 'npm test', evidence: ['test'], sandbox_permissions: 'danger-full-access', justification: 'Retry denied tests' }), /not approved/)
145
+ assert.equal(f.runs.length, 0)
146
+ assert.equal(JSON.stringify(f.state()), before)
147
+ })
148
+
149
+ test('状态明确区分内置节点门禁与附加技能命令回执,避免重复验证', async () => {
150
+ const f = fixture('代码审核')
151
+ const status = JSON.parse(await f.call({ operation: 'status' }))
152
+ assert.deepEqual(status.skill_obligations.find(row => row.stage === '代码审核').command_receipts_required, [])
153
+ assert.deepEqual(status.skill_obligations.find(row => row.stage === '完成').command_receipts_required, ['software-testing'])
154
+ })
155
+
156
+ test('status 披露当前阶段记录字段和缺项,字段误用不写入且可按提示恢复', async () => {
157
+ const f = fixture('需求评审')
158
+ const before = JSON.stringify(f.state())
159
+ const status = JSON.parse(await f.call({ operation: 'status' }))
160
+ assert.deepEqual(status.artifact_requirements, [{ id: 'requirement', name: '需求说明', fields: ['scope', 'acceptance_criteria'], missing_fields: ['scope', 'acceptance_criteria'] }])
161
+ assert.equal(JSON.stringify(f.state()), before)
162
+ await assert.rejects(f.call({ operation: 'record', artifact: 'requirement', fields: { scope: 'goal', 待确认取舍: 'question' } }), /status.artifact_requirements/)
163
+ assert.equal(JSON.stringify(f.state()), before)
164
+ await f.call({ operation: 'record', artifact: 'requirement', fields: { scope: '目标及待确认取舍', acceptance_criteria: ' ' } })
165
+ assert.deepEqual(JSON.parse(await f.call({ operation: 'status' })).artifact_requirements[0].missing_fields, ['acceptance_criteria'])
166
+ await f.call({ operation: 'record', artifact: 'requirement', fields: { acceptance_criteria: '可执行验收' } })
167
+ assert.deepEqual(JSON.parse(await f.call({ operation: 'status' })).artifact_requirements[0].missing_fields, [])
168
+ })
169
+
170
+ test('记录字段来自冻结流程,精简流程不硬编码标准字段,无记录阶段返回空列表', async () => {
171
+ const config = resolveFlow('agile').config
172
+ const f = fixture('需求', { flow: { flow: 'agile', version: 1, config } })
173
+ assert.deepEqual(JSON.parse(await f.call({ operation: 'status' })).artifact_requirements, [{ id: 'requirement', name: '需求说明', fields: ['scope'], missing_fields: ['scope'] }])
174
+ await f.call({ operation: 'record', artifact: 'requirement', fields: { scope: '目标、验收与疑问' } })
175
+ assert.deepEqual(JSON.parse(await f.call({ operation: 'status' })).artifact_requirements[0].missing_fields, [])
176
+ assert.deepEqual(JSON.parse(await fixture('开发').call({ operation: 'status' })).artifact_requirements, [])
177
+ assert.deepEqual(JSON.parse(await fixture('设计').call({ operation: 'status' })).artifact_requirements[0].fields, ['approach', 'risks', 'impact'])
178
+ })
179
+
180
+ test('可选内置技能回执不会因范围变更升级成额外流转门禁', async () => {
181
+ const f = fixture('需求评审', { artifacts: { requirement: { scope: 'device picker', acceptance_criteria: 'contract and selection' } } })
182
+ f.load('requirement-analysis')
183
+ await f.call({ operation: 'skill_result', skill_name: 'requirement-analysis', command: 'check', evidence: ['optional check'] })
184
+ await f.call({ operation: 'scope', files: ['app.js', 'new-vo.java'] })
185
+ await f.call({ operation: 'advance', target_stage: '设计' })
186
+ assert.equal(f.state().stage, '设计')
187
+ })
188
+
189
+ test('验证使用会话策略与取消信号,并明确返回失败及沙箱信息', async () => {
190
+ const f = fixture('交付')
191
+ const result = JSON.parse(await f.call({ operation: 'verify', command: 'fail' }))
192
+ assert.equal(result.ok, false)
193
+ assert.equal(f.runs[0].sandboxPolicy, f.policy)
194
+ assert.equal(f.runs[0].signal, f.exec.signal)
195
+ assert.equal(result.verification.receipt.sandbox.mode, 'workspace-write')
196
+ })
197
+
198
+ test('只声明已测试、或加载失败,均不能冒充执行绑定技能', async () => {
199
+ const f = fixture('代码审核')
200
+ f.load('code-review'); f.load('code-commit'); f.load('software-testing', false)
201
+ await assert.rejects(f.call({ operation: 'skill_result', target_stage: '完成', skill_name: 'software-testing', command: 'check', evidence: ['tested'] }), /load skill/)
202
+ await assert.rejects(f.call({ operation: 'advance', target_stage: '完成' }), /software-testing/)
203
+ f.load('software-testing')
204
+ await assert.rejects(f.call({ operation: 'skill_result', target_stage: '完成', skill_name: 'software-testing', evidence: ['claimed pass'] }), /real validation command/)
205
+ })
206
+
207
+ test('绑定技能失败阻止完成;成功后仍须提交真实 Git 回执', async () => {
208
+ const f = fixture('代码审核', { review: { outcome: 'pass' }, artifacts: { review: { conclusion: 'pass', issues: 'none' } }, verification: { passed: true, evidence: ['checks'] } })
209
+ f.load('code-review'); f.load('code-commit'); f.load('software-testing')
210
+ await f.call({ operation: 'verify', command: 'check', evidence: ['real checks'] })
211
+ await f.call({ operation: 'skill_result', target_stage: '完成', skill_name: 'software-testing', command: 'fail', evidence: ['scenario failed'] })
212
+ await assert.rejects(f.call({ operation: 'advance', target_stage: '完成' }), /skill_result/)
213
+ await f.call({ operation: 'skill_result', target_stage: '完成', skill_name: 'software-testing', command: 'check', evidence: ['scenario passed'] })
214
+ await assert.rejects(f.call({ operation: 'advance', target_stage: '完成' }), /commit required/)
215
+ await f.call({ operation: 'commit', files: ['app.js'], message: '【LIVE-1】【TASK】done' })
216
+ await assert.rejects(f.call({ operation: 'commit', files: ['app.js'], message: '【LIVE-1】【TASK】done', hash: 'bbbbbbb' }), /does not match/)
217
+ await f.call({ operation: 'commit', files: ['app.js'], message: '【LIVE-1】【TASK】done', hash: 'abcdef1' })
218
+ await f.call({ operation: 'advance', target_stage: '完成' })
219
+ assert.equal(f.state().stage, '完成')
220
+ assert.equal(f.state().skill_results.完成['software-testing'].load_call_id.startsWith('call-'), true)
221
+ })
222
+
223
+ test('验证后修改代码会使旧回执失效,必须重新验证', async () => {
224
+ const f = fixture('交付')
225
+ f.load('code-verify')
226
+ await f.call({ operation: 'verify', command: 'check' })
227
+ f.records.set(join(f.cwd, 'app.js'), 'modified source')
228
+ await assert.rejects(f.call({ operation: 'advance', target_stage: '代码审核' }), /stale/)
229
+ await f.call({ operation: 'verify', command: 'check' })
230
+ await f.call({ operation: 'advance', target_stage: '代码审核' })
231
+ assert.equal(f.state().stage, '代码审核')
232
+ })
233
+
234
+ test('status 提交提示与真实门禁一致,分别披露过期验证和附加技能回执且不改台账', async () => {
235
+ const f = fixture('代码审核', { review: { outcome: 'pass' }, artifacts: { review: { conclusion: 'pass', issues: 'none' } } })
236
+ await f.call({ operation: 'verify', command: 'check' })
237
+ assert.equal(JSON.parse(await f.call({ operation: 'status' })).commit.allowed, false)
238
+ f.load('code-review'); f.load('code-commit'); f.load('software-testing')
239
+ await f.call({ operation: 'skill_result', target_stage: '完成', skill_name: 'software-testing', command: 'check', evidence: ['tested'] })
240
+ assert.equal(JSON.parse(await f.call({ operation: 'status' })).commit.allowed, true)
241
+ f.records.set(join(f.cwd, 'app.js'), 'changed after testing')
242
+ const before = JSON.stringify(f.state())
243
+ const stale = JSON.parse(await f.call({ operation: 'status' }))
244
+ assert.equal(stale.commit.allowed, false)
245
+ assert.match(stale.commit.reason, /stale/)
246
+ assert.equal(stale.evidence_blockers.length, 2)
247
+ assert.equal(JSON.stringify(f.state()), before)
248
+ await assert.rejects(f.call({ operation: 'commit', files: ['app.js'], message: '【LIVE-1】【TASK】done' }), /stale/)
249
+ await f.call({ operation: 'verify', command: 'check' })
250
+ const partial = JSON.parse(await f.call({ operation: 'status' }))
251
+ assert.equal(partial.commit.allowed, false)
252
+ assert.equal(partial.evidence_blockers.length, 1)
253
+ assert.match(partial.evidence_blockers[0], /software-testing/)
254
+ await f.call({ operation: 'skill_result', target_stage: '完成', skill_name: 'software-testing', command: 'check', evidence: ['retested'] })
255
+ const current = JSON.parse(await f.call({ operation: 'status' }))
256
+ assert.equal(current.commit.allowed, true)
257
+ assert.deepEqual(current.evidence_blockers, [])
258
+ })
259
+
260
+ test('同名技能与规则在项目和个人目录各自保留管理入口', async t => {
261
+ const root = mkdtempSync(join(tmpdir(), 'engine-catalog-'))
262
+ const original = process.env.DSH_HOME
263
+ t.after(() => { if (original === undefined) delete process.env.DSH_HOME; else process.env.DSH_HOME = original; assert.ok(resolve(root).startsWith(resolve(tmpdir()) + sep)); rmSync(root, { recursive: true, force: true }) })
264
+ process.env.DSH_HOME = join(root, 'user')
265
+ const project = join(root, 'project')
266
+ for (const base of [join(project, '.dsh'), process.env.DSH_HOME]) {
267
+ mkdirSync(join(base, 'skills/software-testing'), { recursive: true })
268
+ mkdirSync(join(base, 'rules'), { recursive: true })
269
+ writeFileSync(join(base, 'skills/software-testing/SKILL.md'), '\uFEFF---\r\nname: software-testing\r\ndescription: tests\r\n---\r\nRun tests.\r\n')
270
+ writeFileSync(join(base, 'rules/test-rule.md'), 'Test rule\n')
271
+ }
272
+ const receiver = { authorizedPath: async path => path }
273
+ const skills = await Controller.prototype.listSkills.call(receiver, project)
274
+ const rules = await Controller.prototype.listRules.call(receiver, project)
275
+ assert.deepEqual(skills.skills.filter(x => x.name === 'software-testing').map(x => x.source), ['project', 'user'])
276
+ assert.deepEqual(rules.rules.filter(x => x.name === 'test-rule').map(x => x.source), ['project', 'user'])
277
+ const read = await Controller.prototype.readSkill.call(receiver, { path: project, level: 'project', name: 'software-testing' })
278
+ assert.equal(read.ok, true)
279
+ assert.match(read.content, /Run tests/)
280
+ })
281
+
282
+ test('安装包的七个内置技能实际注册且可在资源目录发现', async () => {
283
+ const registered = []
284
+ registerShippedSkills({ get: name => name === 'skills' ? { register: skill => { registered.push(skill); return () => {} } } : undefined })
285
+ const expected = ['code-commit', 'code-implement', 'code-review', 'code-verify', 'eng-delivery', 'requirement-analysis', 'solution-design']
286
+ assert.deepEqual(registered.map(skill => skill.name).sort(), expected)
287
+ assert.ok(registered.every(skill => skill.content.trim().length > 0))
288
+ const catalog = await Controller.prototype.listSkills.call({ authorizedPath: async path => path }, '')
289
+ assert.deepEqual(catalog.skills.filter(skill => skill.source === 'bundled').map(skill => skill.name).sort(), expected)
290
+ })