The endpoint ended after MOSAIC_FIXTURE_TTL_S, but init passed it only PATH, so the lifetime was always the 14400 s default. Its record stayed after it ended, and init refused fixture-exists until `dev fixture stop`. init now takes --ttl SECONDS (0 = no expiry; otherwise 60 to 2147483, the longest delay a Node timer holds; the default stays 14400), passes it to the endpoint, and records ttlSeconds and expiresAt. /health and status report them. A record whose pid is no longer the endpoint is stale: init removes it and says which pid it replaced. A live endpoint still refuses fixture-exists. A running bus keeps the endpoint it read at start, so after a replacement the bus needs a restart; --ttl 0 avoids the expiry in the first place.
119 lines
7.0 KiB
JavaScript
119 lines
7.0 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { request } from 'node:http';
|
|
import { createLoader, hasModule } from '../src/loader.mjs';
|
|
import { selectModelProvider } from '../src/model-provider-selection.mjs';
|
|
import * as fixture from '../model-provider/fixture/index.mjs';
|
|
import { createEndpoint, HOST, lifetimeSeconds, serve, TTL_DEFAULT_S, TTL_MAX_S, TTL_MIN_S } from '../model-provider/fixture/endpoint.mjs';
|
|
|
|
const { FIXTURE_ENTRY, FIXTURE_KEY, FIXTURE_MODEL, FIXTURE_WORD } = fixture;
|
|
|
|
test('the loader accepts the fixture as a model-provider module', async () => {
|
|
assert.equal(hasModule('model-provider', 'fixture'), true);
|
|
const log = [];
|
|
const loader = createLoader({ log: e => log.push(e) });
|
|
const r = await loader.load({ 'model-provider': ['fixture'] });
|
|
assert.deepEqual(r.refused, []);
|
|
assert.deepEqual(log, []);
|
|
assert.deepEqual(loader.snapshot('model-provider').map(m => m.name), ['fixture']);
|
|
});
|
|
|
|
test('catalogue offers one static model for pi with no upstream credential', async () => {
|
|
const c = await fixture.catalogue({ now: () => '2026-11-07T00:00:00.000Z' });
|
|
assert.equal(c.status, 'ready');
|
|
assert.deepEqual(c.models.map(m => [m.key, m.provider, m.model, m.upstream, m.available, m.harnesses]),
|
|
[['fixture-deterministic', 'proxy', FIXTURE_MODEL, 'fixture', true, ['pi']]]);
|
|
});
|
|
|
|
test('route answers only the fixture entry and only for pi', () => {
|
|
const route = fixture.route({ selection: FIXTURE_ENTRY, harness: 'pi' });
|
|
assert.deepEqual(route, { provider: 'proxy', model: FIXTURE_MODEL, upstream: 'fixture', routeId: 'fixture', endpoint: 'root', api: 'anthropic-messages', piProvider: 'mosaic-fixture', compat: null });
|
|
assert.deepEqual(fixture.route({ selection: { ...FIXTURE_ENTRY, model: 'claude-opus' }, harness: 'pi' }), { code: 'model-provider-unavailable' });
|
|
assert.deepEqual(fixture.route({ selection: { ...FIXTURE_ENTRY, upstream: 'zai' }, harness: 'pi' }), { code: 'model-provider-unavailable' });
|
|
assert.deepEqual(fixture.route({ selection: { ...FIXTURE_ENTRY, module: 'cliproxy' }, harness: 'pi' }), { code: 'model-provider-unavailable' });
|
|
assert.deepEqual(fixture.route({ selection: { ...FIXTURE_ENTRY, provider: 'anthropic' }, harness: 'pi' }), { code: 'model-provider-unavailable' });
|
|
assert.deepEqual(fixture.route({ selection: FIXTURE_ENTRY, harness: 'claude-code' }), { code: 'model-harness-incompatible' });
|
|
});
|
|
|
|
test('preflight allows the fixture model for pi without a catalogue and refuses anything else', () => {
|
|
assert.deepEqual(fixture.preflight({ selection: FIXTURE_ENTRY, harness: 'pi', catalogue: { status: 'unavailable' } }), { allow: true });
|
|
assert.deepEqual(fixture.preflight({ selection: FIXTURE_ENTRY, harness: 'claude-code' }), { allow: false, code: 'model-harness-incompatible' });
|
|
assert.deepEqual(fixture.preflight({ selection: { ...FIXTURE_ENTRY, model: 'other' }, harness: 'pi' }), { allow: false, code: 'model-provider-unavailable' });
|
|
});
|
|
|
|
test('selection picks the fixture module for its entry', () => {
|
|
const picked = selectModelProvider([{ name: 'fixture', module: fixture }], FIXTURE_ENTRY);
|
|
assert.equal(picked.name, 'fixture');
|
|
assert.equal(selectModelProvider([{ name: 'fixture', module: fixture }], { ...FIXTURE_ENTRY, module: 'cliproxy' }), null);
|
|
});
|
|
|
|
function call(baseUrl, { method = 'POST', path = '/v1/messages', key = FIXTURE_KEY, body } = {}) {
|
|
return new Promise((resolve, reject) => {
|
|
const req = request(`${baseUrl}${path}`, { method, headers: { 'content-type': 'application/json', ...(key ? { 'x-api-key': key } : {}) } }, res => {
|
|
let text = ''; res.setEncoding('utf8'); res.on('data', c => { text += c; }); res.on('end', () => resolve({ status: res.statusCode, type: res.headers['content-type'], text }));
|
|
});
|
|
req.on('error', reject);
|
|
req.end(body === undefined ? undefined : JSON.stringify(body));
|
|
});
|
|
}
|
|
|
|
test('the endpoint binds loopback, answers the fixed word and refuses a wrong key or model', async t => {
|
|
const ep = createEndpoint();
|
|
const { port, baseUrl } = await ep.listen(0);
|
|
t.after(() => ep.close());
|
|
assert.equal(ep.server.address().address, HOST);
|
|
assert.equal(baseUrl, `http://127.0.0.1:${port}`);
|
|
const health = JSON.parse((await call(baseUrl, { method: 'GET', path: '/health' })).text);
|
|
assert.deepEqual([health.ok, health.upstream, health.model], [true, 'none', FIXTURE_MODEL]);
|
|
|
|
const plain = await call(baseUrl, { body: { model: FIXTURE_MODEL, max_tokens: 8, messages: [{ role: 'user', content: 'hi' }] } });
|
|
assert.equal(plain.status, 200);
|
|
assert.equal(JSON.parse(plain.text).content[0].text, FIXTURE_WORD);
|
|
|
|
const stream = await call(baseUrl, { body: { model: FIXTURE_MODEL, stream: true, messages: [] } });
|
|
assert.equal(stream.status, 200);
|
|
assert.match(stream.type, /^text\/event-stream/);
|
|
assert.match(stream.text, new RegExp(`"text":"${FIXTURE_WORD}"`));
|
|
assert.match(stream.text, /event: message_stop/);
|
|
|
|
assert.equal((await call(baseUrl, { key: 'sk-ant-real-looking', body: { model: FIXTURE_MODEL } })).status, 401);
|
|
assert.equal((await call(baseUrl, { key: null, body: { model: FIXTURE_MODEL } })).status, 401);
|
|
assert.equal((await call(baseUrl, { body: { model: 'claude-opus' } })).status, 404);
|
|
assert.equal((await call(baseUrl, { method: 'GET' })).status, 404);
|
|
assert.deepEqual(ep.counts, { messages: 2, refused: 4 });
|
|
});
|
|
|
|
// P19 #882: the lifetime an endpoint process takes from MOSAIC_FIXTURE_TTL_S.
|
|
test('the endpoint lifetime: a default, 0 for no expiry, and bounds a Node timer can hold', () => {
|
|
for (const raw of [undefined, '', 'soon']) assert.equal(lifetimeSeconds(raw), TTL_DEFAULT_S, String(raw));
|
|
assert.equal(TTL_DEFAULT_S, 14400);
|
|
assert.equal(lifetimeSeconds('0'), 0);
|
|
assert.equal(lifetimeSeconds('120'), 120);
|
|
for (const raw of ['5', '-3']) assert.equal(lifetimeSeconds(raw), TTL_MIN_S, raw);
|
|
// Past 2^31-1 ms a timer fires after 1 ms, so a longer lifetime is held to the longest one.
|
|
for (const raw of [String(TTL_MAX_S + 1), '1e12']) assert.equal(lifetimeSeconds(raw), TTL_MAX_S, raw);
|
|
assert.ok(TTL_MAX_S * 1000 <= 2 ** 31 - 1);
|
|
});
|
|
|
|
test('serve expires the endpoint when its lifetime ends, and never with a lifetime of 0', async t => {
|
|
const open = await serve({ ttlSeconds: 0, expire: () => assert.fail('a lifetime of 0 expired') });
|
|
t.after(() => open.endpoint.close());
|
|
assert.equal(open.timer, null);
|
|
const health = JSON.parse((await call(open.baseUrl, { method: 'GET', path: '/health' })).text);
|
|
assert.deepEqual([health.ok, health.ttlSeconds, health.expiresAt], [true, 0, null]);
|
|
|
|
t.mock.timers.enable({ apis: ['setTimeout'] });
|
|
let expired = 0;
|
|
const before = Date.now();
|
|
const timed = await serve({ ttlSeconds: 120, expire: () => { expired++; } });
|
|
t.after(() => timed.endpoint.close());
|
|
const at = Date.parse(timed.expiresAt) - before;
|
|
assert.ok(at >= 120000 && at < 130000, timed.expiresAt);
|
|
t.mock.timers.tick(119999);
|
|
assert.equal(expired, 0);
|
|
t.mock.timers.tick(1);
|
|
assert.equal(expired, 1);
|
|
t.mock.timers.tick(TTL_MAX_S * 1000);
|
|
assert.equal(expired, 1, 'a lifetime of 0 set no timer');
|
|
});
|