@godv61/dsh-task-engine 0.29.8 → 0.30.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/.adaptive-test.mjs +257 -178
  2. package/.evidence-test.mjs +16 -16
  3. package/.resource-test.mjs +19 -19
  4. package/.roundtrip-test.mjs +5 -5
  5. package/.sonar-credential-test.mjs +38 -38
  6. package/.sonarlint-local-test.mjs +67 -61
  7. package/.workflow-test.mjs +51 -21
  8. package/README.md +216 -114
  9. package/docs/development.md +45 -53
  10. package/docs/manual.html +176 -127
  11. package/hooks/commit-msg +18 -0
  12. package/lib/client.js +261 -37
  13. package/lib/client.js.map +2 -2
  14. package/lib/controller.d.ts +42 -0
  15. package/lib/controller.js +234 -1
  16. package/lib/controller.js.map +1 -1
  17. package/lib/dev-task.js +93 -50
  18. package/lib/dev-task.js.map +1 -1
  19. package/lib/engine.d.ts +4 -0
  20. package/lib/engine.js +6 -0
  21. package/lib/engine.js.map +1 -1
  22. package/lib/project-init.d.ts +11 -0
  23. package/lib/project-init.js +32 -1
  24. package/lib/project-init.js.map +1 -1
  25. package/lib/sonar-report.d.ts +1 -1
  26. package/lib/sonar-report.js +13 -0
  27. package/lib/sonar-report.js.map +1 -1
  28. package/lib/sonar.d.ts +12 -0
  29. package/lib/sonar.js +8 -0
  30. package/lib/sonar.js.map +1 -1
  31. package/lib/sonarlint-local.d.ts +2 -0
  32. package/lib/sonarlint-local.js +11 -1
  33. package/lib/sonarlint-local.js.map +1 -1
  34. package/lib/verification-tests.d.ts +10 -0
  35. package/lib/verification-tests.js +15 -0
  36. package/lib/verification-tests.js.map +1 -0
  37. package/package.json +10 -10
  38. package/scripts/verify-package.mjs +3 -1
  39. package/skills/code-review/SKILL.md +3 -1
  40. package/skills/eng-delivery/SKILL.md +5 -4
  41. package/skills/task-orchestration/SKILL.md +1 -1
  42. package/skills/test-validation/SKILL.md +3 -1
  43. package/docs/BRIEF-FOR-REVIEW.md +0 -163
  44. package/docs/CHANGELOG.md +0 -421
  45. package/docs/README.md +0 -42
  46. package/docs/adaptive-workflows.md +0 -88
  47. package/docs/assets/workflow-banner.svg +0 -29
  48. package/docs/configuration.md +0 -80
  49. package/docs/faq.md +0 -65
  50. package/docs/getting-started.md +0 -55
  51. package/docs/listing/godv61__dsh-task-engine.yml +0 -6
  52. package/docs/listing/submission.md +0 -84
  53. package/docs/manual-legacy.html +0 -380
  54. package/docs/releases/0.23.0.md +0 -32
  55. package/docs/releases/0.23.1.md +0 -58
  56. package/docs/releases/0.23.2.md +0 -21
  57. package/docs/resource-install.md +0 -64
  58. package/docs/roadmap.md +0 -33
  59. package/docs/testing/0.23.0//346/265/213/350/257/225/346/211/247/350/241/214/350/256/260/345/275/225.md +0 -189
  60. package/docs/testing/0.23.0//346/265/213/350/257/225/346/212/245/345/221/212.md +0 -42
  61. package/docs/testing/0.23.1//346/265/213/350/257/225/346/212/245/345/221/212.md +0 -34
  62. package/docs/testing/0.23.1//350/207/252/345/212/250/345/214/226/346/265/213/350/257/225/346/230/216/347/273/206.md +0 -41
  63. package/docs/testing/0.23.2/R02/344/270/232/345/212/241/346/265/213/350/257/225/346/230/216/347/273/206.md +0 -56
  64. package/docs/testing/0.23.2/R03/344/270/232/345/212/241/346/265/213/350/257/225/346/230/216/347/273/206.md +0 -38
  65. package/docs/testing/0.23.2//346/265/213/350/257/225/346/212/245/345/221/212.md +0 -65
  66. package/docs/testing/0.23.2//350/207/252/345/212/250/345/214/226/346/265/213/350/257/225/346/230/216/347/273/206.md +0 -43
  67. package/docs/workflow-regression.md +0 -36
