Files
msd-core/tests/adapter-imperative.test.cjs
Tom Boucher da368311ea feat(#1680): ADR-1239 Phase C-1 — imperative embedding adapter (composes loadRegistry) [AC2] (#1803)
* feat(#1680): ADR-1239 Phase C-1 — imperative embedding adapter (composes loadRegistry) [AC2]

Phase 3 slice 2 (AC2). The engine-as-library path: createImperativeAdapter
composes loadRegistry({includeInstalled:true}) — first-party-wins + consent +
fail-closed gates, identical trust semantics to the CLI — and binds the engine
surface behind the SAME HostIntegrationInterface the declarative adapter (AC1)
satisfies, plus a registry accessor for the composed capability set.

- src/adapter-imperative.cts: createImperativeAdapter({runtime}, {loadOptions})
  → ImperativeAdapter (kind:'imperative' + .registry + install/uninstall
  delegating to install-engine). Thin: delegates the loop, does not reimplement.
- tests/adapter-imperative.test.cjs: kind (16 runtimes), registry composition
  (loadRegistry called with includeInstalled:true), loadOptions pass-through,
  install/uninstall delegation, fail-closed construction.
- eslint.config.mjs + docs/INVENTORY-MANIFEST.json: ADR-457 ignores entry +
  cli_modules entry for the new emitted .cjs (the two drift gates that bit AC1,
  applied proactively here).

Concrete host binding (OpenCode/VS Code/pi) deferred to Phase 5 (#1682).

* test: remove dead readStateMd helper from bug-1760 test

readStateMd was defined but never called (writeStateMd is the only state-md
helper this test uses). Clears the lone no-unused-vars warning so the repo
lints fully clean (0 problems). No behavior change — test still passes 2/2.

* chore(changeset): add Changed fragment for imperative embedding adapter (#1680)

* fix(adapter-imperative): reword comment to avoid injection-scan substring match

The prompt-injection scan regex 'act\s+as\s+(?:a|an|the)' was matching the
'act as the' substring inside 'contract as the declarative adapter' (contrACT
AS THE). Reword 'contract as' -> 'shape as' — no 'act' substring, identical
meaning. Clears the 'lib source files are clean' + 'codebase prompt injection
scan' security-gate failures.
2026-06-28 11:10:59 -04:00

99 lines
5.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict';
/**
* Tests for the imperative embedding adapter (ADR-1239 Phase C-1, AC2 / #1680).
*
* Pins:
* 1. KIND — `kind: 'imperative'`, satisfies the same HostIntegrationInterface
* as the declarative adapter (both bind one engine).
* 2. REGISTRY COMPOSITION — the adapter composes loadRegistry({includeInstalled:
* true}) and exposes the result as `.registry` (first-party ∪ installed, so
* an in-process host gets identical trust semantics to the CLI).
* 3. DELEGATION — install/uninstall delegate in-process to install-engine
* (byte-identity link, same as the declarative adapter).
* 4. FAIL-CLOSED CONSTRUCTION — missing/invalid runtime throws.
*
* Behavioral tests only; delegation + loadRegistry verified via module-ref
* monkeypatch (Node module cache shares the one module object).
*/
const { test } = require('node:test');
const assert = require('node:assert/strict');
const { createImperativeAdapter } = require('../gsd-core/bin/lib/adapter-imperative.cjs');
const installEngine = require('../gsd-core/bin/lib/install-engine.cjs');
const capabilityLoader = require('../gsd-core/bin/lib/capability-loader.cjs');
const registry = require('../gsd-core/bin/lib/capability-registry.cjs');
const RUNTIMES = Object.keys(registry.runtimes);
test('imperative adapter: kind === "imperative" + runtime echoed + registry present, for all 16 runtimes', () => {
for (const r of RUNTIMES) {
const adapter = createImperativeAdapter({ runtime: r });
assert.strictEqual(adapter.kind, 'imperative', `${r}: kind must be 'imperative'`);
assert.strictEqual(adapter.runtime, r, `${r}: adapter must echo the runtime id`);
assert.ok(adapter.registry && typeof adapter.registry === 'object', `${r}: registry must be present (composed loadRegistry result)`);
assert.strictEqual(typeof adapter.install, 'function', `${r}: install must be a function`);
assert.strictEqual(typeof adapter.uninstall, 'function', `${r}: uninstall must be a function`);
}
});
test('imperative adapter: loadRegistry composed with includeInstalled:true (host gets CLI-equivalent trust semantics)', () => {
// Restore-able spy on capabilityLoader.loadRegistry (module-ref, shared via Node cache).
const original = capabilityLoader.loadRegistry;
const calls = [];
capabilityLoader.loadRegistry = function (opts) {
calls.push(opts);
// Return a minimal stand-in registry so the factory does not crash.
return { _spy: true, runtimes: registry.runtimes };
};
try {
const adapter = createImperativeAdapter({ runtime: 'opencode' });
assert.ok(calls.length >= 1, 'loadRegistry must be invoked once during construction');
assert.strictEqual(calls[0].includeInstalled, true, 'loadRegistry must be called with includeInstalled:true (compose first-party ∪ installed)');
assert.strictEqual(adapter.registry._spy, true, 'adapter.registry must expose the composed loadRegistry result');
} finally {
capabilityLoader.loadRegistry = original;
}
});
test('imperative adapter: loadOptions forwarded to loadRegistry (cwd/gsdHome/hostVersion pass-through)', () => {
const original = capabilityLoader.loadRegistry;
let captured = null;
capabilityLoader.loadRegistry = function (opts) { captured = opts; return { runtimes: registry.runtimes }; };
try {
createImperativeAdapter({ runtime: 'codex' }, { loadOptions: { cwd: '/tmp/proj', hostVersion: '1.7.0' } });
assert.strictEqual(captured.includeInstalled, true, 'includeInstalled default preserved');
assert.strictEqual(captured.cwd, '/tmp/proj', 'loadOptions.cwd forwarded');
assert.strictEqual(captured.hostVersion, '1.7.0', 'loadOptions.hostVersion forwarded');
} finally {
capabilityLoader.loadRegistry = original;
}
});
test('imperative adapter.install/uninstall delegate in-process to install-engine with exact args', () => {
const origInstall = installEngine.installRuntimeArtifacts;
const origUninstall = installEngine.uninstallRuntimeArtifacts;
try {
for (const r of RUNTIMES) {
const adapter = createImperativeAdapter({ runtime: r });
let installArgs = null;
let uninstallArgs = null;
installEngine.installRuntimeArtifacts = function (...a) { installArgs = a; return undefined; };
installEngine.uninstallRuntimeArtifacts = function (...a) { uninstallArgs = a; return undefined; };
adapter.install({ configDir: '/tmp/imp/' + r, scope: 'global', resolvedProfile: { p: 1 } });
adapter.uninstall({ configDir: '/tmp/imp/' + r, scope: 'local' });
assert.deepStrictEqual(installArgs, [r, '/tmp/imp/' + r, 'global', { p: 1 }, undefined], `${r}: install delegation args`);
assert.deepStrictEqual(uninstallArgs, [r, '/tmp/imp/' + r, 'local'], `${r}: uninstall delegation args`);
}
} finally {
installEngine.installRuntimeArtifacts = origInstall;
installEngine.uninstallRuntimeArtifacts = origUninstall;
}
});
test('createImperativeAdapter: throws on missing/invalid runtime (fail-closed construction)', () => {
for (const bad of ['', undefined, null]) {
assert.throws(() => createImperativeAdapter({ runtime: bad }), TypeError, `runtime=${JSON.stringify(bad)} must throw`);
}
assert.throws(() => createImperativeAdapter({}), TypeError, 'missing runtime must throw');
});