@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.
- package/.adaptive-test.mjs +257 -178
- 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 +67 -61
- package/.workflow-test.mjs +51 -21
- package/README.md +216 -114
- package/docs/development.md +45 -53
- package/docs/manual.html +176 -127
- package/hooks/commit-msg +18 -0
- package/lib/client.js +261 -37
- package/lib/client.js.map +2 -2
- package/lib/controller.d.ts +42 -0
- package/lib/controller.js +234 -1
- package/lib/controller.js.map +1 -1
- package/lib/dev-task.js +93 -50
- package/lib/dev-task.js.map +1 -1
- package/lib/engine.d.ts +4 -0
- package/lib/engine.js +6 -0
- package/lib/engine.js.map +1 -1
- package/lib/project-init.d.ts +11 -0
- package/lib/project-init.js +32 -1
- package/lib/project-init.js.map +1 -1
- package/lib/sonar-report.d.ts +1 -1
- package/lib/sonar-report.js +13 -0
- package/lib/sonar-report.js.map +1 -1
- package/lib/sonar.d.ts +12 -0
- package/lib/sonar.js +8 -0
- package/lib/sonar.js.map +1 -1
- package/lib/sonarlint-local.d.ts +2 -0
- package/lib/sonarlint-local.js +11 -1
- package/lib/sonarlint-local.js.map +1 -1
- package/lib/verification-tests.d.ts +10 -0
- package/lib/verification-tests.js +15 -0
- package/lib/verification-tests.js.map +1 -0
- package/package.json +10 -10
- package/scripts/verify-package.mjs +3 -1
- package/skills/code-review/SKILL.md +3 -1
- package/skills/eng-delivery/SKILL.md +5 -4
- package/skills/task-orchestration/SKILL.md +1 -1
- package/skills/test-validation/SKILL.md +3 -1
- package/docs/BRIEF-FOR-REVIEW.md +0 -163
- package/docs/CHANGELOG.md +0 -421
- package/docs/README.md +0 -42
- package/docs/adaptive-workflows.md +0 -88
- package/docs/assets/workflow-banner.svg +0 -29
- package/docs/configuration.md +0 -80
- package/docs/faq.md +0 -65
- package/docs/getting-started.md +0 -55
- package/docs/listing/godv61__dsh-task-engine.yml +0 -6
- package/docs/listing/submission.md +0 -84
- package/docs/manual-legacy.html +0 -380
- package/docs/releases/0.23.0.md +0 -32
- package/docs/releases/0.23.1.md +0 -58
- package/docs/releases/0.23.2.md +0 -21
- package/docs/resource-install.md +0 -64
- package/docs/roadmap.md +0 -33
- 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
- package/docs/testing/0.23.0//346/265/213/350/257/225/346/212/245/345/221/212.md +0 -42
- package/docs/testing/0.23.1//346/265/213/350/257/225/346/212/245/345/221/212.md +0 -34
- 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
- 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
- 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
- package/docs/testing/0.23.2//346/265/213/350/257/225/346/212/245/345/221/212.md +0 -65
- 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
- package/docs/workflow-regression.md +0 -36
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
|
+
})
|
|
@@ -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
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
+
})
|
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 } 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
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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 () => {
|