fix(dsh): command-check в списках пакета/проверок, self-check логирует до wiring и уважает конфиг (T4.1-T4.4, #69)

This commit is contained in:
Andrey Epifancev 2026-10-25 00:43:27 +04:00
parent f1bbbe0412
commit 9a5fa8cac2
6 changed files with 75 additions and 20 deletions

View File

@ -104,11 +104,22 @@ export function apply(ctx, rawConfig = {}) {
const guard = createGuard({ hooksDir, logger: ctx.logger })
const selfCheckReport =
config.enabled && config.selfCheck ? collectSelfCheck(ctx) : null
config.enabled && config.selfCheck ? collectSelfCheck(ctx, config) : null
const guardsOk = selfCheckReport
? selfCheckReport.find((entry) => entry.feature === 'guards')?.ok === true
: true
// Log every not-ok feature before any wiring runs, so a service that is
// missing from this DSH version leaves a diagnostic line even when a later
// wiring block throws on first use.
if (selfCheckReport) {
for (const entry of selfCheckReport) {
if (!entry.ok) {
ctx.logger?.error?.('[dsh-harness] self-check: %s not-ok: %s', entry.feature, entry.detail)
}
}
}
if (config.enabled && config.guards && guardsOk) {
// A synchronous, monotonic guard: it runs after the extensible
// `tools/pre-execute` waterfall, so no other listener can turn a denial
@ -165,21 +176,13 @@ export function apply(ctx, rawConfig = {}) {
if (config.checkCommand) {
registerCheckCommand({
commands: commandCtx.commands,
collect: () => collectSelfCheck(ctx),
collect: () => collectSelfCheck(ctx, config),
logger: ctx.logger,
})
}
})
}
if (selfCheckReport) {
for (const entry of selfCheckReport) {
if (!entry.ok) {
ctx.logger?.error?.('[dsh-harness] self-check: %s not-ok: %s', entry.feature, entry.detail)
}
}
}
ctx.logger?.info?.(
'[dsh-harness] ready: hooks=%s, bootstrap=%s, /role=%s',
config.guards ? hooksDir : 'off',

View File

@ -32,16 +32,25 @@ const CONTRACT = [
/**
* Inspect a ctx-like object without calling anything on it.
* @param {object} [config] - bundle config; a feature whose config switch is
* false is skipped entirely (workingDirectory is always checked, and
* `commands` is checked while either `roleCommand` or `checkCommand` is on).
* @returns {{feature: 'guards'|'bootstrap'|'commands'|'workingDirectory',
* ok: boolean, detail: string}[]}
*/
export function collectSelfCheck(ctxLike) {
return CONTRACT.map(({ feature, label, available }) => {
const ok = available(ctxLike)
return {
feature,
ok,
detail: ok ? `${label}: available` : `${label}: missing or not a function`,
}
})
export function collectSelfCheck(ctxLike, config = {}) {
const disabled = {
guards: config?.guards === false,
bootstrap: config?.bootstrap === false,
commands: config?.roleCommand === false && config?.checkCommand === false,
}
return CONTRACT.filter(({ feature }) => feature === 'workingDirectory' || !disabled[feature])
.map(({ feature, label, available }) => {
const ok = available(ctxLike)
return {
feature,
ok,
detail: ok ? `${label}: available` : `${label}: missing or not a function`,
}
})
}

View File

@ -12,6 +12,7 @@
"files": [
"index.js",
"command-role.js",
"command-check.js",
"lib",
"dsh-tools.md",
"cordis.patch.yml",

View File

@ -55,10 +55,13 @@ test('runtime files have no external @deepseek-ai imports', () => {
for (const file of [
'index.js',
'command-role.js',
'command-check.js',
'lib/message.js',
'lib/guards.js',
'lib/bootstrap.js',
'lib/role.js',
'lib/legacy.js',
'lib/selfcheck.js',
]) {
const source = readFileSync(join(bundleDir, file), 'utf8')
assert.doesNotMatch(source, /from ['"]@deepseek-ai\//, `${file} must not import DSH internals`)

View File

@ -23,7 +23,7 @@ ensure_bundle_hooks() {
ensure_bundle_hooks
for f in dsh/bundle/index.js dsh/bundle/command-role.js dsh/bundle/lib/*.js dsh/tests/*.mjs; do
for f in dsh/bundle/index.js dsh/bundle/command-role.js dsh/bundle/command-check.js dsh/bundle/lib/*.js dsh/tests/*.mjs; do
node --check "$f"
done

View File

@ -55,6 +55,45 @@ test('collectSelfCheck flags missing on()', () => {
}
})
test('collectSelfCheck skips features disabled by config', () => {
const report = collectSelfCheck(fullCtx(), {
guards: false,
bootstrap: false,
roleCommand: false,
checkCommand: false,
})
assert.deepEqual(
report.map((entry) => entry.feature),
['workingDirectory'],
)
})
test('collectSelfCheck does not flag a disabled feature that is also missing', () => {
const ctx = fullCtx()
ctx.tools = {}
ctx.on = undefined
const report = collectSelfCheck(ctx, { guards: false, bootstrap: false })
assert.deepEqual(
report.map((entry) => entry.feature),
['commands', 'workingDirectory'],
)
assert.ok(report.every((entry) => entry.ok), 'nothing not-ok in the report')
const noInject = fullCtx()
noInject.inject = undefined
const commandsOff = collectSelfCheck(noInject, { roleCommand: false, checkCommand: false })
assert.ok(!commandsOff.some((entry) => entry.feature === 'commands'), 'commands skipped')
assert.ok(commandsOff.every((entry) => entry.ok), 'missing inject not flagged')
const roleOnly = collectSelfCheck(noInject, { roleCommand: false })
assert.ok(roleOnly.some((entry) => entry.feature === 'commands'), 'commands still checked')
assert.equal(
roleOnly.find((entry) => entry.feature === 'commands').ok,
false,
'checkCommand alone still needs ctx.inject',
)
})
test('registerCheckCommand builds report text', async () => {
let captured
const commands = {