Files
msd-core/tests/kimi-imperative-reference.test.cjs
Tom Boucher ab04916682 feat(#2095): migrate Kimi CLI onto EoS imperative adapter + native hook-bus + background dispatch (ADR-1239)
Fold all runtime==='kimi'/isKimi logic branches into descriptor-driven
hostBehaviors (localInstallDeferred, verificationStyle, agentManifestStyle,
reapplyCommand, doneBannerStyle) + add 'kimi' to _DESCRIPTOR_AGENTS_RUNTIMES.
Kimi's skills/kimi-agents dispatch was already descriptor-driven (converter-by-
name + kimi-agents kind). Zero isKimi/runtime==='kimi' branches remain.

UPGRADE 1 (native hook bus): new hooksSurface 'kimi-hooks-toml' + a marker-
delimited config.toml [[hooks]] emitter (buildKimiHooksTomlBlock/writeKimiHooksToml
in runtime-hooks-surface.cts; resolveKimiHooksTomlDir in runtime-homes.cts).
GSD's lifecycle hooks now wire into Kimi's native ~/.kimi/config.toml (Context7-
confirmed path) at SessionStart/PreToolUse/Stop/PreCompact/SubagentStart/
SubagentStop — kimi becomes a hooks/ consumer (the 3 && !isKimi exclusion guards
removed). config.toml holds absolute install paths so it's golden-excluded via
an exact relative-path (.kimi/config.toml), not a basename (which would blind
Codex's config.toml). New hooksSurface value added to the closed enum in
capability-validator + runtime-config-adapter-registry.
UPGRADE 2 (background dispatch): flip dispatch.backgroundDispatch true (Kimi's
Agent tool takes run_in_background; root agent already gets the Agent tool), so
negotiation no longer flattens dispatch. subagentToolkit stays 'undocumented'
per AC (coder/explore/plan have distinct tool policies).
MCP transport explicitly deferred (no installer-driven MCP for any runtime).

Golden: only kimi.json changes (hooks/ scripts now installed); all 15 others +
claude-local byte-identical (kilo/zcode keep their own exclusions). Tests:
kimi-imperative-reference (adapter/axes/fail-closed/hostBehaviors + source-grep
guard) + kimi-upgrades (config.toml [[hooks]] SessionStart + marker idempotency
+ backgroundDispatch negotiation). CONTEXT.md glossary + matrix + how-to updated;
changeset (Added).

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

156 lines
7.5 KiB
JavaScript

// allow-test-rule: structural-regression-guard — AC2 requires asserting no `runtime === 'kimi'` string-equality branch (nor an `isKimi` helper) remains in bin/install.js, src/install-engine.cts, and src/runtime-artifact-conversion.cts — the descriptor-migration contract is a property of the source text, so a source-grep is the only faithful check (#2095)
'use strict';
/**
* kimi imperative reference host — ADR-1239 Phase D / #2095 (EoS/kimi).
*
* Proves kimi is driven through the PUBLIC Host-Integration Interface (the
* imperative adapter), that its negotiated axes classify + negotiate correctly,
* that negotiation fails CLOSED on a corrupted descriptor, and that the
* migration retired the hardcoded `runtime === 'kimi'` / `isKimi` branches
* across the install engine and artifact conversion modules (folded into
* descriptor-driven `runtime.hostBehaviors`).
*
* kimi contributes two real upgrades covered in tests/kimi-upgrades.test.cjs:
*
* UPGRADE 1 — native hook bus: `hooksSurface` moved from `"none"` to
* `"kimi-hooks-toml"` — GSD's lifecycle hooks are now registered as
* `[[hooks]]` array-of-tables entries in Kimi's own native
* `~/.kimi/config.toml` (Beta feature on Kimi's side), marker-delimited so
* reinstall only ever touches GSD's own block.
*
* UPGRADE 2 — background dispatch: `hostIntegration.dispatch.backgroundDispatch`
* flipped `false` → `true`, which flips `shouldFlattenDispatch` to `false`
* for kimi (joining codex/cursor/opencode as background-eligible).
*
* kimi's `dispatch.subagentToolkit` stays `'undocumented'` (no authoritative
* doc found), so this file also proves that axis degrades closed to
* `'read-only'` on negotiation rather than being trusted at face value.
*/
const { test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const { createImperativeAdapter } = require('../gsd-core/bin/lib/adapter-imperative.cjs');
const {
profileOf,
negotiateHostCapabilities,
PROFILE_BASELINES,
UNDOCUMENTED,
} = require('../gsd-core/bin/lib/host-integration.cjs');
const KIMI_CAP = JSON.parse(
fs.readFileSync(path.join(__dirname, '..', 'capabilities', 'kimi', 'capability.json'), 'utf8'),
);
const KIMI_AXES = KIMI_CAP.runtime.hostIntegration;
// -- AC2: driven through the public interface (imperative adapter) -----------
test('createImperativeAdapter classifies kimi as imperative + composes the registry', () => {
const adapter = createImperativeAdapter({ runtime: 'kimi' });
assert.equal(adapter.kind, 'imperative');
assert.equal(adapter.runtime, 'kimi');
assert.ok(adapter.registry && typeof adapter.registry === 'object');
assert.equal(typeof adapter.install, 'function');
assert.equal(typeof adapter.uninstall, 'function');
});
test('kimi axes classify as the programmatic-cli reference profile', () => {
assert.equal(profileOf(KIMI_AXES), 'programmatic-cli');
});
// -- AC3: all axes populated + validated -------------------------------------
test('kimi descriptor declares all 8 axes + 6 dispatch sub-axes with exact values', () => {
assert.equal(KIMI_AXES.embeddingMode, 'imperative');
assert.equal(KIMI_AXES.commandSurface, 'slash-file');
assert.equal(KIMI_AXES.modelMode, 'passive');
assert.equal(KIMI_AXES.hookBus, 'host');
assert.equal(KIMI_AXES.stateIO, 'filesystem');
assert.equal(KIMI_AXES.transport, 'mcp');
assert.equal(KIMI_AXES.runtime, 'python');
const d = KIMI_AXES.dispatch;
assert.equal(d.namedDispatch, true);
assert.equal(d.nested, false);
assert.equal(d.maxDepth, 1);
assert.equal(d.background, true);
assert.equal(d.subagentToolkit, 'undocumented');
assert.equal(d.backgroundDispatch, true);
});
// -- AC5: negotiation fails CLOSED on a corrupted descriptor ------------------
test('negotiateHostCapabilities never throws for kimi, even fully corrupted', () => {
assert.doesNotThrow(() => negotiateHostCapabilities({}));
assert.doesNotThrow(() => negotiateHostCapabilities({ ...KIMI_AXES, embeddingMode: UNDOCUMENTED }));
assert.doesNotThrow(() => negotiateHostCapabilities({ ...KIMI_AXES, embeddingMode: 'future-unknown' }));
assert.doesNotThrow(() => negotiateHostCapabilities({ ...KIMI_AXES, dispatch: 'corrupted-not-an-object' }));
assert.doesNotThrow(() => negotiateHostCapabilities({ ...KIMI_AXES, dispatch: { ...KIMI_AXES.dispatch, maxDepth: 'not-a-number' } }));
});
test('a partial/empty kimi descriptor degrades to the safe floor, not the programmatic-cli baseline', () => {
const result = negotiateHostCapabilities({});
assert.equal(result.effective.embeddingMode, 'declarative', 'omitted embeddingMode degrades closed');
assert.equal(result.effective.hookBus, 'none');
assert.notDeepEqual(result.effective, PROFILE_BASELINES['programmatic-cli']);
assert.ok(result.warnings.length > 0);
});
test("kimi's undocumented dispatch.subagentToolkit degrades closed to 'read-only' on negotiation", () => {
const { effective, warnings } = negotiateHostCapabilities(KIMI_AXES);
assert.equal(KIMI_AXES.dispatch.subagentToolkit, 'undocumented', 'sanity: the descriptor itself stays undocumented');
assert.equal(effective.dispatch.subagentToolkit, 'read-only', 'undocumented never propagates as full — degrades closed');
assert.ok(
warnings.some((w) => /subagentToolkit is undocumented/.test(w)),
'a warning must be raised for the undocumented axis',
);
});
// -- AC2: the folded-in behaviors ---------------------------------------------
test('kimi descriptor declares runtime.hostBehaviors (the folded-in behaviors)', () => {
const hb = KIMI_CAP.runtime.hostBehaviors;
assert.ok(hb && typeof hb === 'object');
assert.equal(hb.reapplyCommand, '/skill:gsd-update --reapply');
assert.equal(hb.localInstallDeferred, true);
assert.equal(hb.verificationStyle, 'kimi');
assert.equal(hb.agentManifestStyle, 'kimi-nested');
assert.equal(hb.doneBannerStyle, 'kimi-agent-file');
});
// -- boundary/negative: hooksSurface + extendedHookEvents are exactly what's documented
test('capabilities/kimi/capability.json hooksSurface is "kimi-hooks-toml" and extendedHookEvents is exactly the 4 documented events', () => {
assert.equal(KIMI_CAP.runtime.hooksSurface, 'kimi-hooks-toml');
const events = KIMI_CAP.runtime.extendedHookEvents;
assert.deepEqual(events, ['SubagentStop', 'Stop', 'PreCompact', 'SubagentStart']);
assert.equal(events.length, 4);
});
// -- AC2: the hardcoded branches are retired across all folded modules -------
test('no `runtime === "kimi"` string-equality branch (nor an `isKimi` helper) remains in the descriptor-migrated modules (AC2)', () => {
const strip = (src) => src
.replace(/\/\*[\s\S]*?\*\//g, '')
.replace(/\/\/[^\r\n]*/g, '')
.replace(/`[^`]*`/g, '');
const repoRoot = path.join(__dirname, '..');
const files = [
path.join(repoRoot, 'bin', 'install.js'),
path.join(repoRoot, 'src', 'install-engine.cts'),
path.join(repoRoot, 'src', 'runtime-artifact-conversion.cts'),
];
for (const file of files) {
const src = fs.readFileSync(file, 'utf8');
const stripped = strip(src);
const offenders = stripped.match(/runtime\s*[!=]==\s*'kimi'/g) || [];
assert.deepEqual(offenders, [],
`AC2: no hardcoded runtime==='kimi' branch may remain in ${path.relative(repoRoot, file)}; found: ${offenders.join(', ')}`);
const isKimiHits = stripped.match(/\bisKimi\b/g) || [];
assert.deepEqual(isKimiHits, [],
`AC2: no isKimi helper may remain in ${path.relative(repoRoot, file)}; found ${isKimiHits.length} occurrence(s)`);
}
});