← back to Exo Helper

test/exo-client.test.mjs

126 lines

import test from 'node:test';
import assert from 'node:assert/strict';
import { ExoClient, DEFAULT_MODEL, DEEPSEEK_MODEL, summarizeState } from '../exo-client.mjs';

const state = (ready = true) => ({
  topology: { nodes: ['node-a'] },
  nodeIdentities: { 'node-a': { chipId: 'Test chip' } },
  instances: { instance: { MlxRingInstance: { shardAssignments: {
    modelId: DEFAULT_MODEL, nodeToRunner: { 'node-a': 'runner-a' }, runnerToShard: { 'runner-a': {} },
  } } } },
  runners: { 'runner-a': ready ? { RunnerReady: {} } : { RunnerLoading: {} } },
});
const json = data => ({ ok: true, json: async () => data });

test('an online cluster does not imply model readiness', () => {
  assert.equal(summarizeState(state(false), DEFAULT_MODEL).model_ready, false);
  const missing = state(); missing.topology.nodes = [];
  assert.equal(summarizeState(missing, DEFAULT_MODEL).model_ready, false);
  assert.equal(summarizeState(state(), 'another-model').model_ready, false);
});

test('not-ready model never sends inference or fallback requests', async () => {
  const calls = [];
  const client = new ExoClient({ fetchImpl: async url => { calls.push(url); return json(state(false)); } });
  await assert.rejects(client.ask({ prompt: 'hello' }), { code: 'MODEL_NOT_READY' });
  assert.deepEqual(calls, ['http://127.0.0.1:52415/state']);
});

test('input limits reject before contacting Exo', async () => {
  const client = new ExoClient({ fetchImpl: () => assert.fail('must not fetch') });
  await assert.rejects(client.ask({ prompt: 'x'.repeat(16000), context: 'x' }), { code: 'INPUT_TOO_LARGE' });
  await assert.rejects(client.ask({ prompt: 'hello', max_tokens: 99999 }), { code: 'INVALID_INPUT' });
  await assert.rejects(client.ask({ prompt: '   ' }), { code: 'INVALID_INPUT' });
});

test('only explicit loopback origins are accepted', () => {
  for (const baseUrl of ['https://api.openai.com', 'http://127.0.0.1.evil.test', 'http://user:pass@127.0.0.1', 'http://127.0.0.1/v1']) {
    assert.throws(() => new ExoClient({ baseUrl }), /loopback/);
  }
});

test('successful answer retains model, correlation, usage, and truncation evidence', async () => {
  const client = new ExoClient({ fetchImpl: async (url, init) => {
    if (url.endsWith('/state')) return json(state());
    const body = JSON.parse(init.body);
    assert.equal(body.model, DEFAULT_MODEL);
    assert.equal(body.max_tokens, 12);
    assert.equal(init.redirect, 'error');
    assert.ok(init.headers['X-Request-ID']);
    assert.equal(body.messages.at(-1).content, 'Summarize\n\n<context>\nhello\n</context>');
    return json({ id: 'response-a', model: DEFAULT_MODEL, choices: [{ message: { content: 'Summary' }, finish_reason: 'length' }], usage: { total_tokens: 17 } });
  } });
  const result = await client.ask({ prompt: 'Summarize', context: 'hello', max_tokens: 12 });
  assert.equal(result.answer, 'Summary'); assert.equal(result.truncated, true);
  assert.equal(result.response_id, 'response-a'); assert.equal(result.review_required, true);
});

test('timeout produces one failure, no retry, and releases process capacity', async () => {
  let count = 0;
  const client = new ExoClient({ fetchImpl: async url => {
    if (url.endsWith('/state')) return json(state());
    count++; throw Object.assign(new Error('timeout'), { name: 'TimeoutError' });
  } });
  await assert.rejects(client.ask({ prompt: 'hello' }), { code: 'TIMEOUT' });
  assert.equal(count, 1); assert.equal(client.busy, false);
});

test('HTTP failures do not expose arbitrary upstream response bodies', async () => {
  const client = new ExoClient({ fetchImpl: async () => ({ ok: false, status: 401, text: () => assert.fail('must not echo response body') }) });
  await assert.rejects(client.ask({ prompt: 'hello' }), { code: 'EXO_HTTP_ERROR' });
});

test('simultaneous requests in one client are rejected before extra work', async () => {
  let release;
  const client = new ExoClient({ fetchImpl: () => new Promise(resolve => { release = resolve; }) });
  const first = client.ask({ prompt: 'hello' });
  await assert.rejects(client.ask({ prompt: 'second' }), { code: 'BUSY' });
  release(json(state(false)));
  await assert.rejects(first, { code: 'MODEL_NOT_READY' });
});

test('a different model response cannot count as success', async () => {
  const client = new ExoClient({ fetchImpl: async url => json(url.endsWith('/state') ? state() : {
    model: 'wrong-model', choices: [{ message: { content: 'hello' }, finish_reason: 'stop' }],
  }) });
  await assert.rejects(client.ask({ prompt: 'hello' }), { code: 'MODEL_MISMATCH' });
});

test('DeepSeek selection routes explicitly and does not change the default', async () => {
  const models = [];
  const both = state();
  both.instances.deepseek = { MlxRingInstance: { shardAssignments: {
    modelId: DEEPSEEK_MODEL, nodeToRunner: { 'node-a': 'runner-b' }, runnerToShard: { 'runner-b': {} },
  } } };
  both.runners['runner-b'] = { RunnerReady: {} };
  const client = new ExoClient({ fetchImpl: async (url, init) => {
    if (url.endsWith('/state')) return json(both);
    const body = JSON.parse(init.body); models.push(body.model);
    if (body.model === DEEPSEEK_MODEL) {
      assert.equal(body.max_tokens, 1024);
      assert.deepEqual(body.messages.map(m => m.role), ['user']);
    }
    return json({ id: 'selected-response', model: body.model, choices: [{ message: { content: '42' }, finish_reason: 'stop' }] });
  } });
  assert.equal((await client.ask({ model: 'deepseek', prompt: 'Add' })).model, DEEPSEEK_MODEL);
  assert.equal((await client.ask({ prompt: 'Add' })).model, DEFAULT_MODEL);
  assert.deepEqual(models, [DEEPSEEK_MODEL, DEFAULT_MODEL]);
  const status = await client.status(undefined, 'deepseek');
  assert.equal(status.model_ready, true);
  assert.deepEqual(status.selectable_models.map(m => m.ready), [true, true]);
});

test('unready DeepSeek cannot fall back to a ready Qwen instance', async () => {
  const calls = [];
  const client = new ExoClient({ fetchImpl: async url => { calls.push(url); return json(state()); } });
  await assert.rejects(client.ask({ model: 'deepseek', prompt: 'hello' }), { code: 'MODEL_NOT_READY' });
  assert.equal(calls.length, 1);
});

test('unknown or malformed model selection fails before network access', async () => {
  const client = new ExoClient({ fetchImpl: () => assert.fail('must not fetch') });
  for (const model of ['untrusted/model', 'toString', '__proto__', null, 7]) {
    await assert.rejects(client.ask({ model, prompt: 'hello' }), { code: 'INVALID_MODEL' });
  }
});