@@ -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
+ })
@@ -1,65 +1,71 @@
1
- import assert from 'node:assert/strict'
2
- import { execFileSync } from 'node:child_process'
3
- import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'
4
- import { tmpdir } from 'node:os'
5
- import { join } from 'node:path'
6
- import { test } from 'node:test'
7
- import { changedLines, uncoveredLocalFiles } from './lib/sonarlint-local.js'
8
- import { isIncludedAuditPath, validAuditIncludePaths } from './lib/sonar.js'
9
- import { renderSonarReport, sonarReportPath } from './lib/sonar-report.js'
10
-
11
- test('project scan roots mirror Maven backend scope without including the Vue app', () => {
12
- const roots = ['fp-bussiness', 'fp-ruoyi', 'fp-ruoyi-spring-boot', 'pom.xml']
13
- assert.equal(validAuditIncludePaths(roots), true)
14
- assert.equal(isIncludedAuditPath('fp-bussiness/src/main/java/A.java', roots), true)
15
- assert.equal(isIncludedAuditPath('fp-ruoyi-ui/src/views/auth.vue', roots), false)
16
- assert.equal(validAuditIncludePaths(['../outside']), false)
17
- assert.equal(validAuditIncludePaths(['C:/outside']), false)
18
- })
19
-
20
- test('one review gets a credential-free Markdown report path and issue details', () => {
21
- const audit = { analysis_id: 'abc12345-0000', checked_at: '2026-10-03T10:11:12.000Z',
22
- target: 'qdm_gw_chemical_260930', gate: 'ERROR', findings: [{key:'x',rule:'java:S103',
23
- severity:'MAJOR',file:'A.java',line:12,message:'Line too long'}], blocking:[{key:'x'}],
24
- scanned_files:['A.java'],uncovered_files:[] }
25
- const path = sonarReportPath('QMS-1', audit)
26
- assert.match(path, /^\.dsh\/reviews\/QMS-1\/2026-/)
27
- const report = renderSonarReport('QMS-1', {source:'ide-local',project_key:'project',include_paths:['fp-bussiness']}, audit)
28
- assert.match(report, /java:S103/)
29
- assert.match(report, /A.java:12/)
30
- assert.equal(report.includes('token-secret'),false)
31
- })
32
-
1
+ import assert from 'node:assert/strict'
2
+ import { execFileSync } from 'node:child_process'
3
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'
4
+ import { tmpdir } from 'node:os'
5
+ import { join } from 'node:path'
6
+ import { test } from 'node:test'
7
+ import { changedLines, missingLocalAuditScope, uncoveredLocalFiles } from './lib/sonarlint-local.js'
8
+ import { isIncludedAuditPath, validAuditIncludePaths } from './lib/sonar.js'
9
+ import { renderSonarReport, sonarReportPath } from './lib/sonar-report.js'
10
+
11
+ test('project scan roots mirror Maven backend scope without including the Vue app', () => {
12
+ const roots = ['fp-bussiness', 'fp-ruoyi', 'fp-ruoyi-spring-boot', 'pom.xml']
13
+ assert.equal(validAuditIncludePaths(roots), true)
14
+ assert.equal(isIncludedAuditPath('fp-bussiness/src/main/java/A.java', roots), true)
15
+ assert.equal(isIncludedAuditPath('fp-ruoyi-ui/src/views/auth.vue', roots), false)
16
+ assert.equal(validAuditIncludePaths(['../outside']), false)
17
+ assert.equal(validAuditIncludePaths(['C:/outside']), false)
18
+ })
19
+
20
+ test('one review gets a credential-free Markdown report path and issue details', () => {
21
+ const audit = { analysis_id: 'abc12345-0000', checked_at: '2026-10-03T10:11:12.000Z',
22
+ target: 'qdm_gw_chemical_260930', gate: 'ERROR', findings: [{key:'x',rule:'java:S103',
23
+ severity:'MAJOR',file:'A.java',line:12,message:'Line too long'}], blocking:[{key:'x'}],
24
+ scanned_files:['A.java'],uncovered_files:[] }
25
+ const path = sonarReportPath('QMS-1', audit)
26
+ assert.match(path, /^\.dsh\/reviews\/QMS-1\/2026-/)
27
+ const report = renderSonarReport('QMS-1', {source:'ide-local',project_key:'project',include_paths:['fp-bussiness']}, audit)
28
+ assert.match(report, /java:S103/)
29
+ assert.match(report, /A.java:12/)
30
+ assert.equal(report.includes('token-secret'),false)
31
+ })
32
+
33
33
  test('unsupported files block only when the project has active rules for their language', () => {
34
- const paths = ['migration.sql', 'settings.json', 'script.py']
35
- const profiles = [
36
- { language: 'java', activeRuleCount: 66 },
37
- { language: 'json', activeRuleCount: 0 },
38
- { language: 'py', activeRuleCount: 3 },
39
- ]
40
- assert.deepEqual(uncoveredLocalFiles(paths, profiles), ['script.py'])
41
- assert.deepEqual(uncoveredLocalFiles(paths, [...profiles, { language: 'plsql', activeRuleCount: 5 }]),
42
- ['migration.sql', 'script.py'])
34
+ const paths = ['migration.sql', 'settings.json', 'script.py']
35
+ const profiles = [
36
+ { language: 'java', activeRuleCount: 66 },
37
+ { language: 'json', activeRuleCount: 0 },
38
+ { language: 'py', activeRuleCount: 3 },
39
+ ]
40
+ assert.deepEqual(uncoveredLocalFiles(paths, profiles), ['script.py'])
41
+ assert.deepEqual(uncoveredLocalFiles(paths, [...profiles, { language: 'plsql', activeRuleCount: 5 }]),
42
+ ['migration.sql', 'script.py'])
43
43
  })
