@godv61/dsh-task-engine 0.27.0 → 0.28.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 +9 -4
- package/.assessment-batch1.mjs +14 -11
- package/.codex-project-test.mjs +80 -0
- package/.enforce-test.mjs +11 -12
- package/.evidence-test.mjs +8 -8
- package/.filter-test.mjs +3 -5
- package/.freeze-test.mjs +40 -31
- package/.p0-test.mjs +16 -19
- package/.preset-test.mjs +11 -1
- package/.revision-test.mjs +16 -16
- package/.roundtrip-test.mjs +247 -259
- package/.workflow-test.mjs +117 -119
- package/README.md +12 -10
- package/cordis.patch.yml +158 -9
- package/defaults/eng.json +1 -97
- package/docs/CHANGELOG.md +39 -26
- package/docs/configuration.md +21 -22
- package/docs/development.md +1 -1
- package/docs/faq.md +5 -5
- package/docs/manual.html +41 -59
- package/docs/resource-install.md +2 -0
- package/docs/roadmap.md +2 -2
- package/hooks/commit-msg +4 -33
- package/lib/client.js +94 -108
- package/lib/client.js.map +3 -3
- package/lib/controller.d.ts +4 -3
- package/lib/controller.js +12 -6
- package/lib/controller.js.map +1 -1
- package/lib/dev-task.js +33 -27
- package/lib/dev-task.js.map +1 -1
- package/lib/engine.d.ts +2 -3
- package/lib/engine.js +2 -2
- package/lib/engine.js.map +1 -1
- package/lib/shipped-skills.d.ts +2 -4
- package/lib/shipped-skills.js +2 -4
- package/lib/shipped-skills.js.map +1 -1
- package/lib/skill-audit.d.ts +4 -4
- package/lib/skill-audit.js +19 -14
- package/lib/skill-audit.js.map +1 -1
- package/lib/workflows.d.ts +3 -4
- package/lib/workflows.js +2 -41
- package/lib/workflows.js.map +1 -1
- package/package.json +13 -15
- package/preset/agent.cordis.yml +3 -3
- package/preset/persona.md +2 -2
- package/scripts/verify-package.mjs +2 -0
- package/skills/eng-delivery/SKILL.md +17 -17
- package/rules/coding-conventions.md +0 -7
- package/rules/commit-conventions.md +0 -6
- package/rules/security-redlines.md +0 -6
- package/skills/code-commit/SKILL.md +0 -13
- package/skills/code-implement/SKILL.md +0 -24
- package/skills/code-review/SKILL.md +0 -13
- package/skills/code-verify/SKILL.md +0 -20
- package/skills/requirement-analysis/SKILL.md +0 -16
- package/skills/solution-design/SKILL.md +0 -19
package/.roundtrip-test.mjs
CHANGED
|
@@ -5,266 +5,254 @@
|
|
|
5
5
|
* commit rule, artifact declarations or a review depth were resolved for the preview
|
|
6
6
|
* and then dropped on save. A user could set them, see them applied, and lose them.
|
|
7
7
|
*/
|
|
8
|
-
import test from 'node:test'
|
|
9
|
-
import assert from 'node:assert/strict'
|
|
10
|
-
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
11
|
-
import { tmpdir } from 'node:os'
|
|
12
|
-
import { dirname, join, resolve, sep } from 'node:path'
|
|
13
|
-
import { resolveFlow, adoptRecommendation, compactProjectConfig, materializeBundledReferences } from './lib/workflows.js'
|
|
14
|
-
import Controller from './lib/controller.js'
|
|
15
|
-
import { assertAdvance, validateWorkflow } from './lib/engine.js'
|
|
16
|
-
import { TYPERT_REMOTE } from './src/client/remote.ts'
|
|
8
|
+
import test from 'node:test'
|
|
9
|
+
import assert from 'node:assert/strict'
|
|
10
|
+
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
11
|
+
import { tmpdir } from 'node:os'
|
|
12
|
+
import { dirname, join, resolve, sep } from 'node:path'
|
|
13
|
+
import { resolveFlow, adoptRecommendation, compactProjectConfig, materializeBundledReferences } from './lib/workflows.js'
|
|
14
|
+
import Controller from './lib/controller.js'
|
|
15
|
+
import { assertAdvance, validateWorkflow } from './lib/engine.js'
|
|
16
|
+
import { TYPERT_REMOTE } from './src/client/remote.ts'
|
|
17
17
|
|
|
18
18
|
/** The fields a project config can carry, so a round-trip is checked on all of them. */
|
|
19
|
-
const FIELDS = ['flow', 'stage_bindings', 'commit', 'artifacts', 'review_depth', 'commit_required']
|
|
20
|
-
|
|
21
|
-
test('browser remote codec retains source-qualified bindings and all saved fields', () => {
|
|
22
|
-
const write = TYPERT_REMOTE.descriptors.find(descriptor => descriptor.method === 'write')
|
|
23
|
-
const read = TYPERT_REMOTE.descriptors.find(descriptor => descriptor.method === 'read')
|
|
24
|
-
const adopted = compactProjectConfig(adoptRecommendation('minimal'))
|
|
25
|
-
const request = { path: 'project', ...adopted, review_depth: 'single', commit_required: false,
|
|
26
|
-
materialize_bundled: 'project' }
|
|
27
|
-
assert.deepEqual(write.parameters[0].codec.schema.parse(request), request)
|
|
28
|
-
const resolved = resolveFlow('minimal', request).config
|
|
29
|
-
const view = { ok: true, source: 'project', flow: 'minimal', config: resolved, problems: [] }
|
|
30
|
-
assert.deepEqual(read.result.schema.parse(view), view)
|
|
31
|
-
const catalog = TYPERT_REMOTE.descriptors.find(descriptor => descriptor.method === 'listSkills')
|
|
32
|
-
const skill = { name: 'mine', description: 'test', source: 'project',
|
|
33
|
-
ref: { source: 'project', name: 'mine' }, sourceLabel: '项目' }
|
|
34
|
-
assert.deepEqual(catalog.result.schema.parse({ skills: [skill] }), { skills: [skill] })
|
|
35
|
-
const saveUserProfile = TYPERT_REMOTE.descriptors.find(descriptor => descriptor.method === 'writeUserSkillProfile')
|
|
36
|
-
const profileRequest = { name: 'mine', profile: { rules: [{ source: 'user', name: 'my-rule' }], evidence: 'manual' } }
|
|
37
|
-
assert.deepEqual(saveUserProfile.parameters[0].codec.schema.parse(profileRequest), profileRequest)
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
test('one user skill profile is shared across workspaces and cannot depend on a project rule', async () => {
|
|
41
|
-
const root = mkdtempSync(join(tmpdir(), 'dsh-user-profile-'))
|
|
42
|
-
assert.ok(resolve(root).startsWith(resolve(tmpdir()) + sep))
|
|
43
|
-
const priorHome = process.env.DSH_HOME
|
|
44
|
-
const userHome = join(root, 'user-home')
|
|
45
|
-
process.env.DSH_HOME = userHome
|
|
46
|
-
const skill = { source: 'user', name: 'my-skill' }
|
|
47
|
-
const config = { flow: 'minimal', stage_bindings: {
|
|
48
|
-
'开发': { skill_refs: [skill] }, '交付': { skill_refs: [skill] },
|
|
49
|
-
} }
|
|
50
|
-
const fs = {
|
|
51
|
-
resolve: async (path, { cwd }) => join(cwd, path),
|
|
52
|
-
readText: async path => { try { return readFileSync(path, 'utf8') } catch { return undefined } },
|
|
53
|
-
writeText: async (path, body) => { mkdirSync(dirname(path), { recursive: true }); writeFileSync(path, body) },
|
|
54
|
-
}
|
|
55
|
-
const receiver = { authorizedPath: async path => path, fs: () => fs }
|
|
56
|
-
try {
|
|
57
|
-
const skillDir = join(userHome, 'skills', skill.name)
|
|
58
|
-
mkdirSync(skillDir, { recursive: true })
|
|
59
|
-
writeFileSync(join(skillDir, 'SKILL.md'), '---\nname: my-skill\ndescription: test\n---\nbody\n')
|
|
60
|
-
const profile = { rules: [{ source: 'user', name: 'my-rule' }], evidence: 'manual' }
|
|
61
|
-
const saved = await Controller.prototype.writeUserSkillProfile.call(receiver, { name: skill.name, profile })
|
|
62
|
-
assert.equal(saved.ok, true)
|
|
63
|
-
for (const projectName of ['one', 'two']) {
|
|
64
|
-
const project = join(root, projectName)
|
|
65
|
-
mkdirSync(join(project, '.dsh'), { recursive: true })
|
|
66
|
-
writeFileSync(join(project, '.dsh', 'eng.json'), JSON.stringify(config))
|
|
67
|
-
const loaded = await Controller.prototype.read.call(receiver, project)
|
|
68
|
-
assert.equal(loaded.ok, true)
|
|
69
|
-
assert.deepEqual(loaded.config.stage_bindings['开发'].skills[0].rules, profile.rules)
|
|
70
|
-
}
|
|
71
|
-
const updatedProfile = { rules: [{ source: 'user', name: 'other-rule' }], evidence: 'none' }
|
|
72
|
-
const updated = await Controller.prototype.writeUserSkillProfile.call(receiver,
|
|
73
|
-
{ name: skill.name, profile: updatedProfile })
|
|
74
|
-
assert.equal(updated.ok, true)
|
|
75
|
-
for (const projectName of ['one', 'two']) {
|
|
76
|
-
const loaded = await Controller.prototype.read.call(receiver, join(root, projectName))
|
|
77
|
-
assert.equal(loaded.ok, true)
|
|
78
|
-
assert.deepEqual(loaded.config.stage_bindings['开发'].skills[0].rules, updatedProfile.rules)
|
|
79
|
-
}
|
|
80
|
-
const bad = await Controller.prototype.writeUserSkillProfile.call(receiver, { name: skill.name,
|
|
81
|
-
profile: { rules: [{ source: 'project', name: 'local-rule' }] } })
|
|
82
|
-
assert.equal(bad.ok, false)
|
|
83
|
-
assert.deepEqual(JSON.parse(readFileSync(join(skillDir, 'profile.json'), 'utf8')), updatedProfile)
|
|
84
|
-
} finally {
|
|
85
|
-
if (priorHome === undefined) delete process.env.DSH_HOME
|
|
86
|
-
else process.env.DSH_HOME = priorHome
|
|
87
|
-
rmSync(root, { recursive: true, force: true })
|
|
88
|
-
}
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
test('recommendation adoption
|
|
92
|
-
const { config, copies } = materializeBundledReferences(adoptRecommendation('standard'), 'project')
|
|
93
|
-
assert.
|
|
94
|
-
assert.
|
|
95
|
-
|
|
96
|
-
assert.
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const
|
|
111
|
-
assert.
|
|
112
|
-
|
|
113
|
-
})
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
assert.equal(
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
assert.
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
}
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
assert.deepEqual(
|
|
210
|
-
assert.deepEqual(
|
|
211
|
-
})
|
|
212
|
-
|
|
213
|
-
test('
|
|
214
|
-
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
})
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
})
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
})
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
skill_profiles: { 'project:my-skill': { rules: 'not-an-array' } },
|
|
257
|
-
})
|
|
258
|
-
assert.ok(validateWorkflow(malformed.config).some(problem => problem.includes('rules array')))
|
|
259
|
-
})
|
|
260
|
-
|
|
261
|
-
test('commit_required false also removes the checkpoint exit requirement', () => {
|
|
262
|
-
const adopted = adoptRecommendation('standard')
|
|
263
|
-
const config = resolveFlow('standard', { ...adopted, commit_required: false }).config
|
|
264
|
-
const state = { stage: '代码审核', execution_version: 1, review: { outcome: 'pass' },
|
|
265
|
-
artifacts: { review: { conclusion: 'pass', issues: 'none' } }, commits: [] }
|
|
266
|
-
assert.equal(assertAdvance(state, '完成', config).ok, true)
|
|
267
|
-
})
|
|
19
|
+
const FIELDS = ['flow', 'stage_bindings', 'commit', 'artifacts', 'review_depth', 'commit_required']
|
|
20
|
+
|
|
21
|
+
test('browser remote codec retains source-qualified bindings and all saved fields', () => {
|
|
22
|
+
const write = TYPERT_REMOTE.descriptors.find(descriptor => descriptor.method === 'write')
|
|
23
|
+
const read = TYPERT_REMOTE.descriptors.find(descriptor => descriptor.method === 'read')
|
|
24
|
+
const adopted = compactProjectConfig(adoptRecommendation('minimal'))
|
|
25
|
+
const request = { path: 'project', ...adopted, review_depth: 'single', commit_required: false,
|
|
26
|
+
materialize_bundled: 'project' }
|
|
27
|
+
assert.deepEqual(write.parameters[0].codec.schema.parse(request), request)
|
|
28
|
+
const resolved = resolveFlow('minimal', request).config
|
|
29
|
+
const view = { ok: true, source: 'project', flow: 'minimal', config: resolved, problems: [] }
|
|
30
|
+
assert.deepEqual(read.result.schema.parse(view), view)
|
|
31
|
+
const catalog = TYPERT_REMOTE.descriptors.find(descriptor => descriptor.method === 'listSkills')
|
|
32
|
+
const skill = { name: 'mine', description: 'test', source: 'project',
|
|
33
|
+
ref: { source: 'project', name: 'mine' }, sourceLabel: '项目' }
|
|
34
|
+
assert.deepEqual(catalog.result.schema.parse({ skills: [skill] }), { skills: [skill] })
|
|
35
|
+
const saveUserProfile = TYPERT_REMOTE.descriptors.find(descriptor => descriptor.method === 'writeUserSkillProfile')
|
|
36
|
+
const profileRequest = { name: 'mine', profile: { rules: [{ source: 'user', name: 'my-rule' }], evidence: 'manual' } }
|
|
37
|
+
assert.deepEqual(saveUserProfile.parameters[0].codec.schema.parse(profileRequest), profileRequest)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test('one user skill profile is shared across workspaces and cannot depend on a project rule', async () => {
|
|
41
|
+
const root = mkdtempSync(join(tmpdir(), 'dsh-user-profile-'))
|
|
42
|
+
assert.ok(resolve(root).startsWith(resolve(tmpdir()) + sep))
|
|
43
|
+
const priorHome = process.env.DSH_HOME
|
|
44
|
+
const userHome = join(root, 'user-home')
|
|
45
|
+
process.env.DSH_HOME = userHome
|
|
46
|
+
const skill = { source: 'user', name: 'my-skill' }
|
|
47
|
+
const config = { flow: 'minimal', stage_bindings: {
|
|
48
|
+
'开发': { skill_refs: [skill] }, '交付': { skill_refs: [skill] },
|
|
49
|
+
} }
|
|
50
|
+
const fs = {
|
|
51
|
+
resolve: async (path, { cwd }) => join(cwd, path),
|
|
52
|
+
readText: async path => { try { return readFileSync(path, 'utf8') } catch { return undefined } },
|
|
53
|
+
writeText: async (path, body) => { mkdirSync(dirname(path), { recursive: true }); writeFileSync(path, body) },
|
|
54
|
+
}
|
|
55
|
+
const receiver = { authorizedPath: async path => path, fs: () => fs }
|
|
56
|
+
try {
|
|
57
|
+
const skillDir = join(userHome, 'skills', skill.name)
|
|
58
|
+
mkdirSync(skillDir, { recursive: true })
|
|
59
|
+
writeFileSync(join(skillDir, 'SKILL.md'), '---\nname: my-skill\ndescription: test\n---\nbody\n')
|
|
60
|
+
const profile = { rules: [{ source: 'user', name: 'my-rule' }], evidence: 'manual' }
|
|
61
|
+
const saved = await Controller.prototype.writeUserSkillProfile.call(receiver, { name: skill.name, profile })
|
|
62
|
+
assert.equal(saved.ok, true)
|
|
63
|
+
for (const projectName of ['one', 'two']) {
|
|
64
|
+
const project = join(root, projectName)
|
|
65
|
+
mkdirSync(join(project, '.dsh'), { recursive: true })
|
|
66
|
+
writeFileSync(join(project, '.dsh', 'eng.json'), JSON.stringify(config))
|
|
67
|
+
const loaded = await Controller.prototype.read.call(receiver, project)
|
|
68
|
+
assert.equal(loaded.ok, true)
|
|
69
|
+
assert.deepEqual(loaded.config.stage_bindings['开发'].skills[0].rules, profile.rules)
|
|
70
|
+
}
|
|
71
|
+
const updatedProfile = { rules: [{ source: 'user', name: 'other-rule' }], evidence: 'none' }
|
|
72
|
+
const updated = await Controller.prototype.writeUserSkillProfile.call(receiver,
|
|
73
|
+
{ name: skill.name, profile: updatedProfile })
|
|
74
|
+
assert.equal(updated.ok, true)
|
|
75
|
+
for (const projectName of ['one', 'two']) {
|
|
76
|
+
const loaded = await Controller.prototype.read.call(receiver, join(root, projectName))
|
|
77
|
+
assert.equal(loaded.ok, true)
|
|
78
|
+
assert.deepEqual(loaded.config.stage_bindings['开发'].skills[0].rules, updatedProfile.rules)
|
|
79
|
+
}
|
|
80
|
+
const bad = await Controller.prototype.writeUserSkillProfile.call(receiver, { name: skill.name,
|
|
81
|
+
profile: { rules: [{ source: 'project', name: 'local-rule' }] } })
|
|
82
|
+
assert.equal(bad.ok, false)
|
|
83
|
+
assert.deepEqual(JSON.parse(readFileSync(join(skillDir, 'profile.json'), 'utf8')), updatedProfile)
|
|
84
|
+
} finally {
|
|
85
|
+
if (priorHome === undefined) delete process.env.DSH_HOME
|
|
86
|
+
else process.env.DSH_HOME = priorHome
|
|
87
|
+
rmSync(root, { recursive: true, force: true })
|
|
88
|
+
}
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
test('recommendation adoption contains no bundled resources', () => {
|
|
92
|
+
const { config, copies } = materializeBundledReferences(adoptRecommendation('standard'), 'project')
|
|
93
|
+
assert.deepEqual(copies, [])
|
|
94
|
+
assert.deepEqual(config.stage_bindings, {})
|
|
95
|
+
assert.equal(JSON.stringify(config).includes('bundled:'), false)
|
|
96
|
+
assert.deepEqual(validateWorkflow(resolveFlow('standard', config).config), [])
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
test('project migration leaves a global user skill profile and its bundled rules untouched', () => {
|
|
100
|
+
const userSkill = { source: 'user', name: 'global-check' }
|
|
101
|
+
const bundledRule = { source: 'bundled', name: 'security-redlines' }
|
|
102
|
+
const project = { flow: 'minimal', stage_bindings: { '开发': { skill_refs: [userSkill] } },
|
|
103
|
+
skill_profiles: { 'user:global-check': { rules: [bundledRule] } } }
|
|
104
|
+
const { config, copies } = materializeBundledReferences(project, 'project')
|
|
105
|
+
assert.deepEqual(config.skill_profiles['user:global-check'].rules, [bundledRule])
|
|
106
|
+
assert.deepEqual(copies, [])
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
test('recommendation saves only editable project configuration, without resource copies', async () => {
|
|
110
|
+
const root = mkdtempSync(join(tmpdir(), 'dsh-adopt-'))
|
|
111
|
+
assert.ok(resolve(root).startsWith(resolve(tmpdir()) + sep))
|
|
112
|
+
const fs = {
|
|
113
|
+
resolve: async (path, { cwd }) => join(cwd, path),
|
|
114
|
+
readText: async path => { try { return readFileSync(path, 'utf8') } catch { return undefined } },
|
|
115
|
+
writeText: async (path, text) => { mkdirSync(dirname(path), { recursive: true }); writeFileSync(path, text) },
|
|
116
|
+
}
|
|
117
|
+
const receiver = { authorizedPath: async path => path, fs: () => fs }
|
|
118
|
+
try {
|
|
119
|
+
const recommended = adoptRecommendation('standard')
|
|
120
|
+
const first = await Controller.prototype.write.call(receiver,
|
|
121
|
+
{ path: root, ...recommended, materialize_bundled: 'project' })
|
|
122
|
+
assert.equal(first.ok, true)
|
|
123
|
+
assert.equal(first.adoption.created.length, 0)
|
|
124
|
+
const file = join(root, '.dsh', 'eng.json')
|
|
125
|
+
const saved = JSON.parse(readFileSync(file, 'utf8'))
|
|
126
|
+
assert.equal(JSON.stringify(saved).includes('"bundled"'), false)
|
|
127
|
+
const second = await Controller.prototype.write.call(receiver,
|
|
128
|
+
{ path: root, ...recommended, materialize_bundled: 'project' })
|
|
129
|
+
assert.equal(second.ok, true)
|
|
130
|
+
assert.equal(second.adoption.created.length, 0)
|
|
131
|
+
assert.equal(second.adoption.reused.length, 0)
|
|
132
|
+
const loaded = await Controller.prototype.read.call(receiver, root)
|
|
133
|
+
assert.equal(loaded.ok, true)
|
|
134
|
+
assert.deepEqual(loaded.config.stage_bindings, {})
|
|
135
|
+
} finally {
|
|
136
|
+
rmSync(root, { recursive: true, force: true })
|
|
137
|
+
}
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
test('round-trip: stages store references while skill rules have one owner', () => {
|
|
141
|
+
const adopted = { ...adoptRecommendation('standard'), stage_bindings: {
|
|
142
|
+
'开发': { skill_refs: [{ source: 'project', name: 'my-implementation' }] },
|
|
143
|
+
}, skill_profiles: { 'project:my-implementation': { rules: [{ source: 'project', name: 'my-rule' }] } } }
|
|
144
|
+
const compact = compactProjectConfig(adopted)
|
|
145
|
+
assert.ok(compact.stage_bindings['开发'].skill_refs.length > 0)
|
|
146
|
+
assert.equal(compact.stage_bindings['开发'].skills, undefined)
|
|
147
|
+
assert.ok(compact.skill_profiles['project:my-implementation'].rules.length > 0)
|
|
148
|
+
const readBack = resolveFlow('standard', JSON.parse(JSON.stringify(compact)))
|
|
149
|
+
assert.deepEqual(readBack.config.stage_bindings, resolveFlow('standard', adopted).config.stage_bindings)
|
|
150
|
+
const custom = { flow: 'standard', stage_bindings: {
|
|
151
|
+
'开发': { skills: [{ skill: { source: 'project', name: 'same' }, rules: [{ source: 'project', name: 'r1' }] }] },
|
|
152
|
+
'交付': { skills: [{ skill: { source: 'project', name: 'same' }, rules: [{ source: 'project', name: 'r2' }] }] },
|
|
153
|
+
} }
|
|
154
|
+
const invalid = resolveFlow('standard', custom)
|
|
155
|
+
assert.ok(invalid.config.configuration_errors.some(problem => problem.includes('different rules')))
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
test('remote write stores canonical skill profiles and read expands them', async () => {
|
|
159
|
+
let stored
|
|
160
|
+
const fs = {
|
|
161
|
+
resolve: async path => path,
|
|
162
|
+
readText: async () => stored,
|
|
163
|
+
writeText: async (_target, text) => { stored = text },
|
|
164
|
+
}
|
|
165
|
+
const receiver = { authorizedPath: async path => path, fs: () => fs }
|
|
166
|
+
const adopted = { ...adoptRecommendation('standard'), stage_bindings: {
|
|
167
|
+
'开发': { skill_refs: [{ source: 'project', name: 'my-implementation' }] },
|
|
168
|
+
}, skill_profiles: { 'project:my-implementation': { rules: [{ source: 'project', name: 'my-rule' }] } } }
|
|
169
|
+
const written = await Controller.prototype.write.call(receiver, { path: 'project', ...adopted })
|
|
170
|
+
assert.equal(written.ok, true)
|
|
171
|
+
const onDisk = JSON.parse(stored)
|
|
172
|
+
assert.ok(onDisk.stage_bindings['开发'].skill_refs.length > 0)
|
|
173
|
+
assert.equal(onDisk.stage_bindings['开发'].skills, undefined)
|
|
174
|
+
assert.ok(onDisk.skill_profiles['project:my-implementation'].rules.length > 0)
|
|
175
|
+
const loaded = await Controller.prototype.read.call(receiver, 'project')
|
|
176
|
+
assert.equal(loaded.ok, true)
|
|
177
|
+
assert.deepEqual(loaded.config.stage_bindings, written.config.stage_bindings)
|
|
178
|
+
assert.deepEqual(loaded.config.skill_profiles, written.config.skill_profiles)
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
test('an explicit skill profile governs every stage even when inline bindings are stale', () => {
|
|
182
|
+
const skill = { source: 'project', name: 'my-skill' }
|
|
183
|
+
const rule = { source: 'project', name: 'my-rule' }
|
|
184
|
+
const project = { flow: 'minimal',
|
|
185
|
+
stage_bindings: {
|
|
186
|
+
'开发': { skills: [{ skill, rules: [] }] },
|
|
187
|
+
'交付': { skills: [{ skill, rules: [] }] },
|
|
188
|
+
},
|
|
189
|
+
skill_profiles: { 'project:my-skill': { rules: [rule] } },
|
|
190
|
+
}
|
|
191
|
+
const resolved = resolveFlow('minimal', project)
|
|
192
|
+
assert.equal(resolved.ok, true)
|
|
193
|
+
assert.deepEqual(validateWorkflow(resolved.config), [])
|
|
194
|
+
assert.deepEqual(resolved.config.stage_bindings['开发'].skills[0].rules, [rule])
|
|
195
|
+
assert.deepEqual(resolved.config.stage_bindings['交付'].skills[0].rules, [rule])
|
|
196
|
+
const compact = compactProjectConfig(project)
|
|
197
|
+
assert.deepEqual(compact.stage_bindings['开发'].skill_refs, [skill])
|
|
198
|
+
assert.deepEqual(resolveFlow('minimal', compact).config.stage_bindings['交付'].skills[0].rules, [rule])
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
test('canonical references to one skill share its rule list without a false conflict', () => {
|
|
202
|
+
const skill = { source: 'project', name: 'my-skill' }
|
|
203
|
+
const rule = { source: 'project', name: 'my-rule' }
|
|
204
|
+
const resolved = resolveFlow('minimal', { flow: 'minimal', stage_bindings: {
|
|
205
|
+
'开发': { skill_refs: [skill] },
|
|
206
|
+
'交付': { skill_refs: [skill] },
|
|
207
|
+
}, skill_profiles: { 'project:my-skill': { rules: [rule] } } })
|
|
208
|
+
assert.deepEqual(validateWorkflow(resolved.config), [])
|
|
209
|
+
assert.deepEqual(resolved.config.stage_bindings['开发'].skills[0].rules, [rule])
|
|
210
|
+
assert.deepEqual(resolved.config.stage_bindings['交付'].skills[0].rules, [rule])
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
test('skill editor save persists one changed profile and read expands it into every stage', async () => {
|
|
214
|
+
let stored
|
|
215
|
+
const fs = { resolve: async path => path, readText: async () => stored,
|
|
216
|
+
writeText: async (_target, content) => { stored = content } }
|
|
217
|
+
const receiver = { authorizedPath: async path => path, fs: () => fs }
|
|
218
|
+
const skill = { source: 'project', name: 'my-skill' }
|
|
219
|
+
const rule = { source: 'project', name: 'my-rule' }
|
|
220
|
+
const initial = compactProjectConfig({ flow: 'minimal', stage_bindings: {
|
|
221
|
+
'开发': { skills: [{ skill, rules: [] }] },
|
|
222
|
+
'交付': { skills: [{ skill, rules: [] }] },
|
|
223
|
+
} })
|
|
224
|
+
const edited = { ...initial, skill_profiles: { ...initial.skill_profiles,
|
|
225
|
+
'project:my-skill': { rules: [rule], evidence: 'none' } } }
|
|
226
|
+
const written = await Controller.prototype.write.call(receiver, { path: 'project', ...edited })
|
|
227
|
+
assert.equal(written.ok, true)
|
|
228
|
+
assert.deepEqual(JSON.parse(stored).stage_bindings['开发'].skill_refs, [skill])
|
|
229
|
+
const loaded = await Controller.prototype.read.call(receiver, 'project')
|
|
230
|
+
assert.equal(loaded.ok, true)
|
|
231
|
+
assert.deepEqual(loaded.config.stage_bindings['开发'].skills[0].rules, [rule])
|
|
232
|
+
assert.deepEqual(loaded.config.stage_bindings['交付'].skills[0].rules, [rule])
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
test('adopting a recommendation retains the user-owned skill profiles', () => {
|
|
236
|
+
const custom = { 'project:my-skill': { rules: [{ source: 'project', name: 'my-rule' }], evidence: 'manual' } }
|
|
237
|
+
const adopted = adoptRecommendation('standard', { flow: 'standard', skill_profiles: custom })
|
|
238
|
+
assert.deepEqual(adopted.skill_profiles, custom)
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
test('malformed canonical skill profiles fail validation without crashing resolution', () => {
|
|
242
|
+
const malformed = resolveFlow('standard', { flow: 'standard',
|
|
243
|
+
stage_bindings: { '开发': { skill_refs: [{ source: 'project', name: 'my-skill' }] } },
|
|
244
|
+
skill_profiles: { 'project:my-skill': { rules: 'not-an-array' } },
|
|
245
|
+
})
|
|
246
|
+
assert.ok(validateWorkflow(malformed.config).some(problem => problem.includes('rules array')))
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
test('commit_required false also removes the checkpoint exit requirement', () => {
|
|
250
|
+
const adopted = adoptRecommendation('standard')
|
|
251
|
+
const config = resolveFlow('standard', { ...adopted, commit_required: false }).config
|
|
252
|
+
const state = { stage: '代码审核', execution_version: 1, review: { outcome: 'pass' },
|
|
253
|
+
artifacts: { review: { conclusion: 'pass', issues: 'none' } }, commits: [] }
|
|
254
|
+
assert.equal(assertAdvance(state, '完成', config).ok, true)
|
|
255
|
+
})
|
|
268
256
|
|
|
269
257
|
test('round-trip: every configurable field survives being written and read back', () => {
|
|
270
258
|
// The save path serialises the payload and the read path parses it back through
|
|
@@ -314,4 +302,4 @@ test('round-trip: the payload the panel sends contains every edited field', () =
|
|
|
314
302
|
// And the whole payload must be accepted by the resolver the write path uses.
|
|
315
303
|
const resolved = resolveFlow(payload.flow, payload)
|
|
316
304
|
assert.equal(resolved.ok, true, 'the payload the panel sends must resolve without problems')
|
|
317
|
-
})
|
|
305
|
+
})
|