diff --git a/dsh/bundle/index.js b/dsh/bundle/index.js index a3a13ac..b6544b4 100644 --- a/dsh/bundle/index.js +++ b/dsh/bundle/index.js @@ -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', diff --git a/dsh/bundle/lib/selfcheck.js b/dsh/bundle/lib/selfcheck.js index d61d978..222ac9e 100644 --- a/dsh/bundle/lib/selfcheck.js +++ b/dsh/bundle/lib/selfcheck.js @@ -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`, + } + }) } diff --git a/dsh/bundle/package.json b/dsh/bundle/package.json index 7542b74..00b2214 100644 --- a/dsh/bundle/package.json +++ b/dsh/bundle/package.json @@ -12,6 +12,7 @@ "files": [ "index.js", "command-role.js", + "command-check.js", "lib", "dsh-tools.md", "cordis.patch.yml", diff --git a/dsh/tests/package.test.mjs b/dsh/tests/package.test.mjs index dc63957..acb7a5d 100644 --- a/dsh/tests/package.test.mjs +++ b/dsh/tests/package.test.mjs @@ -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`) diff --git a/dsh/tests/run.sh b/dsh/tests/run.sh index c6ac70b..6ce8e30 100755 --- a/dsh/tests/run.sh +++ b/dsh/tests/run.sh @@ -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 diff --git a/dsh/tests/selfcheck.test.mjs b/dsh/tests/selfcheck.test.mjs index 32098f2..8a229b8 100644 --- a/dsh/tests/selfcheck.test.mjs +++ b/dsh/tests/selfcheck.test.mjs @@ -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 = {