@godv61/dsh-task-engine 0.29.3 → 0.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.adaptive-test.mjs +380 -185
- package/.codex-project-test.mjs +18 -18
- package/.evidence-test.mjs +14 -1
- package/.hook-test.mjs +19 -19
- package/.resource-test.mjs +17 -0
- package/.roundtrip-test.mjs +8 -3
- package/.sonar-credential-test.mjs +38 -0
- package/.sonarlint-local-test.mjs +71 -0
- package/.workflow-test.mjs +56 -9
- package/README.md +35 -112
- package/docs/development.md +45 -53
- package/docs/manual.html +140 -84
- package/hooks/commit-msg +19 -1
- package/lib/adaptive.js +1 -1
- package/lib/adaptive.js.map +1 -1
- package/lib/client.js +297 -9
- package/lib/client.js.map +1 -1
- package/lib/controller.d.ts +44 -0
- package/lib/controller.js +73 -0
- package/lib/controller.js.map +1 -1
- package/lib/dev-task.d.ts +2 -0
- package/lib/dev-task.js +342 -42
- package/lib/dev-task.js.map +1 -1
- package/lib/engine.d.ts +5 -1
- package/lib/engine.js +6 -0
- package/lib/engine.js.map +1 -1
- package/lib/hook.js +3 -3
- package/lib/hook.js.map +1 -1
- package/lib/project-init.d.ts +4 -0
- package/lib/project-init.js +21 -2
- package/lib/project-init.js.map +1 -1
- package/lib/skill-audit.js +9 -1
- package/lib/skill-audit.js.map +1 -1
- package/lib/sonar-credential.d.ts +17 -0
- package/lib/sonar-credential.js +17 -0
- package/lib/sonar-credential.js.map +1 -0
- package/lib/sonar-report.d.ts +4 -0
- package/lib/sonar-report.js +53 -0
- package/lib/sonar-report.js.map +1 -0
- package/lib/sonar.d.ts +35 -2
- package/lib/sonar.js +63 -4
- package/lib/sonar.js.map +1 -1
- package/lib/sonarlint-local.d.ts +17 -0
- package/lib/sonarlint-local.js +317 -0
- package/lib/sonarlint-local.js.map +1 -0
- 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 +6 -4
- package/preset/enable.mjs +2 -2
- package/preset/persona.md +4 -4
- package/scripts/verify-dsh-compat.mjs +14 -14
- package/scripts/verify-package.mjs +10 -8
- package/skills/architecture-design/SKILL.md +11 -11
- package/skills/code-development/SKILL.md +11 -11
- package/skills/code-review/SKILL.md +12 -10
- package/skills/eng-delivery/SKILL.md +10 -7
- package/skills/requirements-analysis/SKILL.md +11 -11
- package/skills/task-orchestration/SKILL.md +11 -11
- package/skills/test-validation/SKILL.md +11 -9
- package/docs/BRIEF-FOR-REVIEW.md +0 -163
- package/docs/CHANGELOG.md +0 -407
- package/docs/README.md +0 -42
- package/docs/adaptive-workflows.md +0 -76
- package/docs/assets/workflow-banner.svg +0 -29
- package/docs/configuration.md +0 -80
- package/docs/faq.md +0 -59
- 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/.adaptive-test.mjs
CHANGED
|
@@ -1,189 +1,384 @@
|
|
|
1
|
-
import test from 'node:test'
|
|
2
|
-
import assert from 'node:assert/strict'
|
|
3
|
-
import { join, resolve } from 'node:path'
|
|
4
|
-
import { mkdtempSync, rmSync } from 'node:fs'
|
|
5
|
-
import { tmpdir } from 'node:os'
|
|
6
|
-
import { adaptiveWorkflow, COMPLEXITY_OPTIONS } from './lib/adaptive.js'
|
|
7
|
-
import { validateWorkflow } from './lib/engine.js'
|
|
8
|
-
import { registerDevTask } from './lib/dev-task.js'
|
|
9
|
-
import { inspectSonar } from './lib/sonar.js'
|
|
10
|
-
import Controller from './lib/controller.js'
|
|
11
|
-
|
|
12
|
-
const home = mkdtempSync(join(tmpdir(), 'dsh-adaptive-'))
|
|
13
|
-
process.env.DSH_HOME = home
|
|
14
|
-
process.on('exit', () => rmSync(home, { recursive: true, force: true }))
|
|
15
|
-
|
|
16
|
-
function fixture(extra = {}) {
|
|
17
|
-
const cwd = resolve('adaptive-project')
|
|
18
|
-
const files = new Map([[join(cwd, 'package.json'), '{"name":"adaptive-project","dependencies":{"vue":"^2.7.0"}}'],
|
|
19
|
-
[join(cwd, '.git/HEAD'), 'ref: refs/heads/feature'] , ...Object.entries(extra).map(([p, content]) => [join(cwd, p), content])])
|
|
20
|
-
const events = []
|
|
21
|
-
const session = { id: 'adaptive', header: { cwd }, snapshotEvents: () => events }
|
|
22
|
-
const key = (path, options) => join(options?.cwd ?? cwd, path)
|
|
23
|
-
let execute
|
|
24
|
-
const fs = {
|
|
25
|
-
resolve: async (path, options) => ({ path: key(path, options) }),
|
|
26
|
-
readText: async target => files.get(target.path),
|
|
27
|
-
lstat: async (path, options) => files.has(key(path, options)) ? { version: 'v1' } : undefined,
|
|
28
|
-
writeText: async (target, content, intent) => {
|
|
29
|
-
if (intent?.kind === 'createIfAbsent' && files.has(target.path)) throw new Error('already exists')
|
|
30
|
-
files.set(target.path, content)
|
|
31
|
-
},
|
|
32
|
-
listDir: async target => [...new Set([...files.keys()].filter(path => path.startsWith(target.path + '\\'))
|
|
33
|
-
.map(path => path.slice(target.path.length + 1).split('\\')[0]))].map(name => ({ name })),
|
|
34
|
-
}
|
|
35
|
-
const ctx = { fs, tools: { register(tool) { execute = tool.execute; return () => {} } }, get(name) {
|
|
36
|
-
if (name
|
|
37
|
-
return
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
assert.deepEqual(
|
|
49
|
-
assert.
|
|
50
|
-
assert.ok(configs[
|
|
51
|
-
assert.ok(configs.
|
|
52
|
-
assert.ok(configs.every(config => config.
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
'.dsh/skills/requirements-analysis/
|
|
59
|
-
'.dsh/
|
|
60
|
-
'.dsh/
|
|
61
|
-
'.dsh/
|
|
62
|
-
'.dsh/skills/adaptive-project-project-map/
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
assert.
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
assert.equal(state.flow.
|
|
72
|
-
assert.
|
|
73
|
-
assert.
|
|
74
|
-
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
assert.
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
assert.
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
assert.ok(
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { join, resolve } from 'node:path'
|
|
4
|
+
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
|
5
|
+
import { tmpdir } from 'node:os'
|
|
6
|
+
import { adaptiveWorkflow, COMPLEXITY_OPTIONS } from './lib/adaptive.js'
|
|
7
|
+
import { validateWorkflow } from './lib/engine.js'
|
|
8
|
+
import { plannedItemIds, registerDevTask } from './lib/dev-task.js'
|
|
9
|
+
import { assertLocalSonarReady, inspectSonar, validLocalScanCommand } from './lib/sonar.js'
|
|
10
|
+
import Controller from './lib/controller.js'
|
|
11
|
+
|
|
12
|
+
const home = mkdtempSync(join(tmpdir(), 'dsh-adaptive-'))
|
|
13
|
+
process.env.DSH_HOME = home
|
|
14
|
+
process.on('exit', () => rmSync(home, { recursive: true, force: true }))
|
|
15
|
+
|
|
16
|
+
function fixture(extra = {}, services = {}) {
|
|
17
|
+
const cwd = resolve('adaptive-project')
|
|
18
|
+
const files = new Map([[join(cwd, 'package.json'), '{"name":"adaptive-project","dependencies":{"vue":"^2.7.0"}}'],
|
|
19
|
+
[join(cwd, '.git/HEAD'), 'ref: refs/heads/feature'] , ...Object.entries(extra).map(([p, content]) => [join(cwd, p), content])])
|
|
20
|
+
const events = []
|
|
21
|
+
const session = { id: 'adaptive', header: { cwd }, snapshotEvents: () => events }
|
|
22
|
+
const key = (path, options) => join(options?.cwd ?? cwd, path)
|
|
23
|
+
let execute
|
|
24
|
+
const fs = {
|
|
25
|
+
resolve: async (path, options) => ({ path: key(path, options) }),
|
|
26
|
+
readText: async target => files.get(target.path),
|
|
27
|
+
lstat: async (path, options) => files.has(key(path, options)) ? { version: 'v1' } : undefined,
|
|
28
|
+
writeText: async (target, content, intent) => {
|
|
29
|
+
if (intent?.kind === 'createIfAbsent' && files.has(target.path)) throw new Error('already exists')
|
|
30
|
+
files.set(target.path, content)
|
|
31
|
+
},
|
|
32
|
+
listDir: async target => [...new Set([...files.keys()].filter(path => path.startsWith(target.path + '\\'))
|
|
33
|
+
.map(path => path.slice(target.path.length + 1).split('\\')[0]))].map(name => ({ name })),
|
|
34
|
+
}
|
|
35
|
+
const ctx = { fs, tools: { register(tool) { execute = tool.execute; return () => {} } }, get(name) {
|
|
36
|
+
if (name in services) return services[name]
|
|
37
|
+
if (name === 'sandboxPolicy') return { resolve: () => ({ mode: 'workspace-write', workspaceRoot: cwd, sessionId: session.id }) }
|
|
38
|
+
return undefined
|
|
39
|
+
} }
|
|
40
|
+
registerDevTask(ctx)
|
|
41
|
+
const exec = { agent: { session }, signal: new AbortController().signal }
|
|
42
|
+
return { cwd, files, fs, events, call: args => execute(args, exec), state: id => JSON.parse(files.get(join(cwd, `.dsh/task-${id}.json`))) }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
test('each task grade has a valid and increasing meta-skill sequence', () => {
|
|
46
|
+
assert.deepEqual(COMPLEXITY_OPTIONS.map(option => option.id), ['low', 'medium', 'high', 'ultra'])
|
|
47
|
+
const configs = COMPLEXITY_OPTIONS.map(option => adaptiveWorkflow(option.id))
|
|
48
|
+
for (const config of configs) assert.deepEqual(validateWorkflow(config), [])
|
|
49
|
+
assert.deepEqual(configs.map(config => config.stages.length), [4, 5, 6, 7])
|
|
50
|
+
assert.ok(configs[2].stages.includes('任务编排'))
|
|
51
|
+
assert.ok(configs[3].stages.includes('架构设计'))
|
|
52
|
+
assert.ok(configs.every(config => config.transitions.some(edge => edge.requires?.includes('verified'))))
|
|
53
|
+
assert.ok(configs.every(config => config.commit.checkpoints.includes('测试')))
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('adaptive task freezes a project override and its own Rules, not the bundled same-name Skill', async () => {
|
|
57
|
+
const f = fixture({
|
|
58
|
+
'.dsh/skills/requirements-analysis/SKILL.md': '# Project requirement method',
|
|
59
|
+
'.dsh/skills/requirements-analysis/profile.json': JSON.stringify({ rules: [{ source: 'project', name: 'team-scope' }], evidence: 'artifact' }),
|
|
60
|
+
'.dsh/rules/team-scope.md': '# Team scope rule',
|
|
61
|
+
'.dsh/meta.json': JSON.stringify({ meta_bindings: { 'code-development': ['adaptive-project-project-map'] } }),
|
|
62
|
+
'.dsh/skills/adaptive-project-project-map/SKILL.md': '# Project map',
|
|
63
|
+
'.dsh/skills/adaptive-project-project-map/profile.json': JSON.stringify({ rules: [], evidence: 'none' }),
|
|
64
|
+
})
|
|
65
|
+
const preview = JSON.parse(await f.call({ operation: 'assess', complexity: 'high' }))
|
|
66
|
+
assert.equal(preview.meta_skills['需求分析'][0].effective_source, 'project')
|
|
67
|
+
assert.ok(preview.meta_skills['代码开发'].some(skill => skill.logical_name === 'adaptive-project-project-map'))
|
|
68
|
+
await f.call({ operation: 'create', task_id: 'A-1', title: 'cross-module change', branch: 'feature',
|
|
69
|
+
complexity: 'high', complexity_reason: 'Several implementation steps depend on earlier interfaces', files: ['app.js'] })
|
|
70
|
+
const state = f.state('A-1')
|
|
71
|
+
assert.equal(state.flow.flow, 'adaptive-high')
|
|
72
|
+
assert.equal(state.flow.config.stage_bindings['需求分析'].skills[0].skill.source, 'project')
|
|
73
|
+
assert.ok(state.flow.resources.some(entry => entry.ref.name === 'team-scope' && entry.ref.source === 'project'))
|
|
74
|
+
assert.match(await f.call({ operation: 'load_skill', task_id: 'A-1', skill_name: 'project:requirements-analysis' }), /Project requirement method/)
|
|
75
|
+
await assert.rejects(f.call({ operation: 'load_skill', task_id: 'A-1', skill_name: 'bundled:requirements-analysis' }), /exact source:name/)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('init_project proposes project-specific Skills and binds reviewed resources', async () => {
|
|
79
|
+
const f = fixture()
|
|
80
|
+
const inventory = JSON.parse(await f.call({ operation: 'init_project', phase: 'inspect' }))
|
|
81
|
+
const projectMap = inventory.suggestions.find(entry => entry.name === 'adaptive-project-project-map')
|
|
82
|
+
assert.ok(projectMap)
|
|
83
|
+
assert.match(projectMap.why, /entire repository/)
|
|
84
|
+
assert.match(projectMap.why, /current feature request/)
|
|
85
|
+
assert.match(inventory.caution, /repository-wide/)
|
|
86
|
+
assert.ok(inventory.manifests.some(entry => entry.facts.includes('vue=^2.7.0')))
|
|
87
|
+
const resources = [
|
|
88
|
+
{ kind: 'rule', name: 'component-boundary', content: 'Keep component boundary calls behind the existing service interface.' },
|
|
89
|
+
{ kind: 'skill', name: 'adaptive-project-project-map', description: 'Project structure and entry points',
|
|
90
|
+
content: 'The root package.json declares Vue 2.7; inspect src for the actual component layout.',
|
|
91
|
+
meta_skills: ['requirements-analysis', 'code-development'], rules: ['component-boundary'] },
|
|
92
|
+
]
|
|
93
|
+
const proposed = JSON.parse(await f.call({ operation: 'init_project', phase: 'propose', resources }))
|
|
94
|
+
assert.match(proposed.instruction, /whole repository/)
|
|
95
|
+
assert.match(proposed.instruction, /current-feature details/)
|
|
96
|
+
assert.equal(f.files.has(join(f.cwd, '.dsh/meta.json')), false)
|
|
97
|
+
await f.call({ operation: 'init_project', phase: 'apply', resources, expected_hash: proposed.expected_hash,
|
|
98
|
+
existing_hash: proposed.existing_hash })
|
|
99
|
+
const meta = JSON.parse(f.files.get(join(f.cwd, '.dsh/meta.json')))
|
|
100
|
+
assert.deepEqual(meta.meta_bindings['code-development'], ['adaptive-project-project-map'])
|
|
101
|
+
assert.ok(f.files.has(join(f.cwd, '.dsh/skills/adaptive-project-project-map/profile.json')))
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
test('task-plan item headings preserve the full implementation sequence', async () => {
|
|
105
|
+
assert.deepEqual(plannedItemIds('## Steps\n- I1 data model\n- I2 service\n### I3 UI\n- I2 service'), ['I1', 'I2', 'I3'])
|
|
106
|
+
const f = fixture()
|
|
107
|
+
await f.call({ operation: 'create', task_id: 'PLAN-1', title: 'Cross-module change', branch: 'feature',
|
|
108
|
+
complexity: 'high', complexity_reason: 'Backend then frontend dependency' })
|
|
109
|
+
const state = f.state('PLAN-1')
|
|
110
|
+
state.stage = '任务编排'
|
|
111
|
+
state.artifacts['task-plan'] = { steps: '- I1 model\n- I2 service', dependencies: 'I2 after I1', handoffs: 'Model to service' }
|
|
112
|
+
state.items = [{ id: 'I2', title: 'service', status: 'todo' }]
|
|
113
|
+
f.files.set(join(f.cwd, '.dsh/task-PLAN-1.json'), JSON.stringify(state))
|
|
114
|
+
const status = JSON.parse(await f.call({ operation: 'status', task_id: 'PLAN-1' }))
|
|
115
|
+
assert.match(status.task_plan_item_blockers[0], /I1/)
|
|
116
|
+
await assert.rejects(f.call({ operation: 'advance', task_id: 'PLAN-1', target_stage: '代码开发' }), /I1/)
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
test('init_project rejects a feature-only project map that omits discovered modules', async () => {
|
|
120
|
+
const f = fixture({
|
|
121
|
+
'backend/pom.xml': '<project><properties><java.version>8</java.version></properties></project>',
|
|
122
|
+
'frontend/package.json': '{"dependencies":{"vue":"^2.7.0"}}',
|
|
123
|
+
})
|
|
124
|
+
const draft = [{ kind: 'skill', name: 'adaptive-project-project-map',
|
|
125
|
+
description: 'Reusable repository map', meta_skills: ['requirements-analysis', 'code-development'],
|
|
126
|
+
content: '# Feature implementation\nThe root package.json and backend contain the feature API.' }]
|
|
127
|
+
await assert.rejects(f.call({ operation: 'init_project', phase: 'propose', resources: draft }), /frontend/)
|
|
128
|
+
draft[0].content = '# Repository map\nRoot package.json, backend/pom.xml and frontend/package.json define separate modules.'
|
|
129
|
+
const proposed = JSON.parse(await f.call({ operation: 'init_project', phase: 'propose', resources: draft }))
|
|
130
|
+
assert.deepEqual(proposed.project_map_coverage, ['backend', 'frontend', 'package.json'])
|
|
131
|
+
await f.call({ operation: 'init_project', phase: 'apply', resources: draft,
|
|
132
|
+
expected_hash: proposed.expected_hash, existing_hash: proposed.existing_hash })
|
|
133
|
+
assert.ok(f.files.has(join(f.cwd, '.dsh/skills/adaptive-project-project-map/SKILL.md')))
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
test('workbench adaptive settings persist meta bindings with a version check', async () => {
|
|
137
|
+
const f = fixture()
|
|
138
|
+
const receiver = { authorizedPath: async path => path, fs: () => f.fs,
|
|
139
|
+
listSkills: async () => ({ skills: [{ name: 'adaptive-project-project-map', source: 'project' }] }),
|
|
140
|
+
readAdaptive: Controller.prototype.readAdaptive }
|
|
141
|
+
const before = await Controller.prototype.readAdaptive.call(receiver, f.cwd)
|
|
142
|
+
assert.equal(before.source, 'default')
|
|
143
|
+
const config = { meta_bindings: { 'code-development': ['adaptive-project-project-map'] }, sonar: { enabled: false } }
|
|
144
|
+
const after = await Controller.prototype.writeAdaptive.call(receiver,
|
|
145
|
+
{ path: f.cwd, config, expected_hash: before.hash })
|
|
146
|
+
assert.equal(after.ok, true)
|
|
147
|
+
assert.deepEqual(after.config.meta_bindings['code-development'], ['adaptive-project-project-map'])
|
|
148
|
+
await assert.rejects(Controller.prototype.writeAdaptive.call(receiver,
|
|
149
|
+
{ path: f.cwd, config, expected_hash: before.hash }), /已被其他会话修改/)
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
test('Sonar audit reads the exact CI analysis gate and filters new-code medium/high findings', async () => {
|
|
153
|
+
const calls = []
|
|
154
|
+
const fetcher = async (url, options) => {
|
|
155
|
+
calls.push({ url: String(url), authorization: options.headers.Authorization })
|
|
156
|
+
const body = String(url).includes('/ce/task')
|
|
157
|
+
? { task: { status: 'SUCCESS', analysisId: 'AN-1', componentKey: 'project' } }
|
|
158
|
+
: String(url).includes('/qualitygates/project_status')
|
|
159
|
+
? { projectStatus: { status: 'ERROR' } }
|
|
160
|
+
: { paging: { total: 2 }, issues: [
|
|
161
|
+
{ key: 'ISSUE-1', rule: 'java:S111', message: 'problem', severity: 'MAJOR', component: 'project:src/A.java', line: 3 },
|
|
162
|
+
{ key: 'ISSUE-2', rule: 'java:S222', message: 'minor', severity: 'MINOR', component: 'project:src/B.java' },
|
|
163
|
+
] }
|
|
164
|
+
return { ok: true, json: async () => body }
|
|
165
|
+
}
|
|
166
|
+
const policy = { enabled: true, host_url: 'https://sonar.example', project_key: 'project', mode: 'branch', token_env: 'SONAR_TOKEN' }
|
|
167
|
+
const audit = await inspectSonar(policy, 'secret-token', 'CE-TASK-123', 'feature', 'scope-hash', fetcher)
|
|
168
|
+
assert.equal(audit.analysis_id, 'AN-1')
|
|
169
|
+
assert.equal(audit.gate, 'ERROR')
|
|
170
|
+
assert.deepEqual(audit.blocking.map(issue => issue.key), ['ISSUE-1'])
|
|
171
|
+
assert.ok(calls[2].url.includes('inNewCodePeriod=true'))
|
|
172
|
+
assert.ok(calls.every(call => call.authorization === 'Bearer secret-token'))
|
|
173
|
+
assert.equal(JSON.stringify(audit).includes('secret-token'), false)
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
test('IDE-style local Sonar settings save and move only that task commit after review', async () => {
|
|
177
|
+
const sonar = { enabled: true, host_url: 'http://sonar.example', project_key: 'project',
|
|
178
|
+
mode: 'branch', source: 'ide-local', reference_branch: 'main', token_env: 'SONAR_TOKEN' }
|
|
179
|
+
const f = fixture()
|
|
180
|
+
const receiver = { authorizedPath: async path => path, fs: () => f.fs,
|
|
181
|
+
listSkills: async () => ({ skills: [] }), readAdaptive: Controller.prototype.readAdaptive }
|
|
182
|
+
const before = await Controller.prototype.readAdaptive.call(receiver, f.cwd)
|
|
183
|
+
const saved = await Controller.prototype.writeAdaptive.call(receiver,
|
|
184
|
+
{ path: f.cwd, config: { sonar }, expected_hash: before.hash })
|
|
185
|
+
assert.equal(saved.ok, true)
|
|
186
|
+
await f.call({ operation: 'create', task_id: 'L-1', title: 'local review', branch: 'feature',
|
|
187
|
+
complexity: 'low', complexity_reason: 'Small isolated change', files: ['app.java'] })
|
|
188
|
+
const state = f.state('L-1')
|
|
189
|
+
assert.equal(state.sonar_policy.source, 'ide-local')
|
|
190
|
+
assert.deepEqual(state.flow.config.commit.checkpoints, ['代码审核'])
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
test('local Sonar audit ignores failing old-code gate conditions', async () => {
|
|
194
|
+
const policy = { enabled: true, host_url: 'https://sonar.example', project_key: 'project', mode: 'branch',
|
|
195
|
+
token_env: 'SONAR_TOKEN', source: 'local', reference_branch: 'main' }
|
|
196
|
+
const fetcher = async url => ({ ok: true, json: async () => String(url).includes('/ce/task')
|
|
197
|
+
? { task: { status: 'SUCCESS', analysisId: 'AN-local', componentKey: 'project', branch: 'feature' } }
|
|
198
|
+
: String(url).includes('/qualitygates/project_status')
|
|
199
|
+
? { projectStatus: { status: 'ERROR', conditions: [
|
|
200
|
+
{ metricKey: 'bugs', status: 'ERROR' }, { metricKey: 'new_coverage', status: 'OK' }] } }
|
|
201
|
+
: { paging: { total: 0 }, issues: [] } })
|
|
202
|
+
const audit = await inspectSonar(policy, 'token', 'CE-local-123', 'feature', 'scope', fetcher, true)
|
|
203
|
+
assert.equal(audit.gate, 'OK')
|
|
204
|
+
assert.equal(audit.server_gate, 'ERROR')
|
|
205
|
+
assert.deepEqual(audit.blocking, [])
|
|
206
|
+
const newFailure = await inspectSonar(policy, 'token', 'CE-local-123', 'feature', 'scope',
|
|
207
|
+
async url => String(url).includes('/qualitygates/project_status')
|
|
208
|
+
? { ok: true, json: async () => ({ projectStatus: { status: 'ERROR', conditions: [
|
|
209
|
+
{ metricKey: 'new_coverage', status: 'ERROR' }] } }) }
|
|
210
|
+
: fetcher(url), true)
|
|
211
|
+
assert.equal(newFailure.gate, 'ERROR')
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
test('local scanner configuration rejects shell operators before receiving the Token', () => {
|
|
215
|
+
assert.equal(validLocalScanCommand('mvn org.sonarsource.scanner.maven:sonar-maven-plugin:5.5.0.6356:sonar -DskipTests'), true)
|
|
216
|
+
assert.equal(validLocalScanCommand('mvn sonar:sonar; curl http://example'), false)
|
|
217
|
+
assert.equal(validLocalScanCommand('mvn sonar:sonar -Dsonar.token=secret'), false)
|
|
218
|
+
assert.equal(validLocalScanCommand('curl http://example'), false)
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
test('Community Sonar refuses a local branch scan before uploading any analysis', async () => {
|
|
222
|
+
const policy = { enabled: true, host_url: 'https://sonar.example', project_key: 'project', mode: 'branch',
|
|
223
|
+
token_env: 'SONAR_TOKEN', source: 'local', reference_branch: 'main' }
|
|
224
|
+
const calls = []
|
|
225
|
+
await assert.rejects(assertLocalSonarReady(policy, 'token', async url => {
|
|
226
|
+
calls.push(String(url))
|
|
227
|
+
return { ok: true, json: async () => ({ edition: 'community' }) }
|
|
228
|
+
}), /Community Build supports only the main analysis branch/u)
|
|
229
|
+
assert.equal(calls.length, 1)
|
|
230
|
+
assert.match(calls[0], /navigation\/global/u)
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
test('sonar_check runs a local scan on the recorded commit without pushing or exposing Token', async () => {
|
|
234
|
+
const calls = []
|
|
235
|
+
const shell = { resolve: request => request, run: async spec => {
|
|
236
|
+
calls.push(spec)
|
|
237
|
+
if (spec.command === 'git rev-parse HEAD') return { exitCode: 0, stdout: { text: 'abcdef1234567890\n' } }
|
|
238
|
+
if (spec.command === 'git branch --show-current') return { exitCode: 0, stdout: { text: 'feature\n' } }
|
|
239
|
+
if (spec.command === 'git status --porcelain --untracked-files=all') return { exitCode: 0, stdout: { text: '' } }
|
|
240
|
+
const metadata = /-Dsonar\.scanner\.metadataFilePath='([^']+)'/u.exec(spec.command)?.[1]
|
|
241
|
+
assert.ok(metadata)
|
|
242
|
+
writeFileSync(metadata, 'ceTaskId=CE-local-456\n')
|
|
243
|
+
return { exitCode: 0 }
|
|
244
|
+
} }
|
|
245
|
+
const f = fixture({ '.dsh/meta.json': JSON.stringify({ sonar: { enabled: true, host_url: 'https://sonar.example',
|
|
246
|
+
project_key: 'project', mode: 'branch', source: 'local', reference_branch: 'main' } }) }, { shell })
|
|
247
|
+
await f.call({ operation: 'create', task_id: 'LOCAL-1', title: 'change', branch: 'feature', complexity: 'low',
|
|
248
|
+
complexity_reason: 'One local change', files: ['app.js'], items: [{ id: 'one', title: 'change', status: 'done' }] })
|
|
249
|
+
const state = f.state('LOCAL-1')
|
|
250
|
+
state.stage = '代码审核'
|
|
251
|
+
state.verification = { passed: true, evidence: ['verified'] }
|
|
252
|
+
state.commits = [{ label: 'TASK', hash: 'abcdef1234567890' }]
|
|
253
|
+
f.files.set(join(f.cwd, '.dsh/task-LOCAL-1.json'), JSON.stringify(state))
|
|
254
|
+
const oldFetch = globalThis.fetch
|
|
255
|
+
const oldToken = process.env.SONAR_TOKEN
|
|
256
|
+
process.env.SONAR_TOKEN = 'private-token'
|
|
257
|
+
globalThis.fetch = async url => ({ ok: true, json: async () => String(url).includes('/navigation/global')
|
|
258
|
+
? { edition: 'developer' }
|
|
259
|
+
: String(url).includes('/project_branches/list') ? { branches: [{ name: 'main' }] }
|
|
260
|
+
: String(url).includes('/ce/task')
|
|
261
|
+
? { task: { status: 'SUCCESS', analysisId: 'AN-local', componentKey: 'project', branch: 'feature' } }
|
|
262
|
+
: String(url).includes('/qualitygates/project_status')
|
|
263
|
+
? { projectStatus: { status: 'ERROR', conditions: [{ metricKey: 'bugs', status: 'ERROR' }] } }
|
|
264
|
+
: { paging: { total: 0 }, issues: [] } })
|
|
265
|
+
try {
|
|
266
|
+
const result = JSON.parse(await f.call({ operation: 'sonar_check', task_id: 'LOCAL-1' }))
|
|
267
|
+
assert.equal(result.gate, 'OK')
|
|
268
|
+
assert.equal(result.server_gate, 'ERROR')
|
|
269
|
+
assert.equal(calls.length, 4)
|
|
270
|
+
assert.equal(calls[3].env.SONAR_TOKEN, 'private-token')
|
|
271
|
+
assert.equal(calls[3].command.includes('private-token'), false)
|
|
272
|
+
assert.match(calls[3].command, /sonar\.newCode\.referenceBranch='main'/u)
|
|
273
|
+
assert.equal(JSON.stringify(f.state('LOCAL-1')).includes('private-token'), false)
|
|
274
|
+
assert.equal(calls.some(call => /git push/u.test(call.command)), false)
|
|
275
|
+
} finally {
|
|
276
|
+
globalThis.fetch = oldFetch
|
|
277
|
+
if (oldToken === undefined) delete process.env.SONAR_TOKEN
|
|
278
|
+
else process.env.SONAR_TOKEN = oldToken
|
|
279
|
+
}
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
test('review-only Sonar gate blocks pass until the task records CI analysis of new code', async () => {
|
|
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' } }) })
|
|
285
|
+
await f.call({ operation: 'create', task_id: 'S-1', title: 'change', branch: 'feature', complexity: 'low',
|
|
286
|
+
complexity_reason: 'One local change', files: ['app.js'], items: [{ id: 'one', title: 'change', status: 'done' }] })
|
|
287
|
+
const state = f.state('S-1')
|
|
288
|
+
state.stage = '代码审核'
|
|
289
|
+
state.verification = { passed: true, evidence: ['verified'] }
|
|
290
|
+
state.commits = [{ label: 'TASK', hash: 'abcdef1234567890' }]
|
|
291
|
+
f.files.set(join(f.cwd, '.dsh/task-S-1.json'), JSON.stringify(state))
|
|
292
|
+
await assert.rejects(f.call({ operation: 'review', task_id: 'S-1', outcome: 'pass' }), /SonarQube audit is enabled/)
|
|
293
|
+
const oldFetch = globalThis.fetch
|
|
294
|
+
const oldToken = process.env.SONAR_TOKEN
|
|
295
|
+
process.env.SONAR_TOKEN = 'secret-token'
|
|
296
|
+
globalThis.fetch = async url => ({ ok: true, json: async () => String(url).includes('/ce/task')
|
|
297
|
+
? { task: { status: 'SUCCESS', analysisId: 'AN-2', componentKey: 'project' } }
|
|
298
|
+
: String(url).includes('/qualitygates/project_status') ? { projectStatus: { status: 'ERROR',
|
|
299
|
+
conditions: [{ metricKey: 'bugs', status: 'ERROR' }, { metricKey: 'new_coverage', status: 'OK' }] } }
|
|
300
|
+
: { paging: { total: 0 }, issues: [] } })
|
|
301
|
+
try {
|
|
302
|
+
const check = JSON.parse(await f.call({ operation: 'sonar_check', task_id: 'S-1', ce_task_id: 'CE-123456' }))
|
|
303
|
+
assert.equal(check.gate, 'OK')
|
|
304
|
+
assert.equal(check.server_gate, 'ERROR')
|
|
305
|
+
assert.match(check.report_path, /^\.dsh\/reviews\/S-1\//)
|
|
306
|
+
assert.match(f.files.get(join(f.cwd, check.report_path)), /SonarQube 代码审核/)
|
|
307
|
+
assert.equal(f.state('S-1').sonar_audit.report_path, check.report_path)
|
|
308
|
+
await f.call({ operation: 'review', task_id: 'S-1', outcome: 'pass' })
|
|
309
|
+
assert.equal(f.state('S-1').review.outcome, 'pass')
|
|
310
|
+
} finally {
|
|
311
|
+
globalThis.fetch = oldFetch
|
|
312
|
+
if (oldToken === undefined) delete process.env.SONAR_TOKEN
|
|
313
|
+
else process.env.SONAR_TOKEN = oldToken
|
|
314
|
+
}
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
test('a failed Sonar finding becomes a reviewed project Rule attached to the coding Skill', async () => {
|
|
318
|
+
const f = fixture()
|
|
319
|
+
await f.call({ operation: 'create', task_id: 'L-1', title: 'change', branch: 'feature', complexity: 'low',
|
|
320
|
+
complexity_reason: 'One local change', files: ['app.js'] })
|
|
321
|
+
const state = f.state('L-1')
|
|
322
|
+
const finding = { key: 'ISSUE-9', rule: 'java:S999', message: 'Handle null', severity: 'MAJOR', file: 'A.java' }
|
|
323
|
+
state.stage = '代码审核'
|
|
324
|
+
state.sonar_audit = { ce_task_id: 'CE-123456', analysis_id: 'AN-9', gate: 'ERROR', checked_at: 'now',
|
|
325
|
+
scope_hash: 'old', findings: [finding], blocking: [finding], target: 'feature' }
|
|
326
|
+
f.files.set(join(f.cwd, '.dsh/task-L-1.json'), JSON.stringify(state))
|
|
327
|
+
await f.call({ operation: 'revise', task_id: 'L-1', target_stage: '代码开发',
|
|
328
|
+
revision_kind: 'defect', revision_reason: 'Fix the SonarQube issue' })
|
|
329
|
+
assert.equal(f.state('L-1').sonar_history[0].blocking[0].key, 'ISSUE-9')
|
|
330
|
+
assert.equal(JSON.parse(await f.call({ operation: 'status', task_id: 'L-1' })).rule_learning_candidates[0].rule, 'java:S999')
|
|
331
|
+
const args = { operation: 'learn_rule', task_id: 'L-1', issue_key: 'ISSUE-9', skill_name: 'bundled:code-development',
|
|
332
|
+
rule_name: 'handle-null-boundary', learning_reason: 'This boundary appears in several service methods',
|
|
333
|
+
content: 'At a service boundary, check for null before dereferencing. Show the failing call and a guarded call in the review example.' }
|
|
334
|
+
const proposal = JSON.parse(await f.call({ ...args, phase: 'propose' }))
|
|
335
|
+
assert.equal(f.files.has(join(f.cwd, '.dsh/rules/handle-null-boundary.md')), false)
|
|
336
|
+
await f.call({ ...args, phase: 'apply', expected_hash: proposal.expected_hash })
|
|
337
|
+
assert.match(f.files.get(join(f.cwd, '.dsh/rules/handle-null-boundary.md')), /java:S999/)
|
|
338
|
+
assert.ok(f.files.has(join(f.cwd, '.dsh/skills/code-development/SKILL.md')))
|
|
339
|
+
assert.deepEqual(JSON.parse(f.files.get(join(f.cwd, '.dsh/skills/code-development/profile.json'))).rules,
|
|
340
|
+
[{ source: 'project', name: 'handle-null-boundary' }])
|
|
341
|
+
assert.deepEqual(JSON.parse(await f.call({ operation: 'status', task_id: 'L-1' })).rule_learning_candidates, [])
|
|
164
342
|
})
|
|
165
343
|
|
|
166
|
-
test('a
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
344
|
+
test('a local false positive needs human approval, keeps the raw gate, and expires after code changes', async () => {
|
|
345
|
+
let allow = false
|
|
346
|
+
const approvals = []
|
|
347
|
+
const shell = { resolve: request => request, execute: async () => ({ result: async () => ({
|
|
348
|
+
exitCode: 0, timedOut: false, aborted: false, stdout: { text: 'checks passed' }, stderr: { text: '' },
|
|
349
|
+
}) }) }
|
|
350
|
+
const f = fixture({ '.dsh/meta.json': JSON.stringify({ sonar: { enabled: true,
|
|
351
|
+
host_url: 'https://sonar.example', project_key: 'project', mode: 'branch', source: 'ide-local',
|
|
352
|
+
reference_branch: 'main' } }) }, { shell,
|
|
353
|
+
approval: { request: async request => { approvals.push(request); return allow ? 'allowed-once' : 'rejected' } } })
|
|
354
|
+
await f.call({ operation: 'create', task_id: 'FP-1', title: 'one local finding', branch: 'feature',
|
|
355
|
+
complexity: 'low', complexity_reason: 'Local correction', files: ['app.js'] })
|
|
356
|
+
await f.call({ operation: 'verify', task_id: 'FP-1', command: 'check' })
|
|
357
|
+
const state = f.state('FP-1')
|
|
172
358
|
state.stage = '代码审核'
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
await f.call(
|
|
185
|
-
assert.
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
359
|
+
const finding = { key: 'local-fp-1', rule: 'mycompany-java:AvoidObjectCreationInLoop',
|
|
360
|
+
message: 'Avoid allocation', severity: 'MAJOR', file: 'app.js', line: 3 }
|
|
361
|
+
state.sonar_audit = { ce_task_id: 'local', analysis_id: 'LOCAL-1', gate: 'ERROR',
|
|
362
|
+
checked_at: '2026-10-05T00:00:00.000Z', scope_hash: state.verification.receipt.scope_hash,
|
|
363
|
+
findings: [finding], blocking: [finding], target: 'main', scanned_files: ['app.js'],
|
|
364
|
+
report_path: '.dsh/reviews/FP-1/local.md' }
|
|
365
|
+
f.files.set(join(f.cwd, '.dsh/reviews/FP-1/local.md'), 'original report')
|
|
366
|
+
f.files.set(join(f.cwd, '.dsh/task-FP-1.json'), JSON.stringify(state))
|
|
367
|
+
const args = { operation: 'sonar_disposition', task_id: 'FP-1', issue_key: finding.key,
|
|
368
|
+
disposition_reason: '每次循环需要独立实体对象,复用同一个实例会让多条授权记录互相覆盖,属于规则误报。',
|
|
369
|
+
evidence: ['授权实体逐行添加到集合,复用对象会导致所有行指向末次值'] }
|
|
370
|
+
await assert.rejects(f.call(args), /not approved/)
|
|
371
|
+
assert.equal(f.state('FP-1').sonar_audit.dispositions, undefined)
|
|
372
|
+
allow = true
|
|
373
|
+
const result = JSON.parse(await f.call(args))
|
|
374
|
+
assert.equal(result.raw_gate, 'ERROR')
|
|
375
|
+
assert.equal(result.review_gate, 'OK')
|
|
376
|
+
assert.equal(f.state('FP-1').sonar_audit.dispositions.length, 1)
|
|
377
|
+
assert.deepEqual(JSON.parse(await f.call({ operation: 'status', task_id: 'FP-1' })).rule_learning_candidates, [])
|
|
378
|
+
assert.match(approvals.at(-1).reason, /local-fp-1/)
|
|
379
|
+
assert.match(f.files.get(join(f.cwd, state.sonar_audit.report_path)), /逐项误报复核/)
|
|
380
|
+
await f.call({ operation: 'review', task_id: 'FP-1', outcome: 'pass' })
|
|
381
|
+
await assert.rejects(f.call({ operation: 'learn_rule', task_id: 'FP-1', issue_key: finding.key }), /blocking issue_key/)
|
|
382
|
+
f.files.set(join(f.cwd, 'app.js'), 'new source')
|
|
383
|
+
await assert.rejects(f.call({ operation: 'review', task_id: 'FP-1', outcome: 'pass' }), /stale/)
|
|
189
384
|
})
|