* 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)
This commit is contained in:
73
tests/model-adapter.test.cjs
Normal file
73
tests/model-adapter.test.cjs
Normal file
@@ -0,0 +1,73 @@
|
||||
'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`);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user