@godv61/dsh-task-engine 0.29.8 → 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 +221 -179
- 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 +37 -114
- package/docs/development.md +45 -53
- package/docs/manual.html +176 -127
- package/hooks/commit-msg +18 -0
- package/lib/client.js +27 -11
- package/lib/client.js.map +2 -2
- package/lib/controller.d.ts +9 -0
- package/lib/controller.js +6 -1
- package/lib/controller.js.map +1 -1
- package/lib/dev-task.js +92 -22
- 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/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/.adaptive-test.mjs
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
import test from 'node:test'
|
|
2
2
|
import assert from 'node:assert/strict'
|
|
3
3
|
import { join, resolve } from 'node:path'
|
|
4
|
-
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
|
4
|
+
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
|
5
5
|
import { tmpdir } from 'node:os'
|
|
6
6
|
import { adaptiveWorkflow, COMPLEXITY_OPTIONS } from './lib/adaptive.js'
|
|
7
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'
|
|
8
|
+
import { plannedItemIds, registerDevTask } from './lib/dev-task.js'
|
|
9
|
+
import { assertLocalSonarReady, inspectSonar, validLocalScanCommand } from './lib/sonar.js'
|
|
10
10
|
import Controller from './lib/controller.js'
|
|
11
11
|
|
|
12
12
|
const home = mkdtempSync(join(tmpdir(), 'dsh-adaptive-'))
|
|
13
13
|
process.env.DSH_HOME = home
|
|
14
14
|
process.on('exit', () => rmSync(home, { recursive: true, force: true }))
|
|
15
15
|
|
|
16
|
-
function fixture(extra = {}, services = {}) {
|
|
16
|
+
function fixture(extra = {}, services = {}) {
|
|
17
17
|
const cwd = resolve('adaptive-project')
|
|
18
18
|
const files = new Map([[join(cwd, 'package.json'), '{"name":"adaptive-project","dependencies":{"vue":"^2.7.0"}}'],
|
|
19
19
|
[join(cwd, '.git/HEAD'), 'ref: refs/heads/feature'] , ...Object.entries(extra).map(([p, content]) => [join(cwd, p), content])])
|
|
@@ -32,9 +32,9 @@ function fixture(extra = {}, services = {}) {
|
|
|
32
32
|
listDir: async target => [...new Set([...files.keys()].filter(path => path.startsWith(target.path + '\\'))
|
|
33
33
|
.map(path => path.slice(target.path.length + 1).split('\\')[0]))].map(name => ({ name })),
|
|
34
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 }) }
|
|
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
38
|
return undefined
|
|
39
39
|
} }
|
|
40
40
|
registerDevTask(ctx)
|
|
@@ -42,7 +42,7 @@ function fixture(extra = {}, services = {}) {
|
|
|
42
42
|
return { cwd, files, fs, events, call: args => execute(args, exec), state: id => JSON.parse(files.get(join(cwd, `.dsh/task-${id}.json`))) }
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
test('each task grade has a valid and increasing meta-skill sequence', () => {
|
|
45
|
+
test('each task grade has a valid and increasing meta-skill sequence', () => {
|
|
46
46
|
assert.deepEqual(COMPLEXITY_OPTIONS.map(option => option.id), ['low', 'medium', 'high', 'ultra'])
|
|
47
47
|
const configs = COMPLEXITY_OPTIONS.map(option => adaptiveWorkflow(option.id))
|
|
48
48
|
for (const config of configs) assert.deepEqual(validateWorkflow(config), [])
|
|
@@ -50,7 +50,7 @@ test('each task grade has a valid and increasing meta-skill sequence', () => {
|
|
|
50
50
|
assert.ok(configs[2].stages.includes('任务编排'))
|
|
51
51
|
assert.ok(configs[3].stages.includes('架构设计'))
|
|
52
52
|
assert.ok(configs.every(config => config.transitions.some(edge => edge.requires?.includes('verified'))))
|
|
53
|
-
assert.ok(configs.every(config => config.commit.checkpoints.includes('测试')))
|
|
53
|
+
assert.ok(configs.every(config => config.commit.checkpoints.includes('测试')))
|
|
54
54
|
})
|
|
55
55
|
|
|
56
56
|
test('adaptive task freezes a project override and its own Rules, not the bundled same-name Skill', async () => {
|
|
@@ -75,14 +75,14 @@ test('adaptive task freezes a project override and its own Rules, not the bundle
|
|
|
75
75
|
await assert.rejects(f.call({ operation: 'load_skill', task_id: 'A-1', skill_name: 'bundled:requirements-analysis' }), /exact source:name/)
|
|
76
76
|
})
|
|
77
77
|
|
|
78
|
-
test('init_project proposes project-specific Skills and binds reviewed resources', async () => {
|
|
78
|
+
test('init_project proposes project-specific Skills and binds reviewed resources', async () => {
|
|
79
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/)
|
|
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
86
|
assert.ok(inventory.manifests.some(entry => entry.facts.includes('vue=^2.7.0')))
|
|
87
87
|
const resources = [
|
|
88
88
|
{ kind: 'rule', name: 'component-boundary', content: 'Keep component boundary calls behind the existing service interface.' },
|
|
@@ -90,50 +90,50 @@ test('init_project proposes project-specific Skills and binds reviewed resources
|
|
|
90
90
|
content: 'The root package.json declares Vue 2.7; inspect src for the actual component layout.',
|
|
91
91
|
meta_skills: ['requirements-analysis', 'code-development'], rules: ['component-boundary'] },
|
|
92
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/)
|
|
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
96
|
assert.equal(f.files.has(join(f.cwd, '.dsh/meta.json')), false)
|
|
97
97
|
await f.call({ operation: 'init_project', phase: 'apply', resources, expected_hash: proposed.expected_hash,
|
|
98
98
|
existing_hash: proposed.existing_hash })
|
|
99
99
|
const meta = JSON.parse(f.files.get(join(f.cwd, '.dsh/meta.json')))
|
|
100
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
|
-
})
|
|
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
135
|
|
|
136
|
-
test('workbench adaptive settings persist meta bindings with a version check', async () => {
|
|
136
|
+
test('workbench adaptive settings persist meta bindings with a version check', async () => {
|
|
137
137
|
const f = fixture()
|
|
138
138
|
const receiver = { authorizedPath: async path => path, fs: () => f.fs,
|
|
139
139
|
listSkills: async () => ({ skills: [{ name: 'adaptive-project-project-map', source: 'project' }] }),
|
|
@@ -149,7 +149,7 @@ test('workbench adaptive settings persist meta bindings with a version check', a
|
|
|
149
149
|
{ path: f.cwd, config, expected_hash: before.hash }), /已被其他会话修改/)
|
|
150
150
|
})
|
|
151
151
|
|
|
152
|
-
test('Sonar audit reads the exact CI analysis gate and filters new-code medium/high findings', async () => {
|
|
152
|
+
test('Sonar audit reads the exact CI analysis gate and filters new-code medium/high findings', async () => {
|
|
153
153
|
const calls = []
|
|
154
154
|
const fetcher = async (url, options) => {
|
|
155
155
|
calls.push({ url: String(url), authorization: options.headers.Authorization })
|
|
@@ -171,115 +171,115 @@ test('Sonar audit reads the exact CI analysis gate and filters new-code medium/h
|
|
|
171
171
|
assert.ok(calls[2].url.includes('inNewCodePeriod=true'))
|
|
172
172
|
assert.ok(calls.every(call => call.authorization === 'Bearer secret-token'))
|
|
173
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
|
-
})
|
|
174
|
+
})
|
|
281
175
|
|
|
282
|
-
test('
|
|
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
283
|
const f = fixture({ '.dsh/meta.json': JSON.stringify({ sonar: { enabled: true, host_url: 'https://sonar.example',
|
|
284
284
|
project_key: 'project', mode: 'branch', token_env: 'SONAR_TOKEN' } }) })
|
|
285
285
|
await f.call({ operation: 'create', task_id: 'S-1', title: 'change', branch: 'feature', complexity: 'low',
|
|
@@ -295,16 +295,16 @@ test('review-only Sonar gate blocks pass until the task records CI analysis of n
|
|
|
295
295
|
process.env.SONAR_TOKEN = 'secret-token'
|
|
296
296
|
globalThis.fetch = async url => ({ ok: true, json: async () => String(url).includes('/ce/task')
|
|
297
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' }] } }
|
|
298
|
+
: String(url).includes('/qualitygates/project_status') ? { projectStatus: { status: 'ERROR',
|
|
299
|
+
conditions: [{ metricKey: 'bugs', status: 'ERROR' }, { metricKey: 'new_coverage', status: 'OK' }] } }
|
|
300
300
|
: { paging: { total: 0 }, issues: [] } })
|
|
301
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)
|
|
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
308
|
await f.call({ operation: 'review', task_id: 'S-1', outcome: 'pass' })
|
|
309
309
|
assert.equal(f.state('S-1').review.outcome, 'pass')
|
|
310
310
|
} finally {
|
|
@@ -314,7 +314,7 @@ test('review-only Sonar gate blocks pass until the task records CI analysis of n
|
|
|
314
314
|
}
|
|
315
315
|
})
|
|
316
316
|
|
|
317
|
-
test('a failed Sonar finding becomes a reviewed project Rule attached to the coding Skill', async () => {
|
|
317
|
+
test('a failed Sonar finding becomes a reviewed project Rule attached to the coding Skill', async () => {
|
|
318
318
|
const f = fixture()
|
|
319
319
|
await f.call({ operation: 'create', task_id: 'L-1', title: 'change', branch: 'feature', complexity: 'low',
|
|
320
320
|
complexity_reason: 'One local change', files: ['app.js'] })
|
|
@@ -324,10 +324,10 @@ test('a failed Sonar finding becomes a reviewed project Rule attached to the cod
|
|
|
324
324
|
state.sonar_audit = { ce_task_id: 'CE-123456', analysis_id: 'AN-9', gate: 'ERROR', checked_at: 'now',
|
|
325
325
|
scope_hash: 'old', findings: [finding], blocking: [finding], target: 'feature' }
|
|
326
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')
|
|
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
331
|
const args = { operation: 'learn_rule', task_id: 'L-1', issue_key: 'ISSUE-9', skill_name: 'bundled:code-development',
|
|
332
332
|
rule_name: 'handle-null-boundary', learning_reason: 'This boundary appears in several service methods',
|
|
333
333
|
content: 'At a service boundary, check for null before dereferencing. Show the failing call and a guarded call in the review example.' }
|
|
@@ -336,7 +336,49 @@ test('a failed Sonar finding becomes a reviewed project Rule attached to the cod
|
|
|
336
336
|
await f.call({ ...args, phase: 'apply', expected_hash: proposal.expected_hash })
|
|
337
337
|
assert.match(f.files.get(join(f.cwd, '.dsh/rules/handle-null-boundary.md')), /java:S999/)
|
|
338
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, [])
|
|
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, [])
|
|
342
|
+
})
|
|
343
|
+
|
|
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')
|
|
358
|
+
state.stage = '代码审核'
|
|
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/)
|
|
342
384
|
})
|
package/.evidence-test.mjs
CHANGED
|
@@ -7,11 +7,11 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import test from 'node:test'
|
|
9
9
|
import assert from 'node:assert/strict'
|
|
10
|
-
import { loadedSkills, needsSkillReceipt, skillBlockers } from './lib/skill-audit.js'
|
|
10
|
+
import { loadedSkills, needsSkillReceipt, skillBlockers } from './lib/skill-audit.js'
|
|
11
11
|
import { adoptRecommendation, resolveFlow } from './lib/workflows.js'
|
|
12
12
|
|
|
13
13
|
/** A session that has loaded the named skills. */
|
|
14
|
-
function sessionWith(names) {
|
|
14
|
+
function sessionWith(names) {
|
|
15
15
|
const events = []
|
|
16
16
|
for (const name of names) {
|
|
17
17
|
const callId = 'c' + events.length
|
|
@@ -19,20 +19,20 @@ function sessionWith(names) {
|
|
|
19
19
|
events.push({ type: 'tool/result', data: { message: { content: [{ type: 'tool-result', toolCallId: callId, isError: false }] } } })
|
|
20
20
|
}
|
|
21
21
|
return { id: 's', snapshotEvents: () => events }
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
test('evidence: DSH 0.2 tool messages prove only successful task-scoped skill loads', () => {
|
|
25
|
-
const events = [
|
|
26
|
-
{ type: 'tool/call', data: { name: 'dev_task', callId: 'ok', arguments: JSON.stringify({ operation: 'load_skill', task_id: 'Q-1', skill_name: 'project:qms-project-map' }) } },
|
|
27
|
-
{ type: 'tool/result', data: { message: { role: 'tool', toolCallId: 'ok', isError: false, content: [{ type: 'text', text: 'loaded' }] } } },
|
|
28
|
-
{ type: 'tool/call', data: { name: 'dev_task', callId: 'bad', arguments: JSON.stringify({ operation: 'load_skill', task_id: 'Q-1', skill_name: 'project:qms-tech-stack' }) } },
|
|
29
|
-
{ type: 'tool/result', data: { message: { role: 'tool', toolCallId: 'bad', isError: true, content: [{ type: 'text', text: 'failed' }] } } },
|
|
30
|
-
]
|
|
31
|
-
const loaded = loadedSkills({ snapshotEvents: () => events })
|
|
32
|
-
assert.equal(loaded.get('Q-1#project:qms-project-map'), 'ok')
|
|
33
|
-
assert.equal(loaded.has('Q-1#project:qms-tech-stack'), false)
|
|
34
|
-
assert.equal(loaded.has('Q-2#project:qms-project-map'), false)
|
|
35
|
-
})
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
test('evidence: DSH 0.2 tool messages prove only successful task-scoped skill loads', () => {
|
|
25
|
+
const events = [
|
|
26
|
+
{ type: 'tool/call', data: { name: 'dev_task', callId: 'ok', arguments: JSON.stringify({ operation: 'load_skill', task_id: 'Q-1', skill_name: 'project:qms-project-map' }) } },
|
|
27
|
+
{ type: 'tool/result', data: { message: { role: 'tool', toolCallId: 'ok', isError: false, content: [{ type: 'text', text: 'loaded' }] } } },
|
|
28
|
+
{ type: 'tool/call', data: { name: 'dev_task', callId: 'bad', arguments: JSON.stringify({ operation: 'load_skill', task_id: 'Q-1', skill_name: 'project:qms-tech-stack' }) } },
|
|
29
|
+
{ type: 'tool/result', data: { message: { role: 'tool', toolCallId: 'bad', isError: true, content: [{ type: 'text', text: 'failed' }] } } },
|
|
30
|
+
]
|
|
31
|
+
const loaded = loadedSkills({ snapshotEvents: () => events })
|
|
32
|
+
assert.equal(loaded.get('Q-1#project:qms-project-map'), 'ok')
|
|
33
|
+
assert.equal(loaded.has('Q-1#project:qms-tech-stack'), false)
|
|
34
|
+
assert.equal(loaded.has('Q-2#project:qms-project-map'), false)
|
|
35
|
+
})
|
|
36
36
|
|
|
37
37
|
/** A standard-flow config with one extra skill on 开发 carrying the given evidence. */
|
|
38
38
|
function withEvidence(evidence) {
|
package/.resource-test.mjs
CHANGED
|
@@ -122,7 +122,7 @@ test('ledger shows absent storage as empty and propagates access errors',async()
|
|
|
122
122
|
controller.fs=()=>({resolve:async p=>p,listDir:async()=>{throw Object.assign(new Error('denied'),{code:'EACCES'})}})
|
|
123
123
|
await assert.rejects(()=>controller.readTasks('/project'),/denied/)
|
|
124
124
|
})
|
|
125
|
-
test('ledger rejects malformed records instead of pretending there are no tasks',async()=>{
|
|
125
|
+
test('ledger rejects malformed records instead of pretending there are no tasks',async()=>{
|
|
126
126
|
const {default:Controller}=await import('./lib/controller.js')
|
|
127
127
|
const controller=Object.create(Controller.prototype)
|
|
128
128
|
controller.authorizedPath=async p=>p
|
|
@@ -130,21 +130,21 @@ test('ledger rejects malformed records instead of pretending there are no tasks'
|
|
|
130
130
|
controller.fs=()=>({resolve:async p=>p,listDir:async()=>[{name:'task-broken.json'}],readText:async()=>content})
|
|
131
131
|
await assert.rejects(()=>controller.readTasks('/project'),/任务记录/)
|
|
132
132
|
}
|
|
133
|
-
})
|
|
134
|
-
test('ledger exposes Sonar findings without project credentials',async()=>{
|
|
135
|
-
const {default:Controller}=await import('./lib/controller.js')
|
|
136
|
-
const controller=Object.create(Controller.prototype)
|
|
137
|
-
controller.authorizedPath=async p=>p
|
|
138
|
-
controller.fs=()=>({resolve:async p=>p,listDir:async()=>[{name:'task-sample.json'}],readText:async()=>JSON.stringify({
|
|
139
|
-
id:'sample',title:'Sample',stage:'代码审核',branch:'feature/sample',items:[],
|
|
140
|
-
sonar_policy:{source:'ide-local',token_env:'SONAR_TOKEN',host_url:'https://sonar.example',project_key:'sample'},
|
|
141
|
-
sonar_audit:{gate:'ERROR',checked_at:'2026-10-03T00:00:00Z',target:'main',
|
|
142
|
-
findings:[{key:'issue-1',rule:'java:S103',message:'Line too long',severity:'MAJOR',file:'Sample.java',line:3}],
|
|
143
|
-
blocking:[{key:'issue-1'}],uncovered_files:[]},
|
|
144
|
-
})})
|
|
145
|
-
const {tasks}=await controller.readTasks('/project')
|
|
146
|
-
assert.equal(tasks[0].sonar.audit.gate,'ERROR')
|
|
147
|
-
assert.equal(tasks[0].sonar.audit.blocking_count,1)
|
|
148
|
-
assert.deepEqual(tasks[0].sonar.audit.findings[0],{key:'issue-1',rule:'java:S103',message:'Line too long',severity:'MAJOR',file:'Sample.java',line:3})
|
|
149
|
-
assert.equal(JSON.stringify(tasks).includes('SONAR_TOKEN'),false)
|
|
150
|
-
})
|
|
133
|
+
})
|
|
134
|
+
test('ledger exposes Sonar findings without project credentials',async()=>{
|
|
135
|
+
const {default:Controller}=await import('./lib/controller.js')
|
|
136
|
+
const controller=Object.create(Controller.prototype)
|
|
137
|
+
controller.authorizedPath=async p=>p
|
|
138
|
+
controller.fs=()=>({resolve:async p=>p,listDir:async()=>[{name:'task-sample.json'}],readText:async()=>JSON.stringify({
|
|
139
|
+
id:'sample',title:'Sample',stage:'代码审核',branch:'feature/sample',items:[],
|
|
140
|
+
sonar_policy:{source:'ide-local',token_env:'SONAR_TOKEN',host_url:'https://sonar.example',project_key:'sample'},
|
|
141
|
+
sonar_audit:{gate:'ERROR',checked_at:'2026-10-03T00:00:00Z',target:'main',
|
|
142
|
+
findings:[{key:'issue-1',rule:'java:S103',message:'Line too long',severity:'MAJOR',file:'Sample.java',line:3}],
|
|
143
|
+
blocking:[{key:'issue-1'}],uncovered_files:[]},
|
|
144
|
+
})})
|
|
145
|
+
const {tasks}=await controller.readTasks('/project')
|
|
146
|
+
assert.equal(tasks[0].sonar.audit.gate,'ERROR')
|
|
147
|
+
assert.equal(tasks[0].sonar.audit.blocking_count,1)
|
|
148
|
+
assert.deepEqual(tasks[0].sonar.audit.findings[0],{key:'issue-1',rule:'java:S103',message:'Line too long',severity:'MAJOR',file:'Sample.java',line:3})
|
|
149
|
+
assert.equal(JSON.stringify(tasks).includes('SONAR_TOKEN'),false)
|
|
150
|
+
})
|
package/.roundtrip-test.mjs
CHANGED
|
@@ -13,16 +13,16 @@ import { dirname, join, resolve, sep } from 'node:path'
|
|
|
13
13
|
import { resolveFlow, adoptRecommendation, compactProjectConfig, materializeBundledReferences } from './lib/workflows.js'
|
|
14
14
|
import Controller from './lib/controller.js'
|
|
15
15
|
import { assertAdvance, validateWorkflow } from './lib/engine.js'
|
|
16
|
-
// The npm package ships the built client, not its TypeScript source. Keep the
|
|
17
|
-
// source codec assertion in repository tests while allowing shipped tests to run.
|
|
18
|
-
const remoteSource = new URL('./src/client/remote.ts', import.meta.url)
|
|
19
|
-
const hasRemoteSource = existsSync(remoteSource)
|
|
16
|
+
// The npm package ships the built client, not its TypeScript source. Keep the
|
|
17
|
+
// source codec assertion in repository tests while allowing shipped tests to run.
|
|
18
|
+
const remoteSource = new URL('./src/client/remote.ts', import.meta.url)
|
|
19
|
+
const hasRemoteSource = existsSync(remoteSource)
|
|
20
20
|
const TYPERT_REMOTE = hasRemoteSource ? (await import(remoteSource.href)).TYPERT_REMOTE : undefined
|
|
21
21
|
|
|
22
22
|
/** The fields a project config can carry, so a round-trip is checked on all of them. */
|
|
23
23
|
const FIELDS = ['flow', 'stage_bindings', 'commit', 'artifacts', 'review_depth', 'commit_required']
|
|
24
24
|
|
|
25
|
-
test('browser remote codec retains source-qualified bindings and all saved fields',
|
|
25
|
+
test('browser remote codec retains source-qualified bindings and all saved fields',
|
|
26
26
|
{ skip: hasRemoteSource ? false : 'TypeScript source is excluded from the npm package' }, () => {
|
|
27
27
|
const write = TYPERT_REMOTE.descriptors.find(descriptor => descriptor.method === 'write')
|
|
28
28
|
const read = TYPERT_REMOTE.descriptors.find(descriptor => descriptor.method === 'read')
|