@godv61/dsh-task-engine 0.29.3 → 0.29.8
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 +302 -149
- package/.codex-project-test.mjs +18 -18
- package/.evidence-test.mjs +16 -3
- package/.hook-test.mjs +19 -19
- package/.resource-test.mjs +19 -2
- package/.roundtrip-test.mjs +8 -3
- package/.sonar-credential-test.mjs +38 -0
- package/.sonarlint-local-test.mjs +65 -0
- package/.workflow-test.mjs +29 -12
- package/README.md +22 -22
- package/docs/CHANGELOG.md +33 -19
- package/docs/README.md +6 -6
- package/docs/adaptive-workflows.md +85 -73
- package/docs/configuration.md +4 -4
- package/docs/faq.md +24 -18
- package/docs/getting-started.md +9 -9
- package/docs/manual-legacy.html +1 -1
- package/docs/manual.html +124 -117
- package/docs/roadmap.md +7 -7
- package/hooks/commit-msg +1 -1
- package/lib/adaptive.js +1 -1
- package/lib/adaptive.js.map +1 -1
- package/lib/client.js +289 -17
- package/lib/client.js.map +2 -2
- package/lib/controller.d.ts +35 -0
- package/lib/controller.js +68 -0
- package/lib/controller.js.map +1 -1
- package/lib/dev-task.d.ts +2 -0
- package/lib/dev-task.js +261 -31
- package/lib/dev-task.js.map +1 -1
- package/lib/engine.d.ts +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 +40 -0
- package/lib/sonar-report.js.map +1 -0
- package/lib/sonar.d.ts +23 -2
- package/lib/sonar.js +55 -4
- package/lib/sonar.js.map +1 -1
- package/lib/sonarlint-local.d.ts +15 -0
- package/lib/sonarlint-local.js +307 -0
- package/lib/sonarlint-local.js.map +1 -0
- package/package.json +9 -7
- 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 +7 -7
- package/skills/architecture-design/SKILL.md +11 -11
- package/skills/code-development/SKILL.md +11 -11
- package/skills/code-review/SKILL.md +11 -11
- package/skills/eng-delivery/SKILL.md +6 -4
- package/skills/requirements-analysis/SKILL.md +11 -11
- package/skills/task-orchestration/SKILL.md +11 -11
- package/skills/test-validation/SKILL.md +11 -11
package/.adaptive-test.mjs
CHANGED
|
@@ -1,189 +1,342 @@
|
|
|
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
|
-
}
|
|
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
35
|
const ctx = { fs, tools: { register(tool) { execute = tool.execute; return () => {} } }, get(name) {
|
|
36
|
+
if (name in services) return services[name]
|
|
36
37
|
if (name === 'sandboxPolicy') return { resolve: () => ({ mode: 'workspace-write', workspaceRoot: cwd, sessionId: session.id }) }
|
|
37
|
-
return undefined
|
|
38
|
-
} }
|
|
39
|
-
registerDevTask(ctx)
|
|
40
|
-
const exec = { agent: { session }, signal: new AbortController().signal }
|
|
41
|
-
return { cwd, files, fs, events, call: args => execute(args, exec), state: id => JSON.parse(files.get(join(cwd, `.dsh/task-${id}.json`))) }
|
|
42
|
-
}
|
|
43
|
-
|
|
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
|
+
|
|
44
45
|
test('each task grade has a valid and increasing meta-skill sequence', () => {
|
|
45
|
-
assert.deepEqual(COMPLEXITY_OPTIONS.map(option => option.id), ['low', 'medium', 'high', 'ultra'])
|
|
46
|
-
const configs = COMPLEXITY_OPTIONS.map(option => adaptiveWorkflow(option.id))
|
|
47
|
-
for (const config of configs) assert.deepEqual(validateWorkflow(config), [])
|
|
48
|
-
assert.deepEqual(configs.map(config => config.stages.length), [4, 5, 6, 7])
|
|
49
|
-
assert.ok(configs[2].stages.includes('任务编排'))
|
|
50
|
-
assert.ok(configs[3].stages.includes('架构设计'))
|
|
51
|
-
assert.ok(configs.every(config => config.transitions.some(edge => edge.requires?.includes('verified'))))
|
|
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'))))
|
|
52
53
|
assert.ok(configs.every(config => config.commit.checkpoints.includes('测试')))
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
test('adaptive task freezes a project override and its own Rules, not the bundled same-name Skill', async () => {
|
|
56
|
-
const f = fixture({
|
|
57
|
-
'.dsh/skills/requirements-analysis/SKILL.md': '# Project requirement method',
|
|
58
|
-
'.dsh/skills/requirements-analysis/profile.json': JSON.stringify({ rules: [{ source: 'project', name: 'team-scope' }], evidence: 'artifact' }),
|
|
59
|
-
'.dsh/rules/team-scope.md': '# Team scope rule',
|
|
60
|
-
'.dsh/meta.json': JSON.stringify({ meta_bindings: { 'code-development': ['adaptive-project-project-map'] } }),
|
|
61
|
-
'.dsh/skills/adaptive-project-project-map/SKILL.md': '# Project map',
|
|
62
|
-
'.dsh/skills/adaptive-project-project-map/profile.json': JSON.stringify({ rules: [], evidence: 'none' }),
|
|
63
|
-
})
|
|
64
|
-
const preview = JSON.parse(await f.call({ operation: 'assess', complexity: 'high' }))
|
|
65
|
-
assert.equal(preview.meta_skills['需求分析'][0].effective_source, 'project')
|
|
66
|
-
assert.ok(preview.meta_skills['代码开发'].some(skill => skill.logical_name === 'adaptive-project-project-map'))
|
|
67
|
-
await f.call({ operation: 'create', task_id: 'A-1', title: 'cross-module change', branch: 'feature',
|
|
68
|
-
complexity: 'high', complexity_reason: 'Several implementation steps depend on earlier interfaces', files: ['app.js'] })
|
|
69
|
-
const state = f.state('A-1')
|
|
70
|
-
assert.equal(state.flow.flow, 'adaptive-high')
|
|
71
|
-
assert.equal(state.flow.config.stage_bindings['需求分析'].skills[0].skill.source, 'project')
|
|
72
|
-
assert.ok(state.flow.resources.some(entry => entry.ref.name === 'team-scope' && entry.ref.source === 'project'))
|
|
73
|
-
assert.match(await f.call({ operation: 'load_skill', task_id: 'A-1', skill_name: 'project:requirements-analysis' }), /Project requirement method/)
|
|
74
|
-
await assert.rejects(f.call({ operation: 'load_skill', task_id: 'A-1', skill_name: 'bundled:requirements-analysis' }), /exact source:name/)
|
|
75
|
-
})
|
|
76
|
-
|
|
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
|
+
|
|
77
78
|
test('init_project proposes project-specific Skills and binds reviewed resources', async () => {
|
|
78
|
-
const f = fixture()
|
|
79
|
+
const f = fixture()
|
|
79
80
|
const inventory = JSON.parse(await f.call({ operation: 'init_project', phase: 'inspect' }))
|
|
80
|
-
|
|
81
|
-
assert.ok(
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
+
]
|
|
88
93
|
const proposed = JSON.parse(await f.call({ operation: 'init_project', phase: 'propose', resources }))
|
|
89
|
-
assert.
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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'])
|
|
94
101
|
assert.ok(f.files.has(join(f.cwd, '.dsh/skills/adaptive-project-project-map/profile.json')))
|
|
95
102
|
})
|
|
96
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
|
+
|
|
97
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' }
|
|
98
179
|
const f = fixture()
|
|
99
180
|
const receiver = { authorizedPath: async path => path, fs: () => f.fs,
|
|
100
|
-
listSkills: async () => ({ skills: [
|
|
101
|
-
readAdaptive: Controller.prototype.readAdaptive }
|
|
181
|
+
listSkills: async () => ({ skills: [] }), readAdaptive: Controller.prototype.readAdaptive }
|
|
102
182
|
const before = await Controller.prototype.readAdaptive.call(receiver, f.cwd)
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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, ['代码审核'])
|
|
111
191
|
})
|
|
112
192
|
|
|
113
|
-
test('Sonar audit
|
|
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' }
|
|
114
224
|
const calls = []
|
|
115
|
-
|
|
116
|
-
calls.push(
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
: { paging: { total: 2 }, issues: [
|
|
122
|
-
{ key: 'ISSUE-1', rule: 'java:S111', message: 'problem', severity: 'MAJOR', component: 'project:src/A.java', line: 3 },
|
|
123
|
-
{ key: 'ISSUE-2', rule: 'java:S222', message: 'minor', severity: 'MINOR', component: 'project:src/B.java' },
|
|
124
|
-
] }
|
|
125
|
-
return { ok: true, json: async () => body }
|
|
126
|
-
}
|
|
127
|
-
const policy = { enabled: true, host_url: 'https://sonar.example', project_key: 'project', mode: 'branch', token_env: 'SONAR_TOKEN' }
|
|
128
|
-
const audit = await inspectSonar(policy, 'secret-token', 'CE-TASK-123', 'feature', 'scope-hash', fetcher)
|
|
129
|
-
assert.equal(audit.analysis_id, 'AN-1')
|
|
130
|
-
assert.equal(audit.gate, 'ERROR')
|
|
131
|
-
assert.deepEqual(audit.blocking.map(issue => issue.key), ['ISSUE-1'])
|
|
132
|
-
assert.ok(calls[2].url.includes('inNewCodePeriod=true'))
|
|
133
|
-
assert.ok(calls.every(call => call.authorization === 'Bearer secret-token'))
|
|
134
|
-
assert.equal(JSON.stringify(audit).includes('secret-token'), false)
|
|
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)
|
|
135
231
|
})
|
|
136
232
|
|
|
137
|
-
test('
|
|
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
|
+
} }
|
|
138
245
|
const f = fixture({ '.dsh/meta.json': JSON.stringify({ sonar: { enabled: true, host_url: 'https://sonar.example',
|
|
139
|
-
project_key: 'project', mode: 'branch',
|
|
140
|
-
await f.call({ operation: 'create', task_id: '
|
|
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',
|
|
141
248
|
complexity_reason: 'One local change', files: ['app.js'], items: [{ id: 'one', title: 'change', status: 'done' }] })
|
|
142
|
-
const state = f.state('
|
|
249
|
+
const state = f.state('LOCAL-1')
|
|
143
250
|
state.stage = '代码审核'
|
|
144
251
|
state.verification = { passed: true, evidence: ['verified'] }
|
|
145
252
|
state.commits = [{ label: 'TASK', hash: 'abcdef1234567890' }]
|
|
146
|
-
f.files.set(join(f.cwd, '.dsh/task-
|
|
147
|
-
await assert.rejects(f.call({ operation: 'review', task_id: 'S-1', outcome: 'pass' }), /SonarQube audit is enabled/)
|
|
253
|
+
f.files.set(join(f.cwd, '.dsh/task-LOCAL-1.json'), JSON.stringify(state))
|
|
148
254
|
const oldFetch = globalThis.fetch
|
|
149
255
|
const oldToken = process.env.SONAR_TOKEN
|
|
150
|
-
process.env.SONAR_TOKEN = '
|
|
151
|
-
globalThis.fetch = async url => ({ ok: true, json: async () => String(url).includes('/
|
|
152
|
-
? {
|
|
153
|
-
: String(url).includes('/
|
|
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' }] } }
|
|
154
264
|
: { paging: { total: 0 }, issues: [] } })
|
|
155
265
|
try {
|
|
156
|
-
await f.call({ operation: 'sonar_check', task_id: '
|
|
157
|
-
|
|
158
|
-
assert.equal(
|
|
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)
|
|
159
275
|
} finally {
|
|
160
276
|
globalThis.fetch = oldFetch
|
|
161
277
|
if (oldToken === undefined) delete process.env.SONAR_TOKEN
|
|
162
278
|
else process.env.SONAR_TOKEN = oldToken
|
|
163
279
|
}
|
|
164
280
|
})
|
|
165
|
-
|
|
166
|
-
test('
|
|
167
|
-
const f = fixture(
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
const
|
|
172
|
-
state.stage = '代码审核'
|
|
173
|
-
state.
|
|
174
|
-
|
|
175
|
-
f.files.set(join(f.cwd, '.dsh/task-
|
|
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))
|
|
176
327
|
await f.call({ operation: 'revise', task_id: 'L-1', target_stage: '代码开发',
|
|
177
328
|
revision_kind: 'defect', revision_reason: 'Fix the SonarQube issue' })
|
|
178
329
|
assert.equal(f.state('L-1').sonar_history[0].blocking[0].key, 'ISSUE-9')
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
assert.
|
|
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')))
|
|
187
339
|
assert.deepEqual(JSON.parse(f.files.get(join(f.cwd, '.dsh/skills/code-development/profile.json'))).rules,
|
|
188
340
|
[{ source: 'project', name: 'handle-null-boundary' }])
|
|
341
|
+
assert.deepEqual(JSON.parse(await f.call({ operation: 'status', task_id: 'L-1' })).rule_learning_candidates, [])
|
|
189
342
|
})
|
package/.codex-project-test.mjs
CHANGED
|
@@ -29,7 +29,7 @@ function diskFs() {
|
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
test('Codex project Skills are discovered separately; new DSH Skills stay in .dsh', async t => {
|
|
32
|
+
test('Codex project Skills are discovered separately; new DSH Skills stay in .dsh', async t => {
|
|
33
33
|
const root = fixture(t)
|
|
34
34
|
const body = '---\nname: same\ndescription: Codex method\n---\nCodex body v1\n'
|
|
35
35
|
put(join(root, '.agents/skills/same/SKILL.md'), body)
|
|
@@ -42,23 +42,23 @@ test('Codex project Skills are discovered separately; new DSH Skills stay in .ds
|
|
|
42
42
|
const created = await Controller.prototype.writeSkill.call(receiver, { level: 'project', name: 'new-one', description: 'New DSH method', whenToUse: '', content: 'body', path: root })
|
|
43
43
|
assert.equal(created.ok, true)
|
|
44
44
|
assert.equal(readFileSync(join(root, '.dsh/skills/new-one/SKILL.md'), 'utf8').includes('body'), true)
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
test('a project Skill can shadow a bundled meta Skill without replacing an existing project copy', async t => {
|
|
48
|
-
const root = fixture(t)
|
|
49
|
-
const receiver = { authorizedPath: async path => path }
|
|
50
|
-
const request = { level: 'project', name: 'requirements-analysis', description: 'Team requirements method',
|
|
51
|
-
whenToUse: 'During requirement analysis', content: 'Project method', path: root, createOnly: true }
|
|
52
|
-
const first = await Controller.prototype.writeSkill.call(receiver, request)
|
|
53
|
-
assert.equal(first.ok, true)
|
|
54
|
-
const file = join(root, '.dsh/skills/requirements-analysis/SKILL.md')
|
|
55
|
-
assert.match(readFileSync(file, 'utf8'), /Project method/)
|
|
56
|
-
const second = await Controller.prototype.writeSkill.call(receiver, { ...request, content: 'Should not replace' })
|
|
57
|
-
assert.equal(second.ok, false)
|
|
58
|
-
assert.match(readFileSync(file, 'utf8'), /Project method/)
|
|
59
|
-
const catalog = await Controller.prototype.listSkills.call(receiver, root)
|
|
60
|
-
assert.deepEqual(catalog.skills.filter(skill => skill.name === 'requirements-analysis').map(skill => skill.ref.source).sort(), ['bundled', 'project'])
|
|
61
|
-
})
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test('a project Skill can shadow a bundled meta Skill without replacing an existing project copy', async t => {
|
|
48
|
+
const root = fixture(t)
|
|
49
|
+
const receiver = { authorizedPath: async path => path }
|
|
50
|
+
const request = { level: 'project', name: 'requirements-analysis', description: 'Team requirements method',
|
|
51
|
+
whenToUse: 'During requirement analysis', content: 'Project method', path: root, createOnly: true }
|
|
52
|
+
const first = await Controller.prototype.writeSkill.call(receiver, request)
|
|
53
|
+
assert.equal(first.ok, true)
|
|
54
|
+
const file = join(root, '.dsh/skills/requirements-analysis/SKILL.md')
|
|
55
|
+
assert.match(readFileSync(file, 'utf8'), /Project method/)
|
|
56
|
+
const second = await Controller.prototype.writeSkill.call(receiver, { ...request, content: 'Should not replace' })
|
|
57
|
+
assert.equal(second.ok, false)
|
|
58
|
+
assert.match(readFileSync(file, 'utf8'), /Project method/)
|
|
59
|
+
const catalog = await Controller.prototype.listSkills.call(receiver, root)
|
|
60
|
+
assert.deepEqual(catalog.skills.filter(skill => skill.name === 'requirements-analysis').map(skill => skill.ref.source).sort(), ['bundled', 'project'])
|
|
61
|
+
})
|
|
62
62
|
|
|
63
63
|
test('a bound Codex Skill loads its attached DSH Rule and reads edits next time', async t => {
|
|
64
64
|
const root = fixture(t)
|
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 { 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,7 +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
|
-
}
|
|
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
|
+
})
|
|
23
36
|
|
|
24
37
|
/** A standard-flow config with one extra skill on 开发 carrying the given evidence. */
|
|
25
38
|
function withEvidence(evidence) {
|