@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.
@@ -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('workbench initializes reviewed project resources directly and preserves Sonar settings', async () => {
345
- const f = fixture({
346
- 'backend/pom.xml': '<project><properties><java.version>8</java.version></properties></project>',
347
- 'frontend/package.json': '{"dependencies":{"vue":"^2.7.0"}}',
348
- '.dsh/meta.json': JSON.stringify({ sonar: { enabled: false, project_key: 'keep-me' } }),
349
- })
350
- const resources = [
351
- { kind: 'rule', name: 'backend-convention', content: 'Use the backend module Maven Java 8 build for backend changes.' },
352
- { kind: 'skill', name: 'adaptive-project-project-map', description: 'Reusable repository map',
353
- content: 'Root package.json, backend/pom.xml and frontend/package.json define the backend and frontend modules.',
354
- meta_skills: ['requirements-analysis', 'code-development'], rules: ['backend-convention'] },
355
- ]
356
- const receiver = {
357
- authorizedPath: async path => path, fs: () => f.fs, listSkills: async () => ({ skills: [] }),
358
- projectInitRoot: Controller.prototype.projectInitRoot,
359
- previewProjectInit: Controller.prototype.previewProjectInit,
360
- ctx: { get(name) {
361
- if (name === 'llm') return { async *stream() { yield { type: 'text-delta', text: JSON.stringify({ resources }) } } }
362
- if (name === 'agentDefaultModel') return { currentSelection: () => ({ provider: 'test', model: 'test' }) }
363
- } },
364
- }
365
- const draft = await Controller.prototype.generateProjectInit.call(receiver, { path: f.cwd })
366
- assert.deepEqual(draft.project_map_coverage, ['backend', 'frontend', 'package.json'])
367
- assert.equal(f.files.has(join(f.cwd, '.dsh/rules/backend-convention.md')), false)
368
- await assert.rejects(Controller.prototype.applyProjectInit.call(receiver,
369
- { path: f.cwd, resources: draft.resources, expected_hash: 'stale', existing_hash: draft.existing_hash }), /改变/)
370
- const applied = await Controller.prototype.applyProjectInit.call(receiver,
371
- { path: f.cwd, resources: draft.resources, expected_hash: draft.expected_hash, existing_hash: draft.existing_hash })
372
- assert.equal(applied.ok, true)
373
- assert.equal(f.files.get(join(f.cwd, '.dsh/meta.json')).includes('keep-me'), true)
374
- assert.equal(JSON.parse(f.files.get(join(f.cwd, '.dsh/meta.json'))).meta_bindings['code-development'][0],
375
- 'adaptive-project-project-map')
376
- assert.ok(f.files.has(join(f.cwd, '.dsh/skills/adaptive-project-project-map/SKILL.md')))
377
- await assert.rejects(Controller.prototype.previewProjectInit.call(receiver,
378
- { path: f.cwd, resources }), /不能覆盖/)
379
- })
380
-
381
- test('a local false positive needs human approval, keeps the raw gate, and expires after code changes', async () => {
382
- let allow = false
383
- const approvals = []
384
- const shell = { resolve: request => request, execute: async () => ({ result: async () => ({
385
- exitCode: 0, timedOut: false, aborted: false, stdout: { text: 'checks passed' }, stderr: { text: '' },
386
- }) }) }
387
- const f = fixture({ '.dsh/meta.json': JSON.stringify({ sonar: { enabled: true,
388
- host_url: 'https://sonar.example', project_key: 'project', mode: 'branch', source: 'ide-local',
389
- reference_branch: 'main' } }) }, { shell,
390
- approval: { request: async request => { approvals.push(request); return allow ? 'allowed-once' : 'rejected' } } })
391
- await f.call({ operation: 'create', task_id: 'FP-1', title: 'one local finding', branch: 'feature',
392
- complexity: 'low', complexity_reason: 'Local correction', files: ['app.js'] })
393
- await f.call({ operation: 'verify', task_id: 'FP-1', command: 'check' })
394
- const state = f.state('FP-1')
395
- state.stage = '代码审核'
396
- const finding = { key: 'local-fp-1', rule: 'mycompany-java:AvoidObjectCreationInLoop',
397
- message: 'Avoid allocation', severity: 'MAJOR', file: 'app.js', line: 3 }
398
- state.sonar_audit = { ce_task_id: 'local', analysis_id: 'LOCAL-1', gate: 'ERROR',
399
- checked_at: '2026-10-05T00:00:00.000Z', scope_hash: state.verification.receipt.scope_hash,
400
- findings: [finding], blocking: [finding], target: 'main', scanned_files: ['app.js'],
401
- report_path: '.dsh/reviews/FP-1/local.md' }
402
- f.files.set(join(f.cwd, '.dsh/reviews/FP-1/local.md'), 'original report')
403
- f.files.set(join(f.cwd, '.dsh/task-FP-1.json'), JSON.stringify(state))
404
- const args = { operation: 'sonar_disposition', task_id: 'FP-1', issue_key: finding.key,
405
- disposition_reason: '每次循环需要独立实体对象,复用同一个实例会让多条授权记录互相覆盖,属于规则误报。',
406
- evidence: ['授权实体逐行添加到集合,复用对象会导致所有行指向末次值'] }
407
- await assert.rejects(f.call(args), /not approved/)
408
- assert.equal(f.state('FP-1').sonar_audit.dispositions, undefined)
409
- allow = true
410
- const result = JSON.parse(await f.call(args))
411
- assert.equal(result.raw_gate, 'ERROR')
412
- assert.equal(result.review_gate, 'OK')
413
- assert.equal(f.state('FP-1').sonar_audit.dispositions.length, 1)
414
- assert.deepEqual(JSON.parse(await f.call({ operation: 'status', task_id: 'FP-1' })).rule_learning_candidates, [])
415
- assert.match(approvals.at(-1).reason, /local-fp-1/)
416
- assert.match(f.files.get(join(f.cwd, state.sonar_audit.report_path)), /逐项误报复核/)
417
- await f.call({ operation: 'review', task_id: 'FP-1', outcome: 'pass' })
418
- await assert.rejects(f.call({ operation: 'learn_rule', task_id: 'FP-1', issue_key: finding.key }), /blocking issue_key/)
419
- f.files.set(join(f.cwd, 'app.js'), 'new source')
420
- await assert.rejects(f.call({ operation: 'review', task_id: 'FP-1', outcome: 'pass' }), /stale/)
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
+ })
@@ -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) {
@@ -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
+ })
@@ -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-'))
@@ -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()