44
44
 
45
- test('local Sonar audit considers Git new code but excludes generated DSH records', async () => {
46
- const root = mkdtempSync(join(tmpdir(), 'dsh-sonarlint-diff-test-'))
47
- const git = (...args) => execFileSync('git', args, { cwd: root, stdio: 'pipe' })
48
- try {
49
- git('init', '--quiet')
50
- writeFileSync(join(root, 'Sample.java'), 'class Sample {}\n')
51
- git('add', 'Sample.java')
52
- git('-c', 'user.name=Test', '-c', 'user.email=test@example.invalid', 'commit', '--quiet', '-m', 'baseline')
53
- writeFileSync(join(root, 'Sample.java'), 'class Sample {}\n// new code\n')
54
- writeFileSync(join(root, 'change.sql'), 'SELECT 1;\n')
55
- mkdirSync(join(root, '.dsh'))
56
- writeFileSync(join(root, '.dsh', 'task.json'), '{"generated":true}\n')
57
- const changed = await changedLines(root, 'HEAD')
58
- assert.deepEqual(changed.map(file => file.path).sort(), ['Sample.java', 'change.sql'])
59
- assert.deepEqual([...changed.find(file => file.path === 'Sample.java').lines], [2])
60
- assert.equal(changed.find(file => file.path === 'change.sql').newFile, true)
61
- assert.deepEqual((await changedLines(root, 'HEAD', ['Sample.java'])).map(file => file.path), ['Sample.java'])
62
- } finally {
63
- rmSync(root, { recursive: true, force: true })
64
- }
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']), [])
65
49
  })
