Files
msd-core/tests/model-adapter.test.cjs
Tom Boucher b152f7e64c feat(#1680): ADR-1239 Phase C-1 — model adapter seam (passive + active) [AC3] (#1804)
* feat(#1680): ADR-1239 Phase C-1 — model adapter seam (passive + active) [AC3]

Phase 3 slice 3 (AC3). Two model-layer adapters selected by the negotiated
modelMode axis (host-integration.cts):

- passive: formalizes today's tier routing from src/model-resolver.cts —
  resolveModel delegates straight to resolveModelForTier (byte-for-behavior).
  This is the CLI runtimes (claude/gemini/codex/opencode/cursor/...): GSD injects
  prompts / a per-agent model field.
- active: a host-supplied sendRequest seam (VS Code vscode.lm / pi providers) —
  GSD calls the model through the host. Ships as a fail-closed seam (throws until
  a provider is bound); Phase 5 wires a concrete provider.

createModelAdapter({modelMode}, {sendRequest?}) — factory gating throws on
invalid mode. Proactive CI gates applied: ADR-457 eslint ignores entry +
INVENTORY-MANIFEST cli_modules entry + comment-wording audited for the
injection-scan substring trap.

* chore(changeset): add Changed fragment for model adapter seam (#1680)
2026-06-28 11:27:44 -04:00

74 lines
3.2 KiB
JavaScript

'use strict';
/**
* Tests for the model adapter seam (ADR-1239 Phase C-1, AC3 / #1680).
*
* Pins:
* 1. PASSIVE — `resolveModel` delegates to model-resolver's tier routing
* (reproduces today's behavior).
* 2. ACTIVE — `sendRequest` calls the host-supplied primitive; FAIL-CLOSED
* (throws) when no provider is bound.
* 3. FACTORY GATING — invalid modelMode throws.
*
* Behavioral tests only; delegation verified via module-ref monkeypatch.
*/
const { test } = require('node:test');
const assert = require('node:assert/strict');
const { createModelAdapter } = require('../gsd-core/bin/lib/model-adapter.cjs');
const modelResolver = require('../gsd-core/bin/lib/model-resolver.cjs');
test('passive model adapter: resolveModel delegates to model-resolver tier routing (reproduces today behavior)', () => {
const adapter = createModelAdapter({ modelMode: 'passive' });
assert.strictEqual(adapter.mode, 'passive');
const original = modelResolver.resolveModelForTier;
let captured = null;
modelResolver.resolveModelForTier = function (...a) { captured = a; return 'sonnet'; };
try {
const out = adapter.resolveModel({ cwd: '/tmp/proj', agentType: 'planner', attempt: 2 });
assert.strictEqual(out, 'sonnet', 'resolveModel must return the resolver result');
assert.deepStrictEqual(captured, ['/tmp/proj', 'planner', 2], 'must delegate (cwd, agentType, attempt) verbatim');
} finally {
modelResolver.resolveModelForTier = original;
}
});
test('passive model adapter: attempt is optional (delegates undefined when omitted)', () => {
const adapter = createModelAdapter({ modelMode: 'passive' });
const original = modelResolver.resolveModelForTier;
let captured = null;
modelResolver.resolveModelForTier = function (...a) { captured = a; return 'haiku'; };
try {
adapter.resolveModel({ cwd: '/tmp/proj', agentType: 'executor' });
assert.deepStrictEqual(captured, ['/tmp/proj', 'executor', undefined], 'attempt defaults to undefined');
} finally {
modelResolver.resolveModelForTier = original;
}
});
test('active model adapter: sendRequest invokes the bound host provider', () => {
const calls = [];
const adapter = createModelAdapter(
{ modelMode: 'active' },
{ sendRequest: (req) => { calls.push(req); return { ok: true, echo: req }; } },
);
assert.strictEqual(adapter.mode, 'active');
const out = adapter.sendRequest({ prompt: 'hi' });
assert.deepStrictEqual(calls, [{ prompt: 'hi' }], 'host provider invoked with the request');
assert.deepStrictEqual(out, { ok: true, echo: { prompt: 'hi' } }, 'host provider return value passed through');
});
test('active model adapter: FAIL-CLOSED when no host provider is bound (sendRequest throws)', () => {
const adapter = createModelAdapter({ modelMode: 'active' });
assert.throws(
() => adapter.sendRequest({ prompt: 'hi' }),
/no host provider bound/,
'sendRequest must throw when no provider is bound (fail-closed, never silently no-op)',
);
});
test('createModelAdapter: invalid modelMode throws (fail-closed construction)', () => {
for (const bad of ['host', 'dynamic', '', null, undefined, 3]) {
assert.throws(() => createModelAdapter({ modelMode: bad }), TypeError, `modelMode=${JSON.stringify(bad)} must throw TypeError`);
}
});