Docs/Skills and code/tests/

tests/qualification-uses-existing-llm.test.js

Code157 lines8.1 KBtests/

import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { callLLM, getLLMConfig, resetLLMState } from "../src/llm.js";

const here = path.dirname(fileURLToPath(import.meta.url));
const stagePath = path.join(here, "../src/stages/25-qualify-channels-with-llm.js");

test("batched qualification stage reuses the established src/llm.js API", () => {
  const source = fs.readFileSync(stagePath, "utf8");
  assert.match(source, /from ["']\.\.\/llm\.js["']/);
  assert.doesNotMatch(source, /from ["']\.\.\/llm-client\.js["']/);
  assert.match(source, /callLLM\(\{\s*system,\s*user,\s*stream:\s*false\s*\}\)/);
});

test("existing callLLM sends a non-streaming Ollama request using its established model config", async () => {
  const previousFetch = globalThis.fetch;
  const previousHost = process.env.OLLAMA_HOST;
  const previousModel = process.env.OLLAMA_MODEL;
  const previousProvider = process.env.LLM_PROVIDER;
  let captured;
  try {
    process.env.LLM_PROVIDER = "ollama";
    process.env.OLLAMA_HOST = "http://ollama.test:11434";
    process.env.OLLAMA_MODEL = "gemma4:31b-cloud-test";
    globalThis.fetch = async (url, options) => {
      captured = { url, options, body: JSON.parse(options.body) };
      return new Response(JSON.stringify({ response: '{"ok":true}' }), {
        status: 200,
        headers: { "content-type": "application/json" }
      });
    };
    const response = await callLLM({ system: "Return JSON", user: "{}", stream: false });
    assert.equal(response.content, '{"ok":true}');
    assert.equal(response.model, "gemma4:31b-cloud-test");
    assert.equal(captured.url, "http://ollama.test:11434/api/generate");
    assert.equal(captured.body.model, "gemma4:31b-cloud-test");
    assert.equal(captured.body.stream, false);
    assert.match(captured.body.prompt, /Return JSON/);
  } finally {
    globalThis.fetch = previousFetch;
    if (previousHost === undefined) delete process.env.OLLAMA_HOST; else process.env.OLLAMA_HOST = previousHost;
    if (previousModel === undefined) delete process.env.OLLAMA_MODEL; else process.env.OLLAMA_MODEL = previousModel;
    if (previousProvider === undefined) delete process.env.LLM_PROVIDER; else process.env.LLM_PROVIDER = previousProvider;
  }
});

test("qualification logs use the model reported by the existing helper", () => {
  const cfg = getLLMConfig();
  assert.ok(cfg.model);
  assert.ok(cfg.host);
});

// Runs fn with a fake fetch and a controlled environment, then restores everything.
async function withLlmEnv(env, handler, fn) {
  const names = ["LLM_PROVIDER", "LLM_FALLBACK", "GEMINI_API_KEY", "GOOGLE_API_KEY", "GEMINI_MODEL", "GEMINI_THINKING",
    "GEMINI_MAX_RETRIES", "OLLAMA_HOST", "OLLAMA_MODEL"];
  const previous = Object.fromEntries(names.map((name) => [name, process.env[name]]));
  const previousFetch = globalThis.fetch;
  const previousWarn = console.warn;
  const calls = [];
  const warnings = [];
  try {
    for (const name of names) delete process.env[name];
    Object.assign(process.env, { OLLAMA_HOST: "http://ollama.test:11434", OLLAMA_MODEL: "gemma4:test", GEMINI_MAX_RETRIES: "0", ...env });
    resetLLMState();
    console.warn = (...args) => warnings.push(args.join(" "));
    globalThis.fetch = async (url, options) => {
      const call = { url: String(url), options, body: options?.body ? JSON.parse(options.body) : null };
      calls.push(call);
      return handler(call);
    };
    await fn({ calls, warnings });
  } finally {
    globalThis.fetch = previousFetch;
    console.warn = previousWarn;
    resetLLMState();
    for (const name of names) {
      if (previous[name] === undefined) delete process.env[name]; else process.env[name] = previous[name];
    }
  }
}

const json = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
const geminiOk = (text) => json(200, { candidates: [{ content: { parts: [{ thought: true, text: "hidden" }, { text }] }, finishReason: "STOP" }], usageMetadata: { totalTokenCount: 9 } });
const dailyLimit = () => json(429, { error: { code: 429, status: "RESOURCE_EXHAUSTED", message: "Quota exceeded for metric: requests per day" } });
const isGemini = (call) => call.url.includes("generativelanguage.googleapis.com");

test("callLLM defaults to Gemma on the Gemini API when a key is set", async () => {
  await withLlmEnv({ GEMINI_API_KEY: "test-key" }, (call) => (isGemini(call) ? geminiOk('{"ok":true}') : json(500, {})), async ({ calls }) => {
    assert.equal(getLLMConfig().provider, "gemini");
    assert.equal(getLLMConfig().model, "gemma-4-26b-a4b-it");
    assert.equal(getLLMConfig().fallback.model, "gemma4:test");

    const response = await callLLM({ system: "Return JSON", user: "{}", stream: false });
    assert.equal(response.content, '{"ok":true}');
    assert.equal(response.model, "gemma-4-26b-a4b-it");
    assert.equal(response.provider, "gemini");
    assert.equal(response.fellBack, false);
    assert.equal(calls.length, 1);
    assert.match(calls[0].url, /models\/gemma-4-26b-a4b-it:generateContent$/);
    assert.ok(!calls[0].url.includes("test-key"), "the key must not appear in the URL");
    assert.equal(calls[0].options.headers["x-goog-api-key"], "test-key");
    assert.equal(calls[0].body.contents[0].parts[0].text, "Return JSON\n\n{}");
    assert.equal(calls[0].body.systemInstruction, undefined);
    assert.equal(calls[0].body.generationConfig.thinkingConfig.thinkingLevel, "minimal");
    assert.equal(calls[0].body.generationConfig.responseMimeType, "application/json");
  });
});

test("callLLM falls back to Ollama on a Gemini daily limit and then skips Gemini", async () => {
  const handler = (call) => (isGemini(call) ? dailyLimit() : json(200, { response: "from ollama" }));
  await withLlmEnv({ GEMINI_API_KEY: "test-key" }, handler, async ({ calls, warnings }) => {
    const first = await callLLM({ user: "a", stream: false });
    assert.equal(first.content, "from ollama");
    assert.equal(first.model, "gemma4:test");
    assert.equal(first.provider, "ollama");
    assert.equal(first.fellBack, true);
    assert.match(warnings.join("\n"), /Gemini unavailable.*DAILY limit/);

    const second = await callLLM({ user: "b", stream: false });
    assert.equal(second.model, "gemma4:test");
    assert.deepEqual(calls.map(isGemini), [true, false, false], "the daily limit must not be retried or re-hit");
  });
});

test("callLLM rethrows the Gemini error when the fallback is turned off", async () => {
  await withLlmEnv({ GEMINI_API_KEY: "test-key", LLM_FALLBACK: "off" }, (call) => (isGemini(call) ? dailyLimit() : json(200, { response: "x" })), async ({ calls }) => {
    assert.equal(getLLMConfig().fallback, null);
    await assert.rejects(callLLM({ user: "a", stream: false }), (error) => error.kind === "daily-limit");
    assert.equal(calls.filter((call) => !isGemini(call)).length, 0);
  });
});

test("callLLM uses Ollama directly when there is no Gemini key or the provider is pinned", async () => {
  const handler = (call) => (isGemini(call) ? json(500, {}) : json(200, { response: "from ollama" }));
  await withLlmEnv({}, handler, async ({ calls }) => {
    assert.equal(getLLMConfig().provider, "ollama");
    assert.equal((await callLLM({ user: "a", stream: false })).provider, "ollama");
    assert.equal(calls.filter(isGemini).length, 0);
  });
  await withLlmEnv({ GEMINI_API_KEY: "test-key", LLM_PROVIDER: "ollama" }, handler, async ({ calls }) => {
    assert.equal((await callLLM({ user: "a", stream: false })).provider, "ollama");
    assert.equal(calls.filter(isGemini).length, 0);
  });
});

test("callLLM sends the configured thinking level and skips thought parts", async () => {
  await withLlmEnv({ GEMINI_API_KEY: "test-key", GEMINI_THINKING: "minimal" }, (call) => geminiOk("answer"), async ({ calls }) => {
    const response = await callLLM({ user: "a", stream: false });
    assert.equal(response.content, "answer");
    assert.equal(calls[0].body.generationConfig.thinkingConfig.thinkingLevel, "minimal");
  });
});

Shown as it is in the repository at build time (2026-11-03); the home directory appears as <home>. If this page and the file differ, the file is right. Back to the catalogue.