50
+
51
+ test('local Sonar audit considers Git new code but excludes generated DSH records', async () => {
52
+ const root = mkdtempSync(join(tmpdir(), 'dsh-sonarlint-diff-test-'))
53
+ const git = (...args) => execFileSync('git', args, { cwd: root, stdio: 'pipe' })
54
+ try {
55
+ git('init', '--quiet')
56
+ writeFileSync(join(root, 'Sample.java'), 'class Sample {}\n')
57
+ git('add', 'Sample.java')
58
+ git('-c', 'user.name=Test', '-c', 'user.email=test@example.invalid', 'commit', '--quiet', '-m', 'baseline')
59
+ writeFileSync(join(root, 'Sample.java'), 'class Sample {}\n// new code\n')
60
+ writeFileSync(join(root, 'change.sql'), 'SELECT 1;\n')
61
+ mkdirSync(join(root, '.dsh'))
62
+ writeFileSync(join(root, '.dsh', 'task.json'), '{"generated":true}\n')
63
+ const changed = await changedLines(root, 'HEAD')
64
+ assert.deepEqual(changed.map(file => file.path).sort(), ['Sample.java', 'change.sql'])
65
+ assert.deepEqual([...changed.find(file => file.path === 'Sample.java').lines], [2])
66
+ assert.equal(changed.find(file => file.path === 'change.sql').newFile, true)
67
+ assert.deepEqual((await changedLines(root, 'HEAD', ['Sample.java'])).map(file => file.path), ['Sample.java'])
68
+ } finally {
69
+ rmSync(root, { recursive: true, force: true })
70
+ }
71
+ })
@@ -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 } 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'
@@ -71,10 +71,10 @@ function fixture(stage = '开发', extra = {}, services = {}) {
71
71
  const ctx = { fs, tools: { register(tool) { execute = tool.execute; return () => {} } }, get(name) {
72
72
  if (Object.hasOwn(services, name)) return services[name]
73
73
  if (name === 'sandboxPolicy') return { resolve(request) { assert.equal(request.session, session); return policy } }
74
- if (name === 'shell') return { resolve(request) { runs.push(request); return request }, async execute(request) {
75
- const fail = request.command === 'fail'
76
- const outcome = { exitCode: fail ? 1 : 0, timedOut: false, aborted: false, sandbox: { mode: 'workspace-write', denied: false }, stdout: { text: request.command.startsWith('git -c core.quotepath=false log') ? 'abcdef1234567890\n【LIVE-1】【TASK】done\n\napp.js\n' : 'checks passed' }, stderr: { text: '' } }
77
- return { result: async () => outcome }
74
+ if (name === 'shell') return { resolve(request) { runs.push(request); return request }, async execute(request) {
75
+ const fail = request.command === 'fail'
76
+ const outcome = { exitCode: fail ? 1 : 0, timedOut: false, aborted: false, sandbox: { mode: 'workspace-write', denied: false }, stdout: { text: request.command.startsWith('git -c core.quotepath=false log') ? 'abcdef1234567890\n【LIVE-1】【TASK】done\n\napp.js\n' : 'checks passed' }, stderr: { text: '' } }
77
+ return { result: async () => outcome }
78
78
  } }
79
79
  return undefined
80
80
  } }
@@ -194,7 +194,7 @@ test('审查通过后更新状态并激活下一项,保留已完成项的审
194
194
  await assert.rejects(f.call({ operation: 'items', items: [{ id: 'A', title: 'x' }, { id: 'A', title: 'y' }] }), /unique/)
195
195
  })
196
196
 
197
- test('追加修复项可按 id 保留旧标题,拒绝已完成项改名后静默丢审核', async () => {
197
+ test('追加修复项可按 id 保留旧标题,拒绝已完成项改名后静默丢审核', async () => {
198
198
  const f = fixture()
199
199
  await f.call({ operation: 'items', items: [{ id: 'A', title: 'reviewed models', status: 'doing' }] })
200
200
  await f.call({ operation: 'review_item', item_id: 'A', spec_outcome: 'pass', quality_outcome: 'pass' })
@@ -204,23 +204,53 @@ test('追加修复项可按 id 保留旧标题,拒绝已完成项改名后静
204
204
  await assert.rejects(f.call({ operation: 'items', items: [{ id: 'A', title: 'rewritten summary', status: 'done' }, { id: 'B', status: 'doing' }] }), /omit title|reopen/)
205
205
  assert.equal(f.state().items[0].review.quality.outcome, 'pass')
206
206
  assert.equal(f.state().items.length, 2)
207
- await assert.rejects(f.call({ operation: 'items', items: [{ id: 'NEW' }] }), /title/)
208
- })
209
-
210
- test('重排实施项时不静默丢弃已完成项和审查记录', async () => {
211
- const f = fixture()
212
- await f.call({ operation: 'items', items: [
213
- { id: 'I1', title: 'models', status: 'doing' },
214
- { id: 'I2', title: 'mapper', status: 'todo' },
215
- ] })
216
- await f.call({ operation: 'dispatch', item_id: 'I1', description: 'direct implementation' })
217
- await f.call({ operation: 'review_item', item_id: 'I1', spec_outcome: 'pass', quality_outcome: 'pass' })
218
- await f.call({ operation: 'items', items: [{ id: 'I1', status: 'done' }, { id: 'I2' }] })
207
+ await assert.rejects(f.call({ operation: 'items', items: [{ id: 'NEW' }] }), /title/)
208
+ })
209
+
210
+ test('重排实施项时不静默丢弃已完成项和审查记录', async () => {
211
+ const f = fixture()
212
+ await f.call({ operation: 'items', items: [
213
+ { id: 'I1', title: 'models', status: 'doing' },
214
+ { id: 'I2', title: 'mapper', status: 'todo' },
215
+ ] })
216
+ await f.call({ operation: 'dispatch', item_id: 'I1', description: 'direct implementation' })
217
+ await f.call({ operation: 'review_item', item_id: 'I1', spec_outcome: 'pass', quality_outcome: 'pass' })
218
+ await f.call({ operation: 'items', items: [{ id: 'I1', status: 'done' }, { id: 'I2' }] })
219
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' }] })
220
225
  assert.deepEqual(f.state().items.map(item => item.id), ['I3', 'I1'])
221
- assert.equal(f.state().items[1].status, 'done')
222
- assert.equal(f.state().items[1].review.quality.outcome, 'pass')
223
- assert.equal(f.state().items[1].dispatch.description, 'direct implementation')
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/)
224
254
  })
225
255
 
226
256
  test('派发立即反映进行中,重派不复用旧审核且保持单一进行项', async () => {