@godv61/dsh-task-engine 0.30.1 → 0.30.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.
- package/.adaptive-test.mjs +92 -83
- package/.evidence-test.mjs +16 -16
- package/.resource-test.mjs +19 -19
- package/.roundtrip-test.mjs +5 -5
- package/.sonar-credential-test.mjs +38 -38
- package/.sonarlint-local-test.mjs +13 -10
- package/.workflow-test.mjs +37 -37
- package/README.md +217 -216
- package/docs/development.md +45 -45
- package/docs/manual.html +175 -178
- package/lib/client.js +302 -142
- package/lib/client.js.map +3 -3
- package/lib/controller.d.ts +13 -0
- package/lib/controller.js +20 -1
- package/lib/controller.js.map +1 -1
- package/lib/dev-task.js +5 -4
- package/lib/dev-task.js.map +1 -1
- package/lib/sonar-report.js +11 -1
- package/lib/sonar-report.js.map +1 -1
- package/lib/sonar.d.ts +6 -0
- package/lib/sonar.js.map +1 -1
- package/lib/sonarlint-install.d.ts +7 -0
- package/lib/sonarlint-install.js +150 -0
- package/lib/sonarlint-install.js.map +1 -0
- package/lib/sonarlint-local.d.ts +5 -1
- package/lib/sonarlint-local.js +35 -21
- package/lib/sonarlint-local.js.map +1 -1
- package/package.json +1 -1
- package/scripts/verify-package.mjs +3 -3
- package/skills/code-review/SKILL.md +4 -4
- package/skills/eng-delivery/SKILL.md +2 -2
- package/skills/task-orchestration/SKILL.md +1 -1
- package/skills/test-validation/SKILL.md +3 -3
package/.adaptive-test.mjs
CHANGED
|
@@ -116,7 +116,7 @@ test('task-plan item headings preserve the full implementation sequence', async
|
|
|
116
116
|
await assert.rejects(f.call({ operation: 'advance', task_id: 'PLAN-1', target_stage: '代码开发' }), /I1/)
|
|
117
117
|
})
|
|
118
118
|
|
|
119
|
-
test('init_project rejects a feature-only project map that omits discovered modules', async () => {
|
|
119
|
+
test('init_project rejects a feature-only project map that omits discovered modules', async () => {
|
|
120
120
|
const f = fixture({
|
|
121
121
|
'backend/pom.xml': '<project><properties><java.version>8</java.version></properties></project>',
|
|
122
122
|
'frontend/package.json': '{"dependencies":{"vue":"^2.7.0"}}',
|
|
@@ -281,7 +281,7 @@ test('sonar_check runs a local scan on the recorded commit without pushing or ex
|
|
|
281
281
|
|
|
282
282
|
test('review-only Sonar gate blocks pass until the task records CI analysis of new code', async () => {
|
|
283
283
|
const f = fixture({ '.dsh/meta.json': JSON.stringify({ sonar: { enabled: true, host_url: 'https://sonar.example',
|
|
284
|
-
project_key: 'project', mode: 'branch', token_env: 'SONAR_TOKEN' } }) })
|
|
284
|
+
project_key: 'project', mode: 'branch', source: 'ci', token_env: 'SONAR_TOKEN' } }) })
|
|
285
285
|
await f.call({ operation: 'create', task_id: 'S-1', title: 'change', branch: 'feature', complexity: 'low',
|
|
286
286
|
complexity_reason: 'One local change', files: ['app.js'], items: [{ id: 'one', title: 'change', status: 'done' }] })
|
|
287
287
|
const state = f.state('S-1')
|
|
@@ -314,7 +314,7 @@ test('review-only Sonar gate blocks pass until the task records CI analysis of n
|
|
|
314
314
|
}
|
|
315
315
|
})
|
|
316
316
|
|
|
317
|
-
test('a failed Sonar finding becomes a reviewed project Rule attached to the coding Skill', async () => {
|
|
317
|
+
test('a failed Sonar finding becomes a reviewed project Rule attached to the coding Skill', async () => {
|
|
318
318
|
const f = fixture()
|
|
319
319
|
await f.call({ operation: 'create', task_id: 'L-1', title: 'change', branch: 'feature', complexity: 'low',
|
|
320
320
|
complexity_reason: 'One local change', files: ['app.js'] })
|
|
@@ -339,83 +339,92 @@ test('a failed Sonar finding becomes a reviewed project Rule attached to the cod
|
|
|
339
339
|
assert.deepEqual(JSON.parse(f.files.get(join(f.cwd, '.dsh/skills/code-development/profile.json'))).rules,
|
|
340
340
|
[{ source: 'project', name: 'handle-null-boundary' }])
|
|
341
341
|
assert.deepEqual(JSON.parse(await f.call({ operation: 'status', task_id: 'L-1' })).rule_learning_candidates, [])
|
|
342
|
-
})
|
|
343
|
-
|
|
344
|
-
test('
|
|
345
|
-
const f = fixture({
|
|
346
|
-
'
|
|
347
|
-
|
|
348
|
-
'
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
const
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
assert.
|
|
377
|
-
await assert.rejects(Controller.prototype.
|
|
378
|
-
{ path: f.cwd, resources }),
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
const
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
f.
|
|
403
|
-
f.
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
assert.
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
test('new Sonar projects default to pre-commit review against HEAD', async () => {
|
|
345
|
+
const f = fixture({ '.dsh/meta.json': JSON.stringify({ sonar: { enabled: true,
|
|
346
|
+
host_url: 'https://sonar.example', project_key: 'project' } }) })
|
|
347
|
+
await f.call({ operation: 'create', task_id: 'PRE-1', title: 'new code', branch: 'feature',
|
|
348
|
+
complexity: 'low', complexity_reason: 'Small change', files: ['app.java'] })
|
|
349
|
+
assert.equal(f.state('PRE-1').sonar_policy.source, 'ide-local')
|
|
350
|
+
assert.equal(f.state('PRE-1').sonar_policy.reference_branch, 'HEAD')
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
test('workbench initializes reviewed project resources directly and preserves Sonar settings', async () => {
|
|
354
|
+
const f = fixture({
|
|
355
|
+
'backend/pom.xml': '<project><properties><java.version>8</java.version></properties></project>',
|
|
356
|
+
'frontend/package.json': '{"dependencies":{"vue":"^2.7.0"}}',
|
|
357
|
+
'.dsh/meta.json': JSON.stringify({ sonar: { enabled: false, project_key: 'keep-me' } }),
|
|
358
|
+
})
|
|
359
|
+
const resources = [
|
|
360
|
+
{ kind: 'rule', name: 'backend-convention', content: 'Use the backend module Maven Java 8 build for backend changes.' },
|
|
361
|
+
{ kind: 'skill', name: 'adaptive-project-project-map', description: 'Reusable repository map',
|
|
362
|
+
content: 'Root package.json, backend/pom.xml and frontend/package.json define the backend and frontend modules.',
|
|
363
|
+
meta_skills: ['requirements-analysis', 'code-development'], rules: ['backend-convention'] },
|
|
364
|
+
]
|
|
365
|
+
const receiver = {
|
|
366
|
+
authorizedPath: async path => path, fs: () => f.fs, listSkills: async () => ({ skills: [] }),
|
|
367
|
+
projectInitRoot: Controller.prototype.projectInitRoot,
|
|
368
|
+
previewProjectInit: Controller.prototype.previewProjectInit,
|
|
369
|
+
ctx: { get(name) {
|
|
370
|
+
if (name === 'llm') return { async *stream() { yield { type: 'text-delta', text: JSON.stringify({ resources }) } } }
|
|
371
|
+
if (name === 'agentDefaultModel') return { currentSelection: () => ({ provider: 'test', model: 'test' }) }
|
|
372
|
+
} },
|
|
373
|
+
}
|
|
374
|
+
const draft = await Controller.prototype.generateProjectInit.call(receiver, { path: f.cwd })
|
|
375
|
+
assert.deepEqual(draft.project_map_coverage, ['backend', 'frontend', 'package.json'])
|
|
376
|
+
assert.equal(f.files.has(join(f.cwd, '.dsh/rules/backend-convention.md')), false)
|
|
377
|
+
await assert.rejects(Controller.prototype.applyProjectInit.call(receiver,
|
|
378
|
+
{ path: f.cwd, resources: draft.resources, expected_hash: 'stale', existing_hash: draft.existing_hash }), /改变/)
|
|
379
|
+
const applied = await Controller.prototype.applyProjectInit.call(receiver,
|
|
380
|
+
{ path: f.cwd, resources: draft.resources, expected_hash: draft.expected_hash, existing_hash: draft.existing_hash })
|
|
381
|
+
assert.equal(applied.ok, true)
|
|
382
|
+
assert.equal(f.files.get(join(f.cwd, '.dsh/meta.json')).includes('keep-me'), true)
|
|
383
|
+
assert.equal(JSON.parse(f.files.get(join(f.cwd, '.dsh/meta.json'))).meta_bindings['code-development'][0],
|
|
384
|
+
'adaptive-project-project-map')
|
|
385
|
+
assert.ok(f.files.has(join(f.cwd, '.dsh/skills/adaptive-project-project-map/SKILL.md')))
|
|
386
|
+
await assert.rejects(Controller.prototype.previewProjectInit.call(receiver,
|
|
387
|
+
{ path: f.cwd, resources }), /不能覆盖/)
|
|
388
|
+
})
|
|
389
|
+
|
|
390
|
+
test('a local false positive needs human approval, keeps the raw gate, and expires after code changes', async () => {
|
|
391
|
+
let allow = false
|
|
392
|
+
const approvals = []
|
|
393
|
+
const shell = { resolve: request => request, execute: async () => ({ result: async () => ({
|
|
394
|
+
exitCode: 0, timedOut: false, aborted: false, stdout: { text: 'checks passed' }, stderr: { text: '' },
|
|
395
|
+
}) }) }
|
|
396
|
+
const f = fixture({ '.dsh/meta.json': JSON.stringify({ sonar: { enabled: true,
|
|
397
|
+
host_url: 'https://sonar.example', project_key: 'project', mode: 'branch', source: 'ide-local',
|
|
398
|
+
reference_branch: 'main' } }) }, { shell,
|
|
399
|
+
approval: { request: async request => { approvals.push(request); return allow ? 'allowed-once' : 'rejected' } } })
|
|
400
|
+
await f.call({ operation: 'create', task_id: 'FP-1', title: 'one local finding', branch: 'feature',
|
|
401
|
+
complexity: 'low', complexity_reason: 'Local correction', files: ['app.js'] })
|
|
402
|
+
await f.call({ operation: 'verify', task_id: 'FP-1', command: 'check' })
|
|
403
|
+
const state = f.state('FP-1')
|
|
404
|
+
state.stage = '代码审核'
|
|
405
|
+
const finding = { key: 'local-fp-1', rule: 'mycompany-java:AvoidObjectCreationInLoop',
|
|
406
|
+
message: 'Avoid allocation', severity: 'MAJOR', file: 'app.js', line: 3 }
|
|
407
|
+
state.sonar_audit = { ce_task_id: 'local', analysis_id: 'LOCAL-1', gate: 'ERROR',
|
|
408
|
+
checked_at: '2026-10-05T00:00:00.000Z', scope_hash: state.verification.receipt.scope_hash,
|
|
409
|
+
findings: [finding], blocking: [finding], target: 'main', scanned_files: ['app.js'],
|
|
410
|
+
report_path: '.dsh/reviews/FP-1/local.md' }
|
|
411
|
+
f.files.set(join(f.cwd, '.dsh/reviews/FP-1/local.md'), 'original report')
|
|
412
|
+
f.files.set(join(f.cwd, '.dsh/task-FP-1.json'), JSON.stringify(state))
|
|
413
|
+
const args = { operation: 'sonar_disposition', task_id: 'FP-1', issue_key: finding.key,
|
|
414
|
+
disposition_reason: '每次循环需要独立实体对象,复用同一个实例会让多条授权记录互相覆盖,属于规则误报。',
|
|
415
|
+
evidence: ['授权实体逐行添加到集合,复用对象会导致所有行指向末次值'] }
|
|
416
|
+
await assert.rejects(f.call(args), /not approved/)
|
|
417
|
+
assert.equal(f.state('FP-1').sonar_audit.dispositions, undefined)
|
|
418
|
+
allow = true
|
|
419
|
+
const result = JSON.parse(await f.call(args))
|
|
420
|
+
assert.equal(result.raw_gate, 'ERROR')
|
|
421
|
+
assert.equal(result.review_gate, 'OK')
|
|
422
|
+
assert.equal(f.state('FP-1').sonar_audit.dispositions.length, 1)
|
|
423
|
+
assert.deepEqual(JSON.parse(await f.call({ operation: 'status', task_id: 'FP-1' })).rule_learning_candidates, [])
|
|
424
|
+
assert.match(approvals.at(-1).reason, /local-fp-1/)
|
|
425
|
+
assert.match(f.files.get(join(f.cwd, state.sonar_audit.report_path)), /逐项误报复核/)
|
|
426
|
+
await f.call({ operation: 'review', task_id: 'FP-1', outcome: 'pass' })
|
|
427
|
+
await assert.rejects(f.call({ operation: 'learn_rule', task_id: 'FP-1', issue_key: finding.key }), /blocking issue_key/)
|
|
428
|
+
f.files.set(join(f.cwd, 'app.js'), 'new source')
|
|
429
|
+
await assert.rejects(f.call({ operation: 'review', task_id: 'FP-1', outcome: 'pass' }), /stale/)
|
|
430
|
+
})
|
package/.evidence-test.mjs
CHANGED
|
@@ -7,11 +7,11 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import test from 'node:test'
|
|
9
9
|
import assert from 'node:assert/strict'
|
|
10
|
-
import { loadedSkills, needsSkillReceipt, skillBlockers } from './lib/skill-audit.js'
|
|
10
|
+
import { loadedSkills, needsSkillReceipt, skillBlockers } from './lib/skill-audit.js'
|
|
11
11
|
import { adoptRecommendation, resolveFlow } from './lib/workflows.js'
|
|
12
12
|
|
|
13
13
|
/** A session that has loaded the named skills. */
|
|
14
|
-
function sessionWith(names) {
|
|
14
|
+
function sessionWith(names) {
|
|
15
15
|
const events = []
|
|
16
16
|
for (const name of names) {
|
|
17
17
|
const callId = 'c' + events.length
|
|
@@ -19,20 +19,20 @@ function sessionWith(names) {
|
|
|
19
19
|
events.push({ type: 'tool/result', data: { message: { content: [{ type: 'tool-result', toolCallId: callId, isError: false }] } } })
|
|
20
20
|
}
|
|
21
21
|
return { id: 's', snapshotEvents: () => events }
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
test('evidence: DSH 0.2 tool messages prove only successful task-scoped skill loads', () => {
|
|
25
|
-
const events = [
|
|
26
|
-
{ type: 'tool/call', data: { name: 'dev_task', callId: 'ok', arguments: JSON.stringify({ operation: 'load_skill', task_id: 'Q-1', skill_name: 'project:qms-project-map' }) } },
|
|
27
|
-
{ type: 'tool/result', data: { message: { role: 'tool', toolCallId: 'ok', isError: false, content: [{ type: 'text', text: 'loaded' }] } } },
|
|
28
|
-
{ type: 'tool/call', data: { name: 'dev_task', callId: 'bad', arguments: JSON.stringify({ operation: 'load_skill', task_id: 'Q-1', skill_name: 'project:qms-tech-stack' }) } },
|
|
29
|
-
{ type: 'tool/result', data: { message: { role: 'tool', toolCallId: 'bad', isError: true, content: [{ type: 'text', text: 'failed' }] } } },
|
|
30
|
-
]
|
|
31
|
-
const loaded = loadedSkills({ snapshotEvents: () => events })
|
|
32
|
-
assert.equal(loaded.get('Q-1#project:qms-project-map'), 'ok')
|
|
33
|
-
assert.equal(loaded.has('Q-1#project:qms-tech-stack'), false)
|
|
34
|
-
assert.equal(loaded.has('Q-2#project:qms-project-map'), false)
|
|
35
|
-
})
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
test('evidence: DSH 0.2 tool messages prove only successful task-scoped skill loads', () => {
|
|
25
|
+
const events = [
|
|
26
|
+
{ type: 'tool/call', data: { name: 'dev_task', callId: 'ok', arguments: JSON.stringify({ operation: 'load_skill', task_id: 'Q-1', skill_name: 'project:qms-project-map' }) } },
|
|
27
|
+
{ type: 'tool/result', data: { message: { role: 'tool', toolCallId: 'ok', isError: false, content: [{ type: 'text', text: 'loaded' }] } } },
|
|
28
|
+
{ type: 'tool/call', data: { name: 'dev_task', callId: 'bad', arguments: JSON.stringify({ operation: 'load_skill', task_id: 'Q-1', skill_name: 'project:qms-tech-stack' }) } },
|
|
29
|
+
{ type: 'tool/result', data: { message: { role: 'tool', toolCallId: 'bad', isError: true, content: [{ type: 'text', text: 'failed' }] } } },
|
|
30
|
+
]
|
|
31
|
+
const loaded = loadedSkills({ snapshotEvents: () => events })
|
|
32
|
+
assert.equal(loaded.get('Q-1#project:qms-project-map'), 'ok')
|
|
33
|
+
assert.equal(loaded.has('Q-1#project:qms-tech-stack'), false)
|
|
34
|
+
assert.equal(loaded.has('Q-2#project:qms-project-map'), false)
|
|
35
|
+
})
|
|
36
36
|
|
|
37
37
|
/** A standard-flow config with one extra skill on 开发 carrying the given evidence. */
|
|
38
38
|
function withEvidence(evidence) {
|
package/.resource-test.mjs
CHANGED
|
@@ -122,7 +122,7 @@ test('ledger shows absent storage as empty and propagates access errors',async()
|
|
|
122
122
|
controller.fs=()=>({resolve:async p=>p,listDir:async()=>{throw Object.assign(new Error('denied'),{code:'EACCES'})}})
|
|
123
123
|
await assert.rejects(()=>controller.readTasks('/project'),/denied/)
|
|
124
124
|
})
|
|
125
|
-
test('ledger rejects malformed records instead of pretending there are no tasks',async()=>{
|
|
125
|
+
test('ledger rejects malformed records instead of pretending there are no tasks',async()=>{
|
|
126
126
|
const {default:Controller}=await import('./lib/controller.js')
|
|
127
127
|
const controller=Object.create(Controller.prototype)
|
|
128
128
|
controller.authorizedPath=async p=>p
|
|
@@ -130,21 +130,21 @@ test('ledger rejects malformed records instead of pretending there are no tasks'
|
|
|
130
130
|
controller.fs=()=>({resolve:async p=>p,listDir:async()=>[{name:'task-broken.json'}],readText:async()=>content})
|
|
131
131
|
await assert.rejects(()=>controller.readTasks('/project'),/任务记录/)
|
|
132
132
|
}
|
|
133
|
-
})
|
|
134
|
-
test('ledger exposes Sonar findings without project credentials',async()=>{
|
|
135
|
-
const {default:Controller}=await import('./lib/controller.js')
|
|
136
|
-
const controller=Object.create(Controller.prototype)
|
|
137
|
-
controller.authorizedPath=async p=>p
|
|
138
|
-
controller.fs=()=>({resolve:async p=>p,listDir:async()=>[{name:'task-sample.json'}],readText:async()=>JSON.stringify({
|
|
139
|
-
id:'sample',title:'Sample',stage:'代码审核',branch:'feature/sample',items:[],
|
|
140
|
-
sonar_policy:{source:'ide-local',token_env:'SONAR_TOKEN',host_url:'https://sonar.example',project_key:'sample'},
|
|
141
|
-
sonar_audit:{gate:'ERROR',checked_at:'2026-10-03T00:00:00Z',target:'main',
|
|
142
|
-
findings:[{key:'issue-1',rule:'java:S103',message:'Line too long',severity:'MAJOR',file:'Sample.java',line:3}],
|
|
143
|
-
blocking:[{key:'issue-1'}],uncovered_files:[]},
|
|
144
|
-
})})
|
|
145
|
-
const {tasks}=await controller.readTasks('/project')
|
|
146
|
-
assert.equal(tasks[0].sonar.audit.gate,'ERROR')
|
|
147
|
-
assert.equal(tasks[0].sonar.audit.blocking_count,1)
|
|
148
|
-
assert.deepEqual(tasks[0].sonar.audit.findings[0],{key:'issue-1',rule:'java:S103',message:'Line too long',severity:'MAJOR',file:'Sample.java',line:3})
|
|
149
|
-
assert.equal(JSON.stringify(tasks).includes('SONAR_TOKEN'),false)
|
|
150
|
-
})
|
|
133
|
+
})
|
|
134
|
+
test('ledger exposes Sonar findings without project credentials',async()=>{
|
|
135
|
+
const {default:Controller}=await import('./lib/controller.js')
|
|
136
|
+
const controller=Object.create(Controller.prototype)
|
|
137
|
+
controller.authorizedPath=async p=>p
|
|
138
|
+
controller.fs=()=>({resolve:async p=>p,listDir:async()=>[{name:'task-sample.json'}],readText:async()=>JSON.stringify({
|
|
139
|
+
id:'sample',title:'Sample',stage:'代码审核',branch:'feature/sample',items:[],
|
|
140
|
+
sonar_policy:{source:'ide-local',token_env:'SONAR_TOKEN',host_url:'https://sonar.example',project_key:'sample'},
|
|
141
|
+
sonar_audit:{gate:'ERROR',checked_at:'2026-10-03T00:00:00Z',target:'main',
|
|
142
|
+
findings:[{key:'issue-1',rule:'java:S103',message:'Line too long',severity:'MAJOR',file:'Sample.java',line:3}],
|
|
143
|
+
blocking:[{key:'issue-1'}],uncovered_files:[]},
|
|
144
|
+
})})
|
|
145
|
+
const {tasks}=await controller.readTasks('/project')
|
|
146
|
+
assert.equal(tasks[0].sonar.audit.gate,'ERROR')
|
|
147
|
+
assert.equal(tasks[0].sonar.audit.blocking_count,1)
|
|
148
|
+
assert.deepEqual(tasks[0].sonar.audit.findings[0],{key:'issue-1',rule:'java:S103',message:'Line too long',severity:'MAJOR',file:'Sample.java',line:3})
|
|
149
|
+
assert.equal(JSON.stringify(tasks).includes('SONAR_TOKEN'),false)
|
|
150
|
+
})
|
package/.roundtrip-test.mjs
CHANGED
|
@@ -13,16 +13,16 @@ import { dirname, join, resolve, sep } from 'node:path'
|
|
|
13
13
|
import { resolveFlow, adoptRecommendation, compactProjectConfig, materializeBundledReferences } from './lib/workflows.js'
|
|
14
14
|
import Controller from './lib/controller.js'
|
|
15
15
|
import { assertAdvance, validateWorkflow } from './lib/engine.js'
|
|
16
|
-
// The npm package ships the built client, not its TypeScript source. Keep the
|
|
17
|
-
// source codec assertion in repository tests while allowing shipped tests to run.
|
|
18
|
-
const remoteSource = new URL('./src/client/remote.ts', import.meta.url)
|
|
19
|
-
const hasRemoteSource = existsSync(remoteSource)
|
|
16
|
+
// The npm package ships the built client, not its TypeScript source. Keep the
|
|
17
|
+
// source codec assertion in repository tests while allowing shipped tests to run.
|
|
18
|
+
const remoteSource = new URL('./src/client/remote.ts', import.meta.url)
|
|
19
|
+
const hasRemoteSource = existsSync(remoteSource)
|
|
20
20
|
const TYPERT_REMOTE = hasRemoteSource ? (await import(remoteSource.href)).TYPERT_REMOTE : undefined
|
|
21
21
|
|
|
22
22
|
/** The fields a project config can carry, so a round-trip is checked on all of them. */
|
|
23
23
|
const FIELDS = ['flow', 'stage_bindings', 'commit', 'artifacts', 'review_depth', 'commit_required']
|
|
24
24
|
|
|
25
|
-
test('browser remote codec retains source-qualified bindings and all saved fields',
|
|
25
|
+
test('browser remote codec retains source-qualified bindings and all saved fields',
|
|
26
26
|
{ skip: hasRemoteSource ? false : 'TypeScript source is excluded from the npm package' }, () => {
|
|
27
27
|
const write = TYPERT_REMOTE.descriptors.find(descriptor => descriptor.method === 'write')
|
|
28
28
|
const read = TYPERT_REMOTE.descriptors.find(descriptor => descriptor.method === 'read')
|
|
@@ -1,38 +1,38 @@
|
|
|
1
|
-
import assert from 'node:assert/strict'
|
|
2
|
-
import { test } from 'node:test'
|
|
3
|
-
import { resolveSonarToken, sonarCredentialRef } from './lib/sonar-credential.js'
|
|
4
|
-
import Controller from './lib/controller.js'
|
|
5
|
-
|
|
6
|
-
test('Sonar credential references stay distinct for different workspaces', () => {
|
|
7
|
-
const qms = sonarCredentialRef('D:/projects/qms')
|
|
8
|
-
const eam = sonarCredentialRef('D:/projects/eam')
|
|
9
|
-
assert.match(qms, /^DSH_TASK_ENGINE_SONAR_[A-F0-9]{24}$/)
|
|
10
|
-
assert.notEqual(qms, eam)
|
|
11
|
-
assert.equal(qms, sonarCredentialRef('D:/projects/qms'))
|
|
12
|
-
assert.ok(!qms.includes('qms'))
|
|
13
|
-
if (process.platform === 'win32') assert.equal(qms, sonarCredentialRef('d:/PROJECTS/QMS'))
|
|
14
|
-
})
|
|
15
|
-
|
|
16
|
-
test('Sonar token management keeps each workspace value private', async () => {
|
|
17
|
-
const stored = new Map()
|
|
18
|
-
const credentials = {
|
|
19
|
-
describe: async ref => ({ configured: stored.has(ref), writable: true, source: stored.has(ref) ? 'file' : undefined }),
|
|
20
|
-
set: async (ref, value) => { stored.set(ref, value) },
|
|
21
|
-
unset: async ref => { stored.delete(ref) },
|
|
22
|
-
resolve: async ref => stored.has(ref) ? { value: stored.get(ref), source: 'file' } : undefined,
|
|
23
|
-
}
|
|
24
|
-
const controller = Object.create(Controller.prototype)
|
|
25
|
-
controller.authorizedPath = async path => path
|
|
26
|
-
Object.defineProperty(controller, 'ctx', { value: { get: name => name === 'credentials' ? credentials : undefined } })
|
|
27
|
-
const qms = 'D:/projects/qms'
|
|
28
|
-
const eam = 'D:/projects/eam'
|
|
29
|
-
const result = await controller.setSonarToken({ path: qms, token: 'qms-secret' })
|
|
30
|
-
assert.equal(result.configured, true)
|
|
31
|
-
assert.ok(!JSON.stringify(result).includes('qms-secret'))
|
|
32
|
-
assert.equal((await controller.describeSonarToken(eam)).configured, false)
|
|
33
|
-
assert.equal((await credentials.resolve(sonarCredentialRef(qms))).value, 'qms-secret')
|
|
34
|
-
assert.equal(await resolveSonarToken(credentials, qms, 'old-env-token'), 'qms-secret')
|
|
35
|
-
assert.equal(await resolveSonarToken(credentials, eam, 'old-env-token'), 'old-env-token')
|
|
36
|
-
await controller.unsetSonarToken(qms)
|
|
37
|
-
assert.equal((await controller.describeSonarToken(qms)).configured, false)
|
|
38
|
-
})
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { test } from 'node:test'
|
|
3
|
+
import { resolveSonarToken, sonarCredentialRef } from './lib/sonar-credential.js'
|
|
4
|
+
import Controller from './lib/controller.js'
|
|
5
|
+
|
|
6
|
+
test('Sonar credential references stay distinct for different workspaces', () => {
|
|
7
|
+
const qms = sonarCredentialRef('D:/projects/qms')
|
|
8
|
+
const eam = sonarCredentialRef('D:/projects/eam')
|
|
9
|
+
assert.match(qms, /^DSH_TASK_ENGINE_SONAR_[A-F0-9]{24}$/)
|
|
10
|
+
assert.notEqual(qms, eam)
|
|
11
|
+
assert.equal(qms, sonarCredentialRef('D:/projects/qms'))
|
|
12
|
+
assert.ok(!qms.includes('qms'))
|
|
13
|
+
if (process.platform === 'win32') assert.equal(qms, sonarCredentialRef('d:/PROJECTS/QMS'))
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
test('Sonar token management keeps each workspace value private', async () => {
|
|
17
|
+
const stored = new Map()
|
|
18
|
+
const credentials = {
|
|
19
|
+
describe: async ref => ({ configured: stored.has(ref), writable: true, source: stored.has(ref) ? 'file' : undefined }),
|
|
20
|
+
set: async (ref, value) => { stored.set(ref, value) },
|
|
21
|
+
unset: async ref => { stored.delete(ref) },
|
|
22
|
+
resolve: async ref => stored.has(ref) ? { value: stored.get(ref), source: 'file' } : undefined,
|
|
23
|
+
}
|
|
24
|
+
const controller = Object.create(Controller.prototype)
|
|
25
|
+
controller.authorizedPath = async path => path
|
|
26
|
+
Object.defineProperty(controller, 'ctx', { value: { get: name => name === 'credentials' ? credentials : undefined } })
|
|
27
|
+
const qms = 'D:/projects/qms'
|
|
28
|
+
const eam = 'D:/projects/eam'
|
|
29
|
+
const result = await controller.setSonarToken({ path: qms, token: 'qms-secret' })
|
|
30
|
+
assert.equal(result.configured, true)
|
|
31
|
+
assert.ok(!JSON.stringify(result).includes('qms-secret'))
|
|
32
|
+
assert.equal((await controller.describeSonarToken(eam)).configured, false)
|
|
33
|
+
assert.equal((await credentials.resolve(sonarCredentialRef(qms))).value, 'qms-secret')
|
|
34
|
+
assert.equal(await resolveSonarToken(credentials, qms, 'old-env-token'), 'qms-secret')
|
|
35
|
+
assert.equal(await resolveSonarToken(credentials, eam, 'old-env-token'), 'old-env-token')
|
|
36
|
+
await controller.unsetSonarToken(qms)
|
|
37
|
+
assert.equal((await controller.describeSonarToken(qms)).configured, false)
|
|
38
|
+
})
|
|
@@ -4,7 +4,7 @@ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'
|
|
|
4
4
|
import { tmpdir } from 'node:os'
|
|
5
5
|
import { join } from 'node:path'
|
|
6
6
|
import { test } from 'node:test'
|
|
7
|
-
import { changedLines, missingLocalAuditScope, uncoveredLocalFiles } from './lib/sonarlint-local.js'
|
|
7
|
+
import { changedLines, missingLocalAuditScope, uncoveredLocalFiles } from './lib/sonarlint-local.js'
|
|
8
8
|
import { isIncludedAuditPath, validAuditIncludePaths } from './lib/sonar.js'
|
|
9
9
|
import { renderSonarReport, sonarReportPath } from './lib/sonar-report.js'
|
|
10
10
|
|
|
@@ -21,16 +21,19 @@ test('one review gets a credential-free Markdown report path and issue details',
|
|
|
21
21
|
const audit = { analysis_id: 'abc12345-0000', checked_at: '2026-10-03T10:11:12.000Z',
|
|
22
22
|
target: 'qdm_gw_chemical_260930', gate: 'ERROR', findings: [{key:'x',rule:'java:S103',
|
|
23
23
|
severity:'MAJOR',file:'A.java',line:12,message:'Line too long'}], blocking:[{key:'x'}],
|
|
24
|
-
scanned_files:['A.java'],uncovered_files:[]
|
|
24
|
+
scanned_files:['A.java'],uncovered_files:[],
|
|
25
|
+
profile_coverage:[{language:'JAVA',active_rules:66,analyzer:'SYNCED'}] }
|
|
25
26
|
const path = sonarReportPath('QMS-1', audit)
|
|
26
27
|
assert.match(path, /^\.dsh\/reviews\/QMS-1\/2026-/)
|
|
27
28
|
const report = renderSonarReport('QMS-1', {source:'ide-local',project_key:'project',include_paths:['fp-bussiness']}, audit)
|
|
28
29
|
assert.match(report, /java:S103/)
|
|
29
30
|
assert.match(report, /A.java:12/)
|
|
31
|
+
assert.match(report, /JAVA:服务端启用 66 条规则;本地分析器 SYNCED/)
|
|
32
|
+
assert.match(report, /不等同 SonarQube 服务端完整扫描或 Quality Gate/)
|
|
30
33
|
assert.equal(report.includes('token-secret'),false)
|
|
31
34
|
})
|
|
32
35
|
|
|
33
|
-
test('unsupported files block only when the project has active rules for their language', () => {
|
|
36
|
+
test('unsupported files block only when the project has active rules for their language', () => {
|
|
34
37
|
const paths = ['migration.sql', 'settings.json', 'script.py']
|
|
35
38
|
const profiles = [
|
|
36
39
|
{ language: 'java', activeRuleCount: 66 },
|
|
@@ -40,13 +43,13 @@ test('unsupported files block only when the project has active rules for their l
|
|
|
40
43
|
assert.deepEqual(uncoveredLocalFiles(paths, profiles), ['script.py'])
|
|
41
44
|
assert.deepEqual(uncoveredLocalFiles(paths, [...profiles, { language: 'plsql', activeRuleCount: 5 }]),
|
|
42
45
|
['migration.sql', 'script.py'])
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
test('local audit detects changed backend files omitted from the task scope', () => {
|
|
46
|
-
assert.deepEqual(missingLocalAuditScope(['fp-bussiness/A.java', 'fp-bussiness/B.java'],
|
|
47
|
-
['fp-bussiness/A.java', 'fp-ruoyi-ui/view.vue']), ['fp-bussiness/B.java'])
|
|
48
|
-
assert.deepEqual(missingLocalAuditScope(['fp-bussiness/A.java'], ['./FP-BUSSINESS\\A.java']), [])
|
|
49
|
-
})
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
test('local audit detects changed backend files omitted from the task scope', () => {
|
|
49
|
+
assert.deepEqual(missingLocalAuditScope(['fp-bussiness/A.java', 'fp-bussiness/B.java'],
|
|
50
|
+
['fp-bussiness/A.java', 'fp-ruoyi-ui/view.vue']), ['fp-bussiness/B.java'])
|
|
51
|
+
assert.deepEqual(missingLocalAuditScope(['fp-bussiness/A.java'], ['./FP-BUSSINESS\\A.java']), [])
|
|
52
|
+
})
|
|
50
53
|
|
|
51
54
|
test('local Sonar audit considers Git new code but excludes generated DSH records', async () => {
|
|
52
55
|
const root = mkdtempSync(join(tmpdir(), 'dsh-sonarlint-diff-test-'))
|
package/.workflow-test.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'
|
|
|
4
4
|
import { tmpdir } from 'node:os'
|
|
5
5
|
import { join, resolve, sep } from 'node:path'
|
|
6
6
|
import { registerDevTask } from './lib/dev-task.js'
|
|
7
|
-
import { newTask, validateWorkflow, verificationBlockers } from './lib/engine.js'
|
|
7
|
+
import { newTask, validateWorkflow, verificationBlockers } from './lib/engine.js'
|
|
8
8
|
import { adoptRecommendation, resolveFlow, retainStageBindings } from './lib/workflows.js'
|
|
9
9
|
import Controller from './lib/controller.js'
|
|
10
10
|
import { registerShippedSkills } from './lib/shipped-skills.js'
|
|
@@ -216,42 +216,42 @@ test('重排实施项时不静默丢弃已完成项和审查记录', async () =>
|
|
|
216
216
|
await f.call({ operation: 'dispatch', item_id: 'I1', description: 'direct implementation' })
|
|
217
217
|
await f.call({ operation: 'review_item', item_id: 'I1', spec_outcome: 'pass', quality_outcome: 'pass' })
|
|
218
218
|
await f.call({ operation: 'items', items: [{ id: 'I1', status: 'done' }, { id: 'I2' }] })
|
|
219
|
-
await f.call({ operation: 'items', items: [{ id: 'I3', title: 'service', status: 'todo' }] })
|
|
220
|
-
assert.deepEqual(f.state().items.map(item => item.id), ['I1', 'I2', 'I3'])
|
|
221
|
-
assert.equal(f.state().items[0].status, 'done')
|
|
222
|
-
assert.equal(f.state().items[0].review.quality.outcome, 'pass')
|
|
223
|
-
assert.equal(f.state().items[0].dispatch.description, 'direct implementation')
|
|
224
|
-
await f.call({ operation: 'items', items_mode: 'replace', items: [{ id: 'I3', title: 'service', status: 'todo' }] })
|
|
225
|
-
assert.deepEqual(f.state().items.map(item => item.id), ['I3', 'I1'])
|
|
226
|
-
})
|
|
227
|
-
|
|
228
|
-
test('Maven verify rejects zero or missing Surefire tests even when the command exits zero', async () => {
|
|
229
|
-
let output = 'Tests run: 0, Failures: 0, Errors: 0, Skipped: 0'
|
|
230
|
-
const shell = { resolve: request => request, execute: async () => ({ result: async () => ({
|
|
231
|
-
exitCode: 0, timedOut: false, aborted: false, stdout: { text: output }, stderr: { text: '' },
|
|
232
|
-
}) }) }
|
|
233
|
-
const f = fixture('交付', {}, { shell })
|
|
234
|
-
await f.call({ operation: 'verify', command: 'mvn -q test' })
|
|
235
|
-
assert.equal(f.state().verification.passed, false)
|
|
236
|
-
assert.equal(f.state().verification.receipt.test_summary.count, 0)
|
|
237
|
-
output = 'BUILD SUCCESS'
|
|
238
|
-
await f.call({ operation: 'verify', command: 'mvn -q test' })
|
|
239
|
-
assert.equal(f.state().verification.passed, false)
|
|
240
|
-
assert.equal(f.state().verification.receipt.test_summary.count, null)
|
|
241
|
-
output = 'Tests run: 23, Failures: 0, Errors: 0, Skipped: 0'
|
|
242
|
-
await f.call({ operation: 'verify', command: 'mvn test' })
|
|
243
|
-
assert.equal(f.state().verification.passed, true)
|
|
244
|
-
assert.equal(f.state().verification.receipt.test_summary.count, 23)
|
|
245
|
-
})
|
|
246
|
-
|
|
247
|
-
test('a previously stored Maven zero-test pass is blocked by current workflow gates', () => {
|
|
248
|
-
const f = fixture('代码审核')
|
|
249
|
-
const state = f.state()
|
|
250
|
-
state.verification = { passed: true, evidence: [], receipt: { command: 'mvn test', exit_code: 0,
|
|
251
|
-
timed_out: false, aborted: false, started_at: 'now', finished_at: 'now',
|
|
252
|
-
stdout: 'Tests run: 0, Failures: 0, Errors: 0, Skipped: 0', stderr: '' } }
|
|
253
|
-
assert.match(verificationBlockers(state, state.flow.config).join(' '), /no nonzero Surefire/)
|
|
254
|
-
})
|
|
219
|
+
await f.call({ operation: 'items', items: [{ id: 'I3', title: 'service', status: 'todo' }] })
|
|
220
|
+
assert.deepEqual(f.state().items.map(item => item.id), ['I1', 'I2', 'I3'])
|
|
221
|
+
assert.equal(f.state().items[0].status, 'done')
|
|
222
|
+
assert.equal(f.state().items[0].review.quality.outcome, 'pass')
|
|
223
|
+
assert.equal(f.state().items[0].dispatch.description, 'direct implementation')
|
|
224
|
+
await f.call({ operation: 'items', items_mode: 'replace', items: [{ id: 'I3', title: 'service', status: 'todo' }] })
|
|
225
|
+
assert.deepEqual(f.state().items.map(item => item.id), ['I3', 'I1'])
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
test('Maven verify rejects zero or missing Surefire tests even when the command exits zero', async () => {
|
|
229
|
+
let output = 'Tests run: 0, Failures: 0, Errors: 0, Skipped: 0'
|
|
230
|
+
const shell = { resolve: request => request, execute: async () => ({ result: async () => ({
|
|
231
|
+
exitCode: 0, timedOut: false, aborted: false, stdout: { text: output }, stderr: { text: '' },
|
|
232
|
+
}) }) }
|
|
233
|
+
const f = fixture('交付', {}, { shell })
|
|
234
|
+
await f.call({ operation: 'verify', command: 'mvn -q test' })
|
|
235
|
+
assert.equal(f.state().verification.passed, false)
|
|
236
|
+
assert.equal(f.state().verification.receipt.test_summary.count, 0)
|
|
237
|
+
output = 'BUILD SUCCESS'
|
|
238
|
+
await f.call({ operation: 'verify', command: 'mvn -q test' })
|
|
239
|
+
assert.equal(f.state().verification.passed, false)
|
|
240
|
+
assert.equal(f.state().verification.receipt.test_summary.count, null)
|
|
241
|
+
output = 'Tests run: 23, Failures: 0, Errors: 0, Skipped: 0'
|
|
242
|
+
await f.call({ operation: 'verify', command: 'mvn test' })
|
|
243
|
+
assert.equal(f.state().verification.passed, true)
|
|
244
|
+
assert.equal(f.state().verification.receipt.test_summary.count, 23)
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
test('a previously stored Maven zero-test pass is blocked by current workflow gates', () => {
|
|
248
|
+
const f = fixture('代码审核')
|
|
249
|
+
const state = f.state()
|
|
250
|
+
state.verification = { passed: true, evidence: [], receipt: { command: 'mvn test', exit_code: 0,
|
|
251
|
+
timed_out: false, aborted: false, started_at: 'now', finished_at: 'now',
|
|
252
|
+
stdout: 'Tests run: 0, Failures: 0, Errors: 0, Skipped: 0', stderr: '' } }
|
|
253
|
+
assert.match(verificationBlockers(state, state.flow.config).join(' '), /no nonzero Surefire/)
|
|
254
|
+
})
|
|
255
255
|
|
|
256
256
|
test('派发立即反映进行中,重派不复用旧审核且保持单一进行项', async () => {
|
|
257
257
|
const f = fixture()
|