Files
msd-core/tests/codebuddy-upgrades.test.cjs
Tom Boucher 9e1fad1503 feat(#2098): drive CodeBuddy through the EoS descriptor + wire subagent hooks (ADR-1239)
Fold CodeBuddy's residual runtime-literal branches onto descriptor-driven
hostBehaviors (the issue's "zero branches remain" premise was inaccurate — 2
lived):
- Folded the `if (isCodebuddy)` commands/ report duplicate (byte-identical to
  the generic reportCommandsDir block above it) onto
  hostBehaviors.reportCommandsDir:true, matching cursor's #2089 precedent.
- Deleted the dead `else if (isCodebuddy)` agent-conversion arm (codebuddy ∈
  _DESCRIPTOR_AGENTS_RUNTIMES → gated out before the legacy chain, same dead-arm
  pattern removed for augment/trae in #2097).
- Removed isCodebuddy from all 4 destructure sites (now comments only, mirroring
  the #2096 isAntigravity fold). Zero live isCodebuddy reads remain.

UPGRADE 1 (extended hook bus): populate extendedHookEvents with the full
extended set SubagentStop/Stop/PreCompact/SubagentStart (qwen #2092 / kimi
precedent — codebuddy previously had extendedHookEvents:[] so it got NONE of
these). The generic applySettingsJsonHooks loop wires them, so CodeBuddy's
settings.json now gains all four subagent-lifecycle + stop/compact hooks it
previously lacked, matching Qwen/Kimi coverage. No source change (the
HOOK_EVENT_SURFACES SDK catalog is a locked dict; all 4 events are already in
the validator enum; hookEvents already 'claude'). settings.json is golden-
excluded → no golden change.

UPGRADE 2 (background dispatch): surfaced via the negotiated capability contract
only. CodeBuddy's dispatch.background:true legitimately exceeds the declarative-
cli baseline (false) and survives negotiation with no warning. NO agent-file
frontmatter field is emitted: the CodeBuddy CLI (GSD's install target,
~/.codebuddy/agents/) has NO background-dispatch frontmatter field — background
is a caller-side run_in_background invocation param (verified against
codebuddy.ai/docs/cli/sub-agents); the issue's agentMode/enabledAutoRun are
IDE-only (codebuddy.cn, a different product). Emitting them would be a non-
functional false-green, so it is deliberately not done.

Golden: byte-identical for all 16 runtimes (folds are console/failures-report
only; UPGRADE 1 → golden-excluded settings.json; UPGRADE 2 → no artifact
change). Tests: declarative-reference-codebuddy (adapter/axes/fail-closed
negotiation + AC2 source-grep guard) + codebuddy-upgrades (live install asserts
all 4 extended hooks wired; dispatch.background survives negotiation above
baseline). Matrix + install-on-your-runtime updated; changeset (Changed).
capability-registry regenerated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 09:39:34 -04:00

129 lines
6.2 KiB
JavaScript

'use strict';
/**
* CodeBuddy capability UPGRADES — ADR-1239 / #2098 (EoS/codebuddy).
*
* Drives the user-reachable surface (spawned `bin/install.js` via
* `runMinimalInstall`) plus a direct negotiation-contract check to prove the
* two real upgrades CodeBuddy contributes as part of the EoS migration:
*
* UPGRADE 1 — extended hook events: CodeBuddy's `extendedHookEvents` was
* previously `[]` (none of the 4 extended events were wired). This PR
* wires all four — `SubagentStop`/`Stop`/`PreCompact`/`SubagentStart` —
* into `extendedHookEvents` (mirrors qwen #2092 / kimi exactly — same 4
* events), so a live install registers all four as hooks in settings.json
* routed through gsd-context-monitor.js, giving CodeBuddy the
* subagent-lifecycle + stop/compact hooks it previously lacked.
*
* UPGRADE 2 — dispatch.background: CodeBuddy's capability.json already
* declares `dispatch.background: true`, which legitimately EXCEEDS the
* `declarative-cli` profile baseline (`false`, per
* `PROFILE_BASELINES['declarative-cli']` in src/host-integration.cts).
* NO agent-file/frontmatter change is involved — the CodeBuddy CLI has no
* background-dispatch frontmatter field (agentMode/enabledAutoRun are
* IDE-only; verified via codebuddy.ai/docs/cli/sub-agents). This is
* surfaced purely through the negotiated contract: a documented `true`
* value must survive negotiation without a downgrade warning.
*/
const { test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const { runMinimalInstall } = require('./helpers/install-shared.cjs');
const { cleanup } = require('./helpers.cjs');
const {
profileOf,
negotiateHostCapabilities,
PROFILE_BASELINES,
} = require('../gsd-core/bin/lib/host-integration.cjs');
const CODEBUDDY_CAP = JSON.parse(
fs.readFileSync(path.join(__dirname, '..', 'capabilities', 'codebuddy', 'capability.json'), 'utf8'),
);
const CODEBUDDY_AXES = CODEBUDDY_CAP.runtime.hostIntegration;
// ---------------------------------------------------------------------------
// UPGRADE 1: extended hook events — all 4 newly-wired events' live-install coverage
// ---------------------------------------------------------------------------
test('capabilities/codebuddy/capability.json extendedHookEvents contains exactly the 4 documented events', () => {
const events = CODEBUDDY_CAP.runtime.extendedHookEvents;
assert.deepEqual(events, ['SubagentStop', 'Stop', 'PreCompact', 'SubagentStart']);
assert.equal(events.length, 4);
});
test('codebuddy --global: settings.json wires all 4 newly-added extended hook events to the GSD context-monitor hook (UPGRADE 1)', (t) => {
const { configDir, root } = runMinimalInstall({ runtime: 'codebuddy', scope: 'global' });
t.after(() => cleanup(root));
const settingsPath = path.join(configDir, 'settings.json');
assert.ok(fs.existsSync(settingsPath), `${settingsPath} must exist`);
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
// All 4 events were newly wired by this PR — codebuddy's extendedHookEvents
// was previously [], so none of these existed before #2098.
const NEWLY_WIRED_EXTENDED_EVENTS = ['SubagentStop', 'Stop', 'PreCompact', 'SubagentStart'];
const entries = NEWLY_WIRED_EXTENDED_EVENTS.map((eventName) => {
const eventHooks = settings.hooks && settings.hooks[eventName];
assert.ok(Array.isArray(eventHooks) && eventHooks.length > 0,
`settings.hooks.${eventName} must exist and be non-empty`);
const entry = eventHooks[0].hooks[0];
assert.ok(entry.command.includes('gsd-context-monitor'),
`${eventName} command must reference gsd-context-monitor.js, got: ${entry.command}`);
assert.equal(entry.timeout, 10, `${eventName} entry must have timeout 10`);
return entry;
});
const [stopEntry, ...restEntries] = entries;
for (const entry of restEntries) {
assert.equal(entry.command, stopEntry.command,
'all 4 newly-wired extended events must wire the same gsd-context-monitor command');
}
});
test('a runtime whose extendedHookEvents omits SubagentStart does NOT get one (descriptor-gated, not a global default)', (t) => {
const { configDir, root } = runMinimalInstall({ runtime: 'claude', scope: 'global' });
t.after(() => cleanup(root));
const settingsPath = path.join(configDir, 'settings.json');
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
assert.ok(
settings.hooks && Array.isArray(settings.hooks.SubagentStop) && settings.hooks.SubagentStop.length > 0,
'claude must have SubagentStop wired (sanity — proves hooks ARE configured)',
);
assert.ok(
!settings.hooks || settings.hooks.SubagentStart === undefined,
"claude must NOT have SubagentStart wired — it is not in claude's extendedHookEvents",
);
});
// ---------------------------------------------------------------------------
// UPGRADE 2: dispatch.background negotiation — documented true survives,
// exceeding the declarative-cli profile baseline. No agent-file/frontmatter
// change; caller-side invocation param only (no code path emits it).
// ---------------------------------------------------------------------------
test("codebuddy classifies as the 'declarative-cli' profile, whose baseline dispatch.background is false", () => {
assert.equal(profileOf(CODEBUDDY_AXES), 'declarative-cli');
assert.equal(PROFILE_BASELINES['declarative-cli'].dispatch.background, false,
'sanity: the declarative-cli baseline is false — codebuddy legitimately exceeds it');
});
test('negotiateHostCapabilities surfaces codebuddy\'s documented dispatch.background:true with no downgrade warning (UPGRADE 2)', () => {
assert.equal(CODEBUDDY_AXES.dispatch.background, true,
'sanity: the descriptor declares dispatch.background: true (documented, not undocumented)');
const { effective, warnings } = negotiateHostCapabilities(CODEBUDDY_AXES);
assert.equal(effective.dispatch.background, true,
'a documented true value must survive negotiation, exceeding the declarative-cli baseline of false');
assert.ok(
!warnings.some((w) => w.includes('dispatch.background')),
`no warning may be raised for the documented dispatch.background axis, got: ${JSON.stringify(warnings)}`,
);
});